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

inheritance বনাম composition — কখন উত্তরাধিকার, কখন অংশ দিয়ে গড়া

Inheritance vs Composition: super(), Polymorphism, MRO, Mixins & Delegation

Taught in Bangla, this lesson contrasts is-a and has-a relationships in Python: subclassing and overriding, extending __init__ with super(), polymorphism and duck typing, multiple inheritance, the diamond problem and MRO, the Liskov principle, and refactoring deep class trees into composition and delegation.

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

  • is-a আর has-a সম্পর্ক চেনা, subclass বানানো, attribute ও method উত্তরাধিকারে পাওয়া আর override করা
  • super() দিয়ে বাবার method ও __init__ ডাকা, আর ParentName.method(self) কেন দুর্বল পথ
  • Polymorphism আর duck typing; isinstance/issubclass কখন ঠিক, কখন “গন্ধ”
  • Multiple inheritance, diamond, C3 linearization, cooperative super() আর mixin
  • Liskov নীতি, built-in (dict/list) থেকে inherit করার ফাঁদ, আর composition, delegation ও অদলবদলযোগ্য অংশ দিয়ে নকশা

১ · দুটি সম্পর্ক — “এটি একটি …” আর “এর একটি … আছে”

পর্ব ২৯-এ আপনি class বানিয়েছেন — একটি ছাঁচ, যা থেকে অনেক instance তৈরি হয়, প্রত্যেকের নিজের attribute আর সবার জন্য একই method. পর্ব ৩০-এ দেখেছেন __repr__, __eq__-এর মতো dunder method দিয়ে নিজের class-কে Python-এর ভাষার সাথে মিশিয়ে দেওয়া। কিন্তু বাস্তব প্রোগ্রামে class একা থাকে না। একটি ব্যাংক সফটওয়্যারে থাকে সাধারণ Account, সঞ্চয়ী Account, চলতি Account; একটি অনলাইন দোকানে থাকে Order, Customer, পণ্য, ডেলিভারি, পেমেন্ট। প্রশ্ন হলো: এই class-গুলো একে অপরের সাথে কীভাবে যুক্ত হবে?

উত্তর দুই রকম, আর পুরো পর্ব এই দুইয়ের পার্থক্য নিয়ে:

is-a → inheritance

“সঞ্চয়ী হিসাব একটি ব্যাংক হিসাব।” যা কিছু একটি Account পারে — টাকা জমা, তোলা, ব্যালান্স দেখা — সঞ্চয়ী হিসাবও পারে, সঙ্গে সুদ যোগ করতে পারে। তাই SavingsAccount তৈরি হয় Account থেকে, তার সব আচরণ উত্তরাধিকারে পেয়ে। দেখুন: is-a সম্পর্ক.

has-a → composition

“একটি গাড়ির একটি ইঞ্জিন আছে।” গাড়ি ইঞ্জিন নয় — গাড়ি ইঞ্জিন, চাকা, ব্রেক জোড়া দিয়ে তৈরি। তাই Car একটি Engine object-কে নিজের attribute হিসেবে রাখে, আর দরকার হলে তাকে কাজ করতে বলে। দেখুন: has-a সম্পর্ক.

ঢাকার একটি পোশাক কারখানার কথা ভাবুন। “সিনিয়র অপারেটর” একজন সেলাই অপারেটর — সাধারণ অপারেটরের সব কাজ তিনি পারেন, সঙ্গে নতুনদের শেখান। এটি is-a. আবার কারখানার একটি “প্রোডাকশন লাইন”-এ আছে কাটিং টেবিল, সেলাই মেশিন, আয়রন, প্যাকিং — লাইন নিজে কোনো মেশিন নয়, মেশিনগুলো জোড়া দিয়ে বানানো। একটি সেলাই মেশিন নষ্ট হলে খুলে আরেকটি বসিয়ে দেন, লাইন বন্ধ হয় না। এটি has-a. প্রথমটিতে পরিচয় ভাগ হয়, দ্বিতীয়টিতে অংশ জোড়া লাগে।

শুরুতেই পর্বের মূল উপদেশটি বলে রাখি, যাতে পড়ার সময় মাথায় থাকে: is-a যদি সত্যিই, সব পরিস্থিতিতে সত্য হয়, তবেই inheritance. বাকি সব ক্ষেত্রে composition. আর Python-এ অনেক সময় দুটোর কোনোটিই লাগে না — পর্ব ০৬-এ দেখা duck typing-এর কারণে একই নামের method থাকলেই বিভিন্ন object একই জায়গায় কাজ করে, কোনো সম্পর্ক ছাড়াই। এই তিনটি পথ একে একে দেখব।

মূল ধারণা

Inheritance-এর আসল কাজ কোড বাঁচানো নয়, একটি প্রতিশ্রুতি দেওয়া: “যেখানে বাবাকে ব্যবহার করা যায়, সেখানে আমাকেও করা যাবে।” শুধু কয়েকটি method ধার করতে inheritance ব্যবহার করলে প্রতিশ্রুতিটি মিথ্যা হয়ে যায় — আর মিথ্যা প্রতিশ্রুতি পরে বাগ হয়ে ফেরত আসে। কোড ভাগ করার জন্য composition-ই সাধারণত ভালো হাতিয়ার।

২ · Subclass বানানো — উত্তরাধিকারে পাওয়া আর override করা

Python-এ inheritance-এর ব্যাকরণ এক লাইনের: class-এর নামের পরে বন্ধনীতে বাবার নাম। যে class থেকে inherit করা হয় তাকে বলে base class (parent, superclass), আর নতুনটিকে subclass (child, derived class).

class Account:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        if amount > self.balance:
            raise ValueError("ব্যালান্স যথেষ্ট নয়")
        self.balance -= amount

class SavingsAccount(Account):      # SavingsAccount একটি Account
    def add_interest(self, rate=0.05):
        self.deposit(round(self.balance * rate))

SavingsAccount-এর ভেতরে মাত্র একটি method লেখা আছে, তবু savings = SavingsAccount("রিনা", 4000) লিখলে __init__ চলে, savings.deposit(500) চলে, savings.withdraw(100) চলে — সব এসেছে Account থেকে। এটাই উত্তরাধিকার। আর add_interest-এর ভেতরে self.deposit(...) লক্ষ করুন: subclass নিজের method থেকে বাবার method স্বাভাবিকভাবেই ডাকতে পারে, কারণ self তো একটি Account-ও।

একটি কথা এখনই জেনে রাখুন: Python 3-এ প্রতিটি class শেষ পর্যন্ত object থেকে আসে। class Account: লেখা আর class Account(object): লেখা একই। __repr__, __eq__-এর মতো যে dunder method আপনি না লিখলেও পাওয়া যায় (পর্ব ৩০), সেগুলোর ডিফল্ট রূপ আসে এই object থেকেই।

“উত্তরাধিকার” আসলে কী — কপি নয়, খোঁজা

অনেকে ভাবেন subclass তৈরির সময় বাবার method-গুলো তার ভেতরে কপি হয়। তা নয়। SavingsAccount-এর নিজের খাতায় (SavingsAccount.__dict__) শুধু add_interest আছে। savings.withdraw লিখলে Python একটি খোঁজা চালায় — প্রথমে instance-এর নিজের খাতায়, তারপর তার class-এ, তারপর বাবার class-এ, এভাবে উপরে — যেখানে প্রথম পায়, সেটাই ব্যবহার করে। পর্ব ২০-এর LEGB-র মতোই, শুধু এবার নামের বদলে attribute. এই খোঁজার ক্রমকে বলে method resolution order বা MRO. “পরে” চেপে savings.withdraw(6000)-এর খোঁজা দেখুন (ডায়াগ্রামে অংশ ৩-এর runnable প্রোগ্রামের পূর্ণ class-গুলো ধরা হয়েছে, যেখানে Account-এ একটি limit method আর SavingsAccount-এ rate আছে):

  1. instancesavingsনিজের খাতা
  2. SavingsAccountclassনিজের class
  3. Accountbaseবাবা
  4. objectrootসবার আদি
savings.__dict__ধাপ ১: instance-এর নিজের খাতা
{'owner': 'রিনা', 'balance': 4200, 'rate': 0.05}

'withdraw' এখানে আছে?  না  → পরের জায়গায় চলো
SavingsAccount.__dict__ধাপ ২: নিজের class
__init__       ← SavingsAccount নিজে লিখেছে
add_interest   ← SavingsAccount নিজে লিখেছে

'withdraw' এখানে আছে?  না  → MRO-র পরের class
Account.__dict__ধাপ ৩: বাবা — পাওয়া গেল
__init__
limit
withdraw   ← পাওয়া গেল! খোঁজা এখানেই শেষ

savings.withdraw  →  bound method (self = savings)
ডাক:  Account.withdraw(savings, 6000)
withdraw-এর ভেতরে: self.limit()ধাপ ৪: নতুন খোঁজা — আবার নিচ থেকে
self = savings  →  খোঁজা শুরু savings থেকে, Account থেকে নয়!
  savings.__dict__        limit? না
  SavingsAccount          limit? না
  Account                 limit? হ্যাঁ → balance (4200)

self = current (CurrentAccount) হলে:
  current.__dict__        limit? না
  CurrentAccount          limit? হ্যাঁ → balance + overdraft
  Account.limit পর্যন্ত যাওয়াই হয় না
objectধাপ ৫: শেষ ঠিকানা
withdraw আগেই পাওয়া গেছে, তাই object পর্যন্ত আসতে হয়নি।

কোথাও না পেলে (যেমন savings.fly()):
AttributeError: 'SavingsAccount' object has no attribute 'fly'

SavingsAccount.__mro__ =
(SavingsAccount, Account, object)
  1. Instance-এর খাতা। প্রথমে Python দেখে savings object-এর নিজের __dict__-এ — যেখানে __init__-এ বসানো attribute থাকে। সেখানে owner, balance, rate আছে, কিন্তু withdraw নেই। Method সাধারণত instance-এ থাকে না, থাকে class-এ।
  2. নিজের class. এরপর SavingsAccount-এর খাতা। সেখানে আছে শুধু যা এই class নিজে লিখেছে — __init__ আর add_interest. withdraw নেই, তাই খোঁজা যায় MRO-র পরের class-এ।
  3. বাবা — পাওয়া গেল। Account-এর খাতায় withdraw আছে। খোঁজা থামল। Python savings-কে self হিসেবে বেঁধে একটি bound method বানায় (পর্ব ২৯), আর ডাকটি আসলে হয় Account.withdraw(savings, 6000).
  4. ভেতরের ডাক — আবার নিচ থেকে। withdraw-এর ভেতরে self.limit() লেখা থাকলে নতুন খোঁজা শুরু হয় self-এর আসল class থেকে, Account থেকে নয়। তাই self যদি একটি CurrentAccount হয়, তার নিজের limit আগে পাওয়া যায়। বাবার লেখা কোডও সন্তানের override করা method ডাকে — এটাই পরের অংশের polymorphism-এর বীজ।
  5. শেষ ঠিকানা object. সারির শেষে সবসময় object. সেখানেও না পেলে AttributeError. পুরো সারিটি দেখতে চাইলে SavingsAccount.__mro__ লিখুন — একটি tuple, যার ক্রমেই Python খোঁজে।

Override — বাবার method বদলে দেওয়া

