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

dunder method — Python-এর data model

Dunder Methods & the Python Data Model

Taught in Bangla, this lesson explains Python's data model: how len(), +, == and print() call special methods, __repr__ versus __str__, the __eq__ and __hash__ contract, ordering with total_ordering and NotImplemented, arithmetic operators like __add__ and __radd__, container methods, __call__ and __format__, and when operator overloading makes sense.

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

  • Python-এর built-in syntax কীভাবে dunder method-এ অনুবাদ হয়, আর কেন একে “data model” বলে
  • __repr__ বনাম __str__; print, REPL আর f-string-এ কোনটি কখন ডাকা হয়
  • __eq__ আর __hash__-এর চুক্তি, __lt__ আর functools.total_ordering, NotImplemented-এর ভূমিকা
  • __add__, __radd__, __iadd__, __mul__ — টাকার হিসাব আর গেমের Vector দিয়ে
  • __bool__, __len__, __getitem__, __contains__, __setitem__, __call__, __format__, আর কখন operator overload করা উচিত নয়

১ · len(x), x + y, print(x) — আসলে object-এর কাছে অনুরোধ

পর্ব ২৯-এ আপনি নিজের class বানিয়েছেন — __init__ দিয়ে object তৈরি, self দিয়ে তার নিজের ডেটা। সেখানে __repr__-এর এক ঝলকও দেখেছেন। কিন্তু বেশিরভাগ ক্ষেত্রে নিজের object এখনো “পর” লাগে। __repr__ না লিখলে print করলে একটি অস্পষ্ট লেখা আসে, যাতে class-এর নাম আর একটি মেমরি-ঠিকানা থাকে — ব্যবহারকারীর কোনো কাজে আসে না। দুটি object যোগ করতে চাইলে TypeError. দুটি একই রকম object-কে == দিয়ে তুলনা করলে False. অথচ Python-এর নিজের int, str, list এসব কাজ অনায়াসে করে। পার্থক্যটা কোথায়?

পার্থক্যটা কোনো জাদু নয়। Python-এর প্রতিটি বিশেষ syntax আসলে একটি নির্দিষ্ট নামের method-এর ডাক। আপনি যখন লেখেন len(playlist), Python আসলে করে type(playlist).__len__(playlist). a + b লিখলে করে type(a).__add__(a, b). print(x) লিখলে x-এর __str__ ডেকে লেখাটি আনে। এই বিশেষ method-গুলোর নামের দুই পাশে দুটি করে আন্ডারস্কোর থাকে, তাই এদের ডাকনাম dunder method — “double underscore”-এর সংক্ষেপ। অফিসিয়াল নাম special method; কেউ কেউ “magic method”-ও বলেন। পর্ব ১৯-এ +=-এর আচরণ বোঝাতে __iadd__-এর নাম এসেছিল, আর পর্ব ২৯-এ __init__ তো আপনি নিজেই লিখেছেন — দুটিই dunder.

ঢাকার একটি রেস্টুরেন্টে আপনি ওয়েটারকে বলেন “বিল দিন”। আপনি জানেন না রান্নাঘরে বিল কীভাবে হিসাব হয়, কোন খাতায় লেখা হয় — সেটা রেস্টুরেন্টের ব্যাপার। আপনি শুধু একটি সবার চেনা অনুরোধ করলেন। কাচ্চির দোকান, চায়ের স্টল, পাঁচতারা হোটেল — সবাই একই অনুরোধ বোঝে, কিন্তু প্রত্যেকে নিজের মতো করে পূরণ করে। Python-এর len(x) ঠিক এমন একটি অনুরোধ: “দৈর্ঘ্য কত?” x যদি list হয়, list নিজের মতো উত্তর দেয়; x যদি আপনার Playlist হয়, আপনার লেখা __len__ উত্তর দেয়। Python শুধু অনুরোধ পৌঁছে দেয়।

আপনি যা লেখেন → Python যা ডাকে

syntax হলো চেনা অনুরোধ; dunder method হলো তার উত্তর দেওয়ার জায়গা

  1. আপনার কোডa + bচেনা syntax
  2. Python খোঁজেtype(a)object-এর class-এ
  3. ডাকে__add__(a, b)আপনার লেখা method
  4. ফলনতুন objectযা ফেরত দিলেন
ছবি ৩০.১ — একই পথে চলে len, ==, <, [], in, for, bool, print — প্রায় সব built-in আচরণ।

এই অনুবাদের একটি ছোট তালিকা দেখুন। পুরো তালিকা অনেক বড় — Python-এর অফিসিয়াল Language Reference-এ “Data model” নামের একটি গোটা অধ্যায় এর জন্য — কিন্তু প্রতিদিনের কাজে এগুলোই লাগে:

আপনি যা লেখেনPython যা ডাকেকাজ
repr(x), REPL-এ xx.__repr__()ডেভেলপারের জন্য চেহারা
str(x), print(x), f"{x}"x.__str__()ব্যবহারকারীর জন্য চেহারা
x == y, x < yx.__eq__(y), x.__lt__(y)তুলনা
hash(x), set-এ রাখাx.__hash__()dict/set-এর চাবি
x + y, x * 3x.__add__(y), x.__mul__(3)অঙ্ক
len(x), bool(x)x.__len__(), x.__bool__()আকার, সত্য-মিথ্যা
x[i], x[i] = v, v in x__getitem__, __setitem__, __contains__container
x(5)x.__call__(5)object-কে ফাংশনের মতো ডাকা
মূল ধারণা

Python-এর data model হলো একটি চুক্তিপত্র: “আপনার object যদি এই নামের method দেয়, তাহলে এই syntax তার সাথে কাজ করবে।” এই চুক্তি মেনে চললে আপনার class built-in type-এর মতোই স্বাভাবিক লাগে — len, sorted, sum, in, for সব কিছু তার সাথে চলে, আর এদের কাউকে আপনার class-এর কথা আলাদা করে জানতে হয় না। এটাই duck typing-এর শক্তি: কে কোন class, তা নয় — কে কোন অনুরোধে সাড়া দেয়, সেটাই আসল।

একটি জরুরি সতর্কতা: dunder method আপনি নিজে সাধারণত ডাকবেন না. লিখবেন len(x), x.__len__() নয়; লিখবেন a + b, a.__add__(b) নয়। কারণ built-in ফাংশন আর operator শুধু dunder ডাকে না, তার চারপাশে বাড়তি কাজও করে — যেমন len() নিশ্চিত করে যে ফল অঋণাত্মক পূর্ণসংখ্যা, আর + দরকার হলে অন্য পক্ষের __radd__ চেষ্টা করে (অংশ ৫)। Dunder method হলো Python-এর জন্য লেখা, আপনার ডাকার জন্য নয়। আরেকটি নিয়ম: নিজে নতুন dunder নাম (__mything__) বানাবেন না — এই নামগুলো Python-এর নিজের জন্য সংরক্ষিত।

২ · নিজের চেহারা — __repr__ আর __str__

এই পর্বজুড়ে একটি বাস্তব class গড়ে তুলব: টাকার হিসাব রাখার Money. পর্ব ০৭-এ দেখেছিলেন, 0.1 + 0.2 ঠিক 0.3 হয় না — তাই টাকা float-এ রাখা বিপজ্জনক। bKash, ব্যাংক বা যেকোনো হিসাবের সফটওয়্যার সাধারণত সবচেয়ে ছোট একক — পয়সা — পূর্ণসংখ্যায় রাখে। Money(125050) মানে ১,২৫০ টাকা ৫০ পয়সা।

এমন একটি object-এর দুটি আলাদা “চেহারা” দরকার, কারণ দুই রকম মানুষ তাকে দেখে:

  • __repr__ — ডেভেলপারের জন্য। ডিবাগ করার সময়, লগে, REPL-এ। লক্ষ্য: দ্ব্যর্থহীন — দেখেই বোঝা যাবে এটি কোন type, ভেতরে ঠিক কী। সম্ভব হলে এমনভাবে লিখুন যাতে লেখাটি কপি করে Python-এ বসালে একই object তৈরি হয়: Money(125050). একে বলে “eval-যোগ্য” repr, কারণ eval() সেটিকে আবার object বানাতে পারত (যদিও বাস্তবে আমরা eval ব্যবহার করি না)।
  • __str__ — ব্যবহারকারীর জন্য। রসিদে, স্ক্রিনে, মেসেজে। লক্ষ্য: পড়তে সুন্দর — ৳1,250.50. এখানে type-এর নাম দরকার নেই।
class Money:
    def __init__(self, poisha: int):
        self.poisha = poisha

    def __repr__(self):                      # ডেভেলপারের জন্য
        return f"Money({self.poisha})"

    def __str__(self):                       # ব্যবহারকারীর জন্য
        return f"৳{self.poisha // 100:,}.{self.poisha % 100:02d}"

__str__-এ পর্ব ১০-এর format spec কাজে লেগেছে: :, হাজারের কমা বসায়, :02d পয়সাকে দুই অঙ্কে রাখে (৫ পয়সা হলে .05). এবার দেখুন কোন পরিস্থিতিতে Python কোনটি ডাকে। “পরে” চাপুন:

