সরাসরি পর্বে যান
পর্ব ৩৩ · ৫৫-এর মধ্যে · মডিউল ৫

ABC, duck typing আর Protocol — interface-এর তিন রূপ

Interfaces in Python: Duck Typing, Abstract Base Classes & Protocols

Taught in Bangla, this lesson covers three ways to express interfaces in Python: duck typing, nominal contracts with abc.ABC and @abstractmethod, free methods from collections.abc, structural typing with typing.Protocol and runtime_checkable, and dependency injection for swapping parts.

এই পর্বে যা শিখবেন

  • Interface বা চুক্তি কী, আর Python-এ তা প্রকাশের তিনটি উপায়: duck typing, ABC আর Protocol
  • Duck typing বাস্তবে — file-like object, যেকোনো iterable — আর EAFP-এর সাথে এর সম্পর্ক
  • abc.ABC, @abstractmethod, abstract property, ABC-র ভেতরে সাধারণ method (template method) আর register()
  • collections.abc-এর Iterable, Sequence, Mapping, MutableMapping — আর তাদের দেওয়া বিনামূল্যের mixin method
  • typing.Protocol, structural বনাম nominal typing, @runtime_checkable-এর সীমা, আর dependency injection দিয়ে fake object বসিয়ে টেস্ট

১ · Interface মানে একটি চুক্তি

পর্ব ২৯ থেকে আপনি class বানাচ্ছেন, পর্ব ৩১-এ দেখেছেন একটি class আরেকটি থেকে কীভাবে inheritance পায়। এবার একটি গভীর প্রশ্ন: একটি ফাংশন যখন কোনো object হাতে পায়, তখন সে আসলে কী চায়? একটি নির্দিষ্ট class? নাকি শুধু কয়েকটি কাজ করার ক্ষমতা?

একটি অনলাইন দোকানের কথা ভাবুন। অর্ডার নিশ্চিত হলে ক্রেতাকে একটি বার্তা পাঠাতে হবে। আজ সেটি SMS, কাল হয়তো ইমেইল, পরশু কোনো অ্যাপের notification. অর্ডারের কোডের কিন্তু এসব জানার দরকার নেই। তার দরকার শুধু একটি জিনিস — এমন কেউ, যাকে “এই নম্বরে এই বার্তা পাঠাও” বললে সে পাঠিয়ে দেবে. এই “বললে করে দেবে” প্রতিশ্রুতিটিকেই বলে interface: কোন কোন method থাকবে, কী নেবে, কী ফেরত দেবে — একটি চুক্তি।

দেশের প্রায় সব দোকানের সামনে মোবাইল রিচার্জের সাইনবোর্ড দেখেছেন। আপনি যখন টাকা রিচার্জ করতে যান, দোকানদার কে, দোকানটা চায়ের নাকি ওষুধের — তাতে কিছু যায় আসে না। সাইনবোর্ড মানে একটি চুক্তি: “এখানে নম্বর আর টাকা দিলে রিচার্জ হয়”। চুক্তি মানলেই যে কেউ এজেন্ট। Interface ঠিক এমন সাইনবোর্ড — কাজের প্রতিশ্রুতি, কে করছে তার পরিচয় নয়।

চুক্তিটির একটি নাম দিই: Notifier — “যার send(to, text) method আছে”। Python-এ এই চুক্তি প্রকাশ করার তিনটি উপায় আছে, আর তিনটির দর্শন আলাদা:

🦆 Duck typing

কোনো লিখিত চুক্তি নেই। শুধু notifier.send(...) ডেকে দিন। method থাকলে চলবে, না থাকলে চালানোর সময় error. চুক্তিটি থাকে docstring আর প্রোগ্রামারের মাথায়।

📜 Abstract base class

চুক্তি লেখা হয় একটি class হিসেবে। যে চুক্তি মানতে চায়, তাকে সেই class থেকে inherit করতেই হবে আর সব abstract method লিখতে হবে। নাম ধরে সদস্যপদ — একে বলে nominal.

🧩 Protocol

চুক্তি লেখা হয় class-এর মতো, কিন্তু কাউকে inherit করতে হয় না। যার send আছে, সে-ই মিলে যায় — আকার দেখে সদস্যপদ, একে বলে structural. যাচাই করে mypy-র মতো টুল, চালানোর আগেই।

তিনটিই একই প্রশ্নের উত্তর দেয় — “এই object কি আমার কাজে চলবে?” — কিন্তু কখন আর কীভাবে সেটি যাচাই হয়, তাতে পার্থক্য। Duck typing যাচাই করে একদম শেষ মুহূর্তে, method ডাকার সময়। ABC যাচাই করে object বানানোর সময়। Protocol যাচাই করে কোড চালানোর আগেই, একটি আলাদা টুল দিয়ে। এই পর্বের বাকিটা এই তিনটি পথ একে একে দেখা, আর শেষে কোনটি কখন বেছে নেবেন তা ঠিক করা।

মূল ধারণা

ভালো নকশায় কোড নির্ভর করে চুক্তির ওপর, নির্দিষ্ট class-এর ওপর নয়. অর্ডারের কোড যদি “SmsNotifier” চায়, তাহলে ইমেইলে বদলাতে অর্ডারের কোডও বদলাতে হবে। আর যদি চায় “যেকোনো Notifier”, তাহলে নতুন পাঠানোর পদ্ধতি যোগ করতে পুরোনো কোডে হাত দিতে হবে না। এই একটি ভাবনাই পরে টেস্টিং, plugin আর বড় সিস্টেম নকশার ভিত্তি।

২ · Duck typing — হাঁসের মতো ডাকলেই হাঁস

পর্ব ০৬-এ duck typing-এর নাম শুনেছিলেন। পুরো কথাটি একটি পুরোনো প্রবাদ থেকে: “যদি দেখতে হাঁসের মতো, হাঁটে হাঁসের মতো, আর ডাকে হাঁসের মতো — তাহলে এটা হাঁসই”। Python-এর ভাষায়: object-টি কোন class-এর, তা জিজ্ঞেস করবেন না; দেখুন সে দরকারি কাজটি করতে পারে কি না.

আপনি না জেনেই এতদিন duck typing ব্যবহার করছেন। কয়েকটি উদাহরণ:

  • যেকোনো iterable: for x in ... লুপ list, tuple, string, dict, set, ফাইল, range, generator — সবকিছুর ওপর চলে। লুপ কখনো জিজ্ঞেস করে না “এটা কি list?” সে শুধু iter() ডাকে (পর্ব ৩৪-এ এর ভেতরের গল্প)।
  • File-like object: print("হ্যালো", file=x) লিখলে x কোনো আসল ফাইল হতে হবে না — যার একটি write() method আছে, সে-ই চলবে। json.dump(data, x)-ও একই রকম। পর্ব ২৫-এর file object, মেমরিতে লেখার জন্য io.StringIO, নেটওয়ার্ক socket-এর মোড়ক — সবাই এই চুক্তি মানে।
  • len(), in, +: পর্ব ৩০-এ দেখেছেন, len(x) আসলে x.__len__() ডাকে। যার __len__ আছে, তার দৈর্ঘ্য মাপা যায় — class যা-ই হোক। এই dunder method-গুলোই Python-এর অলিখিত interface.

এবার অর্ডারের উদাহরণে duck typing:

class SmsNotifier:
    def send(self, to, text):
        print(f"SMS → {to}: {text}")

class EmailNotifier:                 # SmsNotifier-এর সাথে কোনো সম্পর্ক নেই
    def send(self, to, text):
        print(f"Email → {to}: {text}")

def confirm_order(notifier, phone):
    notifier.send(phone, "অর্ডার নিশ্চিত")   # শুধু ডেকে দেখা

confirm_order(SmsNotifier(), "01700-000000")
confirm_order(EmailNotifier(), "rina@example.com")

দুটি class-এর কোনো সাধারণ পূর্বপুরুষ নেই, কেউ কোনো চুক্তিতে সই করেনি। তবু confirm_order দুজনের সাথেই কাজ করে, কারণ দুজনেরই send আছে। একই ফাংশন ভিন্ন ভিন্ন class-এর object-এর সাথে চলা — একে বলে polymorphism, আর Python-এ এর সবচেয়ে হালকা রূপ duck typing.

Duck typing আর EAFP — জিজ্ঞেস না করে চেষ্টা

পর্ব ২৪-এ দুটি নীতি দেখেছিলেন: LBYL (“লাফ দেওয়ার আগে দেখুন”) আর EAFP (“অনুমতির চেয়ে ক্ষমা চাওয়া সহজ”)। Duck typing স্বাভাবিকভাবেই EAFP-এর সঙ্গী। কেউ যদি ভুল জিনিস পাঠায়, সেটি আগে থেকে পরীক্ষা না করে সরাসরি ডাকুন, আর সমস্যা হলে exception ধরুন:

def save_report(report, out):
    try:
        out.write(report)            # ফাইল, StringIO, socket — যা-ই হোক
    except AttributeError:
        raise TypeError("out-এর একটি write() method থাকতে হবে") from None

এর বিপরীত ধরন, যা অন্য ভাষা থেকে আসা মানুষ (আর AI) প্রায়ই লেখে: if isinstance(out, io.TextIOWrapper): .... এতে কোডটি শুধু আসল ফাইলের সাথে চলে — StringIO বা নিজের বানানো logger পাঠালে প্রত্যাখ্যান করে, যদিও তারা দিব্যি লিখতে পারত। isinstance দিয়ে class পরীক্ষা করা মানে duck typing-এর সুবিধা নিজের হাতে বন্ধ করা।

Duck typing-এর দুর্বলতা: চুক্তিটি কোথাও লেখা নেই। confirm_order-এর def লাইন দেখে বোঝার উপায় নেই notifier-এর কোন কোন method লাগবে। আর ভুল ধরা পড়ে দেরিতে — যে লাইনে method ডাকা হয়, ঠিক সেই লাইনে, হয়তো প্রোগ্রাম চালু হওয়ার কয়েক ঘণ্টা পরে, কোনো বিরল পথে। ছোট স্ক্রিপ্টে এটা সমস্যা নয়; দশজনের দলের বড় প্রজেক্টে এটাই বাগের উৎস। পরের দুটি হাতিয়ার — ABC আর Protocol — এই দুর্বলতা সারানোর জন্যই এসেছে।