Subclass যদি বাবার একই নামের একটি method নিজে লেখে, খোঁজার সময় সেটি আগে পাওয়া যায় — বাবারটি ঢাকা পড়ে। একে বলে override. চলতি হিসাবের (current account) কথা ভাবুন: সেখানে ব্যাংক একটি overdraft সুবিধা দেয় — ব্যালান্সের চেয়ে কিছু বেশি তোলা যায়। তাই “কত পর্যন্ত তোলা যাবে” নিয়মটি আলাদা। পুরো withdraw আবার না লিখে শুধু সীমার নিয়মটি override করাই পরিষ্কার নকশা: বাবা withdraw-এ self.limit() ডাকে, আর CurrentAccount নিজের limit দেয়। বাবা ঠিক করে দিল কাজের কাঠামো, সন্তান বদলাল একটি ধাপ।

নামের বানান ভুল = নিঃশব্দ ব্যর্থতা: override করতে গিয়ে def limt(self) লিখলে কোনো error আসে না — শুধু একটি নতুন method তৈরি হয়, আর বাবার limit-ই চলতে থাকে। Python 3.12 থেকে typing.override decorator আছে: method-এর উপরে @override লিখলে mypy-এর মতো type checker (পর্ব ৩৭) জানিয়ে দেয় বাবার মধ্যে সেই নামের method আসলে নেই। Python নিজে চালানোর সময় এটি যাচাই করে না।

৩ · super() — বাবাকে ডাকা, আর __init__ বাড়ানো

Override মানে সবসময় পুরোপুরি বদলে দেওয়া নয়। প্রায়ই আপনি চান বাবার কাজটুকু হোক, তারপর নিজের অংশ যোগ হোক। সবচেয়ে সাধারণ উদাহরণ __init__. SavingsAccount-এর একটি সুদের হার দরকার, তাই তার নিজের __init__ লাগবে — কিন্তু owner আর balance বসানোর কাজ তো Account.__init__ আগেই করে। সেটি আবার লেখা মানে কোড নকল, আর বাবার নিয়ম বদলালে দুই জায়গায় বদলাতে হবে।

সমাধান super(): “MRO-তে আমার পরের class-এর এই method-টি ডাকো, আর self হিসেবে এই object-টিই দাও”.

class SavingsAccount(Account):
    def __init__(self, owner, balance=0, rate=0.05):
        super().__init__(owner, balance)    # বাবা বসাবে owner, balance
        self.rate = rate                    # নিজের অংশ

এভাবে __init__-কে “বাড়ানো” (extend) হলো: বাবার কাজ আগে, নিজের কাজ পরে। এবার ধাপে ধাপে দেখুন একটি CurrentAccount object কীভাবে দুই তলায় তৈরি হয়:

1class Account:2    def __init__(self, owner, balance=0):3        self.owner = owner4        self.balance = balance56class CurrentAccount(Account):7    def __init__(self, owner, balance=0, overdraft=5000):8        super().__init__(owner, balance)9        self.overdraft = overdraft1011current = CurrentAccount("করিম", 2000)12print(current.owner, current.balance, current.overdraft)
Stack আর তৈরি হতে থাকা object
stack
self.__dict__
current
  1. খালি object তৈরি। CurrentAccount("করিম", 2000) লিখলে Python প্রথমে __new__ দিয়ে একটি খালি object বানায় — কোনো attribute নেই। তারপর সেটিকে self করে __init__ খোঁজে। MRO-তে প্রথমেই CurrentAccount-এর নিজের __init__ পাওয়া যায়।
  2. সন্তানের __init__ শুরু। owner="করিম", balance=2000, overdraft=5000 (default). Object এখনো খালি। লক্ষ করুন: বাবার __init__ নিজে থেকে চলবে না — Python শুধু MRO-তে প্রথম পাওয়া একটি __init__ চালায়।
  3. super().__init__ — বাবার তলায়। লাইন ৮ বাবার __init__ ডাকে, একই self দিয়ে। নতুন frame তৈরি হলো, কিন্তু object একটিই — দুই __init__ একই object-এ কাজ করছে, দুই তলার মিস্ত্রির মতো।
  4. বাবা নিজের অংশ বসাল। self.owner আর self.balance বসল। বাবার frame শেষ, নিয়ন্ত্রণ ফিরল সন্তানের লাইন ৯-এ।
  5. সন্তান নিজের অংশ বসাল। self.overdraft = 5000. এখন object-এ তিনটি attribute — দুটি বাবার, একটি সন্তানের। এই ক্রমটিই স্বাভাবিক: আগে ভিত, তারপর উপরের তলা।
  6. সম্পূর্ণ object. current লেবেলটি লাগল তৈরি হওয়া object-এ, আর ছাপা হলো করিম 2000 5000. এখন ভাবুন লাইন ৮ মুছে দিলে কী হতো: object-এ শুধু overdraft থাকত, আর current.balance লিখলেই AttributeError: 'CurrentAccount' object has no attribute 'balance'. Error আসে ব্যবহারের জায়গায়, ভুলের জায়গায় নয় — তাই এই বাগ খুঁজতে সময় লাগে।

কেন Account.__init__(self, ...) নয়?

পুরোনো কোডে প্রায়ই দেখবেন Account.__init__(self, owner, balance) — বাবার নাম ধরে সরাসরি ডাকা। এক বাবা থাকলে এটি কাজ করে, কিন্তু তিনটি দুর্বলতা আছে। এক, বাবার নাম কোডের ভেতরে গেঁথে গেল: কাল যদি Account আর CurrentAccount-এর মাঝে একটি BankAccount class বসান, প্রতিটি সরাসরি ডাক খুঁজে খুঁজে বদলাতে হবে, আর একটি ভুলে গেলে মাঝের class-এর __init__ নিঃশব্দে বাদ পড়বে। দুই, self হাতে দিতে হয় — ভুলে গেলে অদ্ভুত error. তিন, আর সবচেয়ে গুরুত্বপূর্ণ: একাধিক বাবা থাকলে (অংশ ৫) সরাসরি ডাক একই base class-কে দুবার চালিয়ে দিতে পারে। super() MRO মেনে চলে, তাই প্রতিটি class ঠিক একবার আসে।

Python 2-তে super লিখতে হতো এভাবে: super(CurrentAccount, self).__init__(...). Python 3.0 থেকে বন্ধনী খালি রাখা যায় — compiler নিজে বুঝে নেয় কোন class-এর ভেতরে আছেন আর self কে।

যেকোনো method-এ super() — বাবার কাজ রেখে নিজেরটা যোগ

super() শুধু __init__-এর জন্য নয়। ধরুন সঞ্চয়ী হিসাবে মাসে একটি নির্দিষ্ট সংখ্যার বেশি টাকা তোলা যায় না। withdraw override করে প্রথমে সেই গণনা যাচাই করুন, তারপর return super().withdraw(amount) — ব্যালান্স যাচাই আর কাটার আসল কাজ বাবার কোডই করবে। নিয়মটি মনে রাখুন: override করলে নিজেকে জিজ্ঞেস করুন, বাবার কাজটি কি বাদ দিতে চাই, নাকি তার আগে-পরে কিছু যোগ করতে চাই? দ্বিতীয়টি হলে super().

এবার প্রথম runnable প্রোগ্রাম। এখানে Account-এর withdraw একটি কাঠামো, যা self.limit() ডাকে; CurrentAccount শুধু limit override করেছে। চালানোর আগে অনুমান করুন — রিনার সঞ্চয়ী হিসাবে সুদের পরে কত হবে, আর দুজনের ৬০০০ টাকা তোলার চেষ্টার ফল কী:

class Account:
    def __init__(self, owner, balance=0):
        self.owner, self.balance = owner, balance

    def limit(self):
        return self.balance

    def withdraw(self, amount):
        if amount > self.limit():
            return f"{self.owner}: ব্যর্থ, সীমা {self.limit()}"
        self.balance -= amount
        return f"{self.owner}: বাকি {self.balance}"

class SavingsAccount(Account):
    def __init__(self, owner, balance=0, rate=0.05):
        super().__init__(owner, balance)
        self.rate = rate

    def add_interest(self):
        self.balance += round(self.balance * self.rate)

class CurrentAccount(Account):
    def __init__(self, owner, balance=0, overdraft=5000):
        super().__init__(owner, balance)
        self.overdraft = overdraft

    def limit(self):                    # override
        return self.balance + self.overdraft

savings, current = SavingsAccount("রিনা", 4000), CurrentAccount("করিম", 2000)
savings.add_interest()
for acc in [savings, current]:
    print(acc.withdraw(6000))
print(" → ".join(k.__name__ for k in SavingsAccount.__mro__))
print(isinstance(current, Account), issubclass(bool, int))

আউটপুট চার লাইনের। প্রথম লাইন রিনা: ব্যর্থ, সীমা 4200 — ৪০০০-এর ৫% সুদ ২০০ যোগ হয়ে ব্যালান্স ৪২০০; withdraw এসেছে Account থেকে, আর self.limit()-ও Account-এরটি, তাই সীমা শুধু ব্যালান্স। দ্বিতীয় লাইন করিম: বাকি -4000 — একই withdraw কোড, কিন্তু self একটি CurrentAccount, তাই self.limit() খুঁজে পেল override করা রূপ: ২০০০ + ৫০০০ = ৭০০০ পর্যন্ত তোলা যায়। লুপের ভেতরে acc.withdraw(6000) একটিই লাইন, অথচ দুই object-এ দুই রকম আচরণ। তৃতীয় লাইন SavingsAccount → Account → object — __mro__ থেকে খোঁজার সারি। শেষ লাইন True True — current একটি Account-ও (is-a), আর মজার তথ্য: Python-এর bool আসলে int-এর subclass, তাই True + True হয় 2.

৪ · Polymorphism — একই ডাক, ভিন্ন আচরণ; আর duck typing

উপরের লুপটিতে যা ঘটল তার একটি নাম আছে: polymorphism (গ্রিক শব্দ, “বহু রূপ”)। একই কোড — acc.withdraw(6000) — বিভিন্ন type-এর object-এ চলছে, আর প্রতিটি object নিজের মতো সাড়া দিচ্ছে। কোন method চলবে তা ঠিক হয় চালানোর সময়, object-টির আসল class দেখে। একে বলে dynamic dispatch.

একটি বাংলাদেশি উদাহরণে দেখা যাক। একটি অনলাইন দোকানে ক্রেতা bKash, Nagad বা কার্ডে টাকা দিতে পারেন, আর প্রতিটি পদ্ধতির চার্জ আলাদা (এখানের হারগুলো শুধু উদাহরণ, আসল হার নয়)। Polymorphism ছাড়া লেখা কোড দেখতে এমন হয়:

def fee(payment, amount):
    if isinstance(payment, Bkash):
        return round(amount * 0.015)
    elif isinstance(payment, Nagad):
        return round(amount * 0.012)
    elif isinstance(payment, Card):
        return 0
    raise TypeError("অচেনা পেমেন্ট")

কাজ চলে, কিন্তু নতুন একটি পদ্ধতি (ধরুন Rocket) যোগ করলে এই ফাংশন খুলে নতুন elif লিখতে হবে — আর একই ধরনের if-শৃঙ্খল যদি দশ জায়গায় থাকে (চার্জ, রসিদের লেখা, রিফান্ডের নিয়ম), দশ জায়গাতেই। একটি ভুলে গেলে বাগ। Polymorphism দিয়ে দায়িত্বটি উল্টে দিন: প্রতিটি পেমেন্ট নিজের চার্জ নিজে জানে.