1m = Money(125050)2print(m)3m                     # REPL-এ লিখলে4print([m, Money(99)])5print(f"{m} / {m!r}")6# __str__ মুছে দিলে: print(m)
Python কোনটি ডাকল
ডাক
কেন
  1. তৈরি। Money(125050) শুধু __init__ ডাকে — object তৈরি হলো, কিছু ছাপা হয়নি। চেহারার method-গুলো ডাকা হবে কেবল যখন কেউ object-টিকে লেখায় রূপান্তর করতে চায়।
  2. print → __str__. print প্রতিটি argument-কে str() দিয়ে লেখায় বদলায়, আর str(m) ডাকে __str__. ফল ব্যবহারকারীর উপযোগী: ৳1,250.50.
  3. REPL → __repr__. Python-এর ইন্টারঅ্যাকটিভ প্রম্পটে শুধু m লিখে Enter চাপলে REPL ফলটি দেখায় repr() দিয়ে — কারণ REPL-এ যিনি বসে আছেন তিনি ডেভেলপার, তাঁর জানা দরকার এটি আসলে কী। তাই Money(125050), টাকার চিহ্ন নয়।
  4. Container → ভেতরের সবার __repr__. List ছাপালে list নিজের __str__ দেয়, কিন্তু সে ভেতরের উপাদানগুলোর জন্য সবসময় repr ব্যবহার করে। কারণ: str হলে ["1, 2"] আর [1, 2] ছাপায় একই রকম দেখাত — দ্ব্যর্থতা। তাই __repr__ না লিখলে আপনার object list-এর ভেতরে অপাঠ্য দেখাবে, __str__ যত সুন্দরই হোক।
  5. f-string-এ দুটোই। {m} ডিফল্টে format() হয়ে শেষ পর্যন্ত __str__ ডাকে; {m!r} লিখলে জোর করে repr(). ডিবাগের জন্য Python 3.8 থেকে আরও সহজ রূপ আছে: f"{m=}" লিখলে ছাপে m=Money(125050) — নামসহ repr.
  6. __str__ না থাকলে? সব class-এর পূর্বপুরুষ object-এর ডিফল্ট __str__ নিজেই __repr__ ডাকে। তাই শুধু __repr__ লিখলে দুই জায়গাতেই সেটি চলে। উল্টোটা সত্য নয়: শুধু __str__ লিখলে REPL আর list-এ আবার সেই অস্পষ্ট ডিফল্ট লেখা ফিরে আসে। এজন্য নিয়ম: আগে __repr__, দরকার হলে __str__.

সব repr কি eval-যোগ্য হয়? না, আর জোর করারও দরকার নেই। খোলা ফাইল বা ডেটাবেস connection-এর মতো object-কে লেখা থেকে আবার তৈরি করা যায় না। তখন Python-এর প্রচলিত রীতি হলো কোণ-বন্ধনীর ভেতরে একটি তথ্যপূর্ণ বর্ণনা: যেমন <Playlist 'Road trip': 3 songs>. কোণ-বন্ধনী দেখলেই পাঠক বোঝেন, “এটি কোড নয়, বর্ণনা”। মূল কথা দ্ব্যর্থহীনতা — ডিবাগের সময় repr দেখে যেন বোঝা যায় ঠিক কোন object.

৩ · সমান মানে কী? — __eq__ আর __hash__-এর চুক্তি

পর্ব ০৮-এ দেখেছিলেন is আর == আলাদা: is জিজ্ঞেস করে “একই object?” (identity), == জিজ্ঞেস করে “একই মান?” (equality). কিন্তু “একই মান” মানে কী, তা Python জানবে কীভাবে? আপনার Money-র কোন অংশ মিললে দুটি object সমান? এই প্রশ্নের উত্তর দেয় __eq__. আপনি না লিখলে object-এর ডিফল্ট __eq__ কাজ করে, যা শুধু identity দেখে — অর্থাৎ Money(500) == Money(500) হয় False, কারণ দুটি আলাদা object.

    def __eq__(self, other):
        if not isinstance(other, Money):
            return NotImplemented            # “আমি জানি না” — error নয়
        return self.poisha == other.poisha

লক্ষ করুন, অচেনা type পেলে আমরা False ফেরত দিইনি, NotImplemented দিয়েছি। এর রহস্য অংশ ৪-এ। আপাতত আরেকটি অদ্ভুত ঘটনা — যা AI-এর লেখা কোডে সবচেয়ে বেশি দেখা যায়। __eq__ লেখার সাথে সাথে আপনার object set-এ রাখা যায় না! কেন, তা ধাপে ধাপে দেখুন:

1a, b = Money(500), Money(500)2print(a == b, len({a, b}))3# class-এ __eq__ যোগ করা হলো4print(Money.__hash__)5s = {a, b}6# __hash__ যোগ: hash(self.poisha)7print(a == b, len({a, b}))8s = {a}; a.poisha = 7009print(a in s)
class-এর অবস্থা
__eq__
__hash__
a, b
  1. কোনো __eq__ নেই। ডিফল্ট নিয়মে সমতা মানে identity, আর hash-ও আসে object-এর identity থেকে। a আর b দুটি আলাদা object, তাই a == b হলো False, আর set-এ দুটিই আলাদা উপাদান — দৈর্ঘ্য 2. একই টাকা দুবার গোনা হলো।
  2. __eq__ যোগ করলেন। এখন a == b সত্য হবে। কিন্তু চুপিসারে Python class-এর __hash__-কে None বসিয়ে দিয়েছে। কেন? পুরোনো identity-ভিত্তিক hash এখন আপনার নতুন সমতার সাথে মেলে না: a == b অথচ hash(a) != hash(b) — এটি hash table-এর মূল নিয়ম ভাঙে। ভুল উত্তর দেওয়ার চেয়ে Python উত্তর দিতেই অস্বীকার করে।
  3. Set-এ রাখা ব্যর্থ। set বা dict-এ রাখতে hash লাগে, আর __hash__ এখন None — অর্থাৎ object টি আর hashable নয়। ফল TypeError. Python 3.14-এ বার্তাটি এমন; পুরোনো version-এ শুধু unhashable type: 'Money'.
  4. সঠিক __hash__ যোগ করলেন। def __hash__(self): return hash(self.poisha). চুক্তি: যে দুটি object ==, তাদের hash অবশ্যই সমান. যে ফিল্ড দিয়ে সমতা মাপেন, ঠিক সেই ফিল্ড দিয়েই hash বানান। একাধিক ফিল্ড হলে hash((self.x, self.y)) — একটি tuple-এর hash. এখন a == b, আর set-এ মাত্র একটি উপাদান।
  5. Set-এ রাখার পরে বদলালে বিপদ। a set-এ ঢোকার সময় তার hash ছিল 500-এর hash, সেই অনুযায়ী set তাকে একটি নির্দিষ্ট খোপে রেখেছে। তারপর a.poisha = 700 — এখন a-এর hash অন্য, set তাকে ভুল খোপে খোঁজে: a in s হলো False, যদিও a ঠিক সেখানেই বসে আছে! এজন্যই hashable object-এর সমতার ফিল্ড বদলানো উচিত নয় — এজন্যই list hashable নয়, কিন্তু tuple hashable.
মূল ধারণা

দুটি চুক্তি মনে রাখুন। এক: a == b হলে hash(a) == hash(b) (উল্টোটা জরুরি নয় — আলাদা object-এর hash মিলে যেতেই পারে)। দুই: যে object hashable, তার hash জীবনভর বদলাবে না — তাই সমতার ফিল্ডগুলো কার্যত immutable হতে হবে। টাকা, তারিখ, স্থানাঙ্কের মতো “মান” বোঝায় এমন object-কে বলে value object; এদের সাধারণত immutable আর hashable বানানো হয়। আর ব্যাংক অ্যাকাউন্টের মতো যে object-এর অবস্থা বদলায়, তার জন্য __eq__ না লিখে identity-ভিত্তিক ডিফল্টে থাকাই প্রায়ই ঠিক। পর্ব ৩২-এ @dataclass(frozen=True) দেখবেন, যা এই দুই চুক্তি নিজে থেকে মেনে চলে।

৪ · ছোট-বড় — __lt__, total_ordering আর NotImplemented

== ছাড়াও তুলনার আরও পাঁচটি operator আছে: !=, <, <=, >, >=. এদের dunder যথাক্রমে __ne__, __lt__ (less than), __le__, __gt__, __ge__. এই ছয়টিকে একসাথে বলে rich comparison. sorted(), min(), max(), list.sort() — সবাই ভেতরে < ব্যবহার করে। তাই __lt__ লিখলেই আপনার object সাজানো যায়।

একটি তাপমাত্রার class দেখুন — আবহাওয়া অ্যাপের জন্য, যেখানে বিভিন্ন জেলার তাপমাত্রা সাজাতে হবে। ছয়টি method হাতে লেখা ক্লান্তিকর আর ভুলপ্রবণ। functools module-এর total_ordering decorator এই কাজ সহজ করে: আপনি শুধু __eq__ আর একটি (যেমন __lt__) লিখুন, বাকি তিনটি সে নিজে বানিয়ে দেবে (__ne__ তো Python এমনিতেই __eq__ থেকে বের করে):

from functools import total_ordering

@total_ordering
class Temperature:
    def __init__(self, celsius: float):
        self.celsius = celsius
    def __repr__(self):
        return f"Temperature({self.celsius})"
    def __eq__(self, other):
        if not isinstance(other, Temperature):
            return NotImplemented
        return self.celsius == other.celsius
    def __lt__(self, other):
        if not isinstance(other, Temperature):
            return NotImplemented
        return self.celsius < other.celsius