৩ · abc.ABC আর @abstractmethod — লিখিত চুক্তি

ধরুন আপনার অনলাইন দোকান কয়েকটি পেমেন্ট পদ্ধতি নেয় — কার্ড, মোবাইল ওয়ালেট, ক্যাশ অন ডেলিভারি। প্রতিটির জন্য একটি class, আর সব class-কে দুটি কাজ পারতেই হবে: টাকা কাটা (charge) আর ফেরত দেওয়া (refund). এই নিয়মটি লিখে রাখা যায় একটি abstract base class (সংক্ষেপে ABC) দিয়ে:

from abc import ABC, abstractmethod

class PaymentGateway(ABC):
    @abstractmethod
    def charge(self, amount: int) -> str:
        """টাকা কেটে একটি transaction id ফেরত দেয়।"""

    @abstractmethod
    def refund(self, txn_id: str) -> bool:
        """টাকা ফেরত দেয়; সফল হলে True."""

দুটি জিনিস নতুন। প্রথমত, PaymentGateway inherit করেছে ABC থেকে — এটি abc module-এর একটি সহায়ক class, যা চুক্তি-যাচাইয়ের যন্ত্রটি চালু করে। দ্বিতীয়ত, @abstractmethod decorator দিয়ে চিহ্নিত method-গুলো হলো abstract method — “এর কাজ এখানে লেখা নেই, প্রতিটি subclass-কে নিজে লিখতে হবে”। শরীরে শুধু docstring, কারণ এখানে কাজ করার কিছু নেই।

এর ফল: যতক্ষণ কোনো class-এ অন্তত একটি abstract method অপূর্ণ থাকে, ততক্ষণ সেটি একটি abstract class — তার object বানানো (instantiate করা) যায় না। সব abstract method লেখা হলে class-টি হয় concrete class. ধাপে ধাপে দেখুন — একজন ডেভেলপার কার্ড পেমেন্টের class লিখছেন, কিন্তু প্রথমে refund লিখতে ভুলে গেছেন:

1class PaymentGateway(ABC):2    @abstractmethod3    def charge(self, amount): ...4    @abstractmethod5    def refund(self, txn_id): ...67class CardGateway(PaymentGateway):8    def charge(self, amount): return "TX-1"9    def refund(self, txn_id): return True   # পরে যোগ1011g = CardGateway()12print(g.charge(500), g.refund("TX-1"))
যে abstract method এখনো বাকি
PaymentGateway
CardGateway
g
  1. চুক্তি লেখা হলো। PaymentGateway দুটি abstract method ঘোষণা করল। Python class-টির গায়ে একটি তালিকা রাখে — __abstractmethods__ — যাতে লেখা থাকে কোন কোন method এখনো অপূর্ণ: charge আর refund. এই class-এর নিজেরই object বানানো যায় না, কারণ তালিকা খালি নয়।
  2. Subclass অর্ধেক লিখল। CardGateway শুধু charge লিখল। Class তৈরির মুহূর্তে Python হিসাব করে: পূর্বপুরুষের তালিকা থেকে যেগুলো এই class লিখে দিয়েছে, সেগুলো বাদ। বাকি থাকল {refund}. Class তৈরিতে কোনো error নেই — ভুলটা এখনো চুপচাপ।
  3. Object বানাতে গিয়ে ধরা পড়ল। CardGateway() ডাকামাত্র Python তালিকাটি দেখে: খালি নয়! সঙ্গে সঙ্গে TypeError, আর বার্তায় স্পষ্ট লেখা কোন method বাকি। লক্ষ করুন, error এসেছে object বানানোর সময়, refund ডাকার সময় নয় — duck typing-এর চেয়ে অনেক আগে।
  4. বাকি method যোগ হলো। ডেভেলপার লাইন ৯-এ refund লিখলেন। এখন CardGateway-এর তালিকা খালি — এটি একটি concrete class. লক্ষ করুন, PaymentGateway-এর তালিকা বদলায়নি; চুক্তি নিজে চিরকাল abstract থাকে।
  5. এবার সব চলে। CardGateway() একটি object বানাল, দুটি method-ই কাজ করল: TX-1 True. চুক্তি মানা হয়েছে — অন্তত নামের দিক থেকে। (method ঠিকঠাক কাজ করে কি না, তা ABC জানে না; সেটি টেস্টের কাজ।)

Python 3.14-এ পুরো error বার্তাটি এমন (Python 3.12 থেকে এই ভাষা; আগের version-এ লেখা ছিল with abstract method refund):

TypeError: Can't instantiate abstract class CardGateway without an implementation for abstract method 'refund'

একাধিক method বাকি থাকলে বার্তায় সবগুলোর নাম আসে — abstract methods 'charge', 'refund'. এটি একটি ছোট কিন্তু দামি সুবিধা: ভুলটা ধরা পড়ে প্রোগ্রাম শুরুর প্রথম মুহূর্তেই, যে বিরল পথে refund ডাকা হয় সেখানে পৌঁছানোর অনেক আগে।

ABC ছাড়া @abstractmethod অকেজো: যদি লেখেন class PaymentGateway: (কোনো ABC নেই) আর ভেতরে @abstractmethod, তাহলে কোনো error আসবে না — অসম্পূর্ণ subclass-এর object দিব্যি তৈরি হবে। Decorator শুধু একটি চিহ্ন বসায়; চিহ্নটি পড়ে যাচাই করে ABC-র ভেতরের যন্ত্র (ABCMeta নামের একটি metaclass). AI-এর লেখা কোডে এই ভুল প্রায়ই দেখা যায়।

Abstract property — শুধু method নয়, বৈশিষ্ট্যও চুক্তিতে

কখনো চুক্তিতে একটি মান থাকা দরকার, method নয় — যেমন প্রতিটি পেমেন্ট পদ্ধতির একটি নাম, যা রসিদে ছাপা হবে। পর্ব ৩২-এর property-র সাথে @abstractmethod মিলিয়ে এটি লেখা যায়। ক্রম গুরুত্বপূর্ণ: @property থাকবে বাইরে (ওপরে), @abstractmethod ভেতরে:

class PaymentGateway(ABC):
    @property
    @abstractmethod
    def name(self) -> str: ...

class WalletGateway(PaymentGateway):
    @property
    def name(self) -> str:
        return "মোবাইল ওয়ালেট"

এখন name না লিখলে subclass-এর object তৈরি হবে না — ঠিক method-এর মতোই। একে বলে abstract property. (পুরোনো কোডে @abstractproperty দেখতে পারেন; সেটি Python 3.3 থেকে বাতিলের পথে — নতুন কোডে ওপরের ধরনই লিখুন।)

Python ১৯৯১ থেকে প্রায় দেড় দশক কোনো লিখিত interface ছাড়াই চলেছে — পুরোটাই duck typing. ২০০৭ সালে Guido van Rossum আর Talin লেখেন PEP 3119 — “Introducing Abstract Base Classes”, যা abc module আর collections-এর ABC-গুলো নিয়ে আসে Python 2.6 আর 3.0-এ (২০০৮)। class X(ABC) লেখার সহজ রূপটি এসেছে আরও পরে, Python 3.4-এ; তার আগে লিখতে হতো metaclass=ABCMeta.

৪ · ABC-র ভেতরে সাধারণ method — template method আর register()

ABC মানে শুধু খালি method-এর তালিকা নয়। এর ভেতরে পুরোপুরি কাজ করা সাধারণ method-ও রাখা যায় — আর সেই method abstract method-গুলোকে ডাকতে পারে। এটাই ABC-র আসল শক্তি।

অনলাইন দোকানে প্রতিটি checkout-এ কয়েকটি ধাপ একই: টাকার পরিমাণ যাচাই, টাকা কাটা, রসিদ তৈরি, লগ লেখা। শুধু “টাকা কাটা” ধাপটি পেমেন্ট পদ্ধতিভেদে আলাদা। তাহলে একই ধাপগুলো ABC-তে একবার লিখুন, আর আলাদা অংশটি abstract রাখুন:

class PaymentGateway(ABC):
    @abstractmethod
    def charge(self, amount: int) -> str: ...

    def checkout(self, amount: int) -> str:      # সাধারণ method
        if amount <= 0:
            raise ValueError("টাকার পরিমাণ শূন্যের বেশি হতে হবে")
        txn = self.charge(amount)                 # subclass-এর কাজ
        return f"রসিদ: {txn}, {amount} টাকা"

checkout হলো কাঠামো — কোন ধাপ কখন হবে, তা এখানে স্থির। charge হলো সেই কাঠামোর একটি ফাঁকা ঘর, যা প্রতিটি subclass পূরণ করে। এই নকশার নাম template method pattern — ১৯৯৪ সালের বিখ্যাত “Design Patterns” বই (যার চার লেখককে “Gang of Four” বলা হয়) এটিকে নাম দিয়েছিল। এর লাভ: যাচাইয়ের নিয়ম বদলাতে হলে একটি জায়গায় বদলালেই সব পেমেন্ট পদ্ধতিতে কার্যকর হয়; কোনো নতুন পদ্ধতি যাচাই বাদ দিতে পারে না।

Abstract method-এরও শরীর থাকতে পারে: @abstractmethod দেওয়া method-এর ভেতরে কিছু সাধারণ কোড লিখে রাখা যায়, আর subclass নিজের সংস্করণ থেকে super().charge(amount) দিয়ে সেটি ডাকতে পারে। তবু subclass-কে method-টি নিজে লিখতেই হবে — abstract মানে “এটি অবশ্যই override করতে হবে”, “এখানে কোনো কোড নেই” নয়।

register() — inheritance ছাড়া সদস্যপদ

ধরুন একটি তৃতীয় পক্ষের library-তে LegacyGateway নামে একটি class আছে, যার charge আর refund দুটিই আছে, কিন্তু সেটি আপনার PaymentGateway থেকে inherit করেনি — করবেই বা কীভাবে, সেটি আপনার চুক্তি সম্পর্কে জানেই না। আপনি তার কোড বদলাতে পারবেন না। তবু চান isinstance(x, PaymentGateway) তার জন্য True হোক। এর জন্য আছে:

PaymentGateway.register(LegacyGateway)
isinstance(LegacyGateway(), PaymentGateway)    # True