class Payment:
    def fee(self, amount):
        raise NotImplementedError("প্রতিটি পেমেন্টকে fee() লিখতে হবে")

class Bkash(Payment):
    def fee(self, amount):
        return round(amount * 0.015)

class Card(Payment):
    def fee(self, amount):
        return 0

for p in [Bkash(), Card()]:
    print(type(p).__name__, p.fee(1000))     # Bkash 15, Card 0

এখন Rocket যোগ করতে শুধু একটি নতুন class — কোনো পুরোনো কোড ছোঁয়া লাগে না। Design principle-এর ভাষায় একে বলে Open/Closed নীতি: নতুন কিছু যোগের জন্য খোলা, পুরোনো কোড বদলানোর জন্য বন্ধ।

raise NotImplementedError — “এটি সন্তানদের কাজ”

Payment.fee-এর ভেতরে কোনো হিসাব নেই — আছে শুধু NotImplementedError. এর মানে: “Payment নিজে কোনো আসল পেমেন্ট নয়; প্রতিটি subclass-কে fee() লিখতে হবে”. কেউ ভুলে গেলে error আসবে — কিন্তু ডাকার সময়, object তৈরির সময় নয়। আরও কড়া ব্যবস্থা আছে: abc module-এর abstract base class (ABC) আর @abstractmethod, যা নিয়ম না মানা subclass-এর object তৈরিই করতে দেয় না। সেটির পুরো গল্প পর্ব ৩৩-এ। (একটি ছোট সতর্কতা: NotImplementedError একটি exception; এর সাথে প্রায় একই নামের NotImplemented আলাদা জিনিস — পর্ব ৩০-এর __eq__, __add__-এর মতো operator method যে বিশেষ মান ফেরত দেয়।)

Duck typing — base class ছাড়াই polymorphism

এবার Python-এর একটি বিশেষত্ব। Java বা C++-এ উপরের লুপ চালাতে হলে Bkash আর Card-কে একই base class (বা interface) থেকে আসতেই হবে। Python-এ তা লাগে না। লুপটি শুধু p.fee(1000) ডাকে — p-এর class কী, তার বাবা কে, Python তা জিজ্ঞেস করে না; শুধু দেখে fee নামের attribute আছে কি না। Payment base class পুরোপুরি মুছে দিলেও লুপ একইভাবে চলবে।

মেট্রোরেলের গেটে আপনি MRT Pass ছোঁয়ান বা একক যাত্রার কার্ড — গেট জানতে চায় না কার্ডটি কোন কারখানায় বানানো, কোন “পরিবারের”। শুধু দেখে কার্ডটি ঠিকমতো সাড়া দেয় কি না। Duck typing ঠিক এমন: “যদি হাঁসের মতো হাঁটে আর হাঁসের মতো ডাকে, তবে সেটি হাঁস।” কাজ পারলেই চলবে, বংশপরিচয় লাগবে না।

এর ফল গভীর: Python-এ polymorphism পেতে inheritance লাগে না। Inheritance-এর তখন একমাত্র কাজ থাকে সত্যিকারের ভাগ করা আচরণ বা প্রতিশ্রুতি দেওয়া। আর “কোন method থাকতে হবে” লিখিত চুক্তি চাইলে Python দেয় typing.Protocol — inheritance ছাড়াই type checker-কে চুক্তিটি জানানো। ABC বনাম Protocol বনাম খাঁটি duck typing — পর্ব ৩৩-এর বিষয়।

isinstance আর issubclass — কখন ঠিক, কখন গন্ধ

isinstance(obj, Cls) জানায় obj সেই class বা তার কোনো subclass-এর object কি না; issubclass(A, B) জানায় class A class B থেকে এসেছে কি না। দুটিই inheritance মানে: isinstance(current, Account) হলো True, যদিও type(current) হলো CurrentAccount. তাই type(x) == Account লিখবেন না — subclass-এর object বাদ পড়বে। (দ্বিতীয় argument-এ tuple দেওয়া যায়, আর Python 3.10 থেকে isinstance(x, int | float)-ও লেখা যায়।)

কিন্তু উপরের fee_bad.py-এর মতো isinstance-এর লম্বা শৃঙ্খল সাধারণত একটি code smell — লক্ষণ যে আচরণটি ভুল জায়গায় আছে; object-কে জিজ্ঞেস করার বদলে বাইরে থেকে তার পরিচয় যাচাই হচ্ছে। তবে isinstance-এর সৎ ব্যবহারও আছে: বাইরে থেকে আসা ডেটা যাচাই (JSON থেকে পাওয়া মান সত্যিই int কি না), এমন ফাংশন যা ইচ্ছা করেই একটি string আর একটি list-কে আলাদাভাবে সামলায়, বা পর্ব ৩০-এর __eq__-এ অন্য type-এর সাথে তুলনা এড়ানো। নিয়ম: ডেটার সীমানায় isinstance ঠিক আছে; ব্যবসার যুক্তির ভেতরে isinstance-শৃঙ্খল দেখলে polymorphism ভাবুন. Python 3.10-এর match-এর class pattern (পর্ব ১১) একই সতর্কতা মানে — ডেটার আকার মেলাতে চমৎকার, কিন্তু আচরণ বাছাইয়ের জন্য method-ই ভালো।

৫ · একাধিক বাবা — multiple inheritance, diamond আর MRO

Python একটি class-কে একাধিক বাবা থেকে আসতে দেয় — একে বলে multiple inheritance: class Copier(Printer, Scanner):. একটি ফটোকপি মেশিন একটি প্রিন্টারও, একটি স্ক্যানারও। শুনতে স্বাভাবিক, কিন্তু একটি প্রশ্ন তৈরি হয়: দুই বাবারই যদি একই নামের method থাকে, কোনটি চলবে? আর দুই বাবাই যদি একই দাদা (Device) থেকে আসে?

Diamond — দুই পথে একই দাদা

Copier → Printer → Device, আবার Copier → Scanner → Device

  1. Copier(Printer, Scanner)নাতি
  2. Printer(Device)প্রথম বাবা
  3. Scanner(Device)দ্বিতীয় বাবা
  4. Device(object)দুজনেরই বাবা
ছবি ৩১.১ — গাছটি দেখতে হীরার (diamond) মতো, তাই নাম diamond problem. প্রশ্ন: Device কতবার, আর কোন ক্রমে? Python-এর উত্তর উপরের সারিটিই — প্রতিটি class একবার, Device সবার শেষে।

Python এই প্রশ্নের উত্তর দেয় class তৈরির মুহূর্তেই: পুরো গাছটিকে একটি সরল সারিতে সাজিয়ে ফেলে — MRO. সাজানোর নিয়মটির নাম C3 linearization. এর তিনটি শর্ত সহজ ভাষায়:

  • সন্তান বাবার আগে: প্রতিটি class তার সব বাবার আগে আসবে (Copier আগে, Printer পরে; Printer আগে, Device পরে)।
  • বাবাদের লেখার ক্রম রক্ষা: (Printer, Scanner) লিখেছেন, তাই Printer আসবে Scanner-এর আগে।
  • প্রতিটি class একবার, আর বাবাদের নিজেদের সারির ক্রমও রক্ষা (একে বলে monotonic) — কোনো subclass তার বাবার MRO-র ক্রম উল্টে দিতে পারবে না।

ফল: Copier.__mro__ = (Copier, Printer, Scanner, Device, object). Device আসে একবার, আর দুই বাবার পরে। শর্তগুলো মেলানো অসম্ভব হলে Python class বানাতেই দেয় না — যেমন class X(Device, Printer) লিখলে (বাবাকে সন্তানের আগে বসালে) আসে TypeError: Cannot create a consistent method resolution order (MRO) for bases Device, Printer.

Cooperative super() — “বাবা” নয়, “সারির পরের জন”

এখানে এসে super()-এর আসল অর্থ পরিষ্কার হয়। super() মানে “আমার বাবা” নয়; মানে “এই object-এর MRO-তে আমার ঠিক পরের class”. এক বাবা থাকলে দুটো একই জিনিস। কিন্তু diamond-এ Printer-এর ভেতরের super() একটি Copier object-এর জন্য ডাকে Scanner-কে — Printer যার অস্তিত্বই জানে না! ধাপে ধাপে দেখুন:

1class Device:2    def __init__(self):3        print("Device")4class Printer(Device):5    def __init__(self):6        print("Printer"); super().__init__()7class Scanner(Device):8    def __init__(self):9        print("Scanner"); super().__init__()10class Copier(Printer, Scanner):11    def __init__(self):12        print("Copier"); super().__init__()1314Copier()
MRO আর super()-এর গন্তব্য
MRO
এখন চলছে
super() যাবে
  1. Class তৈরির সময় MRO হিসাব। লাইন ১০ চলার মুহূর্তে C3 নিয়ম পুরো গাছকে সারিতে সাজায়: Copier, Printer, Scanner, Device, object. সারিটি Copier.__mro__-এ জমা থাকে; এর পর প্রতিটি খোঁজা আর প্রতিটি super() এই সারি মেনে চলে।
  2. Copier.__init__. Copier() ডাকায় সারির প্রথম __init__ — Copier-এর নিজেরটি — চলল, ছাপল Copier. তার super() মানে “সারিতে Copier-এর পরের জন” = Printer.
  3. Printer.__init__ — চমক এখানে। ছাপল Printer. এবার Printer-এর ভেতরের super(). Printer লেখা হয়েছে Printer(Device) হিসেবে, তাই মনে হয় এটি Device-কে ডাকবে। কিন্তু self একটি Copier, আর Copier-এর সারিতে Printer-এর পরে আছে Scanner. তাই ডাক যায় Scanner-এ।
  4. Scanner.__init__. ছাপল Scanner. তার super() — সারিতে Scanner-এর পরে Device.
  5. Device.__init__ — একবারই। ছাপল Device. Device আর super() ডাকে না (ডাকলে যেত object-এ, যার __init__ কিছু করে না)। লক্ষ করুন, দুই পথে পৌঁছানো গেলেও Device চলেছে ঠিক একবার।
  6. ফল আর শিক্ষা। চারটি লাইন, MRO-র ক্রমে, প্রত্যেকে একবার। এখন যদি Printer আর Scanner super()-এর বদলে Device.__init__(self) লিখত, Scanner-এর __init__ কখনো চলত না, আর অন্য সাজানোয় Device দুবার চলত। এই শৃঙ্খল টিকে থাকে শুধু যদি সবাই super() ডাকে — তাই নাম cooperative multiple inheritance.

Cooperative নকশায় আরেকটি সমস্যা থাকে: ভিন্ন __init__-এর ভিন্ন argument লাগলে কে কী পাবে? প্রচলিত কৌশল হলো প্রতিটি __init__ নিজের দরকারি argument নাম ধরে নেবে, বাকিগুলো **kwargs-এ জমিয়ে (পর্ব ২০) super().__init__(**kwargs)-এ পাঠিয়ে দেবে। সারির শেষে পৌঁছানোর আগে সব argument কেউ না কেউ নিয়ে নেবে। Raymond Hettinger তাঁর বিখ্যাত লেখা “Super considered super!”-এ এই ধরনটি বিস্তারিত দেখিয়েছেন (নিচের ভাবনার প্রশ্ন ৪)।