days = [Temperature(30), Temperature(-2), Temperature(18)]
print(sorted(days), max(days), Temperature(5) >= Temperature(5))
# [Temperature(-2), Temperature(18), Temperature(30)] Temperature(30) True

এখানে __ge__ আমরা লিখিনি, তবু >= কাজ করল — total_ordering সেটি __lt__ আর __eq__ থেকে বানিয়েছে। functools.total_ordering Python 3.2 থেকে আছে। একটি ছোট দাম আছে: বানানো method-গুলো এক ধাপ ঘুরে কাজ করে, তাই হাতে লেখা method-এর চেয়ে সামান্য ধীর — লাখো তুলনার সময়ই শুধু চোখে পড়ে।

NotImplemented — “আমি জানি না, অন্যজনকে জিজ্ঞেস করো”

এবার সেই রহস্য। Temperature(30) < 25 লিখলে কী হওয়া উচিত? ২৫ কি সেলসিয়াস, নাকি ফারেনহাইট? আমাদের class জানে না। তিনটি সম্ভাব্য উত্তর:

  • False ফেরত দেওয়া — মিথ্যা কথা। ভুল তুলনা নিঃশব্দে পার হয়ে যায়।
  • নিজে raise TypeError — অন্য পক্ষকে সুযোগই দেওয়া হলো না। হয়তো অন্য type-টি আপনার class চেনে আর তুলনা করতে জানে।
  • NotImplemented ফেরত দেওয়া — সঠিক পথ। এটি একটি বিশেষ built-in মান (একটি singleton, None-এর মতো), যার মানে: “এই type-এর সাথে কাজটা আমি জানি না।” Python তখন অন্য পক্ষের উল্টো method চেষ্টা করে — a < b-এর জন্য b.__gt__(a). দুজনেই NotImplemented দিলে তবেই Python নিজে TypeError: '<' not supported between instances of 'Temperature' and 'int' তোলে — পরিষ্কার, সৎ বার্তা।

দুটি নাম, দুটি ভিন্ন জিনিস: NotImplemented একটি মান — return করবেন, binary dunder থেকে। NotImplementedError একটি exception — raise করবেন, যখন কোনো method “এখনো লেখা হয়নি” বা subclass-কে লিখতে হবে (পর্ব ৩৩)। raise NotImplemented লিখলে উল্টো TypeError আসে, কারণ এটি exception নয়। আর ==-এর ক্ষেত্রে দুই পক্ষই NotImplemented দিলে error হয় না — Python শেষে identity (is) দেখে। তাই Temperature(1) == 5 নিঃশব্দে False, কিন্তু Temperature(1) < 5 হলো TypeError.

আরেকটি সূক্ষ্ম ফাঁদ: NotImplemented-কে কখনো if-এ সত্য-মিথ্যা হিসেবে ব্যবহার করবেন না। Python 3.9 থেকে এতে সতর্কবার্তা আসত, আর Python 3.14-এ এটি সরাসরি TypeError. তাই if self.__eq__(other): জাতীয় কোড — যা NotImplemented ফেরত পেতে পারে — এখন ভেঙে যায়। আবারও সেই নিয়ম: dunder নিজে না ডেকে == লিখুন।

৫ · অঙ্কের operator — __add__, __radd__, __iadd__, __mul__

নিজের class-এ +, -, *-এর মতো operator-এর অর্থ দেওয়াকে বলে operator overloading. Money + Money এর আদর্শ উদাহরণ: দুটি টাকা যোগ করলে টাকা — অর্থ স্বতঃস্পষ্ট। __add__ একটি নতুন object ফেরত দেয়, কোনো পক্ষকে বদলায় না — ঠিক যেমন 3 + 4 লিখলে 3 বদলে যায় না।

কিন্তু a + b-এ যদি a আপনার object না হয়? যেমন 0 + m. বাঁ পাশে int, যে Money-র নামও শোনেনি। এখানেই NotImplemented আর reflected method-এর খেলা। __radd__-এর r মানে reflected বা right — “আমি ডান পাশে আছি, বাঁ পাশের জন যোগ করতে পারেনি”। Python-এর সিদ্ধান্তের পথ ধাপে ধাপে দেখুন:

1m = Money(2000)2m + Money(99)30 + m4"৳" + m
Python-এর চেষ্টা
ডাক
ফল
  1. সহজ ক্ষেত্র। m + Money(99): Python প্রথমে বাঁ পাশের object-এর type দেখে — Money — আর ডাকে Money.__add__(m, Money(99)). অন্য পক্ষও Money, তাই নতুন Money(2099) ফেরত এল। কাজ শেষ।
  2. বাঁ পাশ জানে না। 0 + m: বাঁ পাশে int, তাই প্রথম চেষ্টা int.__add__(0, m). int জানে শুধু সংখ্যা যোগ করতে; Money দেখে সে error তোলে না, ভদ্রভাবে NotImplemented ফেরত দেয়। Python থামে না।
  3. ডান পাশকে সুযোগ। এবার Python ডান পাশের type-এ খোঁজে উল্টো method: Money.__radd__(m, 0). লক্ষ করুন argument-এর ক্রম — self হলো m, other হলো 0. আমাদের __radd__ বলে, “অন্যজন 0 হলে আমি নিজেই ফল”। তাই ফল m.
  4. অর্থহীন যোগ। "৳" + m: str-এর পক্ষে Money চেনা সম্ভব নয়, তাই Python Money.__radd__(m, "৳") চেষ্টা করে। other 0 নয়, তাই আমাদের method-ও NotImplemented দেয় — নিজে error তোলে না।
  5. দুজনেই অস্বীকার → TypeError. কেউ কাজটা নিল না, তাই Python নিজে TypeError তোলে (str-এর ক্ষেত্রে বার্তাটি জোড়া লাগানোর ভাষায়; অন্য type-এ সাধারণত unsupported operand type(s) for +). এটাই সঠিক আচরণ: টাকার চিহ্নের সাথে Money “যোগ” করার কোনো স্পষ্ট অর্থ নেই, তাই error-ই সৎ উত্তর।

একটি বিশেষ নিয়ম জেনে রাখুন: ডান পাশের object যদি বাঁ পাশের type-এর subclass হয় আর নিজের __radd__ দেয়, তাহলে Python তার __radd__-কে আগে সুযোগ দেয়। এতে subclass তার parent-এর আচরণ ঠিক করে নিতে পারে (পর্ব ৩১-এ inheritance).

sum() কেন __radd__ চায়

বাস্তবে 0 + m কেউ লেখে না। কিন্তু sum(bill) সবাই লেখে — আর sum() ভেতরে ঠিক এটাই করে। পর্ব ১২-এর accumulator ধাঁচ মনে করুন: sum() একটি শুরুর মান নেয়, যার ডিফল্ট 0, তারপর একে একে যোগ করে:

1bill = [Money(125050), Money(2000), Money(99)]2total = 0                  # sum()-এর start3for m in bill:4    total = total + m5print(total)
ভেতরের অবস্থা
total
ডাক
  1. শুরু 0 দিয়ে। sum()-এর start-এর ডিফল্ট মান পূর্ণসংখ্যা 0. Python জানে না আপনি টাকা যোগ করছেন; সে সবসময় 0 থেকেই শুরু করে।
  2. প্রথম যোগ: 0 + m1. ঠিক আগের stepper-এর ঘটনা: int.__add__ দেয় NotImplemented, তারপর Money.__radd__(m1, 0) দেয় m1. এখন total একটি Money.
  3. দ্বিতীয় যোগ। এবার দুই পাশেই Money — সরাসরি Money.__add__, ফল Money(127050). __radd__-এর দরকার শুধু প্রথম ধাপে ছিল।
  4. তৃতীয় যোগ। একইভাবে Money(127149). প্রতিটি যোগে নতুন object তৈরি হচ্ছে; কোনো Money বদলানো হয়নি।
  5. ছাপা। print ডাকে __str__: ৳1,271.49. Built-in sum() আপনার class সম্পর্কে কিছুই জানে না, তবু কাজ করল — কারণ আপনি data model-এর চুক্তি মেনেছেন।
  6. __radd__ না থাকলে? প্রথম ধাপেই সব থেমে যায়: int অস্বীকার করে, Money-র উল্টো method নেই — TypeError. বিকল্প হলো sum(bill, Money(0)) লিখে start বদলে দেওয়া, কিন্তু প্রতিটি ব্যবহারকারীকে সেটা মনে রাখতে হবে। __radd__-এ শুধু 0 গ্রহণ করাই ভদ্র সমাধান।

এবার এ পর্যন্ত সব একসাথে — একটি সম্পূর্ণ Money. চালানোর আগে অনুমান করুন: দ্বিতীয় লাইনে set-এর দৈর্ঘ্য কত হবে?

class Money:
    def __init__(self, poisha: int):
        self.poisha = poisha              # পয়সায় int, float নয়
    def __repr__(self):
        return f"Money({self.poisha})"
    def __str__(self):
        return f"৳{self.poisha // 100:,}.{self.poisha % 100:02d}"
    def __eq__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        return self.poisha == other.poisha
    def __hash__(self):                   # == হলে hash-ও সমান
        return hash(self.poisha)
    def __add__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        return Money(self.poisha + other.poisha)
    def __radd__(self, other):            # sum() শুরু করে 0 দিয়ে
        return self if other == 0 else NotImplemented
bill = [Money(125050), Money(2000), Money(99)]
print(bill, sum(bill), f"{sum(bill)!r}")
print(Money(500) == Money(500), len({Money(500), Money(500)}))