এভাবে নিবন্ধিত class-কে বলে virtual subclass. তবে সাবধান — এখানে Python কিছুই যাচাই করে না. LegacyGateway-এর refund না থাকলেও register চুপচাপ মেনে নেবে। এটি আপনার একটি ঘোষণা মাত্র: “আমি কথা দিচ্ছি, এই class চুক্তি মানে”। আর virtual subclass ABC-র সাধারণ method (যেমন checkout) পায় না, কারণ আসল inheritance হয়নি। Python নিজে এটি ব্যবহার করে: list, tuple, str কোনো ABC থেকে inherit করে না, কিন্তু collections.abc-এর Sequence-পরিবারে নিবন্ধিত (list আসলে MutableSequence-এ, যা Sequence-এরই subclass) — তাই isinstance([1, 2], Sequence) হয় True.

৫ · collections.abc — দুটি method লিখুন, পাঁচটি বিনামূল্যে

নিজে ABC লেখার আগে জেনে রাখুন, Python-এর standard library-তে আগে থেকেই একগুচ্ছ তৈরি ABC আছে, collections.abc module-এ। এগুলো Python-এর নিজের অলিখিত interface-গুলোকে লিখিত রূপ দেয় — “iterable হওয়া মানে কী”, “sequence হওয়া মানে কী”। সবচেয়ে কাজের কয়েকটি:

ABCআপনাকে লিখতে হবেবিনামূল্যে পাবেন (mixin)Python-এর উদাহরণ
Iterable__iter__—list, dict, set, ফাইল
Sized__len__—যার len() মাপা যায়
Sequence__getitem__, __len____contains__, __iter__, __reversed__, index, countlist, tuple, str, range
Mapping__getitem__, __iter__, __len____contains__, keys, items, values, get, __eq__dict (পড়ার দিক)
MutableMappingওপরের তিনটি + __setitem__, __delitem__Mapping-এর সব + pop, popitem, clear, update, setdefaultdict

তৃতীয় কলামটিই আসল উপহার। এগুলোকে বলে mixin method — ABC নিজেই আপনার লেখা কয়েকটি মৌলিক method ব্যবহার করে বাকিগুলো তৈরি করে দেয়। উদাহরণ: একটি গানের অ্যাপের read-only Playlist, যেটি দেখতে-শুনতে tuple-এর মতো আচরণ করবে। Sequence থেকে inherit করে মাত্র দুটি method লিখি:

1class Playlist(Sequence):2    def __init__(self, songs):3        self._songs = tuple(songs)4    def __getitem__(self, i):5        return self._songs[i]6    def __len__(self):7        return len(self._songs)89p = Playlist(["Nodi", "Megh", "Brishti"])10print("Megh" in p)11print(p.index("Brishti"), list(reversed(p)))
Playlist-এর method কোথা থেকে
__getitem__
__len__
__contains__
__iter__
index, count, __reversed__
  1. Sequence থেকে inherit. Sequence-এর দুটি abstract method: __getitem__ আর __len__. বাকি পাঁচটি method সে নিজেই লিখে রেখেছে — কিন্তু সেগুলো ভেতরে ভেতরে ওই দুটির ওপর নির্ভর করে। তাই আপনার দুটি না লেখা পর্যন্ত object বানানো যাবে না।
  2. __getitem__ লেখা হলো। “i নম্বর গান দাও” — ভেতরের tuple থেকে ফেরত। একটি abstract method পূরণ হলো, একটি বাকি। এখনো এটি abstract class.
  3. __len__ লেখা হলো — চুক্তি পূর্ণ। এখন Playlist concrete. Playlist([...]) দিয়ে object তৈরি হলো, আর সঙ্গে সঙ্গে পাঁচটি mixin method কাজের জন্য তৈরি — যেগুলোর একটিও আপনি লেখেননি।
  4. "Megh" in p — ভেতরে কী ঘটে। in ডাকে __contains__; Sequence-এর __contains__ একে একে উপাদান দেখে, __iter__ দিয়ে। আর Sequence-এর __iter__ আপনার __getitem__-কে ডাকে 0, 1, 2… দিয়ে, যতক্ষণ না IndexError আসে। i=1-এ "Megh" মিলল — True. আপনার লেখা একটি method তিন স্তরের নিচে কাজ করছে।
  5. index আর reversed. p.index("Brishti") একইভাবে খুঁজে 2 দিল। reversed(p) ডাকে __reversed__, যা __len__ দিয়ে শেষ অবস্থান জেনে __getitem__ দিয়ে পেছন থেকে আনে। দুটি মৌলিক method — পাঁচটি সুবিধা। আর isinstance(p, Sequence)-ও এখন True.

এখানে একটি দক্ষতার কথা মনে রাখুন: mixin method সাধারণ, তাই সবসময় সবচেয়ে দ্রুত নয়। Sequence-এর __contains__ প্রতিবার শুরু থেকে খোঁজে; আপনার ডেটা যদি set-এ রাখা থাকে, তাহলে নিজের দ্রুত __contains__ লিখে mixin-টিকে override করতে পারেন। ABC শুধু ন্যূনতম দেয়, সীমা টানে না।

isinstance(x, Iterable) — আর তার সীমা

কিছু ছোট ABC — Iterable, Sized, Container, Hashable, Callable — একটু অন্যভাবে কাজ করে। এদের জন্য inherit বা register কিছুই লাগে না; class-এ দরকারি dunder method থাকলেই isinstance True দেয়। যেমন __len__ আছে এমন যেকোনো class-এর object-এর জন্য isinstance(obj, Sized) সত্য। (এটি হয় __subclasshook__ নামের একটি বিশেষ class method দিয়ে।) অর্থাৎ এই ছোট ABC-গুলো ভেতরে ভেতরে duck typing-ই করছে, শুধু একটি নাম দিয়ে।

তবে Sequence বা Mapping-এর ক্ষেত্রে এটি হয় না। শুধু __getitem__ আর __len__ লিখলেই isinstance(obj, Sequence) সত্য হবে না — inherit বা register করতে হবে। কারণ “sequence” হওয়ার অর্থ শুধু method-এর নামে নয়: __getitem__ একটি dict-এরও আছে, কিন্তু dict sequence নয়।

দুটি সূক্ষ্ম ফাঁদ: (১) isinstance(x, Iterable) শুধু __iter__ খোঁজে। কিন্তু কিছু পুরোনো ধরনের class শুধু __getitem__ দিয়েও লুপে চলে, আর তাদের জন্য উত্তর আসে False. Python-এর অফিসিয়াল ডকুমেন্টেশনও বলে, কোনো কিছু iterable কি না জানার একমাত্র নির্ভরযোগ্য উপায় হলো iter(x) ডেকে দেখা — অর্থাৎ EAFP. (২) str নিজেও একটি Sequence. “একটি list বা tuple চাই” ভেবে isinstance(x, Sequence) লিখলে "abc"-ও পার পেয়ে যাবে, আর আপনার কোড অক্ষরগুলোকে তিনটি আলাদা জিনিস ভেবে কাজ করবে।

এবার প্রথম runnable প্রোগ্রাম: একটি অসম্পূর্ণ পেমেন্ট class-এর TypeError ধরা, সম্পূর্ণ class দিয়ে template method চালানো, আর দুটি method-এর Playlist থেকে বিনামূল্যের method ব্যবহার। চালানোর আগে শেষ লাইনের চারটি মান অনুমান করুন:

from abc import ABC, abstractmethod
from collections.abc import Sequence

class PaymentGateway(ABC):
    @abstractmethod
    def charge(self, amount): ...
    def checkout(self, amount):               # সাধারণ method
        return f"[{self.charge(amount)}] রসিদ তৈরি"

class Draft(PaymentGateway): pass             # charge লেখা হয়নি
try:
    Draft()
except TypeError as e:
    print("TypeError:", e)

class CardGateway(PaymentGateway):
    def charge(self, amount): return f"card {amount} tk"
print(CardGateway().checkout(500))

class Playlist(Sequence):                     # শুধু দুটি method
    def __init__(self, songs): self._songs = tuple(songs)
    def __getitem__(self, i): return self._songs[i]
    def __len__(self): return len(self._songs)

p = Playlist(["Nodi", "Megh", "Brishti"])
print("Megh" in p, p.index("Brishti"), p.count("Nodi"), list(reversed(p)))

আউটপুট তিন লাইনের। প্রথমটি TypeError: Can't instantiate abstract class Draft without an implementation for abstract method 'charge' — Draft চুক্তি পূরণ করেনি, তাই object তৈরির মুহূর্তেই error; আমরা try/except দিয়ে ধরে বার্তাটি ছেপেছি, তাই প্রোগ্রাম থামেনি। দ্বিতীয়টি [card 500 tk] রসিদ তৈরি — checkout লেখা আছে ABC-তে, কিন্তু ভেতরে self.charge ডেকে পেয়েছে CardGateway-এর সংস্করণ (template method). তৃতীয়টি True 2 1 ['Brishti', 'Megh', 'Nodi'] — in, index, count, reversed চারটিই কাজ করল, যদিও Playlist-এ এদের একটিও লেখা নেই। (জায়গা বাঁচাতে কয়েকটি method এক লাইনে লেখা হয়েছে; নিজের কোডে PEP 8 মেনে আলাদা লাইনে লিখুন।)

৬ · typing.Protocol — আকার দেখে চুক্তি

ABC শক্তিশালী, কিন্তু তার একটি শর্ত আছে: চুক্তি মানতে হলে inherit করতেই হবে (অথবা কাউকে register করতে হবে)। এই ধরনকে বলে nominal typing — সদস্যপদ নাম ধরে, বংশপরিচয় দেখে। এতে দুটি সমস্যা:

  • তৃতীয় পক্ষের library-র class আপনার ABC থেকে inherit করে না, আর আপনি তাদের কোড বদলাতে পারেন না।
  • ছোট একটি চাহিদার জন্য (“শুধু একটি area() method চাই”) সবাইকে একটি নির্দিষ্ট বংশে বাঁধা ভারী মনে হয় — বিশেষ করে Python-এ, যেখানে duck typing-ই স্বাভাবিক।

এর সমাধান Protocol, যা এসেছে Python 3.8-এ (PEP 544). Protocol দেখতে একটি class-এর মতো, কিন্তু অর্থ ভিন্ন: “যে class-এ এই method-গুলো আছে, সে-ই আমার সদস্য — inherit করুক বা না করুক”। একে বলে structural typing — সদস্যপদ আকার দেখে। এক কথায়, Protocol হলো লিখিত duck typing.