Mixin — ছোট, মেশানো যায় এমন আচরণ

বাস্তবে multiple inheritance-এর সবচেয়ে নিরাপদ ও প্রচলিত ব্যবহার হলো mixin: একটি ছোট class, যা একটি মাত্র বাড়তি ক্ষমতা দেয়, নিজে একা ব্যবহারের জন্য নয়, আর সাধারণত নিজের কোনো __init__ বা অবস্থা রাখে না। যেমন:

import json

class JsonMixin:
    def to_json(self):
        return json.dumps(self.__dict__, ensure_ascii=False)

class ReprMixin:
    def __repr__(self):
        fields = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
        return f"{type(self).__name__}({fields})"

class Customer(JsonMixin, ReprMixin):
    def __init__(self, name, phone):
        self.name, self.phone = name, phone

c = Customer("রিনা", "01700000000")
print(c)              # Customer(name='রিনা', phone='01700000000')
print(c.to_json())    # {"name": "রিনা", "phone": "01700000000"}

JsonMixin যেকোনো class-কে JSON-এ রূপান্তরের ক্ষমতা দেয়, ReprMixin দেয় পড়ার মতো __repr__ — যেকোনো class-এ মিশিয়ে নিন, অনেকটা চায়ে চিনি বা আদা মেশানোর মতো। বাস্তবে Python-এর standard library-তে socketserver.ThreadingMixIn আছে, আর Django-তে LoginRequiredMixin-এর মতো অনেক mixin. প্রচলিত রীতি: নামের শেষে Mixin, আর class লেখার সময় mixin-গুলো বাঁয়ে, আসল base class সবার ডানে — যাতে MRO-তে mixin-এর method আগে পাওয়া যায়।

Mixin-এর সীমা: mixin দিয়ে ছোট আচরণ যোগ করা ভালো, কিন্তু পাঁচ-ছয়টি mixin একসাথে মেশালে কোন method কোথা থেকে আসছে বোঝা কঠিন হয়ে যায় — দুটি mixin-এর একই নামের method থাকলে নিঃশব্দে একটি অন্যটিকে ঢেকে দেয়। তখন Class.__mro__ ছাপিয়ে দেখুন, আর ভাবুন এখানে composition আরও স্পষ্ট হবে কি না।

৬ · কখন is-a আসলে মিথ্যা — Liskov নীতি আর built-in থেকে inherit করা

অংশ ১-এর প্রতিশ্রুতিটি মনে করুন: “যেখানে বাবাকে ব্যবহার করা যায়, সেখানে আমাকেও করা যাবে।” এই কথাটিরই আনুষ্ঠানিক নাম Liskov substitution principle (LSP) — Barbara Liskov-এর নামে, যিনি ১৯৮৭ সালের একটি বক্তৃতায় ধারণাটি তুলে ধরেন। বিখ্যাত উদাহরণ: গণিতে প্রতিটি বর্গ একটি আয়তক্ষেত্র। তাহলে class Square(Rectangle) লেখা কি ঠিক?

class Rectangle:
    def __init__(self, w, h):
        self.w, self.h = w, h
    def set_width(self, w):
        self.w = w
    def area(self):
        return self.w * self.h

class Square(Rectangle):
    def __init__(self, side):
        super().__init__(side, side)
    def set_width(self, w):          # বর্গ রাখতে উচ্চতাও বদলাতে হয়
        self.w = self.h = w

def stretch(r: Rectangle):
    r.set_width(10)
    return r.area()                  # যে Rectangle(2, 5) দিলে 50 আশা করে

stretch(Rectangle(2, 5)) দেয় 50 — প্রস্থ ১০, উচ্চতা ৫. কিন্তু stretch(Square(5)) দেয় 100, কারণ বর্গ থাকতে গিয়ে উচ্চতাও বদলে গেছে। যে ফাংশন একটি Rectangle-এর জন্য লেখা, সে একটি Square পেয়ে ভুল ফল দিল। গণিতের is-a সত্য, কিন্তু আচরণের is-a মিথ্যা — কারণ একটি “প্রস্থ বদলানো যায় এমন আয়তক্ষেত্র” যা প্রতিশ্রুতি দেয়, বর্গ তা রাখতে পারে না। সমাধান: Square-কে Rectangle-এর সন্তান না বানানো (দুটি আলাদা class, দরকারে একটি সাধারণ area() duck typing দিয়ে), অথবা object-গুলোকে immutable রাখা — তখন “প্রস্থ বদলানো” আচরণটিই থাকে না। SOLID নীতির পুরো আলোচনা, C++-এর উদাহরণসহ, দেখুন C++ কোর্সের design principles পর্বে.

LSP-এর ব্যবহারিক পরীক্ষা

Subclass লেখার আগে জিজ্ঞেস করুন: বাবার জন্য লেখা প্রতিটি কোড কি আমার object পেয়েও ঠিক কাজ করবে? সন্তান কি বাবার চেয়ে কঠিন শর্ত চাইছে (যেমন “শুধু ধনাত্মক সংখ্যা নেব”), বাবার কোনো method-এ NotImplementedError তুলে “এটি আমি পারি না” বলছে, বা বাবা যা ফেরত দেয় তার চেয়ে কম কিছু দিচ্ছে? যেকোনো একটি হলে is-a মিথ্যা।

dict বা list থেকে inherit করা — একটি সূক্ষ্ম ফাঁদ

Python 2.2 থেকে built-in type থেকেও inherit করা যায়: class PhoneBook(dict):. শুনতে দারুণ — সব dict method বিনা পরিশ্রমে। ধরুন আপনি চান প্রতিটি নাম বড় হাতের অক্ষরে জমা হোক, তাই __setitem__ override করলেন। book["rina"] = "017…" ঠিক কাজ করবে। কিন্তু PhoneBook(rina="017…") বা book.update(karim="018…") — এগুলো আপনার __setitem__ ডাকে না! CPython-এর dict C-তে লেখা, আর গতির জন্য তার __init__, update, setdefault সরাসরি ভেতরের তথ্য বদলায়, আপনার override করা method ঘুরে নয়। একইভাবে book.get(k) আপনার override করা __getitem__ ব্যবহার করে না, আর book.copy() ফেরত দেয় সাধারণ dict, PhoneBook নয়। ফল: একই dict-এ কিছু চাবি বড় হাতের, কিছু ছোট হাতের — নিঃশব্দ বাগ।

সমাধান collections module-এর UserDict (আর UserList, UserString). এগুলো Python-এ লেখা wrapper — ভেতরে একটি সাধারণ dict রাখে (self.data), আর update, constructor সহ সব পথ আপনার __setitem__ ঘুরে যায়। অর্থাৎ এগুলো ভেতরে আসলে composition! আরও সরল পথ: dict-কে inherit না করে নিজের class-এ একটি dict attribute রাখুন, আর শুধু যে কয়েকটি method দরকার সেগুলো দিন। নিয়ম: built-in-এর আচরণ বদলাতে চাইলে UserDict/UserList বা composition; শুধু নতুন method যোগ করতে চাইলে (কিছু বদলাতে নয়) built-in থেকে inherit করা মোটামুটি নিরাপদ.

৭ · Composition আর delegation — অংশ দিয়ে গড়া, অংশ অদলবদল

এবার অন্য পথ। একটি অনলাইন অর্ডারের কথা ভাবুন। অর্ডারে আছে একজন ক্রেতা, কয়েকটি পণ্য, একটি ডেলিভারি পদ্ধতি, একটি পেমেন্ট পদ্ধতি, আর অর্ডার নিশ্চিত হলে ক্রেতাকে জানানোর একটি উপায় (SMS বা Email). অর্ডার কি একটি Customer? না। একটি Payment? না। অর্ডারের এগুলো আছে — has-a. তাই Order এগুলোকে নিজের attribute হিসেবে রাখবে: এটাই composition.

আর অর্ডার যখন চার্জ জানতে চায়, সে নিজে হিসাব করে না — পেমেন্ট object-কে জিজ্ঞেস করে: self.payment.fee(subtotal). নিজের কাজের একটি অংশ অন্য object-কে দিয়ে করিয়ে নেওয়াকে বলে delegation. বাংলায় বললে, অর্ডার একজন ম্যানেজার: হিসাবের কাজ হিসাবরক্ষককে, খবর পাঠানোর কাজ পিয়নকে দেয়।

Composition-এর সবচেয়ে বড় সুবিধা: অংশগুলো চালানোর সময় অদলবদল করা যায়। একই Order class — কোনো subclass ছাড়াই — একবার bKash আর SMS দিয়ে, আরেকবার Card আর Email দিয়ে চলতে পারে। এই কৌশলের একটি নাম আছে: strategy pattern — একটি কাজের “কীভাবে” অংশটি আলাদা object বা ফাংশনে রেখে বাইরে থেকে দেওয়া। Python-এ ফাংশনও object (first-class function, পর্ব ১৩), তাই strategy হিসেবে অনেক সময় একটি সাধারণ ফাংশনই যথেষ্ট — নিচে notifier হিসেবে sms আর email শুধু দুটি ফাংশন।

class Bkash:
    name = "bKash"
    def fee(self, amount):
        return round(amount * 0.015)

class Card:
    name = "Card"
    def fee(self, amount):
        return 0

def sms(to, text):
    print(f"SMS → {to}: {text}")

def email(to, text):
    print(f"Email → {to}: {text}")

class Order:
    def __init__(self, customer, items, delivery, payment, notify):
        self.customer, self.items, self.delivery = customer, items, delivery
        self.payment, self.notify = payment, notify

    def checkout(self):
        subtotal = sum(price * qty for price, qty in self.items.values())
        total = subtotal + self.delivery + self.payment.fee(subtotal)
        self.notify(self.customer, f"{self.payment.name}-এ মোট {total} টাকা")

items = {"চাল": (70, 5), "ডাল": (130, 2)}
Order("রিনা", items, 60, Bkash(), sms).checkout()
Order("রিনা", items, 60, Card(), email).checkout()

আউটপুট দুই লাইনের: SMS → রিনা: bKash-এ মোট 679 টাকা আর Email → রিনা: Card-এ মোট 670 টাকা. হিসাব: পণ্যের দাম ৭০×৫ + ১৩০×২ = ৬১০, ডেলিভারি ৬০; bKash-এর উদাহরণ-চার্জ ৬১০-এর ১.৫% = ৯.১৫, round করে ৯ — মোট ৬৭৯; কার্ডে চার্জ ০ — মোট ৬৭০. তিনটি জিনিস লক্ষ করুন। এক, Bkash আর Card-এর কোনো common base class নেই — duck typing; Order শুধু চায় fee() আর name থাকুক। দুই, Order চার্জ নিজে হিসাব করে না, খবরও নিজে পাঠায় না — delegation. তিন, SMS থেকে Email-এ যেতে কোনো SMSOrder/EmailOrder subclass লাগেনি; শুধু অন্য একটি অংশ দেওয়া হয়েছে। টেস্ট লেখার সময় notify-তে এমন একটি ফাংশন দেওয়া যায় যা কিছু না পাঠিয়ে শুধু বার্তাটি একটি list-এ রাখে (পর্ব ৩৯)।