আউটপুট দুই লাইনের। প্রথম লাইন [Money(125050), Money(2000), Money(99)] ৳1,271.49 Money(127149) — list ছাপায় ভেতরের প্রতিটির __repr__ এসেছে; sum(bill) প্রথমে __radd__, তারপর দুবার __add__ চালিয়ে একটি Money দিয়েছে, যা print __str__ দিয়ে দেখাল; !r সেই একই ফলের repr দেখাল। দ্বিতীয় লাইন True 1 — __eq__ poisha মেলায়, তাই দুটি আলাদা object সমান; আর __hash__ একই poisha থেকে একই hash দেয়, তাই set দুটিকে একটিই উপাদান ধরেছে। লক্ষ করুন, ডিফল্ট repr-এর মেমরি-ঠিকানাওয়ালা লেখা কোথাও আসেনি।

__iadd__ বনাম __add__, আর সংখ্যা দিয়ে গুণ

total += m লিখলে Python প্রথমে খোঁজে __iadd__ (in-place add). এটি থাকলে object-টিকে নিজের জায়গায় বদলাতে পারে, তারপর নিজেকেই ফেরত দেয়। না থাকলে Python total = total + m-এ ফিরে যায় — নতুন object তৈরি, তারপর নামটি নতুন object-এ লাগানো। পর্ব ১৯-এ ঠিক এটাই দেখেছিলেন: list-এর __iadd__ আছে, তাই a += [2] একই list বদলায় আর তার সব alias সেই বদল দেখে; tuple-এর নেই, তাই t += (2,) নতুন tuple বানায়। আমাদের Money একটি value object — তাই এতে __iadd__ লিখব না. += তখন __add__ ব্যবহার করে নতুন object দেবে, আর কোথাও কারও হাতে থাকা পুরোনো Money চুপচাপ বদলে যাবে না। set-এ রাখা object-এর ক্ষেত্রে এটা জীবন বাঁচায় (অংশ ৩-এর শেষ ধাপ মনে করুন)।

এবার একটি সহজ 2D গেমের কথা ভাবুন — একটি চরিত্র মাঠে দৌড়ায়, তার অবস্থান আর বেগ দুটোই (x, y) জোড়া। গণিতে vector যোগ আর সংখ্যা দিয়ে গুণ সবার চেনা, তাই এখানে operator overloading পুরোপুরি স্বাভাবিক:

class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"
    def __add__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        return Vector(self.x + other.x, self.y + other.y)
    def __mul__(self, k):                    # vector × সংখ্যা
        if not isinstance(k, (int, float)):
            return NotImplemented
        return Vector(self.x * k, self.y * k)
    __rmul__ = __mul__                       # সংখ্যা × vector একই
    def __abs__(self):                       # abs(v) = দৈর্ঘ্য
        return (self.x ** 2 + self.y ** 2) ** 0.5
    def __bool__(self):                      # (0, 0) হলে False
        return bool(self.x or self.y)

pos, speed = Vector(3, 4), Vector(1, 1)
print(pos + speed, speed * 2, 2 * speed, abs(pos), bool(Vector(0, 0)))
# Vector(4, 5) Vector(2, 2) Vector(2, 2) 5.0 False

__rmul__ = __mul__ লাইনটি খেয়াল করুন: সংখ্যা দিয়ে গুণ দুই দিক থেকেই একই, তাই একই ফাংশনকে দুটি নাম দিলাম। কিন্তু __add__-এ এমনটা সবসময় চলে না — যেমন string জোড়া লাগানোয় "a" + "b" আর "b" + "a" আলাদা। আর Vector * Vector-এর জন্য আমরা ইচ্ছা করে কিছু দিইনি: dot product না cross product — অর্থ অস্পষ্ট, তাই NotImplemented আর শেষে TypeError. অস্পষ্ট হলে নাম দেওয়া method (v.dot(w)) অনেক ভালো। (NumPy এই কারণেই matrix গুণের জন্য আলাদা @ operator ব্যবহার করে, যার dunder __matmul__ — Python 3.5 থেকে।)

৬ · সত্য-মিথ্যা আর container — __bool__, __len__, __getitem__, __contains__

পর্ব ০৬-এ truthiness দেখেছিলেন: খালি list, 0, "" হলো falsy. আপনার object if obj:-এ কী হবে? Python এই ক্রমে খোঁজে:

  1. class-এ __bool__ থাকলে সেটি ডাকে — True বা False ফেরত দিতে হবে।
  2. না থাকলে __len__ খোঁজে — দৈর্ঘ্য 0 হলে False, নইলে True.
  3. কোনোটিই না থাকলে object সবসময় True.

তাই container-ধর্মী class-এ আলাদা __bool__ সাধারণত লাগে না — __len__-ই যথেষ্ট, খালি হলে নিজে থেকেই falsy.

এবার একটি গানের Playlist. আমরা চাই এটি list-এর মতো ব্যবহার করা যাক — len, ইনডেক্স, slicing, in, for — কিন্তু নিজস্ব নিয়মসহ: গান খোঁজা হবে বড়-ছোট হাতের অক্ষর না মেনে। কোন কোন অনুরোধে সাড়া দিলে কোন কোন আচরণ পাওয়া যায়, তার একটি পরিচিত নাম আছে: protocol. Container protocol-এর মূল method তিনটি — __len__, __getitem__ আর __contains__:

class Playlist:
    def __init__(self, name, songs):
        self.name = name
        self._songs = list(songs)
    def __repr__(self):
        return f"<Playlist {self.name!r}: {len(self)} songs>"
    def __len__(self):
        return len(self._songs)
    def __getitem__(self, index):         # road[0], road[-1], road[1:]
        return self._songs[index]
    def __contains__(self, title):        # "..." in road
        return title.casefold() in (s.casefold() for s in self._songs)

road = Playlist("Road trip", ["Ekla Cholo Re", "Purano Sei Diner Kotha", "Tumi Robe Nirobe"])
print(road, len(road), road[0], road[-1], sep=" | ")
print(road[1:], "EKLA CHOLO RE" in road, "Shada" in road)
for i, song in enumerate(road, start=1):
    print(i, song)
print(bool(road), bool(Playlist("খালি", [])))

আউটপুট ছয় লাইনের। প্রথম লাইন <Playlist 'Road trip': 3 songs> | 3 | Ekla Cholo Re | Tumi Robe Nirobe — কোণ-বন্ধনীর repr, __len__ থেকে 3, আর __getitem__ list-এর কাছে ইনডেক্স পাঠিয়ে দেওয়ায় ঋণাত্মক ইনডেক্স -1-ও কাজ করেছে। দ্বিতীয় লাইন ['Purano Sei Diner Kotha', 'Tumi Robe Nirobe'] True False — road[1:]-এ __getitem__ পেয়েছে একটি slice object, আর list সেটিও বোঝে; in আমাদের __contains__ ডেকেছে, তাই বড় হাতের "EKLA CHOLO RE"-ও মিলেছে। পরের তিন লাইন 1 Ekla Cholo Re, 2 Purano Sei Diner Kotha, 3 Tumi Robe Nirobe — আমরা __iter__ লিখিনি, তবু for চলল! শেষ লাইন True False — __bool__ নেই, তাই __len__ দেখে খালি Playlist falsy.

for কীভাবে চলল? Python-এর একটি পুরোনো নিয়ম: class-এ __iter__ না থাকলে কিন্তু __getitem__ থাকলে, for লুপ obj[0], obj[1], obj[2]… ডাকতে থাকে, যতক্ষণ না IndexError আসে — সেটাই “শেষ” সংকেত। একে বলে পুরোনো sequence protocol. __contains__ না থাকলে in-ও একইভাবে একে একে ঘুরে == দিয়ে খুঁজত। আধুনিক কোডে iterable বানানোর মূল পথ __iter__ আর __next__ — সেটা পর্ব ৩৪-এর বিষয়। এখানে শুধু জেনে রাখুন, __getitem__ একাই অনেক দূর নিয়ে যায়।

Playlist-এ গান বদলাতে চাইলে __setitem__ যোগ করুন: road[0] = "Amar Shonar Bangla" তখন type(road).__setitem__(road, 0, "Amar Shonar Bangla") ডাকবে। এর ভাই __delitem__ চালায় del road[0]. কিন্তু খেয়াল করুন: এগুলো যোগ করলে Playlist mutable হয়ে যায় — তখন অংশ ৩-এর নিয়ম অনুযায়ী এর জন্য সমতাভিত্তিক __hash__ লেখা চলবে না।

সব কিছু হাতে লিখতে হয় না: নিজের container বানানোর সময় শুরু থেকে সব dunder লেখার বদলে Python-এর collections.abc module-এর তৈরি কাঠামো ব্যবহার করা যায় — যেমন Sequence-কে parent হিসেবে নিয়ে শুধু __len__ আর __getitem__ লিখলে __contains__, __iter__, index(), count() নিজে থেকে চলে আসে। এর বিস্তারিত পর্ব ৩৩-এ।

৭ · __call__, __format__ — আর Python dunder খোঁজে কোথায়

__call__ — ফাংশনের মতো ডাকা যায় এমন object

পর্ব ২০-এ closure দিয়ে VAT-এর factory বানিয়েছিলেন — একটি ফাংশন যা হার মনে রাখে। একই কাজ object দিয়েও হয়: class-এ __call__ লিখলে তার object-কে ফাংশনের মতো বন্ধনী দিয়ে ডাকা যায়। এমন যেকোনো কিছুকে Python বলে callable; callable(x) দিয়ে যাচাই করা যায়।