import math
from typing import Protocol

class HasArea(Protocol):
    def area(self) -> float: ...

class Circle:                          # HasArea-র নামও জানে না
    def __init__(self, r: float) -> None:
        self.r = r
    def area(self) -> float:
        return math.pi * self.r ** 2

class Label:                           # area নেই
    def __init__(self, text: str) -> None:
        self.text = text

def describe(shape: HasArea) -> str:
    return f"ক্ষেত্রফল {shape.area():.1f}"

এখানে : HasArea একটি type hint. পর্ব ০৬-এ দেখেছিলেন, Python নিজে চালানোর সময় type hint যাচাই করে না। যাচাই করে একটি আলাদা টুল — type checker, যেমন mypy বা pyright — কোড না চালিয়েই, শুধু পড়ে। তাহলে Protocol-এর চুক্তি বাস্তবে কার্যকর হয় type checker-এর চোখে। নিচের ডায়াগ্রামে দেখুন, একই কোড type checker কীভাবে দেখে, আর ABC হলে কী হতো:

  1. চুক্তিHasAreaProtocol
  2. মিলেছেCircleinherit ছাড়াই
  3. মেলেনিLabelarea নেই
  4. ABC হলেShapenominal
  5. চালানোর সময়isinstanceruntime_checkable
has_area.pyচুক্তি: area() → float
class HasArea(Protocol):
    def area(self) -> float: ...      # শরীর ফাঁকা: শুধু আকার
$ mypy has_area.pydescribe(Circle(2))
Success: no issues found in 1 source file
# Circle-এর area() -> float আছে — আকার মিলেছে
$ mypy has_area.pydescribe(Label("দাম"))
error: Argument 1 to "describe" has incompatible type "Label";
expected "HasArea"  [arg-type]
# কোড চালানোর আগেই ধরা পড়ল
যদি Shape(ABC) হতোdescribe(shape: Shape)
describe(Circle(2))
error: ... incompatible type "Circle"; expected "Shape"
isinstance(Circle(2), Shape)   # False
# area() থাকলেও Circle "Shape বংশের" নয়
@runtime_checkable HasAreaPython চালানোর সময়
isinstance(Circle(2), HasArea)     # True
isinstance(Label("x"), HasArea)    # False
class Odd:
    def area(self, unit, scale): ...
isinstance(Odd(), HasArea)         # True (!)
  1. চুক্তি লেখা হলো। HasArea একটি Protocol: “যার area() আছে এবং একটি float ফেরত দেয়”। শরীরে শুধু ... — এখানে কোনো কাজ নেই, শুধু আকারের বর্ণনা। লক্ষ করুন, এটি নিজে কোনো abstract method-এর তালিকা রাখে না; এর কাজ type checker-এর জন্য।
  2. Circle — inherit ছাড়াই মিলল। Circle কখনো HasArea-র নাম শোনেনি। তবু mypy দেখল: area(self) -> float আছে, আকার হুবহু মিলেছে — তাই describe(Circle(2)) বৈধ। এটাই structural typing: বংশ নয়, আকার।
  3. Label — চালানোর আগেই ধরা। Label-এর area নেই। Duck typing হলে ভুলটা ধরা পড়ত describe চালানোর সময়, AttributeError দিয়ে। এখন mypy কোড না চালিয়েই বলে দিল, কোন লাইনে কী ভুল। Duck typing-এর নমনীয়তা আর আগাম যাচাই — দুটোই একসাথে।
  4. ABC হলে কী হতো। যদি চুক্তিটি Shape(ABC) হতো, তাহলে Circle inherit না করা পর্যন্ত mypy-ও আপত্তি করত, আর isinstance-ও False দিত — area থাকা সত্ত্বেও। Nominal typing আকার দেখে না, বংশপরিচয় দেখে।
  5. চালানোর সময় isinstance — আর তার সীমা। সাধারণ Protocol দিয়ে isinstance চলে না (TypeError দেয়)। @runtime_checkable লাগালে চলে, কিন্তু সে শুধু দেখে area নামে কিছু আছে কি না। Odd-এর area দুটি বাড়তি argument চায়, তবু True! signature, return type — কিছুই যাচাই হয় না। পূর্ণ যাচাই শুধু type checker-এর কাজ।

Protocol থেকে inherit করা নিষেধ নয় — চাইলে class Circle(HasArea): লিখে স্পষ্ট করতে পারেন, তাতে type checker নিশ্চিত করবে আপনি চুক্তি ঠিকমতো পূরণ করেছেন। কিন্তু দরকার নেই, আর সেটাই Protocol-এর মূল আকর্ষণ। এভাবে আপনি এমন class-এর জন্যও চুক্তি লিখতে পারেন যা অন্য কেউ, অন্য কোথাও, আপনার চুক্তির কথা না জেনে লিখেছে।

Type hint-এর পুরো গল্প পর্ব ৩৭-এ: mypy কীভাবে install আর চালাবেন, generic type, Iterable[HasArea]-এর মতো জটিল hint, আর gradual typing — সব পর্ব ৩৭-এ। এখানে শুধু এটুকু জানলেই চলবে: Protocol-এর চুক্তি যাচাই হয় type checker দিয়ে, কোড চালানোর আগে; Python নিজে চালানোর সময় type hint দেখে না। মজার তথ্য: Python-এর নিজের Iterable, Sized ইত্যাদিকেও type checker এখন Protocol হিসেবে দেখে — __iter__ থাকলেই iterable.

@runtime_checkable কখন

কখনো সত্যিই চালানোর সময় পরীক্ষা দরকার — যেমন একটি plugin সিস্টেম, যেখানে বাইরে থেকে আসা object-এর ধরন জানা নেই। তখন Protocol-এর ওপরে @runtime_checkable লিখলে isinstance কাজ করে। কিন্তু ডায়াগ্রামের শেষ ধাপটি মনে রাখুন: এটি শুধু method-এর নাম আছে কি না দেখে। এটি একটি দ্রুত ছাঁকনি, পূর্ণ যাচাই নয়। আর দুটি ব্যবহারিক কথা: (১) এই পরীক্ষা সাধারণ isinstance-এর চেয়ে বেশ ধীর, তাই গরম লুপের ভেতরে ব্যবহার করবেন না; (২) পরীক্ষাটি “পাস” করলেই ধরে নেবেন না যে method ঠিকমতো ডাকা যাবে।

৭ · Dependency injection — চুক্তি দিয়ে অংশ বদলানো

এবার সব মিলিয়ে একটি বাস্তব নকশা। ধরুন OrderService অর্ডার নেয় আর ক্রেতাকে বার্তা পাঠায়। প্রথম চেষ্টায় অনেকে লেখেন:

class OrderService:
    def __init__(self):
        self.notifier = SmsNotifier()       # নিজেই বানিয়ে নিল

সমস্যা কোথায়? OrderService এখন চিরকালের জন্য SMS-এর সাথে বাঁধা। ইমেইলে বদলাতে এই class খুলে বদলাতে হবে। আর সবচেয়ে বড় কথা — টেস্ট চালালেই আসল SMS যাবে, প্রতিটি SMS-এর খরচ আছে, আর পরীক্ষার নম্বরে অচেনা মানুষের ফোনে “অর্ডার নিশ্চিত” পৌঁছাবে।

সমাধান: OrderService নিজে notifier বানাবে না; বাইরে থেকে কেউ একটি দেবে। আর OrderService শুধু জানবে চুক্তিটি — Notifier. এই কৌশলকে বলে dependency injection — নির্ভরতা (dependency) বাইরে থেকে “ঢুকিয়ে” দেওয়া:

OrderServiceplace(phone, item)
চুক্তি: Notifiersend(to, text)
📱 SmsNotifierআসল SMS — প্রোডাকশন
✉️ EmailNotifierআসল ইমেইল
🧪 FakeNotifierশুধু list-এ লিখে রাখে
assert fake.sent == [...]টেস্ট পাস, খরচ শূন্য
  1. চুক্তির ওপর নির্ভরতা। OrderService.__init__(self, notifier: Notifier) — সে notifier বানায় না, হাতে পায়। তার কোডে কোথাও SmsNotifier বা EmailNotifier-এর নাম নেই, আছে শুধু self.notifier.send(...). সে জানে কেবল চুক্তি।
  2. প্রোডাকশনে SMS. প্রোগ্রাম শুরুর জায়গায় (একে অনেকে “composition root” বলেন) লেখা হয় OrderService(SmsNotifier()). অর্ডার এলে আসল SMS যায়। কোন অংশ কোথায় বসবে, সেই সিদ্ধান্ত এক জায়গায়।
  3. ইমেইলে বদল — OrderService অপরিবর্তিত। কাল কোম্পানি ইমেইলে যেতে চাইল। শুধু শুরুর লাইনটি বদলান: OrderService(EmailNotifier()). OrderService-এর একটি লাইনও বদলাতে হলো না। নতুন পদ্ধতি যোগ মানে নতুন class, পুরোনো কোডে হাত নয়।
  4. টেস্টে fake. টেস্টের সময় বসানো হয় FakeNotifier — যে আসলে কিছু পাঠায় না, শুধু কাকে কী পাঠাতে বলা হয়েছিল তা একটি list-এ লিখে রাখে। টেস্ট শেষে assert দিয়ে list মিলিয়ে দেখা হয়। কোনো SMS খরচ নেই, নেটওয়ার্ক লাগে না, টেস্ট চলে মিলিসেকেন্ডে। এমন বদলি object-কে বলে fake.

এখানে FakeNotifier একটি fake object — আসল জিনিসের একটি হালকা, পরীক্ষাযোগ্য বদলি। পর্ব ৩৯-এ pytest দিয়ে unit test লেখার সময় এই কৌশল বারবার কাজে লাগবে। এবার দ্বিতীয় runnable প্রোগ্রাম — একটি Protocol চুক্তি, inheritance ছাড়া একটি fake, dependency injection, আর @runtime_checkable-এর সীমা হাতেনাতে:

from typing import Protocol, runtime_checkable

@runtime_checkable
class Notifier(Protocol):
    def send(self, to: str, text: str) -> None: ...