লক্ষ করুন, এখানে ডেলিভারি শুধু একটি সংখ্যা (৬০)। প্রয়োজন বাড়লে — ঢাকার ভেতরে ও বাইরে আলাদা চার্জ, ওজন অনুযায়ী চার্জ — এটিকেও একটি Delivery object বানিয়ে self.delivery.charge(items) দিয়ে delegate করবেন। Composition-এর এই ধাপে ধাপে বাড়ানো সহজ: একটি অংশ জটিল হলে শুধু সেই অংশটি নিজের class পায়, বাকি নকশা অক্ষত থাকে। (পর্ব ৩২-এ দেখবেন @dataclass দিয়ে এই ধরনের ছোট “অংশ” class এক লাইনে বানানো যায়।)

Delegation-এর বিস্তার — কতটা আড়াল করবেন

Composition-এ একটি প্রশ্ন আসে: বাইরের কোড কি order.payment.fee(x) লিখবে, নাকি order.fee()? দ্বিতীয়টি সাধারণত ভালো — Order একটি ছোট method দেয়, যা ভেতরে delegate করে। এতে বাইরের কোড Order-এর ভেতরের গঠন জানে না; কাল পেমেন্ট অংশের নাম বা গঠন বদলালেও বাইরের কোড ভাঙে না। (একে কখনো “Law of Demeter” বলে — “শুধু নিজের কাছের বন্ধুদের সাথে কথা বলো”।) Python-এ অনেক method একবারে delegate করতে __getattr__ ব্যবহার করা যায় — যে attribute নিজের কাছে পাওয়া যায় না, সেটি ভেতরের object-এ খোঁজা — তবে এটি জাদুর মতো আড়াল তৈরি করে, editor-এর auto-complete কাজ করে না; কয়েকটি method হলে হাতে লেখাই পরিষ্কার।

৮ · গভীর inheritance-গাছ ভেঙে composition — আর কোনটি কখন

বাস্তব প্রজেক্টে সমস্যা সাধারণত শুরু হয় ভালো উদ্দেশ্য নিয়ে। একটি নোটিফিকেশন সিস্টেম: প্রথমে Notifier, তা থেকে SMSNotifier আর EmailNotifier. তারপর নতুন চাহিদা — জরুরি বার্তা ব্যর্থ হলে তিনবার চেষ্টা; তারপর — সব বার্তার লগ রাখা। প্রতিটি নতুন চাহিদায় একটি নতুন subclass যোগ করলে কী হয়, দেখুন:

  1. শুরুর গাছ৩টি classসব ঠিক
  2. বিস্ফোরণ৯টি classনতুন চাহিদা
  3. অক্ষ আলাদা৩টি প্রশ্নকী বদলায়?
  4. compositionঅংশ জোড়াযেকোনো মিশ্রণ
প্রথম সংস্করণএকটি অক্ষ: কোন পথে পাঠাব
Notifier
├── SMSNotifier       send() → SMS gateway
└── EmailNotifier     send() → SMTP

এখানে inheritance যুক্তিসঙ্গত: SMSNotifier একটি Notifier.
ছয় মাস পরেদুটি নতুন চাহিদা: retry আর log
Notifier
├── SMSNotifier
│   ├── RetrySMSNotifier
│   ├── LoggedSMSNotifier
│   └── RetryLoggedSMSNotifier     ← কার কাছ থেকে inherit?
└── EmailNotifier
    ├── RetryEmailNotifier
    ├── LoggedEmailNotifier
    └── RetryLoggedEmailNotifier

WhatsApp যোগ করলে আরও ৪টি class.  retry-র নিয়ম বদলালে ৪ জায়গায়।
প্রশ্ন: আসলে কী কী বদলায়?তিনটি স্বাধীন অক্ষ
১. কোন পথে?        SMS · Email · WhatsApp        → channel
২. কতবার চেষ্টা?    ১ বার · ৩ বার                → retries (সংখ্যা)
৩. লগ রাখব?        হ্যাঁ · না                    → logger (ফাংশন বা None)

৩ × ২ × ২ = ১২টি মিশ্রণ — inheritance দিয়ে ১২টি class,
composition দিয়ে ৩টি channel class + ১টি Notifier.
Notifier(channel, retries, logger)অংশ দিয়ে গড়া
class Notifier:
    def __init__(self, channel, retries=1, logger=None):
        self.channel, self.retries, self.logger = channel, retries, logger
    def send(self, to, text):
        for _ in range(self.retries):
            if self.channel.send(to, text):      # delegation
                break
        if self.logger:
            self.logger(f"{to}: {text}")

urgent = Notifier(SMS(), retries=3, logger=print)
weekly = Notifier(Email())
ব্যবহারকারী পছন্দ বদলালে: weekly.channel = WhatsApp()
  1. শুরুটা যুক্তিসঙ্গত। একটি অক্ষ — কোন পথে বার্তা যাবে। SMSNotifier সত্যিই একটি Notifier, আর প্রত্যেকে নিজের send() দেয়। এখানে inheritance বা duck typing — দুটোই ঠিক।
  2. চাহিদা বাড়ল, গাছ ফুলল। retry আর log দুটি নতুন বৈশিষ্ট্য, আর প্রতিটি মিশ্রণের জন্য একটি class. সংখ্যা বাড়ে গুণ করে (একে বলে class explosion). RetryLoggedSMSNotifier-কে কোথা থেকে inherit করাবেন — দুই বাবা থেকে? retry-র নিয়ম বদলালে কয়েকটি class-এ বদলাতে হবে।
  3. অক্ষগুলো আলাদা করুন। নিজেকে জিজ্ঞেস করুন: কোন জিনিসগুলো একে অপরের থেকে স্বাধীনভাবে বদলায়? এখানে তিনটি — পথ, চেষ্টার সংখ্যা, লগ। প্রতিটি স্বাধীন অক্ষ composition-এর একটি “অংশ” হওয়ার প্রার্থী।
  4. Composition. একটি মাত্র Notifier, যার ভেতরে একটি channel object (duck typing — যার send() আছে), একটি সংখ্যা, আর একটি ঐচ্ছিক logger ফাংশন। ১২টি মিশ্রণ এখন ১২টি class নয়, ১২টি ভিন্ন argument. আর বোনাস: চালানোর সময় channel বদলানো যায় — inheritance-এ object-এর class জন্মের পর বদলানো যায় না।

এই refactoring-এর পেছনের নীতিটি পুরোনো ও বিখ্যাত। ১৯৯৪ সালের Design Patterns বইয়ে (যার চার লেখককে “Gang of Four” বলা হয়) একটি বাক্য আছে: “Favor object composition over class inheritance” — class inheritance-এর চেয়ে object composition-কে প্রাধান্য দিন (ভাবনার প্রশ্ন ১)। লক্ষ করুন, বাক্যটি বলে “প্রাধান্য দিন”, “কখনো ব্যবহার করবেন না” নয়।

সিদ্ধান্তের সারণি

পরিস্থিতিভালো পছন্দকেন
সত্যিকারের is-a, আর বাবার প্রতিটি আচরণ সন্তানেও অর্থপূর্ণ (SavingsAccount → Account)Inheritanceভাগ করা আচরণ এক জায়গায়; LSP মানা হয়
একটি object অন্য কয়েকটি জিনিস দিয়ে তৈরি (Order, Car)Compositionhas-a; অংশগুলো আলাদা করে বদলানো ও পরীক্ষা করা যায়
একটি আচরণ চালানোর সময় বদলাতে হবে (পেমেন্ট, notifier)Composition (strategy)object-এর class বদলানো যায় না, কিন্তু attribute বদলানো যায়
শুধু “একই নামের method আছে” — যেমন সব পেমেন্টের fee()Duck typing (দরকারে Protocol)কোনো সম্পর্ক ছাড়াই polymorphism; পর্ব ৩৩
অনেক অসম্পর্কিত class-এ একটি ছোট সাধারণ ক্ষমতা (JSON, repr)Mixinঅবস্থাহীন ছোট আচরণ; MRO-তে নিরাপদ
শুধু কয়েকটি method ধার করতে (Stack-কে list থেকে)Compositioninheritance করলে list-এর insert, sort সবই বাইরে খুলে যায়
Framework বলছে inherit করুন (Django model, Exception)Inheritanceframework-এর চুক্তি; পর্ব ২৪-এ নিজের exception এভাবেই বানিয়েছেন

শেষ সারিটি খেয়াল করুন — inheritance খারাপ নয়। পর্ব ২৪-এ class InsufficientBalance(WalletError): লিখেছিলেন (আর WalletError এসেছিল Exception থেকে); সেটি একদম সঠিক inheritance, কারণ আপনার exception সত্যিই একটি WalletError, আর except WalletError তাকে ধরবে। সমস্যা inheritance-এ নয়, কোড বাঁচানোর জন্য inheritance-এ, আর গভীর গাছে। একটি ব্যবহারিক নিয়ম: inheritance-এর গভীরতা দুই-তিন তলার বেশি হলে থেমে ভাবুন।

C/C++ জানলে

C-তে inheritance নেই; একটি struct-এর ভেতরে আরেকটি struct রাখা (composition) আর ফাংশন pointer দিয়ে আচরণ বদলানো (strategy) — এভাবেই Linux kernel-এর মতো বড় C কোডে “object-oriented” নকশা হয়। C++-এ dynamic dispatch পেতে method-কে virtual ঘোষণা করতে হয়; Python-এ প্রতিটি method নিজে থেকেই “virtual”, কারণ প্রতিবার MRO ধরে খোঁজা হয়। C++-এ diamond সামলাতে virtual inheritance লাগে আর বাবাকে নাম ধরে ডাকতে হয় (Account::withdraw()); Python-এ C3 MRO আর super() এটি স্বয়ংক্রিয় করে। গভীর তুলনার জন্য দেখুন C++-এর inheritance আর polymorphism ও virtual.

পরীক্ষা ও ইন্টারভিউতে
  • Inheritance আর composition-এর পার্থক্য? — Inheritance হলো is-a (subclass বাবার আচরণ পায়); composition হলো has-a (object অন্য object-কে attribute হিসেবে রেখে কাজ delegate করে)।
  • super() কী করে? — Object-এর MRO-তে বর্তমান class-এর পরের class-এর method ডাকে; এক বাবা থাকলে সেটি বাবা, diamond-এ অন্য ভাই-ও হতে পারে।
  • MRO কী, কীভাবে দেখবেন? — Method খোঁজার ক্রম, C3 linearization দিয়ে তৈরি; Class.__mro__ বা Class.mro().
  • Subclass-এর __init__-এ super().__init__() না ডাকলে? — বাবার __init__ চলে না, তার attribute তৈরি হয় না; পরে AttributeError.
  • Polymorphism-এর উদাহরণ দিন। — একই p.fee(amount) ডাক bKash, Card-এর মতো বিভিন্ন object-এ নিজ নিজ fee চালায়; Python-এ duck typing-এর কারণে common base না থাকলেও চলে।

৯ · AI-এর যুগে — অপ্রয়োজনীয় class-গাছ আর ভুলে যাওয়া super() ধরা