class Tax:
    def __init__(self, rate):
        self.rate = rate
    def __call__(self, amount):
        return round(amount * (1 + self.rate))

vat = Tax(0.15)
print(vat(1000), callable(vat), vat.rate)    # 1150 True 0.15

Closure-এর তুলনায় সুবিধা: vat.rate বাইরে থেকে দেখা ও পরীক্ষা করা যায়, আর পরে আরও method যোগ করা যায়। আসলে Python-এ ফাংশনও একটি object, যার type-এ __call__ আছে — আর class নিজেও callable: Money(500) লেখাটাই একটি ডাক, যা নতুন object বানায়। পর্ব ৩৫-এ decorator-এ এই ধারণা আবার কাজে লাগবে।

__format__ — f-string-এর কোলনের পরের অংশ

f"{x:,.2f}" লিখলে Python ডাকে type(x).__format__(x, ",.2f") — কোলনের পরের লেখাটি argument হিসেবে যায়। ডিফল্ট __format__ খালি spec পেলে str(x) দেয়, অন্য কিছু পেলে TypeError. চাইলে নিজের spec বানাতে পারেন — যেমন Money-তে f"{m:bn}" দিলে 1,250 টাকা 50 পয়সা, আর অন্য spec হলে টাকার সংখ্যাটির ওপর format() চালিয়ে দেওয়া। খুব দরকার না হলে এটি লিখবেন না; __str__-ই বেশিরভাগ কাজ সারে।

Python dunder খোঁজে type-এ, object-এর গায়ে নয়

একটি সূক্ষ্ম কিন্তু গুরুত্বপূর্ণ নিয়ম। সাধারণ attribute (m.poisha) খোঁজার সময় Python আগে object-এর নিজের ডেটা দেখে, তারপর class. কিন্তু syntax যখন নিজে থেকে dunder ডাকে (len(x), x + y), Python সরাসরি type-এ খোঁজে — object-এর গায়ে লাগানো কিছু দেখে না। পরীক্ষা করে দেখুন:

class Box:
    def __len__(self):
        return 5

b = Box()
b.__len__ = lambda: 99         # object-এর গায়ে আলাদা __len__
print(len(b), b.__len__())     # 5 99

len(b) দিল 5 — class-এর method; b.__len__() সাধারণ attribute হিসেবে খোঁজায় পেল object-এর গায়ের lambda, তাই 99. কেন এই নিয়ম? প্রথম কারণ গতি: CPython-এ প্রতিটি type-এর ভেতরে C ভাষার একটি কাঠামো আছে, যার নির্দিষ্ট ঘরে (slot) সরাসরি “যোগের ফাংশন”, “দৈর্ঘ্যের ফাংশন” বসানো থাকে। a + b চালাতে Python শুধু সেই ঘরটি দেখে — কোনো dict খোঁজা নেই। দ্বিতীয় কারণ সঠিকতা: class নিজেও একটি object; hash(Money) ডাকলে Python যদি Money.__hash__ খুঁজত, তাহলে পেত instance-এর জন্য লেখা method — ভুল জিনিস। Type-এ খুঁজলে Money-র type (type) থেকে সঠিক উত্তর আসে। ব্যবহারিক শিক্ষা: dunder সবসময় class-এর ভেতরে def দিয়ে লিখুন, চালানোর সময় object-এ বসাবেন না।

৮ · Operator overloading-এর আদব — কখন হ্যাঁ, কখন না

Dunder method শক্তিশালী, আর শক্তির অপব্যবহারও সহজ। একটি সোজা পরীক্ষা: কোড পড়ে একজন নতুন সহকর্মী কি operator-এর অর্থ না জেনেই সঠিক অনুমান করতে পারবেন? পারলে overload করুন; না পারলে নাম দেওয়া method লিখুন।

উদাহরণরায়কেন
Money + Money, Money < Moneyহ্যাঁটাকা যোগ আর তুলনার অর্থ সবার জানা
Vector + Vector, Vector * 2হ্যাঁগণিতের প্রচলিত অর্থ
Path / "data.csv" (pathlib)হ্যাঁ, প্রচলিত হলেফোল্ডার-বিভাজক /-এর সাথে মেলে; পর্ব ২৫-এ দেখেছেন
Money * Moneyনা“টাকা × টাকা” মানে কী? এমন একক বাস্তবে নেই
Student + Studentনাদুজন ছাত্র “যোগ” করে কী হয় — দল? নতুন ছাত্র? Team([s1, s2]) লিখুন
order - item দিয়ে অর্ডার থেকে পণ্য বাদ, যা অর্ডারটি বদলে দেয়না- সাধারণত নতুন object দেয়; বদলানোর জন্য order.remove(item)

আরও কয়েকটি আদব:

  • প্রত্যাশিত নিয়ম মানুন। যোগ থাকলে a + b == b + a আশা করা হয় (string ছাড়া); == থাকলে a == a সবসময় সত্য; __lt__ থাকলে a < b আর b < a একসাথে সত্য হবে না। এগুলো ভাঙলে sorted বা set নিঃশব্দে ভুল ফল দিতে পারে।
  • __add__ কোনো পক্ষ বদলাবে না। নতুন object ফেরত দিন। বদলানোর কাজ __iadd__-এর, তাও শুধু mutable object-এ।
  • অচেনা type → NotImplemented. নিজে error তুলবেন না, False দেবেন না।
  • ফেরত type ঠিক রাখুন। __len__ অঋণাত্মক int, __bool__ bool, __repr__ ও __str__ str ফেরত দেবে — নইলে TypeError.
C/C++ জানলে

C-তে operator overloading একেবারেই নেই; C++-এ operator+ লিখে একই কাজ হয়, আর operator<< দিয়ে std::cout-এ ছাপা অনেকটা Python-এর __str__-এর মতো। বড় পার্থক্য হলো সিদ্ধান্তের সময়: C++ কোন operator+ চলবে তা compile-এর সময় type দেখে ঠিক করে (overload resolution), আর Python ঠিক করে চালানোর সময়, NotImplemented আর __radd__-এর পালা ধরে। C++-এ 0 + money চালাতে একটি free function লিখতে হয়; C++20-এর operator<=> (spaceship) একটি method থেকে সব তুলনা বানায় — total_ordering-এর ভাবনার কাছাকাছি। বিস্তারিত দেখুন C++-এর operator overloading পর্বে।

পরীক্ষা ও ইন্টারভিউতে
  • __repr__ আর __str__-এর পার্থক্য? — repr ডেভেলপারের জন্য, দ্ব্যর্থহীন (সম্ভব হলে কোডের মতো); str ব্যবহারকারীর জন্য, সুন্দর। str না থাকলে repr ব্যবহার হয়, উল্টোটা নয়।
  • __eq__ লিখে __hash__ না লিখলে কী হয়? — Python __hash__-কে None করে দেয়; object set বা dict-এর চাবিতে রাখা যায় না।
  • NotImplemented আর NotImplementedError-এর পার্থক্য? — প্রথমটি binary dunder থেকে return করার মান (“অন্যজনকে চেষ্টা করো”); দ্বিতীয়টি raise করার exception (“এখনো লেখা হয়নি”)।
  • sum() নিজের class-এ চালাতে কী লাগে? — __add__ আর 0 গ্রহণ করে এমন __radd__, কারণ sum() শুরু করে 0 দিয়ে।
  • if obj: কীভাবে সিদ্ধান্ত নেয়? — আগে __bool__, না থাকলে __len__ (0 হলে False), কোনোটি না থাকলে সবসময় True.

৯ · AI-এর যুগে — dunder method-এর ভুল ধরা

AI-কে “একটি Money class লিখে দাও” বললে সাধারণত চলনসই কোড আসে, আর সাধারণ পরীক্ষায় সব ঠিক দেখায়। এই পর্বের ভুলগুলো ধরা পড়ে পরে — যখন object-কে set-এ রাখতে যান, sum() চালান, বা অন্য type-এর সাথে তুলনা হয়। AI-এর প্রশিক্ষণের কোডে এই ধাঁচগুলো প্রচুর, তাই AI-ও সেগুলো নকল করে:

AI প্রায়ই যা করেসমস্যাকী চাইবেন
__eq__ লেখে, __hash__ লেখে নাobject set/dict-এ যায় না; পরে হঠাৎ TypeErrorএকই ফিল্ড দিয়ে __hash__, আর ফিল্ডগুলো বদলানো যাবে না
অচেনা type-এ raise TypeError বা return False__radd__/উল্টো তুলনার সুযোগ নষ্ট; sum() ভাঙেreturn NotImplemented
শুধু __str__, কোনো __repr__ নেইলগে, list-এ, ডিবাগারে অপাঠ্য ডিফল্ট লেখাআগে __repr__, দরকার হলে __str__
অদ্ভুত অর্থে operator (cart + item দিয়ে cart বদলানো)পাঠক বিভ্রান্ত; + পুরোনো object বদলে দেয়নাম দেওয়া method (cart.add(item))
টাকা float-এ রাখেপর্ব ০৭-এর rounding ভুলপয়সায় int, বা Decimal
__radd__ নেইsum(list_of_money) চলে না0 গ্রহণ করে এমন __radd__

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