class FakeNotifier:                     # Notifier-কে inherit করেনি
    def __init__(self) -> None:
        self.sent: list[tuple[str, str]] = []
    def send(self, to: str, text: str) -> None:
        self.sent.append((to, text))

class OrderService:
    def __init__(self, notifier: Notifier) -> None:
        self.notifier = notifier
    def place(self, phone: str, item: str) -> None:
        self.notifier.send(phone, f"{item}: অর্ডার নিশ্চিত")

fake = FakeNotifier()
OrderService(fake).place("01700-000000", "চা")
print(fake.sent)

class Broken:
    def send(self) -> None: ...          # ভুল signature!
print(isinstance(fake, Notifier), isinstance(Broken(), Notifier))

আউটপুট দুই লাইনের। প্রথমটি [('01700-000000', 'চা: অর্ডার নিশ্চিত')] — OrderService ভেবেছে সে বার্তা পাঠিয়েছে, কিন্তু আসলে FakeNotifier শুধু লিখে রেখেছে; টেস্টে ঠিক এটাই মিলিয়ে দেখা হয়। FakeNotifier কোথাও Notifier-এর নাম নেয়নি, তবু mypy চালালে OrderService(fake)-এ কোনো আপত্তি করবে না — structural typing. দ্বিতীয় লাইন True True — আর এখানেই শিক্ষা: Broken-এর send কোনো argument নেয় না, তবু @runtime_checkable-এর isinstance বলছে সে Notifier! এটি শুধু send নামটি খুঁজেছে। OrderService(Broken()) চালালে place-এ গিয়ে TypeError আসত; mypy অবশ্য কোড না চালিয়েই Broken-কে প্রত্যাখ্যান করত।

৮ · কোনটি কখন — আর অন্য ভাষার সাথে তুলনা

তিনটি হাতিয়ারই জানলেন। এবার সবচেয়ে গুরুত্বপূর্ণ দক্ষতা — ঠিক জায়গায় ঠিকটি বেছে নেওয়া:

প্রশ্নDuck typingABCProtocol
চুক্তি কোথায় লেখাকোথাও না (docstring)একটি base class-এএকটি Protocol class-এ
সদস্য হতে inherit লাগে?নাহ্যাঁ (বা register)না
ভুল কখন ধরা পড়েmethod ডাকার সময়object বানানোর সময়কোড চালানোর আগে (type checker)
সাধারণ কোড ভাগ করা যায়?নাহ্যাঁ — template method, mixinসাধারণত না
তৃতীয় পক্ষের class মানাতেসহজকঠিন (register, যাচাইহীন)সহজ
সবচেয়ে ভালো কাজেছোট স্ক্রিপ্ট, এক-দুই জায়গায় ব্যবহারনিজের class-পরিবার, ভাগ করা কোড, কাঠামো জোর করে মানানোলাইব্রেরির interface, dependency injection, বাইরের class গ্রহণ

একটি সহজ নিয়মে বলা যায়: শুরু করুন duck typing দিয়ে. চুক্তিটি যখন কয়েক জায়গায় ব্যবহার হতে শুরু করে আর দলের অন্যদের বোঝা দরকার, তখন Protocol লিখুন — এটি কাউকে বাধ্য করে না, শুধু চুক্তিটি দৃশ্যমান করে। আর যখন একই পরিবারের class-গুলোর মধ্যে কোড ভাগ করতে চান, বা object বানানোর সময়েই অসম্পূর্ণতা আটকাতে চান, তখন ABC. প্রতিটি ছোট class-এর জন্য ABC বানানো Python-এর ধরন নয়।

Java, C++ আর Go কী করে

Java-তে interface একটি আলাদা ভাষা-উপাদান: interface Notifier { void send(String to, String text); }, আর class-কে স্পষ্ট লিখতে হয় implements Notifier. এটি পুরোপুরি nominal — একই method থাকলেও implements না লিখলে মিলবে না, আর যাচাই হয় compile-এর সময়। Python-এর ABC এর সবচেয়ে কাছের আত্মীয়, তবে Python-এ যাচাই হয় object বানানোর সময়। Go উল্টো পথে গেছে: সেখানে interface পূরণ হয় নিঃশব্দে — কোনো type-এর ঠিক method থাকলেই সে interface মানে, কোনো implements লাগে না। এটি Python-এর Protocol-এর খুব কাছের ধারণা, শুধু Go-তে compiler নিজেই যাচাই করে। TypeScript-ও structural typing ব্যবহার করে।

C/C++ জানলে

C++-এ abstract class মানে অন্তত একটি pure virtual function (virtual void send() = 0;) আছে এমন class — Python-এর ABC-র মতোই তার object বানানো যায় না, তবে ভুল ধরা পড়ে compile-এর সময়, আর inherit করতেই হয় (nominal). C++20-এর concepts অনেকটা Protocol-এর মতো structural: template-এর type-এর কী কী operation থাকতে হবে তা লেখা যায়, inheritance ছাড়াই — পার্থক্য হলো C++-এ compiler নিজেই যাচাই করে, Python-এ করে আলাদা type checker. C-তে interface-এর কাছাকাছি জিনিস হলো function pointer-ভরা struct. বিস্তারিত দেখুন C++-এর abstract class ও interface আর C++20 concepts.

পরীক্ষা ও ইন্টারভিউতে
  • Duck typing কী? — object-এর class না দেখে তার আচরণ (দরকারি method আছে কি না) দেখে ব্যবহার করা; “হাঁসের মতো ডাকলেই হাঁস”।
  • Abstract class-এর object বানালে কী হয়? — কোনো abstract method অপূর্ণ থাকলে TypeError: Can't instantiate abstract class ....
  • ABC আর Protocol-এর মূল পার্থক্য? — ABC nominal (inherit করতে হয়, চালানোর সময় যাচাই); Protocol structural (ঠিক method থাকলেই মেলে, যাচাই করে type checker).
  • @runtime_checkable কী যাচাই করে? — শুধু method-এর নাম আছে কি না; argument বা return type নয়।
  • Dependency injection কেন? — নির্ভরতা বাইরে থেকে দিলে অংশ বদলানো যায় কোড না বদলে, আর টেস্টে fake বসানো যায়।

৯ · AI-এর যুগে — interface নকশায় AI-এর ভুল ধরা

“একটি পেমেন্ট সিস্টেম বানিয়ে দাও” বললে AI প্রায়ই একগাদা ABC, interface আর factory class নিয়ে হাজির হয় — যেন Java-র কোনো বড় প্রজেক্ট। কারণ তার প্রশিক্ষণের ডেটায় Java আর C#-এর “enterprise” ধাঁচের কোড প্রচুর। কোডটি চলে, কিন্তু ভারী, আর কিছু সূক্ষ্ম ভুল লুকিয়ে থাকে:

AI প্রায়ই যা করেসমস্যাকী চাইবেন
প্রতিটি class-এর জন্য একটি ABC, যার একটিই subclassঅকারণ জটিলতা; Python-এর ধরন নয়duck typing বা ছোট Protocol; ABC শুধু ভাগ করা কোড থাকলে
@abstractmethod আছে কিন্তু base-এ ABC নেইকোনো যাচাই হয় না; অসম্পূর্ণ class-এর object তৈরি হয়ে যায়class X(ABC) নিশ্চিত করা
“abstract” method-এ @abstractmethod ভুলে raise NotImplementedErrorভুল ধরা পড়ে শুধু ডাকার সময়, অনেক দেরিতে@abstractmethod দিয়ে চিহ্নিত করা
@runtime_checkable দিয়ে isinstance-কে পূর্ণ যাচাই ভাবাশুধু নাম মেলে; ভুল signature-ও পার পায়mypy/pyright চালানো; runtime-এ দরকার হলে EAFP
if isinstance(x, list) বা type(x) == ... দিয়ে শাখাduck typing ভাঙে; tuple, নিজের class বাদ পড়েসরাসরি ব্যবহার (EAFP), বা collections.abc-এর ABC দিয়ে পরীক্ষা
class-এর ভেতরে self.sender = SmsSender()বাঁধা নির্ভরতা; টেস্টে আসল SMS যায়constructor-এ চুক্তি নেওয়া (dependency injection)

AI-কে কোড লিখতে বলার সময় নকশার শর্তগুলো আগেই দিয়ে দিন। নিচের কাঠামোটি কপি করে নিজের কাজ বসান:

আমি Python 3.12+ ব্যবহার করি। একটি অনলাইন দোকানের জন্য কোড লিখে দাও: অর্ডার নিশ্চিত হলে ক্রেতাকে বার্তা যাবে (SMS বা ইমেইল), আর পেমেন্ট হবে কার্ড বা মোবাইল ওয়ালেটে। শর্ত: ১. বার্তা পাঠানোর চুক্তি typing.Protocol দিয়ে লেখো (Notifier, method: send(to, text)). SmsNotifier আর EmailNotifier যেন Notifier থেকে inherit না করে। ২. পেমেন্টের জন্য একটি abc.ABC লেখো, কারণ এখানে ভাগ করা checkout() method থাকবে; প্রতিটি পরিবর্তনশীল ধাপে @abstractmethod দাও, আর base class যেন সত্যিই ABC থেকে inherit করে। ৩. প্রয়োজন ছাড়া আর কোনো ABC বা interface বানাবে না; প্রতিটির জন্য এক লাইনে কারণ বলো। ৪. OrderService নিজে কোনো notifier বা gateway তৈরি করবে না; constructor-এ নেবে (dependency injection). ৫. isinstance দিয়ে type পরীক্ষা করবে না; @runtime_checkable ব্যবহার করলে লিখে দাও এটি কী যাচাই করে না। ৬. একটি FakeNotifier লিখে assert দিয়ে দেখাও যে অর্ডার দিলে ঠিক নম্বরে ঠিক বার্তা "পাঠানো" হয়, আর একটি অসম্পূর্ণ gateway class-এর object বানালে TypeError আসে।

AI-এর লেখা কোড পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১“Duck typing” নামটি কোথা থেকে এল? হাঁসের প্রবাদ কীভাবে একটি প্রোগ্রামিং দর্শনের নাম হয়ে গেল?

