এই পর্বে যা শিখবেন
- 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 হলো তার উত্তর দেওয়ার জায়গা
- আপনার কোড
a + bচেনা syntax - Python খোঁজে
type(a)object-এর class-এ - ডাকে
__add__(a, b)আপনার লেখা method - ফল
নতুন objectযা ফেরত দিলেন
len, ==, <, [], in, for, bool, print — প্রায় সব built-in আচরণ।এই অনুবাদের একটি ছোট তালিকা দেখুন। পুরো তালিকা অনেক বড় — Python-এর অফিসিয়াল Language Reference-এ “Data model” নামের একটি গোটা অধ্যায় এর জন্য — কিন্তু প্রতিদিনের কাজে এগুলোই লাগে:
| আপনি যা লেখেন | Python যা ডাকে | কাজ |
|---|---|---|
repr(x), REPL-এ x | x.__repr__() | ডেভেলপারের জন্য চেহারা |
str(x), print(x), f"{x}" | x.__str__() | ব্যবহারকারীর জন্য চেহারা |
x == y, x < y | x.__eq__(y), x.__lt__(y) | তুলনা |
hash(x), set-এ রাখা | x.__hash__() | dict/set-এর চাবি |
x + y, x * 3 | x.__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)
| ডাক | |
| কেন |
- তৈরি।
Money(125050)শুধু__init__ডাকে — object তৈরি হলো, কিছু ছাপা হয়নি। চেহারার method-গুলো ডাকা হবে কেবল যখন কেউ object-টিকে লেখায় রূপান্তর করতে চায়। print→__str__.printপ্রতিটি argument-কেstr()দিয়ে লেখায় বদলায়, আরstr(m)ডাকে__str__. ফল ব্যবহারকারীর উপযোগী:৳1,250.50.- REPL →
__repr__. Python-এর ইন্টারঅ্যাকটিভ প্রম্পটে শুধুmলিখে Enter চাপলে REPL ফলটি দেখায়repr()দিয়ে — কারণ REPL-এ যিনি বসে আছেন তিনি ডেভেলপার, তাঁর জানা দরকার এটি আসলে কী। তাইMoney(125050), টাকার চিহ্ন নয়। - Container → ভেতরের সবার
__repr__. List ছাপালে list নিজের__str__দেয়, কিন্তু সে ভেতরের উপাদানগুলোর জন্য সবসময় repr ব্যবহার করে। কারণ: str হলে["1, 2"]আর[1, 2]ছাপায় একই রকম দেখাত — দ্ব্যর্থতা। তাই__repr__না লিখলে আপনার object list-এর ভেতরে অপাঠ্য দেখাবে,__str__যত সুন্দরই হোক। - f-string-এ দুটোই।
{m}ডিফল্টেformat()হয়ে শেষ পর্যন্ত__str__ডাকে;{m!r}লিখলে জোর করেrepr(). ডিবাগের জন্য Python 3.8 থেকে আরও সহজ রূপ আছে:f"{m=}"লিখলে ছাপেm=Money(125050)— নামসহ repr. __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)
| __eq__ | |
| __hash__ | |
| a, b |
- কোনো
__eq__নেই। ডিফল্ট নিয়মে সমতা মানে identity, আর hash-ও আসে object-এর identity থেকে।aআরbদুটি আলাদা object, তাইa == bহলোFalse, আর set-এ দুটিই আলাদা উপাদান — দৈর্ঘ্য 2. একই টাকা দুবার গোনা হলো। __eq__যোগ করলেন। এখনa == bসত্য হবে। কিন্তু চুপিসারে Python class-এর__hash__-কেNoneবসিয়ে দিয়েছে। কেন? পুরোনো identity-ভিত্তিক hash এখন আপনার নতুন সমতার সাথে মেলে না:a == bঅথচhash(a) != hash(b)— এটি hash table-এর মূল নিয়ম ভাঙে। ভুল উত্তর দেওয়ার চেয়ে Python উত্তর দিতেই অস্বীকার করে।- Set-এ রাখা ব্যর্থ। set বা dict-এ রাখতে hash লাগে, আর
__hash__এখনNone— অর্থাৎ object টি আর hashable নয়। ফলTypeError. Python 3.14-এ বার্তাটি এমন; পুরোনো version-এ শুধুunhashable type: 'Money'. - সঠিক
__hash__যোগ করলেন।def __hash__(self): return hash(self.poisha). চুক্তি: যে দুটি object==, তাদের hash অবশ্যই সমান. যে ফিল্ড দিয়ে সমতা মাপেন, ঠিক সেই ফিল্ড দিয়েই hash বানান। একাধিক ফিল্ড হলেhash((self.x, self.y))— একটি tuple-এর hash. এখনa == b, আর set-এ মাত্র একটি উপাদান। - Set-এ রাখার পরে বদলালে বিপদ।
aset-এ ঢোকার সময় তার 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
| ডাক | |
| ফল |
- সহজ ক্ষেত্র।
m + Money(99): Python প্রথমে বাঁ পাশের object-এর type দেখে —Money— আর ডাকেMoney.__add__(m, Money(99)). অন্য পক্ষওMoney, তাই নতুনMoney(2099)ফেরত এল। কাজ শেষ। - বাঁ পাশ জানে না।
0 + m: বাঁ পাশেint, তাই প্রথম চেষ্টাint.__add__(0, m).intজানে শুধু সংখ্যা যোগ করতে;Moneyদেখে সে error তোলে না, ভদ্রভাবেNotImplementedফেরত দেয়। Python থামে না। - ডান পাশকে সুযোগ। এবার Python ডান পাশের type-এ খোঁজে উল্টো method:
Money.__radd__(m, 0). লক্ষ করুন argument-এর ক্রম —selfহলোm,otherহলো0. আমাদের__radd__বলে, “অন্যজন 0 হলে আমি নিজেই ফল”। তাই ফলm. - অর্থহীন যোগ।
"৳" + m: str-এর পক্ষেMoneyচেনা সম্ভব নয়, তাই PythonMoney.__radd__(m, "৳")চেষ্টা করে।other0 নয়, তাই আমাদের method-ওNotImplementedদেয় — নিজে error তোলে না। - দুজনেই অস্বীকার →
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 | |
| ডাক |
- শুরু 0 দিয়ে।
sum()-এর start-এর ডিফল্ট মান পূর্ণসংখ্যা0. Python জানে না আপনি টাকা যোগ করছেন; সে সবসময় 0 থেকেই শুরু করে। - প্রথম যোগ:
0 + m1. ঠিক আগের stepper-এর ঘটনা:int.__add__দেয়NotImplemented, তারপরMoney.__radd__(m1, 0)দেয়m1. এখনtotalএকটিMoney. - দ্বিতীয় যোগ। এবার দুই পাশেই
Money— সরাসরিMoney.__add__, ফলMoney(127050).__radd__-এর দরকার শুধু প্রথম ধাপে ছিল। - তৃতীয় যোগ। একইভাবে
Money(127149). প্রতিটি যোগে নতুন object তৈরি হচ্ছে; কোনোMoneyবদলানো হয়নি। - ছাপা।
printডাকে__str__:৳1,271.49. Built-insum()আপনার class সম্পর্কে কিছুই জানে না, তবু কাজ করল — কারণ আপনি data model-এর চুক্তি মেনেছেন। __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 এই ক্রমে খোঁজে:
- class-এ
__bool__থাকলে সেটি ডাকে —TrueবাFalseফেরত দিতে হবে। - না থাকলে
__len__খোঁজে — দৈর্ঘ্য 0 হলেFalse, নইলেTrue. - কোনোটিই না থাকলে 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.15Closure-এর তুলনায় সুবিধা: 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 99len(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-তে 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-কে কোড লিখতে বা রিভিউ করতে বলার সময় এই শর্তগুলো আগেই দিয়ে দিন। নিচের কাঠামোটি কপি করে নিজের কাজ বসান:
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-স্তরের গতিতে চালায়।
অনুশীলন
বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — অনুমান করা, পড়া আর মিলিয়ে নেওয়াই আসল কাজ।
-
কোনটি ডাকা হবে? একটি 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 দিয়ে চলে)। -
Dispatch অনুমান:
Vectorclass-এ__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__, কারণ সংখ্যা দিয়ে গুণ দুই দিক থেকে একই। -
বাগ খুঁজুন: একটি ছাত্র-তালিকায়
Studentclass আছে, যাতে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 ব্যবহার করুন। -
একটি ছোট class লিখুন (এবং AI-কে যাচাই করুন):
Temperature-এর মতো করে একটিDistanceclass লিখুন, যা মিটারে মান রাখে;__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কি না (অনেক AIraise TypeErrorলেখে),__radd__আছে কি না (না থাকলেsum()ভাঙবে), আর repr পড়া যায় কি না।Distance(1) < 5চালিয়ে দেখুন — সঠিক কোডেTypeErrorআসবে,Falseনয়।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমPython-এ 0 + m চালালে (m একটি নিজের class-এর object, যাতে __radd__ আছে) যে ক্রমে ঘটনা ঘটে, সেই ক্রমে সাজান (▲▼ চেপে সরান)
- বাঁ পাশের type-এ খোঁজা: int.__add__(0, m)
- int জানে না, তাই NotImplemented ফেরত
- ডান পাশের type-এ খোঁজা: type(m).__radd__(m, 0)
- __radd__ একটি object ফেরত দেয়
- সেটিই 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__
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Python-এর syntax হলো object-এর কাছে অনুরোধ:
len(x)→type(x).__len__(x),a + b→type(a).__add__(a, b). এই চুক্তির নাম data model; dunder নিজে ডাকবেন না, syntax লিখুন। - আগে
__repr__(দ্ব্যর্থহীন, সম্ভব হলে কোডের মতো), দরকার হলে__str__(সুন্দর)। print আর f-string str নেয়; REPL, container আর!rrepr নেয়। __eq__লিখলে__hash__হয়ে যায়None. সমান হলে hash সমান, আর hashable object-এর সমতার ফিল্ড বদলাবে না — value object-কে immutable রাখুন।- অচেনা type →
return NotImplemented, raise নয়। তখন Python উল্টো পক্ষের__radd__বা উল্টো তুলনা চেষ্টা করে।sum()0 থেকে শুরু করে, তাই__radd__লাগে। ক্রমের জন্যtotal_ordering. __bool__না থাকলে__len__;__getitem__একাই ইনডেক্স, slicing আরforচালায়;__call__object-কে callable করে। Dunder খোঁজা হয় type-এ। Operator overload শুধু তখনই, যখন অর্থ স্বতঃস্পষ্ট।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩১ · inheritance বনাম compositionপরবর্তী পর্বclass থেকে class — কখন উত্তরাধিকার, কখন একটি object-এর ভেতরে আরেকটি।
- Python প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — ৮টি মডিউল, কোন পর্ব কখন।
- পর্ব ২৯ · class ও objectঝালিয়ে নিন
__init__,selfআর attribute — এই পর্বের ভিত্তি। - C++ · operator overloadingতুলনা করুনএকই ধারণা compile-এর সময়ের ভাষায় —
operator+, free function আর spaceship.