আমি Python 3.12+ ব্যবহার করি। একটি দোকানের হিসাবের জন্য Money class লিখে দাও, যা টাকা পয়সায় int হিসেবে রাখে। শর্ত: ১. __repr__ অবশ্যই লিখবে, যা Money(125050) ধাঁচে eval-যোগ্য; আলাদা __str__ দেখাবে ৳1,250.50. ২. __eq__ লিখলে একই ফিল্ড দিয়ে __hash__-ও লিখবে, আর ব্যাখ্যা করবে কেন object-টি বদলানো যাবে না। ৩. তুলনার জন্য functools.total_ordering ব্যবহার করো। ৪. অচেনা type পেলে প্রতিটি binary dunder থেকে NotImplemented return করবে — raise করবে না, False দেবে না। ৫. __add__ আর __radd__ দাও যাতে sum(list_of_money) চলে; Money * Money বা Money + int-এর মতো অর্থহীন operator দেবে না। ৬. __iadd__ দেবে না — += যেন নতুন object দেয়। ৭. প্রমাণ হিসেবে assert লিখে দেখাও: sum কাজ করে, set-এ সমান দুটি Money একটি হয়, Money(1) < 5 TypeError দেয়।

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

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

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

প্র ০১“Data model” কথাটি কোথা থেকে এল, আর Luciano Ramalho-র Fluent Python বইটি কেন dunder method শেখার ধরন বদলে দিয়েছিল?

Python-এর অফিসিয়াল ডকুমেন্টেশনের একটি অংশ হলো Language Reference — ভাষার নিয়মের আনুষ্ঠানিক বর্ণনা। তার তৃতীয় অধ্যায়ের নাম “Data model”. সেখানে শুরুতেই বলা আছে, Python-এ সব ডেটা object দিয়ে প্রকাশ করা হয়, প্রতিটি object-এর একটি identity, একটি type আর একটি value আছে। তারপর একে একে বর্ণনা করা আছে সব special method — কোন syntax কোন method ডাকে, কোন method কী ফেরত দেবে, কোনটি না থাকলে Python কী করে। অর্থাৎ “data model” কোনো লাইব্রেরি নয়, এটি ভাষার নিজের চুক্তিপত্র। (লক্ষ করুন, ডেটাবেসের প্রসঙ্গে “data model” মানে ডেটার নকশা — এই পর্বের অর্থ ভিন্ন।)

দীর্ঘদিন এই অধ্যায়টি ছিল মূলত রেফারেন্স — দরকার হলে খুঁজে দেখার জায়গা। অনেক শেখার বইয়ে special method আসত শেষের দিকে, “magic method” নামে, যেন কোনো গোপন কৌশল। ব্রাজিলের প্রোগ্রামার Luciano Ramalho ২০১৫ সালে O'Reilly থেকে প্রকাশ করেন Fluent Python, আর তার একেবারে প্রথম অধ্যায়ের নাম রাখেন “The Python Data Model”. বইটির দ্বিতীয় সংস্করণ বের হয় ২০২২ সালে। তাঁর যুক্তি ছিল: Python-এ দক্ষ হওয়ার চাবি হলো এই চুক্তি বোঝা, কারণ এটি বুঝলেই বোঝা যায় কেন len(x) লেখা হয়, কেন sorted যেকোনো object সাজাতে পারে, কেন ছোট একটি class হঠাৎ “Pythonic” লাগে।

বইয়ের প্রথম উদাহরণটি বিখ্যাত: একটি তাসের প্যাকেট (FrenchDeck), যাতে শুধু __len__ আর __getitem__ লেখা। এই দুটি method দিয়েই প্যাকেটে len, ইনডেক্স, slicing, for, in, এমনকি random.choice আর key দিয়ে sorted চলে — ঠিক যেমন আমাদের Playlist-এ দেখলেন। Ramalho “magic method” শব্দটির চেয়ে “special method” বা “dunder method” পছন্দ করেন, কারণ এখানে জাদু কিছু নেই — আছে স্পষ্ট লেখা নিয়ম।

বইটির প্রভাবে অনেক শিক্ষক আর ব্লগার data model-কে Python শেখার কেন্দ্রে নিয়ে আসেন, আর “Python data model” কথাটি সাধারণ প্রোগ্রামারদের আলোচনায় পরিচিত হয়ে ওঠে। “Dunder” শব্দটি অবশ্য বইয়ের আগে থেকেই Python সম্প্রদায়ে প্রচলিত ছিল।

মূল উপলব্ধি: Data model কোনো উন্নত বা গোপন বিষয় নয় — এটি Python-এর ব্যাকরণের অর্ধেক। Syntax হলো অনুরোধ, dunder method হলো উত্তর; এই চুক্তি বোঝা মানেই Python-এর মতো করে ভাবা।

প্র ০২Java বা JavaScript-এ লেখা হয় x.length, অথচ Python-এ len(x). Guido van Rossum কেন দৈর্ঘ্যকে method না বানিয়ে ফাংশন বানালেন?

প্রশ্নটি এত বার উঠেছে যে Python-এর অফিসিয়াল Design and History FAQ-তে এর জন্য আলাদা একটি প্রশ্ন আছে: কেন Python কিছু কাজে method (list.index()) আর কিছু কাজে ফাংশন (len(list)) ব্যবহার করে? উত্তরে Guido-র নিজের ব্যাখ্যাই উদ্ধৃত আছে। এর পেছনে ইতিহাসও আছে: Python-এর শুরুর দিকে সব built-in type-এর method ছিল না, তাই অনেক type-এ সাধারণ কাজগুলো ফাংশন হিসেবেই রাখা স্বাভাবিক ছিল।

২০০৬ সালে Python 3000-এর আলোচনায় Guido কারণটি খুলে লেখেন। তাঁর প্রথম যুক্তি ছিল “HCI” — অর্থাৎ মানুষের পড়ার সুবিধা। গণিতে আমরা লিখি x*(a+b), x*a + x*b নয়, আর ফাংশন-রূপ কিছু ধারণা প্রকাশে বেশি স্বাভাবিক। দ্বিতীয় যুক্তি: len(x) পড়লেই পাঠক জানেন এটি দৈর্ঘ্য চাইছে, ফল একটি পূর্ণসংখ্যা, আর x কোনো container. কিন্তু x.len() দেখলে নিশ্চিত হওয়া যায় না — x-এর class হয়তো len নামে সম্পূর্ণ অন্য কিছু করে। Built-in ফাংশন একটি অভিন্ন চুক্তি জোর করে; method-এর নাম যে কেউ যেকোনো অর্থে ব্যবহার করতে পারে।

এই নকশায় আরও কিছু সুবিধা মেলে, যা এই পর্বে দেখলেন। len() শুধু __len__ ডাকে না, ফলও যাচাই করে — ঋণাত্মক বা অ-পূর্ণসংখ্যা হলে error. bool() __bool__ না পেলে __len__-এ ফিরে যায়; iter() __iter__ না পেলে __getitem__-এ। এই “পিছু হটার” বুদ্ধি একটি কেন্দ্রীয় ফাংশনে রাখা সহজ, প্রতিটি class-এ নয়। আর গতি: CPython-এ list বা str-এর দৈর্ঘ্য object-এর ভেতরের একটি ঘর থেকে সরাসরি পড়া হয়, কোনো method খোঁজা ছাড়াই। Ramalho এটিকে Zen of Python-এর একটি লাইন দিয়ে ব্যাখ্যা করেন — “practicality beats purity”.

সমালোচনাও আছে: পুরোপুরি object-oriented ভাষার অনুরাগীরা একে অসংগতি মনে করেন, আর নতুনরা কখনো কখনো x.len() লিখে ভুল করেন। কিন্তু dunder ব্যবস্থার কারণে দুই জগতের সেরাটা মেলে: বাইরে একটি অভিন্ন ফাংশন, ভেতরে প্রতিটি class-এর নিজস্ব method.

মূল উপলব্ধি: len(x) হলো একটি সাধারণ “প্রশ্ন”, যা সব container-কে একই ভাষায় করা যায়; __len__ হলো প্রতিটি class-এর নিজস্ব উত্তর। এই বিভাজন পাঠযোগ্যতা, নিরাপত্তা-যাচাই আর গতি — তিনটিই দেয়।

প্র ০৩NotImplemented আর NotImplementedError নাম দুটি প্রায় এক, কাজ সম্পূর্ণ আলাদা। Python কেন error তোলার বদলে একটি বিশেষ মান ফেরত দেওয়ার পথ বেছে নিল?

প্রথমে পার্থক্যটি স্পষ্ট করি। NotImplementedError একটি exception class, RuntimeError-এর subclass. এর কাজ বলা: “এই method এখনো লেখা হয়নি” বা “subclass-কে এটি লিখতে হবে”। আপনি একে raise করেন। অন্যদিকে NotImplemented একটি মাত্র object — None-এর মতো একটি singleton, যার type-এর নাম NotImplementedType. আপনি একে return করেন, মূলত binary operator-এর dunder থেকে। এর মানে: “এই অন্য type-টির সাথে কাজটা আমি জানি না।”

Python কেন error-এর বদলে মান বেছে নিল? কারণ binary operator-এ দুটি পক্ষ, আর প্রত্যেকে শুধু নিজের type ভালো জানে। 0 + money-এ int জানে না Money কী; কিন্তু Money জানে int কী। Python চায় দুই পক্ষকেই সুযোগ দিতে। যদি int.__add__ সরাসরি TypeError তুলত, Python-কে exception ধরে ধরে দ্বিতীয় পক্ষকে চেষ্টা করতে হতো — আর তখন বোঝা যেত না error-টি “আমি জানি না” অর্থে, নাকি method-এর ভেতরে সত্যিকারের কোনো বাগ থেকে এসেছে। একটি বিশেষ ফেরত-মান এই দ্ব্যর্থতা দূর করে: NotImplemented মানে শুধুই “অন্যজনকে জিজ্ঞেস করো”; সত্যিকারের error তার নিজের মতো ওপরে উঠে যায়। এটি দ্রুতও, কারণ exception তোলা-ধরার চেয়ে একটি মান তুলনা অনেক সস্তা।