“Duck test” নামের প্রবাদটি প্রোগ্রামিংয়ের চেয়ে অনেক পুরোনো: “যদি দেখতে হাঁসের মতো, সাঁতার কাটে হাঁসের মতো, আর ডাকে হাঁসের মতো, তাহলে সম্ভবত এটা হাঁসই”। ইংরেজিতে প্রবাদটি প্রায়ই উনিশ শতকের আমেরিকান কবি James Whitcomb Riley-র নামে চালানো হয়, যদিও আসল উৎস নিশ্চিতভাবে জানা নেই। বিশ শতকে এটি রাজনৈতিক বিতর্কে জনপ্রিয় হয় — কারও পরিচয় ঘোষণা দিয়ে নয়, আচরণ দিয়ে বিচার করার যুক্তি হিসেবে।

প্রোগ্রামিংয়ে ধারণাটির সবচেয়ে বিখ্যাত প্রয়োগ পাওয়া যায় Python সম্প্রদায়ে। ২০০০ সালের দিকে Python-এর আলোচনা-তালিকা (comp.lang.python)-য় Alex Martelli — পরে “Python in a Nutshell” আর “Python Cookbook” বইয়ের জন্য পরিচিত — একটি বার্তায় লেখেন, মোটামুটি এই অর্থে: কোনো কিছু হাঁস কি না পরীক্ষা করবেন না; পরীক্ষা করুন সেটি হাঁসের মতো ডাকে কি না, হাঁসের মতো হাঁটে কি না — অর্থাৎ ঠিক যে আচরণ আপনার দরকার, শুধু সেটুকু। তাঁর লেখা থেকেই “duck typing” কথাটি Python জগতে ছড়িয়ে পড়ে বলে ধরা হয়, পরে Ruby সম্প্রদায়েও (Dave Thomas-এর লেখায়) এটি খুব জনপ্রিয় হয়।

ধারণাটি অবশ্য Python-এর আবিষ্কার নয়। Smalltalk (১৯৭০-এর দশক) সম্পূর্ণ এভাবে চলত: একটি object-কে “বার্তা” পাঠানো হয়, আর সে যদি বার্তাটি বোঝে তো উত্তর দেয়, না বুঝলে “doesNotUnderstand” error. Python-এর ক্ষেত্রে নতুনত্ব হলো এটিকে একটি স্পষ্ট দর্শন হিসেবে প্রকাশ করা — built-in ফাংশন থেকে standard library পর্যন্ত সব জায়গায় “protocol” শব্দটি অনানুষ্ঠানিকভাবে ব্যবহার হতো: “iterator protocol”, “file-like object”, “sequence protocol”. এগুলো কোনো class ছিল না, ছিল কেবল নিয়ম।

এই দর্শনের পেছনের যুক্তি দুটি। প্রথমত, নমনীয়তা: print(file=...) যদি শুধু আসল ফাইল নিত, তাহলে log বা মেমরির বাফারে লেখা যেত না। দ্বিতীয়ত, কম পরিকল্পনা: আগে থেকে class-এর বংশতালিকা আঁকতে হয় না; যা দরকার তা-ই লেখেন। অসুবিধাও স্পষ্ট — চুক্তি অলিখিত, ভুল ধরা পড়ে দেরিতে। ঠিক এই অসুবিধার জবাবেই পরের দুই দশকে এসেছে ABC (২০০৭) আর Protocol (২০১৭–২০১৯)।

মূল উপলব্ধি: “Duck typing” একটি পুরোনো প্রবাদ থেকে নেওয়া নাম, যা Python সম্প্রদায় — বিশেষ করে Alex Martelli-র লেখা — প্রোগ্রামিং দর্শনে পরিণত করে: পরিচয় নয়, আচরণ দেখুন। এই পর্বের ABC আর Protocol সেই দর্শনকে বাতিল করে না, বরং লিখিত রূপ দেয়।

প্র ০২যে ভাষা গর্ব করে duck typing নিয়ে, সেখানে ২০০৭ সালে ABC কেন যোগ হলো? PEP 3119 কোন সমস্যার সমাধান করেছিল?

২০০০-এর দশকের মাঝামাঝি Python বড় প্রজেক্টে ব্যবহার হতে শুরু করে — Google, Zope-এর মতো ওয়েব ফ্রেমওয়ার্ক, বৈজ্ঞানিক লাইব্রেরি। তখন duck typing-এর একটি ব্যবহারিক সমস্যা স্পষ্ট হয়: “এটা কি sequence?” প্রশ্নের নির্ভরযোগ্য উত্তর দেওয়ার উপায় ছিল না। ধরুন একটি ফাংশন একটি mapping (dict-এর মতো কিছু) অথবা একটি sequence নিতে পারে, আর দুটির জন্য আলাদা কাজ করবে। __getitem__ দুজনেরই আছে, তাই “method আছে কি না” দেখে আলাদা করা যায় না। মানুষ hasattr(x, "keys")-এর মতো আন্দাজি কৌশল লিখত, যা ভঙ্গুর।

আগেও interface আনার চেষ্টা হয়েছিল। ২০০১ সালে PEP 245 Python-এ আলাদা interface syntax-এর প্রস্তাব দিয়েছিল, আর PEP 246 “adaptation”-এর — কিন্তু দুটিই গৃহীত হয়নি। Zope প্রজেক্ট নিজের zope.interface library বানিয়ে নিয়েছিল, Twisted-ও তা ব্যবহার করত। অর্থাৎ চাহিদা ছিল, কিন্তু Python-এর মূল ধারায় সমাধান ছিল না।

২০০৭ সালে Guido van Rossum আর Talin লেখেন PEP 3119 — “Introducing Abstract Base Classes”. এর মূল কৌশল চতুর: নতুন কোনো keyword বা syntax নয়, সাধারণ class আর একটি metaclass (ABCMeta) দিয়েই সব। আর মূল উদ্ভাবন ছিল isinstance আর issubclass-কে কাস্টমাইজ করার ক্ষমতা — register() আর __subclasshook__. এতে পুরোনো built-in type (list, dict) নিজেদের কোড না বদলেই নতুন ABC-র সদস্য হতে পারল। PEP-টি স্পষ্ট বলেছিল, ABC duck typing-এর বিকল্প নয়; এটি তাদের জন্য, যাদের আচরণ “পরীক্ষা” করা দরকার।

PEP 3119 এসেছে Python 2.6 আর 3.0-এ (২০০৮), সঙ্গে আসে collections-এর ABC-গুলো — Iterable, Sequence, Mapping ইত্যাদি। প্রায় একই সময়ে PEP 3141 সংখ্যার জন্য একটি ABC-শ্রেণিবিন্যাস আনে — numbers module (Number, Complex, Real, Rational, Integral). পরে Python 3.3-এ collection ABC-গুলো সরে যায় আলাদা collections.abc module-এ, আর পুরোনো জায়গার নামগুলো Python 3.10-এ পুরোপুরি মুছে ফেলা হয় — তাই পুরোনো টিউটোরিয়ালের from collections import Mapping এখন error দেয়।

লক্ষ করার মতো বিষয়, Python ABC-কে ঐচ্ছিক রেখেছে। Java-র মতো সবাইকে interface ঘোষণা করতে বাধ্য করা হয়নি। আজও Python-এর বেশিরভাগ কোড ABC ছাড়াই চলে; ABC ব্যবহার হয় ফ্রেমওয়ার্ক, লাইব্রেরি আর বড় class-পরিবারে।

মূল উপলব্ধি: ABC এসেছিল duck typing বাদ দিতে নয়, তার একটি নির্দিষ্ট ফাঁক পূরণ করতে — “এই object কোন ধরনের আচরণের প্রতিশ্রুতি দেয়”, এই প্রশ্নের নির্ভরযোগ্য উত্তর। সবচেয়ে বড় উদ্ভাবন ছিল register(): পুরোনো class না ছুঁয়েই তাকে নতুন চুক্তিতে যুক্ত করা।

প্র ০৩Structural typing আর nominal typing — দুটি দর্শনের পার্থক্য কী, আর PEP 544 কেন Python-এ Protocol আনল?

Type system-এর জগতে একটি পুরোনো প্রশ্ন: দুটি type কখন “সামঞ্জস্যপূর্ণ”? Nominal typing বলে — যখন একটি অন্যটির বংশধর হিসেবে ঘোষিত. Java, C#, C++-এর class এভাবে চলে। Structural typing বলে — যখন একটির আকার (method আর field) অন্যটির চাহিদা পূরণ করে, ঘোষণা যা-ই হোক। OCaml-এর object, TypeScript আর Go এভাবে চলে। Nominal-এর সুবিধা স্পষ্টতা আর ইচ্ছাকৃত সম্পর্ক; structural-এর সুবিধা নমনীয়তা আর কম ঘোষণা।

Python-এ static type checking-এর গল্প শুরু হয় mypy দিয়ে — Jukka Lehtosalo ২০১২ সালের দিকে কেমব্রিজে তাঁর গবেষণার অংশ হিসেবে এটি শুরু করেন। ২০১৪ সালে Guido van Rossum, Jukka Lehtosalo আর Łukasz Langa লেখেন PEP 484 (type hint), যা আসে Python 3.5-এ (২০১৫)। কিন্তু PEP 484-এর type system ছিল মূলত nominal. ফলে একটি অদ্ভুত পরিস্থিতি তৈরি হলো: আপনার class-এ __iter__ আছে, Python-এর লুপ তাকে দিব্যি iterable মানে — কিন্তু type checker-এর চোখে সেটি Iterable নয়, যতক্ষণ না inherit করছেন। অর্থাৎ type checker duck typing বুঝত না — Python-এর সবচেয়ে মৌলিক দর্শনের সাথে সংঘাত।

এর সমাধান PEP 544 — “Protocols: Structural subtyping (static duck typing)”, লেখক Ivan Levkivskyi, Jukka Lehtosalo আর Łukasz Langa. PEP-টি লেখা শুরু হয় ২০১৭ সালে, আর typing.Protocol আসে Python 3.8-এ (২০১৯); তার আগে typing_extensions প্যাকেজে পাওয়া যেত। শিরোনামের “static duck typing” কথাটিই সারকথা: duck typing-এর আচরণ, কিন্তু কোড চালানোর আগে যাচাই। PEP-টি Iterable, Sized, Iterator-এর মতো typing-এর মৌলিক type-গুলোকেও Protocol হিসেবে গণ্য করল, ফলে __iter__ থাকলেই type checker-এর কাছে সেটি iterable.