AI-কে “একটি ব্যাংক সিস্টেম লিখে দাও” বললে প্রায়ই সে একটি সুন্দর দেখতে class-গাছ বানায় — BaseEntity → BaseAccount → AbstractAccount → SavingsAccount. দেখতে “পেশাদার”, কিন্তু অনেক তলা কোনো আসল কাজ করে না। AI-এর প্রশিক্ষণের কোডে Java-ধাঁচের গভীর গাছ প্রচুর আছে, তাই সে সেটিই নকল করে, Python-এর duck typing আর composition-এর সুবিধা না নিয়ে। সাধারণ ভুলগুলো:

AI প্রায়ই যা করেসমস্যাকী চাইবেন
শুধু কোড ভাগ করতে ৩–৫ তলার class-গাছবদলানো কঠিন; একটি বাবা বদলালে সব সন্তানে প্রভাব (fragile base class)has-a হলে composition; গভীরতা ২ তলার মধ্যে
if isinstance(p, Bkash): … elif isinstance(p, Nagad): …নতুন type যোগ করলে প্রতিটি শৃঙ্খল বদলাতে হয়প্রতিটি class-এ নিজের method — polymorphism
Subclass-এর __init__-এ super().__init__() নেইবাবার attribute তৈরি হয় না; পরে AttributeErrorপ্রতিটি override করা __init__-এর প্রথম লাইনে super().__init__(...)
Parent.__init__(self, …) সরাসরি ডাক, mixin-এর সাথেMRO ভাঙে; কোনো class বাদ পড়ে বা দুবার চলেসবখানে super(), দরকারে **kwargs ফরোয়ার্ড
class Cache(dict) আর __setitem__ overrideupdate(), constructor override এড়িয়ে যায়collections.UserDict বা ভেতরে একটি dict রাখা
সব জায়গায় খালি base class (class Base: pass)কোনো প্রতিশ্রুতি নেই, শুধু শব্দduck typing, বা আসল চুক্তি চাইলে ABC/Protocol (পর্ব ৩৩)

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

আমি Python 3.12+ ব্যবহার করি। একটি অনলাইন দোকানের অর্ডার সিস্টেম লিখে দাও: অর্ডারে ক্রেতা, পণ্যের তালিকা, ডেলিভারি চার্জ, পেমেন্ট পদ্ধতি (bKash, Nagad, Card — প্রত্যেকের নিজের চার্জের নিয়ম) আর নোটিফিকেশন (SMS বা Email) থাকবে। শর্ত: ১. Order-এর অংশগুলো composition দিয়ে রাখো (has-a); Order কোনো Payment বা Customer থেকে inherit করবে না। ২. পেমেন্টের চার্জ polymorphism দিয়ে: প্রতিটি পেমেন্ট class-এর নিজের fee() method; কোথাও isinstance-এর if/elif শৃঙ্খল লিখবে না। ৩. inheritance শুধু যেখানে সত্যিকারের is-a; গভীরতা সর্বোচ্চ ২ তলা। প্রতিটি inheritance-এর পাশে এক লাইনে লেখো কেন এটি is-a. ৪. কোনো subclass-এর __init__ থাকলে তাতে super().__init__(...) ডাকবে; কোথাও ParentName.__init__(self) লিখবে না। ৫. dict বা list থেকে inherit করবে না; দরকার হলে collections.UserDict বা ভেতরে একটি dict রাখো। ৬. নোটিফিকেশন পদ্ধতি চালানোর সময় বদলানো যাবে এমনভাবে লেখো, আর assert দিয়ে দেখাও: একই Order class দুটি ভিন্ন notifier-এ চলে, আর একটি fake notifier দিয়ে টেস্ট করা যায়।

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

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

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

প্র ০১১৯৯৪ সালের Design Patterns বই কেন বলেছিল “Favor object composition over class inheritance”? এত বছর পরেও কথাটি কেন সবচেয়ে বেশি উদ্ধৃত নকশা-উপদেশগুলোর একটি?

১৯৯৪ সালে Erich Gamma, Richard Helm, Ralph Johnson আর John Vlissides প্রকাশ করেন Design Patterns: Elements of Reusable Object-Oriented Software. চার লেখকের কারণে বইটি “Gang of Four” বা GoF বই নামে পরিচিত। বইয়ের শুরুর অধ্যায়ে দুটি মূল নীতি আছে: “Program to an interface, not an implementation” আর “Favor object composition over class inheritance”. বইয়ের ২৩টি pattern-এর অনেকগুলো — Strategy, Decorator, Composite, Bridge, Observer — আসলে দ্বিতীয় নীতিরই বিভিন্ন রূপ।

লেখকেরা inheritance-কে বলেছেন “white-box reuse”: subclass বাবার ভেতরটা দেখতে পায়, protected অংশ ব্যবহার করে, আর বাবার বাস্তবায়নের ওপর নির্ভর করে। বইয়ের একটি বিখ্যাত পর্যবেক্ষণ — inheritance “breaks encapsulation”, কারণ subclass বাবার বাস্তবায়নের খুঁটিনাটির সাথে বাঁধা পড়ে; বাবার ভেতর বদলালে সন্তান ভাঙতে পারে। আর inheritance সম্পর্ক ঠিক হয় কোড লেখার সময় — একটি object জন্মের পর তার class বদলানো যায় না।

Composition হলো “black-box reuse”: একটি object অন্যটিকে শুধু তার প্রকাশ্য ইন্টারফেস দিয়ে চেনে। ভেতরের অংশ বদলালেও বাইরের object ভাঙে না, যতক্ষণ ইন্টারফেস একই থাকে। আর সম্পর্ক ঠিক হয় চালানোর সময় — এই পর্বের Order-এ পেমেন্ট আর notifier অদলবদল করেছেন একটি argument বদলে।

১৯৯০-এর দশকের প্রেক্ষাপট বোঝা জরুরি। তখন C++ আর Smalltalk-এ OOP মানেই বড় class-গাছ — অনেক GUI library-তে দশ-বারো তলার hierarchy সাধারণ ছিল। ধারণা ছিল, inheritance-ই পুনর্ব্যবহারের প্রধান উপায়। বাস্তবে দেখা গেল সেই গাছগুলো বদলানো কঠিন, আর নতুন চাহিদা গাছের আকারে খাপ খায় না (এই পর্বের notifier-এর class explosion). GoF সেই অভিজ্ঞতাকে একটি বাক্যে বেঁধেছিল।

পরে Joshua Bloch-এর Effective Java-তেও “Favor composition over inheritance” একটি আলাদা পরামর্শের (Item) শিরোনাম। আর আধুনিক ভাষাগুলো এই শিক্ষা নিজের নকশায় গেঁথে নিয়েছে: Go-তে class inheritance-ই নেই, আছে struct embedding আর interface; Rust-এ struct inheritance নেই, আছে trait. Python দুটোই দেয়, কিন্তু duck typing-এর কারণে অনেক সময় দুটোর কোনোটিই লাগে না।

তবে মনে রাখুন বাক্যটির শব্দ “favor” — প্রাধান্য দিন। GoF নিজেরাও বইয়ে inheritance ব্যবহার করেছেন, বিশেষত ইন্টারফেস ভাগ করার জন্য। উপদেশটি নিষেধাজ্ঞা নয়, একটি ডিফল্ট: সন্দেহ হলে composition.

মূল উপলব্ধি: Inheritance সম্পর্ক কোড লেখার সময় স্থির হয় আর বাবার ভেতরের খুঁটিনাটির সাথে বেঁধে দেয়; composition চালানোর সময় নমনীয় আর শুধু ইন্টারফেসের ওপর নির্ভর করে। বদল যেহেতু সফটওয়্যারের একমাত্র নিশ্চিত জিনিস, নমনীয় পথটিই সাধারণত টেকে।

প্র ০২C3 linearization কোথা থেকে এল? Python 2.3 কেন MRO-র নিয়ম বদলেছিল, আর পুরোনো নিয়মে সমস্যা কী ছিল?

C3 এসেছে Python থেকে নয়, Dylan নামের একটি প্রোগ্রামিং ভাষা থেকে। Dylan ১৯৯০-এর দশকে Apple-এর উদ্যোগে তৈরি একটি object-oriented ভাষা, যার শিকড় Lisp-পরিবারে (বিশেষত Common Lisp-এর object system, CLOS). ১৯৯৬ সালে Kim Barrett, Bob Cassels, Paul Haahr, David Moon, Keith Playford আর P. Tucker Withington একটি গবেষণাপত্র প্রকাশ করেন — “A Monotonic Superclass Linearization for Dylan” (OOPSLA সম্মেলনে)। সেখানে প্রস্তাবিত নিয়মটিই C3.

সমস্যাটি ছিল এই: multiple inheritance-এ একটি class-গাছকে একটি সরল সারিতে (linearization) সাজাতে হয়, যাতে method খোঁজার ক্রম নির্দিষ্ট থাকে। কিন্তু কোন সাজানো “ঠিক”? Dylan-এর লেখকেরা দেখান, আগের অনেক নিয়ম একটি গুরুত্বপূর্ণ বৈশিষ্ট্য মানত না — monotonicity: যদি class C-এর সারিতে A আসে B-এর আগে, তবে C-এর যেকোনো subclass-এর সারিতেও A আসবে B-এর আগে। এটি না মানলে একটি subclass বানানোমাত্র বাবার method খোঁজার ক্রম উল্টে যেতে পারে — যে কোড বাবার জন্য ঠিক ছিল, সেটি সন্তানে অন্য method পেয়ে যায়। C3 নিশ্চিত করে: সন্তান বাবার আগে, বাবাদের লেখার ক্রম রক্ষা, আর monotonicity — এবং তিনটি শর্ত মেলানো অসম্ভব হলে সাজানো প্রত্যাখ্যান করে।

Python-এর গল্প: Python 2.2 (২০০১)-এ এসেছিল “new-style class” — object থেকে আসা class, যা built-in type আর ব্যবহারকারীর class-কে একীভূত করে (PEP 252, 253). তখন object সবার সাধারণ পূর্বপুরুষ হওয়ায় প্রতিটি multiple inheritance-ই আসলে একটি diamond হয়ে গেল, আর MRO-র প্রশ্ন গুরুত্বপূর্ণ হয়ে উঠল। পুরোনো ধাঁচের class-এ খোঁজা ছিল সহজ depth-first, left-to-right — diamond-এ দাদা সন্তানের ভাইয়ের আগেই পাওয়া যেত। Python 2.2 new-style class-এর জন্য একটি সংশোধিত নিয়ম আনে, কিন্তু Samuele Pedroni (Jython-এর একজন ডেভেলপার) python-dev mailing list-এ দেখান যে সেই নিয়মও কিছু ক্ষেত্রে monotonic নয়।

ফলে Python 2.3 (২০০৩) new-style class-এর জন্য C3 গ্রহণ করে। Michele Simionato-র লেখা “The Python 2.3 Method Resolution Order” প্রবন্ধটি আজও python.org-এ পাওয়া যায়, আর C3-এর সবচেয়ে পরিচিত ব্যাখ্যা হিসেবে গণ্য। Python 3-এ সব class-ই new-style, তাই সবখানে C3. পরে Perl (mro pragma), Raku আর আরও কিছু ভাষাও C3 ব্যবহার করে।

ব্যবহারিক ফল আপনি এই পর্বে দেখেছেন: Copier.__mro__-এ Device আসে Printer ও Scanner দুজনের পরে, আর ক্রম মেলানো অসম্ভব হলে Python class তৈরির সময়েই TypeError দেয় — চালানোর মাঝখানে অদ্ভুত আচরণের বদলে শুরুতেই স্পষ্ট ব্যর্থতা।

মূল উপলব্ধি: C3 একটি গবেষণা-ভাষা থেকে আসা গাণিতিকভাবে যুক্তিসঙ্গত নিয়ম, যা Python বাস্তব বাগ পাওয়ার পর গ্রহণ করেছে। super()-এর পুরো ক্ষমতা দাঁড়িয়ে আছে এই নিশ্চয়তার ওপর যে সারিটি সুসংগত।

প্র ০৩Python কেন multiple inheritance দেয়, অথচ Java class-এর জন্য দেয় না? দুই ভাষার নকশাকারেরা কী ভেবে এই ভিন্ন সিদ্ধান্ত নিয়েছিলেন?

১৯৯০-এর দশকের শুরুতে Java-র নকশাকারদের সামনে ছিল C++-এর অভিজ্ঞতা। C++ multiple inheritance দেয়, আর diamond সামলাতে আলাদা virtual inheritance, নাম-সংঘাত মেটাতে স্পষ্ট qualification, object-এর মেমরি বিন্যাসে জটিলতা — সব মিলিয়ে বৈশিষ্ট্যটি শক্তিশালী কিন্তু ভুল করা সহজ। James Gosling আর তাঁর দল Java-কে “সরল” রাখতে চেয়েছিলেন; Java-র প্রথম দিকের শ্বেতপত্রে (white paper) সিদ্ধান্তটির যুক্তি হিসেবে C++-এর এই জটিলতা এড়ানোর কথা বলা হয়। তাই Java-তে একটি class একটি মাত্র class থেকে extends করতে পারে, কিন্তু অনেকগুলো interface implements করতে পারে। Interface-এ শুরুতে কোনো বাস্তবায়ন ছিল না — শুধু method-এর স্বাক্ষর — তাই “কোন বাবার কোড চলবে” প্রশ্নই উঠত না।

মজার ব্যাপার, Java পরে আংশিক পিছু হটেছে। Java 8 (২০১৪)-এ interface-এ default method যোগ হয় — বাস্তবায়নসহ method. এখন দুটি interface-এ একই নামের default method থাকলে সেই পুরোনো diamond প্রশ্ন আবার ওঠে, আর Java নিয়ম করেছে: class-কে তখন নিজে override করে বেছে নিতে হবে। অর্থাৎ আচরণের multiple inheritance এক রকম ফিরে এসেছে, তবে state (field) ছাড়া।

Python-এর পথ ভিন্ন, কারণ তার প্রেক্ষাপট ভিন্ন। প্রথমত, Python-এ object-এর attribute থাকে একটি dict-এ, C++-এর মতো নির্দিষ্ট মেমরি বিন্যাসে নয় — তাই একাধিক বাবার “অংশ” মেমরিতে সাজানোর জটিলতা প্রায় নেই। দ্বিতীয়ত, C3 MRO আর super() diamond-কে একটি সুসংগত সারিতে পরিণত করে — প্রতিটি class একবার, নির্দিষ্ট ক্রমে। তৃতীয়ত, Python-এর দর্শন “we are all consenting adults” — শক্তিশালী হাতিয়ার দাও, দায়িত্ব প্রোগ্রামারের। আর duck typing থাকায় interface-এর জন্য inheritance লাগেই না, তাই multiple inheritance-এর আসল ব্যবহার দাঁড়ায় mixin-এ, যা ছোট ও অবস্থাহীন — ঝুঁকিও কম।

অন্য ভাষাগুলো মাঝামাঝি পথ নিয়েছে: Ruby-তে single inheritance কিন্তু module mixin; Scala-তে trait; Rust-এ trait আর কোনো struct inheritance নেই; Go-তে inheritance-ই নেই। প্রায় সবাই একই সিদ্ধান্তে এসেছে — অবস্থাসহ (state) একাধিক বাবা ঝুঁকিপূর্ণ, কিন্তু ছোট আচরণ মেশানো (mixin/trait) খুবই কাজের।

বাস্তব Python কোডে এর প্রতিফলন: অভিজ্ঞ ডেভেলপাররা multiple inheritance প্রায় শুধু mixin-এর জন্য ব্যবহার করেন, বা framework-এর নির্দেশে (Django-র class-based view). দুটি “ভারী”, অবস্থাসহ class একসাথে inherit করা বিরল, আর code review-তে প্রশ্ন ওঠে।

মূল উপলব্ধি: Java জটিলতা এড়াতে বৈশিষ্ট্যটি বাদ দিয়েছিল, পরে default method দিয়ে আংশিক ফিরিয়েছে; Python বৈশিষ্ট্যটি রেখে C3 দিয়ে তাকে সুসংগত করেছে। দুই পথই শেষ পর্যন্ত একই শিক্ষায় মেলে — ছোট আচরণ মেশাও, ভারী অবস্থা নয়।

প্র ০৪Raymond Hettinger-এর “Super considered super!” কী বলেছিল? অনেকে super()-কে বিপজ্জনক মনে করতেন কেন, আর তিনি কীভাবে উল্টো যুক্তি দেন?

Raymond Hettinger একজন দীর্ঘদিনের Python core developer — collections module-এর deque, OrderedDict, namedtuple, আর itertools-এর অনেক অংশে তাঁর অবদান। ২০১১ সালে তিনি একটি ব্লগ-প্রবন্ধ লেখেন “Python’s super() considered super!”, আর পরে PyCon 2015-এ একই নামে একটি বক্তৃতা দেন, যা Python কমিউনিটিতে খুব জনপ্রিয়।

শিরোনামটি একটি উত্তর। তার আগে James Knight-এর একটি পরিচিত লেখা ছিল “Python’s Super Considered Harmful” (পরে শিরোনাম কিছুটা নরম করা হয়) — যেখানে দেখানো হয় multiple inheritance-এ super() অপ্রত্যাশিত class-কে ডাকে, আর argument মেলানো কঠিন। দুটি শিরোনামই Dijkstra-র “Go To Statement Considered Harmful”-এর অনুকরণ।

Hettinger-এর মূল যুক্তি: super()-কে “বাবাকে ডাকা” ভাবলেই বিভ্রান্তি। ঠিক মডেল হলো — super() ডাকে instance-এর MRO-তে পরের class-কে, আর সেই সারি ঠিক হয় চূড়ান্ত subclass দিয়ে। এটি দুর্বলতা নয়, শক্তি: এর ফলে আপনি একটি class না ছুঁয়েই তার method-চেইনের মাঝখানে নতুন আচরণ ঢোকাতে পারেন। তাঁর বিখ্যাত উদাহরণ: class LoggingOD(LoggingDict, OrderedDict) — LoggingDict লেখা হয়েছিল সাধারণ dict-এর জন্য, কিন্তু নতুন subclass তৈরি করা মাত্র তার super().__setitem__ এখন OrderedDict-এর method ডাকে। কারও কোড বদলাতে হয়নি — শুধু MRO বদলেছে। এই পর্বের Copier-এ Printer-এর super()-এর Scanner-কে ডাকা ঠিক এই ঘটনা।

এর জন্য তিনি তিনটি বাস্তব নিয়ম দেন: (১) যে method super() দিয়ে ডাকা হয়, সেটি চেইনের শেষে থাকা কোনো class-এ অবশ্যই থাকতে হবে (একটি “root” class, যা নিজে আর super() ডাকে না); (২) ডাক আর ডাকা method-এর argument মিলতে হবে — এজন্য keyword argument আর **kwargs: প্রতিটি class নিজের দরকারি argument নিয়ে বাকিটা পাঠিয়ে দেয়; (৩) চেইনের প্রতিটি class-কে super() ডাকতে হবে। যে বাইরের class এই নিয়ম মানে না, তাকে একটি ছোট “adapter” class দিয়ে মুড়িয়ে নিতে বলেন — আবারও composition-এর মতো কৌশল।

বিতর্কের শিক্ষা হলো দুই পক্ষই কিছুটা ঠিক। super() সত্যিই একটি চুক্তি চায় — সবাইকে সহযোগিতা করতে হবে — আর সেই চুক্তি ভাঙলে বাগ। কিন্তু চুক্তিটি মানলে এটি এমন নমনীয়তা দেয় যা সরাসরি বাবার নাম ধরে ডাকলে অসম্ভব।

মূল উপলব্ধি: super() মানে “বাবা” নয়, “MRO-তে পরের জন” — এই একটি মানসিক মডেল ঠিক থাকলে cooperative multiple inheritance আর mixin পরিষ্কার হয়ে যায়; ভুল থাকলে সবকিছুই রহস্য মনে হয়।

প্র ০৫“Fragile base class problem” কী? বাবার একটি নিরীহ বদল কীভাবে সন্তানকে না ছুঁয়েই ভেঙে দেয়, আর composition কীভাবে রক্ষা করে?

“Fragile base class” মানে ভঙ্গুর বাবা: base class-এ এমন একটি বদল, যা নিজের দিক থেকে নির্দোষ — কোনো প্রকাশ্য method-এর নাম বা অর্থ বদলায়নি — তবু তার subclass-গুলো ভেঙে দেয়। সমস্যাটি ১৯৯০-এর দশকে বড় object-oriented system-এ (যেমন Microsoft-এর COM আর বিভিন্ন GUI framework) ব্যাপকভাবে আলোচিত হয়, আর ১৯৯৮ সালে Leonid Mikhajlov ও Emil Sekerinski-র গবেষণাপত্র “A Study of the Fragile Base Class Problem” একে আনুষ্ঠানিকভাবে বিশ্লেষণ করে।

সবচেয়ে পরিচিত উদাহরণটি Joshua Bloch-এর Effective Java থেকে। একটি HashSet-এর subclass, যা গুনে রাখে মোট কতবার উপাদান যোগ হয়েছে: add() override করে counter এক বাড়ায়, addAll() override করে counter-এ সংগ্রহের আকার যোগ করে, তারপর super ডাকে। তিনটি উপাদান addAll করলে counter হয় ৬! কারণ: বাবার addAll ভেতরে প্রতিটি উপাদানের জন্য add() ডাকে — আর সেই add() এখন সন্তানের override করা রূপ, যা আবার গোনে। সন্তানের সঠিকতা নির্ভর করছিল বাবার একটি অপ্রকাশিত বাস্তবায়ন-খুঁটিনাটির ওপর। আর যদি পরের version-এ বাবা addAll-কে বদলে সরাসরি লেখে, counter আবার ঠিক হয়ে যাবে — অর্থাৎ সন্তানের আচরণ বাবার ভেতরের সিদ্ধান্তের ওপর ঝুলে আছে।

Python-এ আপনি এর একটি রূপ এই পর্বেই দেখেছেন: dict-এর subclass-এ __setitem__ override করলে update() সেটি ডাকে না। এই পর্বের প্রথম প্রোগ্রামেও ঝুঁকিটি লুকিয়ে আছে — Account.withdraw ভেতরে self.limit() ডাকে, আর CurrentAccount তার ওপর নির্ভর করে। কাল কেউ withdraw-কে “সরল” করে self.balance সরাসরি ব্যবহার করলে overdraft নিঃশব্দে উধাও। তাই এমন “hook” method থাকলে বাবার documentation-এ স্পষ্ট লিখতে হয় কোন method সন্তানের override-এর জন্য, আর বাবা তাদের কখন ডাকে — Bloch-এর ভাষায়, “Design and document for inheritance or else prohibit it”.

Composition কীভাবে রক্ষা করে? Bloch-এর সমাধান: subclass না বানিয়ে একটি wrapper class, যার ভেতরে একটি set থাকে, আর প্রতিটি method ভেতরের set-এ delegate করে (forwarding). এখন ভেতরের set-এর addAll যা-ই ডাকুক, সেটি ভেতরের set-এর নিজের add — wrapper-এর গোনার কোড নয়। Wrapper শুধু প্রকাশ্য ইন্টারফেসের ওপর নির্ভর করে, ভেতরের খুঁটিনাটির ওপর নয়। ঠিক এই কারণেই Python-এর UserDict ভেতরে একটি dict রাখে।

Python-এর একটি অতিরিক্ত দিক: এখানে final কিওয়ার্ড নেই, আর সব method-ই override করা যায়। typing.final decorator (Python 3.8 থেকে) type checker-কে জানাতে পারে যে একটি class বা method override করা উচিত নয়, তবে চালানোর সময় তা বাধ্য করা হয় না।

মূল উপলব্ধি: Inheritance সন্তানকে বাবার প্রকাশ্য ইন্টারফেসের সাথে নয়, তার ভেতরের বাস্তবায়নের সাথে বেঁধে দেয়। Composition শুধু ইন্টারফেসের ওপর নির্ভর করে — তাই বাবার ভেতরের বদল আর আপনার কোড ভাঙে না।

অনুশীলন

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

  1. is-a নাকি has-a: প্রতিটি জোড়ার জন্য ঠিক করুন — inheritance, composition, নাকি কোনোটিই নয়: (ক) Car আর Engine; (খ) Manager আর Employee; (গ) Stack আর list; (ঘ) InsufficientBalance আর ValueError; (ঙ) Order আর Customer; (চ) Nagad আর Bkash.

    (ক) composition — গাড়ির একটি ইঞ্জিন আছে। (খ) সাধারণত inheritance — একজন ম্যানেজার একজন কর্মচারী; তবে যদি একজন মানুষের ভূমিকা সময়ের সাথে বদলায় (কর্মচারী থেকে ম্যানেজার), তবে “ভূমিকা”-কে একটি অংশ বানানো (composition) ভালো, কারণ object-এর class জন্মের পর বদলানো যায় না। (গ) composition — Stack-এর ভেতরে একটি list রাখুন; inheritance করলে insert, sort, slicing সব খুলে যায়, যা stack-এর নিয়ম ভাঙে (LSP-এর উল্টো দিক)। (ঘ) inheritance — এটি সত্যিকারের is-a, আর except ValueError একে ধরবে। (ঙ) composition — অর্ডারের একজন ক্রেতা আছে। (চ) কোনোটিই নয় — দুটি সমকক্ষ পেমেন্ট পদ্ধতি; একটি অন্যটি নয়। চাইলে দুজনেই একটি Payment থেকে আসতে পারে, বা শুধু duck typing.

  2. Output অনুমান: class A: def __init__(self): self.x = 1; class B(A): def __init__(self): self.y = 2. b = B() তৈরি করে print(b.y) আর তারপর print(b.x) লিখলে কী হবে? কীভাবে ঠিক করবেন?

    print(b.y) ছাপবে 2. কিন্তু print(b.x)-এ আসবে AttributeError: 'B' object has no attribute 'x'. কারণ B-এর নিজের __init__ আছে, তাই MRO-তে সেটিই প্রথম পাওয়া যায় আর শুধু সেটিই চলে — A.__init__ কখনো ডাকা হয়নি। সমাধান: B.__init__-এর প্রথম লাইনে super().__init__(). লক্ষ করুন, B যদি নিজে __init__ না লিখত, তবে A-এরটি উত্তরাধিকারে চলত আর সমস্যা হতো না — ফাঁদটি শুধু তখনই, যখন override করেন।

  3. MRO হাতে বের করুন: class A: pass, class B(A): pass, class C(A): pass, class D(C, B): pass. D.__mro__ কী হবে? প্রতিটি class-এ def hi(self): print("X"); super().hi() (X = class-এর নাম) আর A-তে শুধু print("A") থাকলে D().hi() কী ছাপবে?

    D.__mro__ = (D, C, B, A, object) — D প্রথমে; তারপর লেখার ক্রম মেনে C, তারপর B (বাবাদের ক্রম (C, B) রক্ষা); A দুজনেরই বাবা, তাই দুজনের পরে; শেষে object. D().hi() ছাপবে চার লাইনে D, C, B, A. C-এর super() ডাকে B-কে (A-কে নয়), কারণ D-এর সারিতে C-এর পরে B. আর A একবারই ছাপা হয়। এখানে যদি class D(A, B) লিখতেন, Python TypeError দিত — A-কে তার নিজের সন্তান B-এর আগে বসানো যায় না।

  4. isinstance-শৃঙ্খল থেকে polymorphism (এবং AI-কে যাচাই করুন): একটি ফাংশন receipt_text(payment) আছে, যার ভেতরে isinstance দিয়ে bKash হলে “bKash TrxID সহ”, কার্ড হলে “কার্ডের শেষ ৪ অঙ্ক সহ” লেখা ফেরত দেয়। এটিকে polymorphism-এ রূপান্তর করুন। তারপর কোনো AI-কে একই কাজ লিখতে বলুন আর দেখুন সে isinstance-শৃঙ্খল লেখে কি না।

    একটি সম্ভাব্য সমাধান — প্রতিটি পেমেন্ট নিজের রসিদের লেখা জানে:

    class Bkash:
        def __init__(self, trx_id):
            self.trx_id = trx_id
        def receipt_text(self):
            return f"bKash · TrxID {self.trx_id}"
    
    class Card:
        def __init__(self, number):
            self.last4 = number[-4:]
        def receipt_text(self):
            return f"Card · **** {self.last4}"
    
    for p in [Bkash("9A7X2"), Card("4111222233334444")]:
        print(p.receipt_text())

    এখন Nagad যোগ করতে শুধু একটি নতুন class লাগবে, কোনো পুরোনো ফাংশন খুলতে হবে না। লক্ষ করুন, কার্ড-নম্বরের পুরোটা নয়, শুধু শেষ চার অঙ্ক রাখা হয়েছে — রসিদে বা লগে পুরো কার্ড-নম্বর কখনো রাখবেন না (পর্ব ৫০)। AI যদি if isinstance(p, Bkash) … elif … লেখে, তাকে বলুন: “প্রতিটি class-এ নিজের receipt_text() method দাও, isinstance ব্যবহার কোরো না।”

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

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

ক্রমclass SavingsAccount(Account)-এর একটি object-এ savings.withdraw লিখলে Python যে ক্রমে খোঁজে, সেই ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🧾 instance-এর নিজের __dict__
  2. 🐷 SavingsAccount class
  3. 🏦 Account class
  4. 🧱 object class
  5. ⚠️ কোথাও না পেলে AttributeError

class Printer(Device), class Scanner(Device) আর class Copier(Printer, Scanner) হলে Copier.__mro__-এর ক্রম কী?

  • Copier, Printer, Device, Scanner, object
  • Copier, Printer, Scanner, Device, object
  • Copier, Scanner, Printer, Device, object
  • Copier, Printer, Device, Scanner, Device, object

একটি subclass নিজের __init__ লিখেছে কিন্তু তাতে super().__init__() ডাকেনি। Base class-এর __init__-এ বসানো একটি attribute পড়তে গেলে সাধারণত কী হয়?

  • AttributeError আসে
  • Python নিজে থেকে বাবার __init__ চালিয়ে দেয়, তাই সমস্যা নেই
  • attribute-টির মান None পাওয়া যায়
  • class সংজ্ঞায়িত করার সময়েই SyntaxError আসে

Composition-এর সবচেয়ে সঠিক বর্ণনা কোনটি?

  • একটি class অন্য class-এর সব method উত্তরাধিকারে পায়
  • একটি class একসাথে একাধিক base class থেকে আসে
  • একই নামের method থাকলেই ভিন্ন object একই জায়গায় চলে
  • একটি object অন্য object-গুলোকে attribute হিসেবে রেখে তাদের কাছে কাজ delegate করে

Python-এ class Upper(dict) লিখে __setitem__ override করা হলো, যাতে প্রতিটি মান বড় হাতের অক্ষরে জমা হয়। d.update(a="x") ডাকলে কী ঘটে?

  • মান "X" হিসেবে জমা হয়
  • TypeError আসে
  • override এড়িয়ে মান "x" হিসেবেই জমা হয়
  • মানটি একেবারেই জমা হয় না

Square-কে Rectangle-এর subclass বানানো হলো, আর set_width ডাকলে বর্গ রাখতে উচ্চতাও বদলায়। এটি কোন নীতি ভাঙে?

  • DRY (Don't Repeat Yourself)
  • Liskov substitution principle
  • Encapsulation
  • Multiple inheritance-এর নিয়ম

Python-এ for p in payments: total += p.fee(amount) কাজ করতে payments-এর object-গুলোর কী দরকার?

  • প্রত্যেকের একটি fee(amount) method থাকা — common base class না থাকলেও চলে
  • সবাইকে অবশ্যই একই base class থেকে আসতে হবে
  • fee-কে virtual ঘোষণা করতে হবে
  • সবাইকে abc.ABC থেকে inherit করতে হবে

শব্দকোষ

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

মনে রাখুন

  1. is-a → inheritance, has-a → composition. Inheritance একটি প্রতিশ্রুতি: বাবার জায়গায় সন্তান সবসময় চলবে (Liskov). শুধু কোড ভাগ করতে inheritance নয়।
  2. Attribute খোঁজা চলে instance → class → বাবা → … → object ক্রমে (__mro__); প্রথম মিলেই থামে। Override মানে আগে পাওয়া যাওয়া; বাবার লেখা কোডও self-এর মাধ্যমে সন্তানের method ডাকে।
  3. Override করা __init__-এ super().__init__(...) ডাকুন, নইলে বাবার অংশ তৈরি হয় না। super() মানে “MRO-তে পরের জন”, সবসময় বাবা নয় — diamond-এ C3 প্রতিটি class একবার রাখে।
  4. Polymorphism: একই ডাক, প্রতিটি object নিজের মতো সাড়া দেয়। Python-এ duck typing-এর কারণে base class না থাকলেও চলে। isinstance-শৃঙ্খল দেখলে method-এ সরান।
  5. Composition + delegation অংশগুলোকে আলাদা রাখে আর চালানোর সময় অদলবদল করতে দেয় (strategy). Mixin ছোট আচরণের জন্য; dict/list-এর আচরণ বদলাতে UserDict/UserList বা ভেতরে রাখা।

পরের পর্বে: attribute পড়া বা বসানোর সময় চুপচাপ যাচাই চালানো যায় কীভাবে? @property, এক লাইনে ছোট “অংশ” class বানানোর @dataclass, আর এগুলোর পেছনের descriptor-এর এক ঝলক — পর্ব ৩২.

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

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