দুই পক্ষই NotImplemented দিলে Python নিজে একটি পরিষ্কার TypeError তোলে, যার বার্তায় দুই type-এর নামই থাকে। ব্যতিক্রম == আর !=: সেখানে Python শেষে identity তুলনা করে, কারণ “এই দুটি সমান কি না” প্রশ্নের একটি নিরাপদ উত্তর সবসময় আছে — আলাদা object হলে সমান নয়।

নাম দুটির মিল নিয়ে বিভ্রান্তি এত বেশি যে Python ধীরে ধীরে ভুল ব্যবহার বন্ধ করছে। NotImplemented-কে if-এ সত্য-মিথ্যা হিসেবে ব্যবহার করলে Python 3.9 থেকে DeprecationWarning আসত, আর Python 3.14-এ এটি সরাসরি TypeError. কারণ এমন ব্যবহার প্রায় সবসময় বাগ — যেমন কেউ if self.__eq__(other): লিখলেন, আর NotImplemented এল, যা আগে “সত্য” ধরা হতো।

মূল উপলব্ধি: NotImplemented হলো দুই পক্ষের আলোচনার একটি ভদ্র সংকেত — “আমি পারি না, ওকে জিজ্ঞেস করো”; NotImplementedError হলো একটি অসম্পূর্ণ কাজের ঘোষণা। প্রথমটি return, দ্বিতীয়টি raise.

প্র ০৪“সমান হলে hash সমান” — এই চুক্তি কেন এত জরুরি, আর mutable object-কে hashable বানালে ঠিক কী ভেঙে যায়?

পর্ব ১৬ আর ১৭-এ দেখেছিলেন, dict আর set ভেতরে একটি hash table. একটি চাবি রাখার সময় Python তার hash হিসাব করে, আর সেই সংখ্যা দিয়ে ঠিক করে চাবিটি table-এর কোন খোপে বসবে। খোঁজার সময় একই কাজ: hash হিসাব করো, সেই খোপে যাও, সেখানে যা আছে তার সাথে == দিয়ে মেলাও। পুরো ব্যবস্থার গতি — গড়ে প্রায় ধ্রুব সময়ে খোঁজা — দাঁড়িয়ে আছে একটি ধরে-নেওয়া কথার ওপর: যে দুটি চাবি সমান, তারা একই খোপে যাবে, অর্থাৎ তাদের hash সমান।

চুক্তি ভাঙলে কী হয়? ধরুন Money(500) == Money(500) সত্য, কিন্তু hash আলাদা (identity থেকে)। তাহলে একটি Money(500) dict-এ রেখে আরেকটি Money(500) দিয়ে খুঁজলে Python ভুল খোপে যাবে — KeyError, যদিও “সমান” চাবি ভেতরেই আছে। set-এ একই টাকা দুবার ঢুকবে। এজন্যই Python 3-এ __eq__ লিখলে, আর __hash__ না লিখলে, Python __hash__-কে None করে দেয়: ভুল উত্তরের চেয়ে স্পষ্ট অস্বীকার ভালো। (Python 2-তে এই সুরক্ষা ছিল না — পুরোনো identity-hash নিঃশব্দে থেকে যেত, আর ঠিক এই বাগটি হতো।)

দ্বিতীয় শর্ত — hash জীবনভর একই থাকবে — এই পর্বের stepper-এ দেখেছেন: set-এ রাখার পর ফিল্ড বদলালে object তার পুরোনো খোপে পড়ে থাকে, অথচ নতুন hash তাকে অন্য খোপে খোঁজে। ফল: a in s মিথ্যা, সরানো যায় না, আর হয়তো একই মানের আরেকটি object ঢুকে যায়। এই বাগ খুঁজে পাওয়া ভয়ংকর কঠিন, কারণ error আসে না — শুধু ভুল উত্তর।

এ কারণেই Python-এর built-in type-গুলো সচেতনভাবে ভাগ করা: int, str, tuple, frozenset immutable এবং hashable; list, dict, set mutable এবং unhashable. tuple-এর ভেতরে list থাকলে সেই tuple-ও hashable নয় — কারণ ভেতরের অংশ বদলাতে পারে। Java-তেও একই চুক্তি আছে — equals() আর hashCode() — আর Joshua Bloch-এর Effective Java বইয়ের একটি পরিচিত পরামর্শ: equals override করলে সবসময় hashCode-ও করুন।

তাহলে mutable object-এর সমতা কীভাবে? দুটি সৎ পথ: (ক) সমতা identity-ভিত্তিক রাখুন (ডিফল্ট) — একটি ব্যাংক অ্যাকাউন্ট “নিজের মতো” একটাই; (খ) মান-ভিত্তিক __eq__ লিখুন, কিন্তু __hash__ = None রেখে unhashable থাকুন, list-এর মতো।

মূল উপলব্ধি: hash হলো dict/set-এর কাছে আপনার দেওয়া একটি প্রতিশ্রুতি — “সমান হলে একই ঠিকানা, আর ঠিকানা কখনো বদলাবে না”। প্রতিশ্রুতি রাখতে না পারলে hashable হবেন না।

প্র ০৫Python কেন dunder method object-এর গায়ে না খুঁজে সরাসরি type-এ খোঁজে? এর পেছনে কোন সঠিকতা আর গতির যুক্তি আছে?

Data model-এর ডকুমেন্টেশনে “Special method lookup” নামে একটি ছোট অংশ আছে, যার মূল কথা: নিজে থেকে ডাকা special method-এর ক্ষেত্রে সঠিক আচরণ নিশ্চিত শুধু তখনই, যখন method-টি object-এর type-এ সংজ্ঞায়িত, object-এর নিজের dict-এ নয়। এই পর্বের Box উদাহরণে দেখলেন, object-এর গায়ে __len__ বসালেও len() তাকে উপেক্ষা করে।

সঠিকতার যুক্তি সবচেয়ে সুন্দরভাবে বোঝা যায় class নিজেকে দিয়ে। Python-এ class-ও একটি object, যার type হলো type. এখন Money class-এর ভেতরে আছে __hash__ আর __repr__ — কিন্তু সেগুলো instance-এর জন্য লেখা, তাদের প্রথম parameter একটি Money object. যদি hash(Money) বা repr(Money) ডাকলে Python সাধারণ attribute খোঁজার নিয়মে Money.__repr__ খুঁজত, সে পেত instance-এর method, আর self ছাড়া ডাকতে গিয়ে ভুল হতো। Type-এ খুঁজলে প্রশ্নটি যায় type-এর কাছে — “একটি class-কে কীভাবে repr করব?” — আর ঠিক উত্তর আসে: <class '__main__.Money'>. ডকুমেন্টেশন একে “metaclass confusion” এড়ানোর উপায় বলে।

গতির যুক্তি CPython-এর ভেতরের নকশায়। C ভাষায় লেখা প্রতিটি type-এর একটি কাঠামো আছে, যার নির্দিষ্ট ঘরে (slot) ফাংশন pointer থাকে — যোগের জন্য একটি, দৈর্ঘ্যের জন্য একটি, hash-এর জন্য একটি ইত্যাদি। আপনি যখন Python class-এ __add__ লেখেন, class তৈরির সময় CPython সেই ঘরে একটি ছোট সংযোগ-ফাংশন বসিয়ে দেয়, যা আপনার method ডাকে। ফলে a + b চালাতে interpreter-কে কোনো নাম খুঁজতে হয় না — সে type-এর ঘর থেকে সরাসরি ফাংশন নিয়ে ডাকে। প্রতিটি +-এর আগে যদি object-এর dict-এ "__add__" খুঁজতে হতো, Python অনেক ধীর হতো, কারণ এই operator-গুলো প্রতি সেকেন্ডে লাখো বার চলে। একই কারণে নিজে থেকে ডাকা dunder সাধারণ __getattribute__ ধাপটিও পার করে যায়।

এই নিয়মের একটি ব্যবহারিক ফল: চালানোর সময় class-এ dunder যোগ করলে (Money.__len__ = ...) Python ঘরগুলো হালনাগাদ করে, তাই সেটা কাজ করে; কিন্তু একটি নির্দিষ্ট object-এ বসালে কাজ করে না। আরেকটি ফল: proxy বা wrapper class বানানোর সময় __getattr__ দিয়ে সব attribute ফরোয়ার্ড করলেও len(proxy) বা proxy + 1 কাজ করবে না — dunder-গুলো আলাদা করে class-এ লিখতে হয়।

মূল উপলব্ধি: Dunder হলো type-এর আচরণ, object-এর ব্যক্তিগত বৈশিষ্ট্য নয়। এই একটি সিদ্ধান্ত class-কে নিজের instance-এর method-এর সাথে গুলিয়ে ফেলা থেকে বাঁচায়, আর operator-গুলোকে C-স্তরের গতিতে চালায়।

অনুশীলন

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

  1. কোনটি ডাকা হবে? একটি class-এ শুধু __str__ আছে (ফেরত দেয় "৳50.00"), কোনো __repr__ নেই। m তার একটি object. খাতায় লিখুন এই চারটিতে কী দেখাবে: print(m), print([m]), f"{m}", f"{m!r}".

    print(m) আর f"{m}" — দুটিই __str__ ডাকে, তাই ৳50.00. print([m]) — list ভেতরের উপাদানের repr দেখায়, আর __repr__ নেই, তাই object-এর ডিফল্ট repr আসে: কোণ-বন্ধনীর ভেতরে class-এর নাম আর একটি মেমরি-ঠিকানা। f"{m!r}"-এও একই ডিফল্ট লেখা। শিক্ষা: শুধু __str__ লিখলে অর্ধেক জায়গায় object অপাঠ্য থাকে; __repr__ লিখলে চারটি জায়গাই ঠিক হতো (str না থাকলেও repr দিয়ে চলে)।

  2. Dispatch অনুমান: Vector class-এ __mul__ আছে, কিন্তু __rmul__ নেই। v * 3 আর 3 * v লিখলে প্রতিটিতে Python কোন কোন method কোন ক্রমে চেষ্টা করবে, আর ফল কী হবে?

    v * 3: প্রথমে Vector.__mul__(v, 3) — সে সংখ্যা চেনে, তাই নতুন Vector ফেরত দেয়। কাজ শেষ। 3 * v: প্রথমে int.__mul__(3, v) — int Vector চেনে না, NotImplemented. তারপর Python খোঁজে Vector.__rmul__ — নেই। কেউ কাজ নিল না, তাই TypeError: unsupported operand type(s) for *: 'int' and 'Vector'. সমাধান: class-এ __rmul__ = __mul__, কারণ সংখ্যা দিয়ে গুণ দুই দিক থেকে একই।

  3. বাগ খুঁজুন: একটি ছাত্র-তালিকায় Student class আছে, যাতে roll আর marks ফিল্ড, আর __eq__ দুটি ফিল্ডই মেলায়। সহকর্মী set-এ রাখার জন্য লিখলেন def __hash__(self): return hash(self.roll). পরে নম্বর সংশোধনে s.marks = 85 করা হয়। এই নকশায় চুক্তি ভাঙছে কি? কোথায় বিপদ?

    প্রথম চুক্তি ভাঙেনি: যে দুটি ছাত্র == (roll আর marks দুটোই সমান), তাদের roll সমান, তাই hash-ও সমান। hash কম ফিল্ড দিয়ে বানানো বৈধ। কিন্তু দ্বিতীয় সমস্যাটি গুরুতর: marks বদলায়, আর marks সমতার অংশ। set-এ থাকা অবস্থায় marks বদলালে একই roll আর নতুন marks-এর আরেকটি object-কে set আলাদা ধরবে, আর পুরোনো object-এর সাথে “সমান” ধরা যাবে না — হিসাব এলোমেলো। পরিষ্কার নকশা: ছাত্রের পরিচয় roll, তাই __eq__ আর __hash__ দুটিই শুধু roll দিয়ে, আর roll কখনো বদলানো যাবে না। অথবা ছাত্রকে set-এ না রেখে {roll: student} dict ব্যবহার করুন।

  4. একটি ছোট class লিখুন (এবং AI-কে যাচাই করুন): Temperature-এর মতো করে একটি Distance class লিখুন, যা মিটারে মান রাখে; __repr__, __eq__, __hash__, __lt__ (total_ordering দিয়ে), __add__ আর __radd__ দিন। তারপর কয়েকটি দূরত্বের sum() আর sorted() চালান। একই কাজ কোনো AI-কে দিয়ে লিখিয়ে দেখুন, তার কোড এই পর্বের চেকলিস্ট পেরোয় কি না।

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

    from functools import total_ordering
    
    @total_ordering
    class Distance:
        def __init__(self, meters: int):
            self.meters = meters
        def __repr__(self):
            return f"Distance({self.meters})"
        def __eq__(self, other):
            if not isinstance(other, Distance):
                return NotImplemented
            return self.meters == other.meters
        def __hash__(self):
            return hash(self.meters)
        def __lt__(self, other):
            if not isinstance(other, Distance):
                return NotImplemented
            return self.meters < other.meters
        def __add__(self, other):
            if not isinstance(other, Distance):
                return NotImplemented
            return Distance(self.meters + other.meters)
        def __radd__(self, other):
            return self if other == 0 else NotImplemented
    
    legs = [Distance(1200), Distance(350), Distance(800)]
    print(sum(legs), sorted(legs))   # Distance(2350) [Distance(350), Distance(800), Distance(1200)]

    AI-এর কোডে দেখুন: __hash__ আছে কি না, অচেনা type-এ NotImplemented কি না (অনেক AI raise TypeError লেখে), __radd__ আছে কি না (না থাকলে sum() ভাঙবে), আর repr পড়া যায় কি না। Distance(1) < 5 চালিয়ে দেখুন — সঠিক কোডে TypeError আসবে, False নয়।

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

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

ক্রমPython-এ 0 + m চালালে (m একটি নিজের class-এর object, যাতে __radd__ আছে) যে ক্রমে ঘটনা ঘটে, সেই ক্রমে সাজান (▲▼ চেপে সরান)

  1. বাঁ পাশের type-এ খোঁজা: int.__add__(0, m)
  2. int জানে না, তাই NotImplemented ফেরত
  3. ডান পাশের type-এ খোঁজা: type(m).__radd__(m, 0)
  4. __radd__ একটি object ফেরত দেয়
  5. সেটিই 0 + m-এর ফল

একটি Python class-এ __eq__ সংজ্ঞায়িত করা হলো, কিন্তু __hash__ নয়। তার একটি object-কে set-এ রাখতে গেলে কী হয়?

  • ডিফল্ট identity-ভিত্তিক hash দিয়ে স্বাভাবিকভাবে রাখা যায়
  • Python __eq__ দেখে নিজে একটি মিলিয়ে নেওয়া hash বানিয়ে দেয়
  • TypeError আসে, কারণ object টি unhashable হয়ে গেছে
  • কোনো error নেই, কিন্তু set খালি থেকে যায়

Python-এ একটি class-এ শুধু __repr__ আছে, __str__ নেই। print(obj) কী ব্যবহার করে?

  • কিছুই না — AttributeError আসে
  • class-এর __repr__-এর ফল
  • সবসময় মেমরি-ঠিকানাসহ ডিফল্ট লেখা
  • object-এর সব attribute-এর একটি তালিকা

Python-এ নিজের class-এর binary operator method (যেমন __add__) অচেনা type-এর argument পেলে কী করা সবচেয়ে সঠিক?

  • raise TypeError
  • raise NotImplementedError
  • return None
  • return NotImplemented

একটি Python class-এ __add__ আছে কিন্তু __radd__ নেই। সেই class-এর object-এর একটি list-এ sum(items) চালালে কী হয়?

  • TypeError, কারণ sum() শুরু করে 0 + প্রথম উপাদান দিয়ে
  • স্বাভাবিকভাবে যোগফল আসে, কারণ sum() প্রথম উপাদান থেকে শুরু করে
  • 0 ফেরত আসে
  • Python __add__ থেকে নিজে __radd__ বানিয়ে নেয়, তাই কাজ করে

Python-এ একটি class-এ __bool__ নেই, কিন্তু __len__ আছে যা 0 ফেরত দেয়। bool(obj)-এর মান কী?

  • True, কারণ __bool__ নেই
  • TypeError, কারণ __bool__ নেই
  • False
  • None

Python-এ functools.total_ordering decorator কাজ করার জন্য class-এ ন্যূনতম কী থাকতে হয়?

  • ছয়টি তুলনার method-ই
  • __eq__ আর __lt__/__le__/__gt__/__ge__-এর যেকোনো একটি
  • শুধু __hash__
  • __repr__ আর __eq__

শব্দকোষ

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

মনে রাখুন

  1. Python-এর syntax হলো object-এর কাছে অনুরোধ: len(x) → type(x).__len__(x), a + b → type(a).__add__(a, b). এই চুক্তির নাম data model; dunder নিজে ডাকবেন না, syntax লিখুন।
  2. আগে __repr__ (দ্ব্যর্থহীন, সম্ভব হলে কোডের মতো), দরকার হলে __str__ (সুন্দর)। print আর f-string str নেয়; REPL, container আর !r repr নেয়।
  3. __eq__ লিখলে __hash__ হয়ে যায় None. সমান হলে hash সমান, আর hashable object-এর সমতার ফিল্ড বদলাবে না — value object-কে immutable রাখুন।
  4. অচেনা type → return NotImplemented, raise নয়। তখন Python উল্টো পক্ষের __radd__ বা উল্টো তুলনা চেষ্টা করে। sum() 0 থেকে শুরু করে, তাই __radd__ লাগে। ক্রমের জন্য total_ordering.
  5. __bool__ না থাকলে __len__; __getitem__ একাই ইনডেক্স, slicing আর for চালায়; __call__ object-কে callable করে। Dunder খোঁজা হয় type-এ। Operator overload শুধু তখনই, যখন অর্থ স্বতঃস্পষ্ট।

পরের পর্বে: একটি class কীভাবে আরেকটির আচরণ উত্তরাধিকার সূত্রে পায়, super() কী করে, আর কেন অভিজ্ঞ প্রোগ্রামাররা প্রায়ই বলেন “inheritance-এর আগে composition ভাবুন” — inheritance বনাম composition.

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

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