PEP 544 সচেতনভাবে runtime যাচাইকে দুর্বল রেখেছে। @runtime_checkable ঐচ্ছিক, আর শুধু method-এর উপস্থিতি দেখে। কারণ Python চালানোর সময় একটি method-এর signature পুরোপুরি যাচাই করা ব্যয়বহুল আর অনেক সময় অসম্ভব (type hint নাও থাকতে পারে, বা dynamic হতে পারে)। দায়িত্ব ভাগ করা হয়েছে: আকারের পূর্ণ যাচাই type checker-এর, চালানোর সময়ের যাচাই হালকা।

দুই দর্শনের কোনোটিই নিখুঁত নয়। Structural typing-এ “ভুলবশত মিল” সম্ভব — দুটি সম্পূর্ণ ভিন্ন অর্থের draw() method (ছবি আঁকা বনাম লটারির ড্র) একই Protocol মেনে ফেলতে পারে। Nominal typing-এ এমন হয় না, কারণ সম্পর্ক ইচ্ছা করে ঘোষণা করতে হয়। Python দুটিই রেখেছে — ABC nominal, Protocol structural — আর বেছে নেওয়ার ভার দিয়েছে আপনাকে।

মূল উপলব্ধি: Protocol এসেছে Python-এর type checker আর তার আসল দর্শনের মধ্যের সংঘাত মেটাতে। Nominal জিজ্ঞেস করে “কী ঘোষণা করা হয়েছে”, structural জিজ্ঞেস করে “কী করতে পারে” — Python-এ দুটিই আছে, কাজ অনুযায়ী।

প্র ০৪Go ভাষায় interface “নিঃশব্দে” পূরণ হয় — কোনো implements লাগে না। এই নকশা কেন বেছে নেওয়া হয়েছিল, আর Python-এর Protocol-এর সাথে এর মিল কোথায়?

Go তৈরি হয় Google-এ, Robert Griesemer, Rob Pike আর Ken Thompson-এর হাতে; ২০০৯ সালে প্রকাশ্যে ঘোষণা হয়, আর Go 1.0 আসে ২০১২ সালে। এর নকশাকারীরা Java আর C++-এর বড় কোডবেসে কাজ করার অভিজ্ঞতা থেকে একটি সমস্যা দেখেছিলেন: nominal interface-এ আগে থেকে সব সম্পর্ক পরিকল্পনা করতে হয়। কোনো class লেখার সময় যদি আপনি না জানেন যে ভবিষ্যতে কেউ একটি Reader interface বানাবে, তাহলে আপনার class কখনো সেই interface মানবে না — যদিও তার ঠিক সেই method-ই আছে। পরে মানাতে হলে মূল কোড বদলাতে হয়, যা হয়তো অন্য কোম্পানির।

Go-র সমাধান: কোনো type-এর method-গুলো যদি একটি interface-এর সব method পূরণ করে, তাহলে সে আপনাআপনি সেই interface মানে — কোনো ঘোষণা নেই। Go-র standard library-র বিখ্যাত উদাহরণ io.Reader আর io.Writer — একটি করে method. ফাইল, নেটওয়ার্ক connection, মেমরির বাফার, gzip compressor — সবাই এগুলো মানে, কেউ কারও কথা না জেনেই। এতে ছোট ছোট interface লেখার সংস্কৃতি তৈরি হয়েছে; Rob Pike-এর একটি জনপ্রিয় উক্তি আছে, মোটামুটি: “interface যত বড়, abstraction তত দুর্বল”।

আরেকটি গুরুত্বপূর্ণ Go-রীতি: interface সাধারণত লেখা হয় ব্যবহারকারীর দিকে, প্রদানকারীর দিকে নয়। অর্থাৎ OrderService-এর প্যাকেজ নিজেই ঘোষণা করে “আমার একটি Notifier লাগবে, যার Send আছে” — SMS library-র লেখককে এর কিছুই জানতে হয় না। এই পর্বের Python উদাহরণে ঠিক এটাই করেছি: Notifier Protocol লেখা হয়েছে OrderService-এর পাশে।

Python-এর Protocol-এর সাথে মিল প্রায় হুবহু: দুটিই structural, দুটিতেই inheritance লাগে না, দুটিই ছোট interface-কে উৎসাহিত করে। পার্থক্য যাচাইয়ে: Go-র compiler প্রতিবার build-এর সময় যাচাই করে, তাই ভুল আকারের type পাঠালে প্রোগ্রাম তৈরিই হবে না। Python-এ যাচাই ঐচ্ছিক — mypy বা pyright না চালালে কেউ কিছু বলবে না, আর ভুল ধরা পড়বে চালানোর সময়, duck typing-এর মতোই। আরেকটি পার্থক্য: Go-তে runtime-এ interface-এর type assertion-এ পুরো method set (signature সহ) মেলানো হয়; Python-এর @runtime_checkable শুধু নাম দেখে।

TypeScript-ও (২০১২, Microsoft) একই পথে গেছে — JavaScript-এর duck typing-এর ওপর structural type system. অর্থাৎ যে ভাষাগুলো আগে থেকে duck typing-এ অভ্যস্ত ছিল, তারা static typing যোগ করার সময় স্বাভাবিকভাবেই structural পথ বেছে নিয়েছে — Python-এর Protocol সেই প্রবণতারই অংশ।

মূল উপলব্ধি: Go দেখিয়েছে যে structural interface বড় সিস্টেমেও ভালো চলে, যদি interface ছোট রাখা হয় আর ব্যবহারকারীর দিকে লেখা হয়। Python-এর Protocol একই ধারণা, কিন্তু যাচাইয়ের দায়িত্ব compiler-এর বদলে ঐচ্ছিক type checker-এর।

প্র ০৫একটি class-এ সব method-এর নাম আর signature ঠিক থাকলেই কি সে চুক্তি মানে? Liskov substitution principle এখানে কী বলে?

না — আর এটাই interface-এর সবচেয়ে গভীর শিক্ষা। ABC বা Protocol যাচাই করে শুধু আকার: method আছে কি না, argument আর return type মেলে কি না। কিন্তু চুক্তির আসল অংশ হলো আচরণ. ধরুন একটি SmsNotifier-এর send কিছুই পাঠায় না, শুধু চুপচাপ ফেরত দেয়। অথবা একটি PaymentGateway-এর refund সবসময় True দেয়, অথচ টাকা ফেরত যায় না। mypy খুশি, ABC খুশি — কিন্তু সিস্টেম ভাঙা।

এই প্রশ্নের সবচেয়ে বিখ্যাত উত্তর দিয়েছিলেন MIT-এর কম্পিউটার বিজ্ঞানী Barbara Liskov. ১৯৮৭ সালে OOPSLA সম্মেলনে তাঁর “Data Abstraction and Hierarchy” বক্তৃতায় তিনি ধারণাটি তুলে ধরেন, আর ১৯৯৪ সালে Jeannette Wing-এর সাথে একটি গবেষণাপত্রে (“A Behavioral Notion of Subtyping”) একে আনুষ্ঠানিক রূপ দেন। মূল কথা: S যদি T-এর subtype হয়, তাহলে প্রোগ্রামে যেখানে T আছে সেখানে S বসালে প্রোগ্রামের সঠিকতা নষ্ট হওয়া চলবে না। একে বলে Liskov substitution principle. পরে Robert C. Martin এটিকে তাঁর “SOLID” নীতিমালার “L” হিসেবে জনপ্রিয় করেন। (Liskov ২০০৮ সালে Turing Award পান, মূলত data abstraction আর প্রোগ্রামিং ভাষা নিয়ে কাজের জন্য।)

এর কাছাকাছি একটি ধারণা দিয়েছিলেন Bertrand Meyer — তাঁর Eiffel ভাষায় (১৯৮০-এর দশক) “Design by Contract”: প্রতিটি method-এর precondition (ডাকার আগে কী সত্য হতে হবে), postcondition (শেষে কী নিশ্চিত হবে) আর invariant (সবসময় কী সত্য থাকবে)। Liskov-এর নীতি এই ভাষায় বলা যায়: subclass precondition কঠিন করতে পারবে না (base যা নিত, তা নিতেই হবে), আর postcondition দুর্বল করতে পারবে না (base যা নিশ্চিত করত, তা করতেই হবে)।

একটি উদাহরণ: PaymentGateway.charge(amount)-এর অলিখিত চুক্তি — “যেকোনো ধনাত্মক পূর্ণসংখ্যা নেবে, সফল হলে একটি অনন্য transaction id দেবে, ব্যর্থ হলে exception তুলবে”। এখন কেউ একটি WalletGateway লিখল, যা ৫০০ টাকার বেশি হলে চুপচাপ "" ফেরত দেয়। আকার পুরোপুরি ঠিক, কিন্তু precondition কঠিন হয়েছে (৫০০-র বেশি নেয় না) আর postcondition দুর্বল (ব্যর্থতায় exception নেই)। checkout-এর template method এই খালি id দিয়ে রসিদ বানিয়ে ফেলবে — ক্রেতা ভাববে টাকা কাটা হয়েছে। এই পর্বের FakeNotifier-ও এই প্রশ্নের মুখে পড়ে: fake আসল জিনিসের আচরণের যত কাছে, টেস্ট তত বিশ্বাসযোগ্য।

তাহলে আচরণের চুক্তি যাচাই হবে কীভাবে? তিনটি হাতিয়ার: স্পষ্ট docstring (চুক্তি লিখে রাখা), প্রতিটি implementation-এর ওপর একই টেস্ট-সেট চালানো (একে কখনো “contract test” বলা হয় — পর্ব ৩৯-এ এর ঝলক পাবেন), আর assert দিয়ে precondition-postcondition পরীক্ষা।

মূল উপলব্ধি: ABC আর Protocol চুক্তির কেবল “ব্যাকরণ” যাচাই করে; চুক্তির “অর্থ” — কী নিলে কী দেবে, কখন ব্যর্থ হবে — রক্ষা করা আপনার দায়িত্ব। Liskov-এর নীতি সেই অর্থের মাপকাঠি: বদলি বসালে কিছু যেন না ভাঙে।

অনুশীলন

বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — অনুমান করা, পড়া আর মিলিয়ে নেওয়াই আসল কাজ।

  1. কোনটির object তৈরি হবে? class Shape(ABC)-এ দুটি abstract method: area আর perimeter. class Square(Shape) শুধু area লিখেছে। class Box(Square) লিখেছে perimeter. আর class Plain: (কোনো base নেই) দুটিই @abstractmethod দিয়ে ঘোষণা করেছে, কিছুই লেখেনি। Shape(), Square(), Box(), Plain() — কোনটি চলবে, কোনটি TypeError?

    Shape() — TypeError, দুটি method-ই বাকি। Square() — TypeError, perimeter বাকি। Box() — চলবে: area পেয়েছে Square থেকে, perimeter নিজে লিখেছে; inheritance-এর যেকোনো স্তর থেকে পূরণ হলেই চলে। Plain() — চলবে, কোনো error নেই! কারণ Plain ABC থেকে inherit করেনি, তাই @abstractmethod চিহ্নটি কেউ পড়ে না। এই শেষটিই AI-এর লেখা কোডে সবচেয়ে বেশি দেখা ভুল।

  2. MutableMapping-এর হিসাব: একটি ছোট key-value store বানাতে চান, যা ভেতরে ডেটা রাখে একটি dict-এ, কিন্তু প্রতিটি চাবিকে ছোট হাতের অক্ষরে বদলে রাখে (যাতে "Dhaka" আর "dhaka" একই চাবি হয়)। collections.abc.MutableMapping থেকে inherit করলে ন্যূনতম কোন কোন method লিখতে হবে, আর বিনামূল্যে কী কী পাবেন? get() আর update() কি চাবি ছোট হাতের করবে?

    লিখতে হবে পাঁচটি: __getitem__, __setitem__, __delitem__, __iter__, __len__. বিনামূল্যে পাবেন __contains__, keys, items, values, get, __eq__, __ne__, pop, popitem, clear, update, setdefault. আর হ্যাঁ — get আর update চাবি ছোট হাতের করবে, যদি আপনি __getitem__ আর __setitem__-এর ভেতরে key.lower() করেন। কারণ mixin method-গুলো ভেতরে ভেতরে আপনার লেখা মৌলিক method-ই ডাকে। এটি সরাসরি dict থেকে inherit করার চেয়ে নিরাপদ: dict-এর update আর get C-তে লেখা, তারা আপনার override করা __setitem__ ডাকে না।

  3. কোনটি বেছে নেবেন? প্রতিটি পরিস্থিতিতে duck typing, ABC না Protocol — এক লাইনে কারণসহ: (ক) একটি ১০০ লাইনের স্ক্রিপ্ট, যা যেকোনো iterable থেকে সংখ্যা যোগ করে; (খ) একটি লাইব্রেরি, যা বাইরের যেকোনো logger গ্রহণ করবে, যার info() আর error() আছে; (গ) একটি রিপোর্ট-জেনারেটর পরিবার (PDF, Excel, HTML), যাদের হেডার-ফুটার তৈরির কোড একই, শুধু মূল অংশ আলাদা।

    (ক) Duck typing — for লুপ এমনিতেই যেকোনো iterable নেয়; ছোট স্ক্রিপ্টে চুক্তি লেখার খরচ লাভের চেয়ে বেশি। (খ) Protocol — বাইরের logger (যেমন standard library-র logging.Logger) আপনার কোনো class থেকে inherit করবে না; structural চুক্তি তাদের বিনা পরিবর্তনে গ্রহণ করে, আর type checker ভুল ধরে। (গ) ABC — হেডার-ফুটারের ভাগ করা কোড template method হিসেবে base class-এ থাকবে, মূল অংশ @abstractmethod; নতুন ফরম্যাট যোগ করলে কেউ মূল অংশ লিখতে ভুলে গেলে object তৈরির সময়েই ধরা পড়বে।

  4. একটি Protocol লিখুন (এবং AI-কে যাচাই করুন): HasArea Protocol (area() -> float) আর একটি ফাংশন total_area(shapes) লিখুন, যা যেকোনো আকৃতির তালিকার মোট ক্ষেত্রফল দেয়। Rect আর Circle দুটি class বানান — কোনোটিই কিছু থেকে inherit করবে না। তারপর একই কাজ কোনো AI-কে করতে বলুন, আর দেখুন সে isinstance বা অকারণ ABC ব্যবহার করে কি না।

    একটি সম্ভাব্য সমাধান:

    import math
    from collections.abc import Iterable
    from typing import Protocol
    
    class HasArea(Protocol):
        def area(self) -> float: ...
    
    class Rect:
        def __init__(self, w: float, h: float) -> None:
            self.w, self.h = w, h
        def area(self) -> float:
            return self.w * self.h
    
    class Circle:
        def __init__(self, r: float) -> None:
            self.r = r
        def area(self) -> float:
            return math.pi * self.r ** 2
    
    def total_area(shapes: Iterable[HasArea]) -> float:
        return sum(s.area() for s in shapes)
    
    print(round(total_area([Rect(2, 3), Circle(1)]), 2))   # 9.14

    লক্ষ করুন, hint-এ list[HasArea] না লিখে Iterable[HasArea] লেখা হয়েছে — তাতে tuple, generator সবই চলে, আর type checker-এর একটি সূক্ষ্ম নিয়মের (list-এর invariance, পর্ব ৩৭) ঝামেলাও এড়ানো যায়। AI যদি if isinstance(s, Rect): ... elif isinstance(s, Circle): ... লেখে, তাহলে নতুন আকৃতি যোগ করলেই total_area বদলাতে হবে — polymorphism-এর পুরো লাভ হারিয়ে যায়।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমPython-এ একটি abstract base class দিয়ে চুক্তি লিখে ব্যবহার করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📥 abc module থেকে ABC আর abstractmethod import করা
  2. 📜 ABC থেকে inherit করে base class লেখা
  3. 🏷️ যে method-গুলো subclass-কে লিখতেই হবে, সেগুলোতে @abstractmethod বসানো
  4. 🧱 subclass-এ সব abstract method লেখা
  5. ✅ subclass-এর object বানিয়ে ব্যবহার করা

Python-এ abc.ABC থেকে inherit করা একটি class-এ দুটি @abstractmethod আছে। একটি subclass শুধু একটি লিখেছে। সেই subclass-এর object বানাতে গেলে কী হয়?

  • object তৈরি হয়, বাকি method ডাকলে AttributeError আসে
  • class লেখার লাইনেই SyntaxError আসে
  • TypeError: Can't instantiate abstract class ... আসে
  • NotImplementedError আসে

@runtime_checkable দেওয়া একটি typing.Protocol-এর বিপরীতে isinstance(obj, P) চালানোর সময় Python ঠিক কী যাচাই করে?

  • প্রতিটি method-এর argument আর return type
  • শুধু Protocol-এর method-গুলোর নাম obj-এ আছে কি না
  • obj-এর class P থেকে inherit করেছে কি না
  • প্রতিটি method একবার ডেকে দেখে error আসে কি না

collections.abc.Sequence থেকে inherit করে একটি concrete class বানাতে ন্যূনতম কোন দুটি method লিখতে হয়?

  • __getitem__ আর __len__
  • __iter__ আর __contains__
  • index আর count
  • __getitem__ আর __setitem__

Type system-এ “structural typing” বলতে কী বোঝায়?

  • দুটি type সামঞ্জস্যপূর্ণ হয় যখন একটি অন্যটি থেকে inherit করার ঘোষণা দেয়
  • শুধু list আর dict-এর মতো ডেটা কাঠামোর type যাচাই
  • type যাচাই শুধু প্রোগ্রাম চালানোর সময় হয়
  • দুটি type সামঞ্জস্যপূর্ণ হয় যখন একটির method ও attribute অন্যটির চাহিদা পূরণ করে, inheritance ছাড়াই

একটি ABC-তে MyABC.register(OtherClass) লিখলে কী হয়?

  • Python যাচাই করে OtherClass-এ সব abstract method আছে কি না
  • isinstance(OtherClass(), MyABC) True হয়, কিন্তু কোনো method যাচাই হয় না
  • OtherClass, MyABC-র সব সাধারণ method উত্তরাধিকারসূত্রে পায়
  • OtherClass-এর object বানানো বন্ধ হয়ে যায়

Python-এ কোনো object-এর ওপর লুপ চালানো যাবে কি না জানার সবচেয়ে নির্ভরযোগ্য উপায় কোনটি?

  • isinstance(x, list) পরীক্ষা করা
  • isinstance(x, collections.abc.Iterable) পরীক্ষা করা
  • iter(x) ডেকে দেখা, TypeError এলে iterable নয়
  • hasattr(x, "__len__") পরীক্ষা করা

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Interface = চুক্তি: কোন method থাকবে, কী নেবে, কী দেবে। কোড নির্ভর করুক চুক্তির ওপর, নির্দিষ্ট class-এর ওপর নয়। Python-এ তিন উপায়: duck typing, ABC, Protocol.
  2. Duck typing সবচেয়ে হালকা — শুধু ডেকে দেখুন, সমস্যা হলে exception ধরুন (EAFP). isinstance(x, list) দিয়ে class পরীক্ষা duck typing ভাঙে।
  3. ABC = class X(ABC) + @abstractmethod; অপূর্ণ subclass-এর object বানালে TypeError. ABC ছাড়া @abstractmethod অকেজো। ABC-তে সাধারণ method রেখে template method; register() যাচাইহীন ঘোষণা।
  4. collections.abc: Sequence-এ __getitem__ + __len__ লিখলে in, লুপ, reversed, index, count বিনামূল্যে। iterable কি না জানতে সবচেয়ে নির্ভরযোগ্য iter(x).
  5. Protocol structural — inheritance ছাড়াই আকার মিললে সদস্য, যাচাই করে mypy/pyright. @runtime_checkable শুধু নাম দেখে। চুক্তি constructor-এ নিলে (dependency injection) অংশ বদলানো আর fake দিয়ে টেস্ট সহজ।

পরের পর্বে: এই পর্বে বারবার দেখা দিয়েছে __iter__ আর “iterable”. এবার তার ভেতরে ঢুকব — for লুপ আসলে কীভাবে চলে, iterator protocol কী, আর yield দিয়ে কীভাবে লাখো উপাদানের ক্রম মেমরি না ভরিয়ে তৈরি করা যায়: iterator, generator আর yield.

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান