এই পর্বে যা শিখবেন
- class কেন — ডেটা, আচরণ আর invariant একসাথে বাঁধা; class বনাম object; Python-এ class নিজেও একটি object
classstatement,__init__(initializer) আরself-এর আসল রূপ; method ডাক কীভাবে ফাংশন ডাকে অনুবাদ হয়- instance attribute বনাম class attribute, খোঁজার ক্রম, আর class-এ রাখা list-এর ফাঁদ
__dict__ওvars(), dynamic attribute,__slots__,_nameও__name(name mangling)@classmethodদিয়ে বিকল্প constructor,@staticmethod, আর কখন class লিখবেন না
১ · class কেন — ডেটা, আচরণ আর একটি নিয়ম একসাথে
মডিউল ৩-এ আপনি ডেটা রাখতে শিখেছেন list, tuple আর dict দিয়ে, আর মডিউল ২-এ কাজ করতে শিখেছেন ফাংশন দিয়ে। এতদিন এই দুটি আলাদা ছিল: ডেটা এক জায়গায়, ফাংশন আরেক জায়গায়। একটি বিকাশ-ধাঁচের মোবাইল wallet ভাবুন। পর্ব ১৬-এর মতো dict দিয়ে লিখলে:
wallet = {"owner": "Rahim", "balance": 500}
def withdraw(w, amount):
if amount > w["balance"]:
raise ValueError("ব্যালান্স কম")
w["balance"] -= amount
withdraw(wallet, 200) # নিয়ম মেনে তোলা
wallet["balance"] = -9000 # কেউ সরাসরি লিখে দিল — কেউ আটকাল না!শেষ লাইনটিই সমস্যা। withdraw ফাংশনটি যত্ন করে নিয়ম মানে — ব্যালান্সের বেশি তোলা যাবে না। কিন্তু dict-টি সবার জন্য খোলা; প্রোগ্রামের যেকোনো জায়গা থেকে যে কেউ wallet["balance"]-এ যা খুশি লিখতে পারে। নিয়মটি থাকে ফাংশনে, আর ডেটা থাকে বাইরে — দুটির মধ্যে কোনো বাঁধন নেই। বানান ভুল করে wallet["balence"] লিখলে নতুন একটি চাবিও চুপচাপ তৈরি হয়ে যায়।
class এই বাঁধনটিই তৈরি করে। একটি class তিনটি জিনিস একসাথে রাখে:
- ডেটা — wallet-এর মালিক, ব্যালান্স, লেনদেনের ইতিহাস। এগুলোকে বলে attribute.
- আচরণ — জমা, তোলা, ইতিহাস দেখা। এগুলো ফাংশন, কিন্তু class-এর ভেতরে লেখা; এদের বলে method.
- একটি নিয়ম — “ব্যালান্স কখনো ঋণাত্মক হবে না”। যে শর্ত object-এর পুরো জীবনে সবসময় সত্য থাকার কথা, তাকে বলে invariant. Method-গুলোর আসল দায়িত্ব এই নিয়ম রক্ষা করা।
একটি ব্যাংকের হিসাব খোলার ফর্ম ভাবুন। ফর্মে ঘর আছে: নাম, ঠিকানা, প্রথম জমা। ফর্মটি নিজে কোনো হিসাব নয় — এটি নকশা। রহিম ফর্ম পূরণ করলে একটি হিসাব তৈরি হয়, করিম পূরণ করলে আরেকটি; একই নকশা, কিন্তু দুটি আলাদা হিসাব, আলাদা ব্যালান্স। আর হিসাবের টাকা আপনি নিজে হাত দিয়ে বদলাতে পারেন না — কাউন্টারে গিয়ে জমা বা উত্তোলনের অনুরোধ করতে হয়, আর ক্যাশিয়ার নিয়ম দেখে তবেই কাজ করেন। ফর্ম = class, পূরণ করা প্রতিটি হিসাব = object, কাউন্টারের কাজ = method, ব্যাংকের নিয়ম = invariant.
এই নকশা থেকে বানানো প্রতিটি জিনিসকে বলে object বা instance — দুটি শব্দ প্রায় একই অর্থে চলে। “instance” শব্দটি সম্পর্ক বোঝায়: acc হলো Account class-এর একটি instance. আর class থেকে object বানানোর কাজটিকে বলে instantiation.
এভাবে ডেটা আর তার নিয়ম একসাথে বেঁধে ভেতরের খুঁটিনাটি বাইরে থেকে আড়াল রাখার ভাবনাকে বলে encapsulation. আর পুরো প্রোগ্রামকে এমন object-এর মেলামেশা হিসেবে সাজানোর পদ্ধতির নাম object-oriented programming (OOP). মডিউল ৫-এর সাতটি পর্ব এই নিয়েই।
👛 Wallet
ডেটা: মালিক, ব্যালান্স, ইতিহাস। আচরণ: deposit, withdraw. নিয়ম: ব্যালান্স ≥ ০, জমার পরিমাণ ধনাত্মক।
🎓 Student
ডেটা: নাম, রোল, প্রতিটি বিষয়ের নম্বর। আচরণ: নম্বর যোগ করা, GPA হিসাব। নিয়ম: নম্বর ০ থেকে ১০০-এর মধ্যে।
🛺 রিকশার ভাড়া-মিটার
ডেটা: মোট দূরত্ব, অপেক্ষার মিনিট। আচরণ: পথ যোগ, ভাড়া দেখা। নিয়ম: দূরত্ব কখনো কমে না।
📚 লাইব্রেরির বই
ডেটা: নাম, কার কাছে আছে। আচরণ: borrow, give_back. নিয়ম: একই বই একসাথে দুজনের কাছে থাকতে পারে না।
Python-এ class নিজেও একটি object
পর্ব ০৫-এ বলেছিলাম, Python-এ সবকিছুই object. class-ও তার ব্যতিক্রম নয়। class Account: লাইনটি চললে মেমরিতে একটি class object তৈরি হয়, আর Account নামটি তার গায়ে লাগে — ঠিক যেমন def একটি function object বানায়। তাই class-কে ভ্যারিয়েবলে রাখা যায়, ফাংশনে পাঠানো যায়, list-এ রাখা যায়। আর class object-টি নিজেও একটি namespace — তার ভেতরে থাকে method আর class attribute-গুলো। type(acc) দেয় Account, আর type(Account) দেয় type — অর্থাৎ class-ও কোনো এক “class-এর class”-এর instance. এর গল্প ভাবনার প্রশ্ন ৪-এ।
২ · প্রথম class — class, __init__ আর একটি object-এর জন্ম
একটি ছোট ব্যাংক হিসাব দিয়ে শুরু করি। প্রথমে শুধু দুটি জিনিস: object তৈরির সময় মালিক আর ব্যালান্স বসানো, আর টাকা তোলা।
class Account:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def withdraw(self, amount):
if amount > self.balance:
raise ValueError(f"ব্যালান্স কম: {self.balance}")
self.balance -= amount
acc = Account("Rahim", 500)কয়েকটি নতুন জিনিস:
class Account:— নতুন class-এর শুরু। নাম PEP 8 অনুযায়ী CapWords ধাঁচে (প্রতিটি শব্দের প্রথম অক্ষর বড়:Account,LibraryBook), ফাংশনের snake_case থেকে আলাদা — যাতে নাম দেখেই বোঝা যায় এটি class.- ভেতরের
def— class-এর ভেতরে লেখা ফাংশনই method. প্রতিটির প্রথম parameterself. __init__— দুই পাশে দুটি করে আন্ডারস্কোর, পর্ব ১৯-এ দেখা সেই dunder method-এর পরিবারের সদস্য। নতুন object তৈরি হওয়ামাত্র Python নিজে এটি ডাকে।self.owner = owner— নতুন object-এর গায়েownerনামের একটি attribute লাগানো। বাঁ দিকেরself.ownerহলো object-এর attribute, ডান দিকেরownerহলো parameter — একই নাম হলেও দুটি আলাদা জিনিস।
এবার শেষ লাইন acc = Account("Rahim", 500)-এর ভেতরে ঠিক কী ঘটে, ধাপে ধাপে দেখুন। লক্ষ করুন, class-কে ফাংশনের মতো বন্ধনী দিয়ে ডাকা হচ্ছে — class object “callable”:
1class Account:2 def __init__(self, owner, balance=0):3 self.owner = owner4 self.balance = balance56 def withdraw(self, amount): ...78acc = Account("Rahim", 500)
| Account | |
| নতুন object | |
| self | |
| acc |
- class statement চলল। Python
class-এর ভেতরের কোড একবার চালাল। দুটিdefদুটি function object বানাল, আর সেগুলো রাখা হলো নতুন class object-এর namespace-এ।Accountনামটি সেই class object-এ লাগল। এখনো কোনো হিসাব (object) নেই — শুধু নকশা। - Account(...) ডাকা — খালি object. class-কে ডাকলে Python প্রথমে একটি নতুন, খালি object বানায়। এই কাজটি করে আরেকটি dunder method,
__new__— সাধারণ class-এ আপনি এটি কখনো লেখেন না, Python-এর নিজেরটাই কাজ করে। - __init__ ডাকা হলো। এবার Python
__init__ডাকে, আর প্রথম argument হিসেবে নিজে থেকেই সেই খালি object-টি পাঠায়। সেটিইself. আপনার দেওয়া"Rahim"আর500যায়ownerআরbalance-এ। - প্রথম attribute.
self.owner = owner— object-এর নিজের খাতায় (__dict__-এ, অংশ ৫)ownerনামটি লেখা হলো। এটি একটি instance attribute: শুধু এই object-এর। - দ্বিতীয় attribute.
self.balance = balance— এখন object-এ দুটি attribute.__init__শেষ হলো এবংNoneফেরত দিল;__init__কিছু ফেরত দেয় না, দিলেTypeError. - acc লেবেল লাগল।
Account(...)-এর ফল সেই সাজানো object, আরaccনামটি তার গায়ে লাগল।__init__-এর frame মুছে গেছে,selfনামটিও — কিন্তু object বেঁচে আছে, কারণaccতাকে ধরে রেখেছে (পর্ব ০৫-এর reference counting).
__init__ object বানায় না, বানানো object-কে সাজায়. বানানোর কাজ __new__-এর, সাজানোর কাজ __init__-এর। তাই Python-এর অফিসিয়াল ভাষায় __init__ হলো initializer, যদিও লোকমুখে একে প্রায়ই “constructor” বলা হয়। __new__ নিজে লেখার দরকার পড়ে খুব বিশেষ ক্ষেত্রে — যেমন int বা tuple-এর মতো immutable type থেকে নতুন class বানাতে (পর্ব ৩১-এর পরে)।
প্রতিটি ডাকে নতুন object — identity আর isinstance
a = Account("Rina", 100) আর b = Account("Rina", 100) — দুটির মালিক আর ব্যালান্স হুবহু এক। তবু a is b হলো False: প্রতিটি ডাক মেমরিতে নতুন একটি object বানায়, যার নিজের identity. a.balance বদলালে b-এর কিছু হয় না। আর মজার কথা, a == b-ও False, কারণ নিজের class-এ ==-এর অর্থ আপনি এখনো বলে দেননি; Python তখন ==-কে is-এর মতোই বিচার করে। দুটি হিসাব কখন “সমান” — সেটি পর্ব ৩০-এর __eq__-এর কাজ।
কোনো object কোন class-এর, তা জানতে type(acc) দেয় Account. তবে “এটি কি একটি Account?” প্রশ্নের জন্য isinstance() ভালো: isinstance(acc, Account) দেয় True, আর পর্ব ৩১-এর inheritance এলে এটি subclass-কেও ঠিকঠাক চেনে।
৩ · self — method কীভাবে জানে কোন object-এর কাজ
নতুনদের কাছে সবচেয়ে রহস্যময় হলো self. def withdraw(self, amount)-এ দুটি parameter, অথচ ডাকার সময় দেন একটি: acc.withdraw(200). বাকিটা কোথা থেকে আসে?
উত্তর একটি লাইনে: acc.withdraw(200) আর Account.withdraw(acc, 200) হুবহু একই কাজ. method আসলে class-এ রাখা একটি সাধারণ ফাংশন। বিন্দুর বাঁ দিকের object-টিকে Python নিজে থেকে প্রথম argument হিসেবে বসিয়ে দেয়। সেই parameter-এর নাম self রাখা শুধু একটি দৃঢ় প্রচলন — me বা this লিখলেও চলবে, কিন্তু কেউ লেখেন না, আর লিখলে অন্যদের পড়তে কষ্ট হবে।
একটি কলেজে একজন হিসাবরক্ষক অনেক ছাত্রের বেতন-খাতা দেখেন। কেউ এসে বললেন “২০০ টাকা জমা নিন” — হিসাবরক্ষক প্রথমেই জিজ্ঞেস করবেন, “কার খাতায়?” self হলো সেই “কার খাতা” প্রশ্নের উত্তর। method একটাই (হিসাবরক্ষক একজন), খাতা অনেক (object অনেক); প্রতিটি ডাকে self বলে দেয় এবার কোন খাতা খুলতে হবে। acc.withdraw(200) লেখা মানে খাতাটি সঙ্গে নিয়েই কাউন্টারে যাওয়া।
নিচের ধাপগুলোতে একই অনুবাদ দেখুন, সঙ্গে invariant রক্ষার দুই পরিণতি — অনুমতি পাওয়া আর বাতিল হওয়া:
1def withdraw(self, amount):2 if amount > self.balance:3 raise ValueError(...)4 self.balance -= amount56acc.withdraw(200)7acc.withdraw(1000)
| Python যা ডাকে | |
| self | |
| শর্ত | |
| acc.balance |
- বিন্দু দিয়ে খোঁজা।
acc.withdraw— Python প্রথমেaccobject-এর নিজের attribute-এwithdrawখোঁজে, পায় না; তারপর তার classAccount-এ খোঁজে, সেখানে ফাংশনটি পায়। - অনুবাদ। ফাংশনটি class থেকে এসেছে কিন্তু একটি object-এর মাধ্যমে চাওয়া হয়েছে — তাই Python একটি bound method বানায়: ফাংশন +
acc, একসাথে বাঁধা। ডাকলে এটি চালায়Account.withdraw(acc, 200).selfএখনacc-এর object. - নিয়ম পরীক্ষা — অনুমতি। লাইন ২:
self.balanceমানেacc-এর ব্যালান্স, 500. চাওয়া 200, যা 500-এর বেশি নয়। শর্ত মিথ্যা, তাইraiseহলো না। - অবস্থা বদলাল। লাইন ৪:
self.balance -= amount—acc-এর ব্যালান্স হলো 300. invariant (ব্যালান্স ≥ ০) এখনো সত্য। method-এর ভেতরে থেকে বদলানো হয়েছে বলেই নিয়মটি পরীক্ষা করা গেছে। - দ্বিতীয় ডাক।
acc.withdraw(1000)— আবার একই অনুবাদ:Account.withdraw(acc, 1000). একই ফাংশন, একইself, শুধুamountআলাদা। - নিয়ম পরীক্ষা — বাতিল। 1000 > 300, তাই
raise ValueError. লাইন ৪ আর চলে না — ব্যালান্স 300-ই থাকে। পর্ব ২৪-এর কথা মনে করুন: ভুল অবস্থায় চুপচাপ এগিয়ে যাওয়ার চেয়ে exception তুলে থামা ভালো। ডাকার জায়গায়try/except ValueErrorদিয়ে এটি সামলানো যায়।
এখানে একটি গভীর কথা আছে। invariant রক্ষা করা যায় কারণ অবস্থা বদলানোর সব পথ method-এর ভেতর দিয়ে যায়. method হলো দরজার পাহারাদার: deposit দেখে পরিমাণ ধনাত্মক কি না, withdraw দেখে ব্যালান্সে কুলায় কি না। __init__-ও একটি পাহারাদার — Account("Rahim", -50) ডাকলে সেখানেই ValueError তোলা উচিত, যাতে জন্ম থেকেই কোনো object ভুল অবস্থায় না থাকে। কিন্তু কেউ যদি method এড়িয়ে সরাসরি acc.balance = -9000 লেখে? Python সেটি আটকায় না। এর উত্তর অংশ ৬-এ, আর পূর্ণ সমাধান (property) পর্ব ৩২-এ।
এবার প্রথম runnable প্রোগ্রাম। এতে আছে class attribute (bank), দুই রকম ডাক একই কাজ করে তার প্রমাণ, object-এর ভেতরের খাতা দেখার vars(), আর বাতিল হওয়া উত্তোলন। চালানোর আগে প্রথম লাইনের ব্যালান্স অনুমান করুন:
class Account:
bank = "ABCL Bank" # class attribute
def __init__(self, owner, balance=0):
self.owner = owner # instance attribute
self.balance = balance
def withdraw(self, amount):
if amount > self.balance:
raise ValueError(f"ব্যালান্স কম: {self.balance}")
self.balance -= amount
acc = Account("Rahim", 500)
acc.withdraw(200) # = Account.withdraw(acc, 200)
Account.withdraw(acc, 100)
print(vars(acc), acc.bank)
try:
acc.withdraw(1000)
except ValueError as e:
print("বাতিল:", e)
print(acc.balance, type(acc).__name__, isinstance(acc, Account))আউটপুট তিন লাইনের। প্রথম লাইন {'owner': 'Rahim', 'balance': 200} ABCL Bank — দুটি উত্তোলন (একটি সাধারণ ডাকে, একটি class-এর নাম দিয়ে acc নিজে পাঠিয়ে) মিলে 300 কমেছে; দুই রকম লেখা একই কাজ করেছে। vars(acc) object-এর নিজের খাতা দেখায় — সেখানে শুধু owner আর balance; bank নেই, অথচ acc.bank ঠিকই কাজ করল, কারণ সেটি পাওয়া গেছে class থেকে (অংশ ৪)। দ্বিতীয় লাইন বাতিল: ব্যালান্স কম: 200 — invariant রক্ষা পেয়েছে। তৃতীয় লাইন 200 Account True — বাতিল উত্তোলনের পরেও ব্যালান্স অক্ষত; type(acc).__name__ class-এর নাম দেয়। লক্ষ করুন, আমরা কোথাও print(acc) লিখিনি — কেন, তা অংশ ৮-এ।
৪ · instance attribute বনাম class attribute — খোঁজার ক্রম আর ভাগ করা list-এর ফাঁদ
প্রথম প্রোগ্রামে দুই জায়গায় attribute ছিল। self.owner-এর মতো যেগুলো __init__-এ (বা অন্য method-এ) self-এর গায়ে লাগানো হয়, সেগুলো instance attribute — প্রতিটি object-এর নিজস্ব। আর bank = "ABCL Bank"-এর মতো যেগুলো সরাসরি class-এর ভেতরে, কোনো method-এর বাইরে লেখা, সেগুলো class attribute — class object-এ একবার রাখা, সব instance ভাগ করে।
Python যখন acc.bank পড়ে, সে একটি নির্দিষ্ট ক্রমে খোঁজে — পর্ব ২০-এর LEGB-এর মতোই একটি ছোট নিয়ম:
(পর্ব ৩১-এ inheritance এলে class-এর পরে parent class-গুলোও খোঁজা হবে, আর পর্ব ৩২-এ property-র মতো কিছু ব্যতিক্রম দেখবেন।) কিন্তু লেখার নিয়ম আলাদা: acc.bank = "X" লিখলে Python class-এ যায় না — সোজা acc-এর নিজের খাতায় নতুন bank লেখে, যা class-এর bank-কে শুধু এই object-এর জন্য ঢেকে দেয়। আর acc.history.append(...)-এর মতো কাজ কোনো নামে assign করে না, শুধু পড়ে-পাওয়া object-টিকে ভেতর থেকে বদলায় (পর্ব ১৯) — ফাঁদের শুরু এখানেই। ধাপে ধাপে দেখুন:
1class Student:2 school = "Collegiate"3 marks = [] # ফাঁদ4 def __init__(self, name):5 self.name = name67a, b = Student("Rina"), Student("Jamil")8print(a.school)9a.school = "Govt. Lab"10a.marks.append(85)11print(b.marks)
| Student (class) | |
| a.__dict__ | |
| b.__dict__ |
- দুটি object. class-এ দুটি class attribute:
schoolআর একটি খালি listmarks. দুটি object তৈরি হলো, প্রত্যেকের নিজের খাতায় শুধুname. - পড়া — class থেকে।
a.school:a-এর খাতায়schoolনেই, তাই Python class-এ গেল, পেল'Collegiate'. সব ছাত্রের একই স্কুল হলে এটাই চাই — মান একবার রাখা, সবাই পড়ে। - লেখা — instance-এ।
a.school = "Govt. Lab"class-কে ছোঁয় না;a-এর নিজের খাতায় নতুনschoolলেখে। এখনa.schoolপাবে নিজেরটা (আগে নিজের খাতা!), কিন্তুb.schoolআরStudent.schoolএখনো'Collegiate'. - ফাঁদ — mutation, assignment নয়।
a.marks.append(85):a-এর খাতায়marksনেই → class-এ গিয়ে সেই একটিমাত্র list পেল →appendতাকে ভেতর থেকে বদলাল। কোনো নামে assign হয়নি, তাইa-এর খাতায় কিছু লেখা হলো না; বদলাল class-এর list. - অন্য object-এ প্রভাব।
b.marks:b-এর খাতায় নেই → class → সেই একই list, যাতে এখন[85]. জামিলের নম্বরের তালিকায় রিনার নম্বর! সব Student একটি list ভাগ করছে — পর্ব ২০-এর mutable default-এর যমজ ভাই। - সমাধান। class থেকে
marks = []মুছে__init__-এ লিখুনself.marks = [].__init__প্রতিটি object-এর জন্য আলাদা চলে, তাই প্রত্যেকে নিজের নতুন list পায়। নিয়ম: class attribute-এ শুধু ভাগ করার মতো, সাধারণত immutable মান (সংখ্যা, string, tuple) রাখুন.
class attribute-এর সঠিক ব্যবহার — একটি ভাগ করা counter
class attribute-এর সবচেয়ে ভালো ব্যবহার হলো সবার জন্য একটি সাধারণ মান — একটি ধ্রুবক (যেমন সর্বোচ্চ দৈনিক লেনদেনের সীমা), বা একটি হিসাব যা পুরো class-এর, কোনো একটি object-এর নয়। যেমন মোট কতটি wallet খোলা হলো: __init__-এ Wallet.count += 1. এখানে ঠিক Wallet.count লিখতে হবে, self.count += 1 নয়! কারণ self.count += 1 মানে self.count = self.count + 1 — ডান দিক class থেকে পড়ে (0), কিন্তু বাঁ দিকের assign হয় object-এর নিজের খাতায়। ফলে প্রতিটি object-এর নিজের count = 1, আর class-এর count চিরকাল 0. পড়া আর লেখার এই অসমতা বুঝলে class attribute-এর সব রহস্য খুলে যায়।
৫ · object-এর ভেতরটা — __dict__, dynamic attribute আর __slots__
“object-এর নিজের খাতা” কথাটি উপমা নয়, আক্ষরিক। সাধারণ Python object তার instance attribute-গুলো রাখে একটি সাধারণ dict-এ, যার নাম __dict__. acc.__dict__ বা আরও পরিষ্কারভাবে vars(acc) লিখলে সেটি দেখা যায় — প্রথম প্রোগ্রামে যেমন দেখেছেন। class-এরও একটি __dict__ আছে (একটু বিশেষ ধরনের, শুধু পড়া যায়), যেখানে method আর class attribute থাকে। তাই acc.balance পড়া মোটামুটি এমন: “acc.__dict__-এ 'balance' চাবি আছে? থাকলে সেটি; না থাকলে Account.__dict__-এ দেখো”। পর্ব ১৬-এর dict-ই OOP-এর নিচে বসে আছে।
এর একটি সরাসরি ফল: যেকোনো সময় নতুন attribute যোগ করা যায়. acc.nickname = "রহিম ভাই" লিখলে কোনো error নেই — খাতায় নতুন একটি চাবি। একে বলে dynamic attribute. এটি কখনো কাজের (পরীক্ষার সময় দ্রুত কিছু জুড়ে দেওয়া), কিন্তু বেশিরভাগ সময় বাগের উৎস: acc.balence = 0 (বানান ভুল) লিখলে Python খুশিমনে নতুন attribute বানায়, আর আসল balance অপরিবর্তিত থাকে। এই কারণে ভালো অভ্যাস: সব instance attribute __init__-এ তৈরি করুন, শুরুর মান None হলেও। তাহলে __init__ পড়েই বোঝা যায় object-এ কী কী থাকে, আর editor ও linter বানান ভুল ধরতে পারে।
অন্যদিকে, যে attribute কখনো তৈরি হয়নি তা পড়তে গেলে আসে AttributeError: 'Account' object has no attribute 'nickname' — dict-এর KeyError-এর OOP সংস্করণ। getattr(acc, "nickname", None) দিয়ে নিরাপদে পড়া যায়, যেমন dict-এ .get().
__slots__ — খাতা বাদ, নির্দিষ্ট ঘর
প্রতিটি object-এর জন্য একটি আলাদা dict রাখতে মেমরি লাগে। দশটি object-এ কিছু যায় আসে না, কিন্তু কয়েক লাখ ছোট object (যেমন একটি মানচিত্রের লাখো বিন্দু) হলে পার্থক্য চোখে পড়ে। এর জন্য Python দেয় __slots__: class-এ __slots__ = ("owner", "balance") লিখলে object আর __dict__ রাখে না; শুধু ওই নামগুলোর জন্য নির্দিষ্ট ঘর থাকে। দুটি ফল: মেমরি কম লাগে আর attribute পড়া একটু দ্রুত হয়; এবং তালিকার বাইরের কোনো নাম (acc.balence = 0) দিলে সঙ্গে সঙ্গে AttributeError — বানান ভুল ধরা পড়ে। দাম হলো নমনীয়তা হারানো। বেশিরভাগ class-এ __slots__ লাগে না; Python 3.10 থেকে @dataclass(slots=True) লিখে এটি সহজে পাওয়া যায় (পর্ব ৩২)।
object = namespace: এই অংশের সারকথা হলো, একটি object আসলে নামের একটি খাতা, যার সাথে কিছু আচরণ জোড়া। পর্ব ২২-এর module-ও একটি namespace (math.pi), class-ও (Account.bank), instance-ও (acc.owner) — সবখানে একই বিন্দু-চিহ্ন, একই ভাবনা: “এই খাতায় এই নামটি খোঁজো”। এজন্যই কখনো কখনো শুধু কয়েকটি সম্পর্কিত ধ্রুবক একসাথে রাখতে class ব্যবহার হয় — যদিও তার জন্য module বা enum প্রায়ই ভালো।
৬ · “private” — _balance, __balance আর “আমরা সবাই প্রাপ্তবয়স্ক”
C++ বা Java-তে private কিওয়ার্ড দিলে class-এর বাইরে থেকে সেই ডেটায় হাত দেওয়া যায় না — compiler আটকে দেয়। Python-এ এমন কোনো কিওয়ার্ড নেই। তার বদলে আছে দুটি প্রচলন।
একটি আন্ডারস্কোর: _balance
শুধু একটি সংকেত: “এটি class-এর ভেতরের ব্যাপার, বাইরে থেকে হাত দেবেন না; ভবিষ্যতে বদলাতে পারে”। পর্ব ২২-এর private name-এর মতোই। Python কিছুই আটকায় না — acc._balance = -9000 চলবে। কিন্তু যিনি লিখলেন, তিনি জেনেশুনে নিয়ম ভাঙলেন।
দুটি আন্ডারস্কোর: __balance
class-এর ভেতরে লেখা self.__balance-কে Python নিজে থেকে নাম বদলে করে self._Account__balance. একে বলে name mangling. বাইরে থেকে acc.__balance লিখলে AttributeError, কিন্তু acc._Account__balance লিখলে ঠিকই পাওয়া যায়।
name mangling-এর আসল উদ্দেশ্য গোপনীয়তা নয় — নামের সংঘর্ষ এড়ানো. পর্ব ৩১-এ দেখবেন, একটি class থেকে আরেকটি class বানানো যায় (inheritance). parent class-এর ভেতরের একটি সহায়ক নাম, ধরুন __cache, যদি child class-এর লেখক না জেনে একই নামে আরেকটি attribute বানান, দুটি মিশে যেত। mangling-এর পরে একটি হয় _Parent__cache, আরেকটি _Child__cache — আলাদা থাকে। Python-এর অফিসিয়াল tutorial-ও এই উদ্দেশ্যই বলে। (নিয়মটি খাটে নামের শুরুতে অন্তত দুটি আন্ডারস্কোর আর শেষে একটির বেশি না থাকলে; তাই __init__-এর মতো dunder নামে mangling হয় না।)
Python সম্প্রদায়ের একটি পুরোনো কথা: “আমরা সবাই সম্মত প্রাপ্তবয়স্ক”। ভাষা প্রোগ্রামারকে তালা দিয়ে আটকায় না; সংকেত দেয় (_), আর বিশ্বাস করে যে প্রোগ্রামার সেই সংকেত মানবেন — না মানলে তার দায় তাঁর। ব্যবহারিক নিয়ম: বাইরের কারও হাত দেওয়ার কথা নয় এমন attribute-এ একটি আন্ডারস্কোর দিন; __ রাখুন শুধু তখন, যখন inheritance-এ নামের সংঘর্ষের সত্যিকারের ভয় আছে। বাইরে থেকে মান পড়ার সুযোগ দিতে চাইলে আর লেখা আটকাতে চাইলে পর্ব ৩২-এর @property আছে।
এর মানে কি invariant রক্ষা অর্থহীন? না। invariant রক্ষা মানে সঠিক পথটিকে সহজ আর স্পষ্ট করা, ভুল পথকে অসম্ভব করা নয়। acc.withdraw(200) লেখা সহজ, আর পরিষ্কার; acc._balance -= 200 লেখা দেখতেই সন্দেহজনক — কোড রিভিউতে, linter-এ, এমনকি লেখকের নিজের চোখে। আর যে ভাষায় সত্যিকারের private আছে, সেখানেও reflection বা pointer-এর কৌশলে তা ভাঙা যায়; সেখানেও শেষ রক্ষাকবচ হলো শৃঙ্খলা।
Tell, don't ask — জিজ্ঞেস না করে বলুন
ছোট, ভালো class নকশার একটি পুরোনো সূত্র হলো tell, don't ask. খারাপ ধাঁচ: বাইরের কোড object-এর ডেটা জিজ্ঞেস করে, নিজে সিদ্ধান্ত নেয়, তারপর ডেটা বদলায় —
# ask — নিয়মটি বাইরে ছড়িয়ে আছে
if book.borrower is None:
book.borrower = "Rina"
# tell — নিয়মটি class-এর ভেতরে, এক জায়গায়
book.borrow("Rina") # ইতিমধ্যে কারও কাছে থাকলে ValueErrorপ্রথম ধাঁচে “একই বই দুজনের কাছে থাকতে পারে না” নিয়মটি প্রতিটি জায়গায় আলাদা করে লিখতে হয়; একজন ভুলে গেলেই নিয়ম ভাঙে। দ্বিতীয় ধাঁচে লাইব্রেরির Book class নিজের নিয়ম নিজে জানে: borrow পরীক্ষা করে, give_back খালি করে। বাইরের কোড শুধু বলে “ধার দাও” — কীভাবে, সেটি বইয়ের দায়িত্ব। একই ভাবনায় রিকশার ভাড়া-মিটার: বাইরে থেকে meter.km += 2 নয়, বরং meter.add_trip(2), যা ঋণাত্মক দূরত্ব ফিরিয়ে দেয়; আর ভাড়া জানতে meter.fare(), যা নিজের নিয়মে (ধরুন প্রথম কিলোমিটার ৩০ টাকা, পরের প্রতি কিলোমিটার ২০ টাকা — শুধু উদাহরণ) হিসাব করে।
৭ · @classmethod আর @staticmethod — বিকল্প constructor
সাধারণ method-এর প্রথম argument হলো object (self). কিন্তু কখনো এমন কাজ লাগে যা কোনো একটি object-এর নয়, পুরো class-এর। Python এর জন্য দুটি বিশেষ ধরন দেয়, দুটিই @ দিয়ে লেখা decorator (পর্ব ৩৫-এ decorator-এর ভেতরের গল্প)।
@classmethod-এর প্রথম argument object নয়, class নিজে — প্রচলিত নাম cls. এর সবচেয়ে বড় ব্যবহার হলো alternative constructor: object বানানোর আরেকটি পথ। ধরুন wallet-এর তথ্য আসে একটি ফাইল থেকে "Karim:300" আকারে। প্রতিবার বাইরে ভেঙে Wallet(name, int(amount)) লেখার বদলে class নিজেই একটি পথ দেয়: Wallet.from_string("Karim:300"). Python-এর নিজের লাইব্রেরিতে এর অনেক উদাহরণ আছে — dict.fromkeys(...), datetime.fromisoformat(...), int.from_bytes(...). নাম সাধারণত from_... দিয়ে শুরু। ধাপে ধাপে দেখুন:
1@classmethod2def from_string(cls, text):3 owner, amount = text.split(":")4 return cls(owner, int(amount))56w = Wallet.from_string("Karim:300")
| cls | |
| text | |
| owner, amount | |
| w |
- class দিয়ে ডাকা।
Wallet.from_string(...)— কোনো object নেই, সরাসরি class-এর নামে ডাক।@classmethodথাকায় Python প্রথম argument হিসেবে object নয়, classWallet-কেই পাঠায়:cls = Wallet. - বাকি argument.
text = "Karim:300"— সাধারণ parameter-এর মতোই। - ভাঙা আর রূপান্তর।
split(":")দিয়ে দুটি string পাওয়া গেল, পর্ব ১৫-এর unpacking দিয়ে দুটি নামে।amountএখনো string'300'. - cls(...) — সাধারণ পথে object.
cls(owner, int(amount))মানেWallet("Karim", 300). অর্থাৎ বিকল্প constructor শেষ পর্যন্ত সাধারণ__init__-কেই ডাকে — তাই__init__-এর সব পরীক্ষা (invariant) এখানেও খাটে, কোনো পেছনের দরজা নেই। - ফেরত। নতুন object ফেরত এল,
wতার গায়ে লাগল।Walletনা লিখেclsলেখার সুবিধা: পর্ব ৩১-এ কেউWalletথেকেSavingsWalletবানালেSavingsWallet.from_string(...)ঠিকSavingsWallet-ই বানাবে।
@staticmethod আরও সরল: এটি কোনো স্বয়ংক্রিয় প্রথম argument পায় না — না self, না cls. এটি আসলে class-এর ভেতরে রাখা একটি সাধারণ ফাংশন, শুধু বিষয়ের মিল আছে বলে সেখানে রাখা। যেমন Wallet.is_valid_amount(amount), যা দেখে পরিমাণটি সীমার মধ্যে কি না। তবে সত্যি বলতে, Python-এ module-স্তরের একটি সাধারণ ফাংশনও প্রায় সবসময় একই কাজ করে, আর অনেক অভিজ্ঞ Python প্রোগ্রামার @staticmethod খুব কম ব্যবহার করেন। তিনটির তুলনা:
| ধরন | প্রথম argument | কখন |
|---|---|---|
| সাধারণ method | self — object | একটি object-এর অবস্থা পড়া বা বদলানো (withdraw) |
@classmethod | cls — class | বিকল্প constructor (from_string), class-স্তরের হিসাব |
@staticmethod | কিছুই না | বিষয়ের সাথে সম্পর্কিত সহায়ক ফাংশন; প্রায়ই module-স্তরের ফাংশন ভালো |
দ্বিতীয় runnable প্রোগ্রামে অংশ ৪-এর list-এর ফাঁদ, তার সঠিক রূপ, class-স্তরের counter, from_string, আর object সুন্দর করে ছাপার জন্য একটি ছোট __repr__ — পর্ব ৩০-এর ঝলক:
class Student:
marks = [] # ফাঁদ: class-এ একটি list, সবার জন্য
a, b = Student(), Student()
a.marks.append(85) # শুধু a-এর জন্য নম্বর যোগ?
print(b.marks, a.marks is b.marks)
class Wallet:
count = 0 # class attribute: মোট কতটি wallet
def __init__(self, owner, balance=0):
self.owner, self._balance = owner, balance
self.history = [] # ঠিক: প্রতিটি wallet-এর নিজের list
Wallet.count += 1
@classmethod
def from_string(cls, text): # "Karim:300" থেকে wallet
owner, amount = text.split(":")
return cls(owner, int(amount))
def __repr__(self):
return f"Wallet({self.owner!r}, {self._balance})"
w1, w2 = Wallet("Rahim", 500), Wallet.from_string("Karim:300")
w1.count = 99 # w1-এর নিজের নতুন attribute
print(w1, w2, Wallet.count, w1.count, w2.count)আউটপুট দুই লাইনের। প্রথম লাইন [85] True — শুধু a-এর জন্য নম্বর যোগ করা হয়েছিল, কিন্তু b.marks-এও 85, আর is প্রমাণ করছে দুটি একই list. দ্বিতীয় লাইন Wallet('Rahim', 500) Wallet('Karim', 300) 2 99 2. প্রথম দুটি অংশ এসেছে __repr__ থেকে — print একটি object ছাপার সময় এই method ডেকে তার লেখা নেয়; !r নামের চারপাশে উদ্ধৃতিচিহ্ন বসায়। দ্বিতীয় wallet বানানো হয়েছে from_string দিয়ে, তবু Wallet.count 2 — কারণ সেটিও শেষে __init__ ডেকেছে। শেষ দুটি সংখ্যা অংশ ৪-এর নিয়ম দেখায়: w1.count = 99 শুধু w1-এর নিজের খাতায় লিখেছে, তাই w1.count 99, কিন্তু w2.count এখনো class থেকে পড়ে 2.
৮ · কখন class, কখন নয় — dict, closure, ফাংশন আর object ছাপা
class শেখার পর সবকিছুকে class বানানোর একটি প্রবল টান আসে। Java-র মতো ভাষায় সব কোড class-এর ভেতরেই লিখতে হয়, তাই সেখান থেকে আসা প্রোগ্রামার আর অনেক AI-ও সেই অভ্যাস Python-এ নিয়ে আসেন। কিন্তু Python-এ ফাংশন, dict আর closure-ও প্রথম শ্রেণির হাতিয়ার। ২০১২ সালের PyCon US সম্মেলনে Jack Diederich-এর বিখ্যাত বক্তৃতা “Stop Writing Classes”-এর মূল কথা ছিল এটাই: যে class-এ মাত্র দুটি method, আর তার একটি __init__, সেটি সম্ভবত একটি ফাংশন হওয়া উচিত।
| পরিস্থিতি | ভালো পছন্দ | কেন |
|---|---|---|
| ইনপুট নিয়ে একটি ফল দেওয়া, কোনো অবস্থা মনে রাখা নেই (VAT হিসাব) | ফাংশন | সবচেয়ে সরল, পরীক্ষা সহজ |
| কিছু ডেটা একসাথে রাখা, কোনো নিয়ম বা আচরণ নেই (JSON থেকে আসা তথ্য) | dict, বা namedtuple/dataclass | অতিরিক্ত কোড ছাড়াই গোছানো (পর্ব ১৬, ১৫, ৩২) |
| একটি ছোট অবস্থা আর একটি কাজ (counter, VAT factory) | closure | হালকা; পর্ব ২০ |
| অবস্থা + কয়েকটি আচরণ + একটি নিয়ম যা রক্ষা করতে হবে (wallet, বই) | class | নিয়ম এক জায়গায়; অনেক object একই নকশায় |
| একই ধরনের অনেক জিনিস, যাদের আলাদা অবস্থা (প্রতিটি ছাত্র, প্রতিটি রিকশা) | class | প্রতিটি instance নিজের খাতা রাখে |
একটি উদাহরণে পার্থক্যটি স্পষ্ট। ছাত্রের GPA হিসাবের জন্য class GPACalculator বানিয়ে GPACalculator(marks).calculate() লেখা অপ্রয়োজনীয় — calculate_gpa(marks) ফাংশনটিই যথেষ্ট। কিন্তু যদি একজন Student-এর নাম, রোল, বিষয়ভিত্তিক নম্বর থাকে, নতুন নম্বর যোগের সময় ০–১০০ সীমা পরীক্ষা করতে হয়, আর GPA বারবার নম্বর থেকে হিসাব হয় — তখন class যুক্তিসঙ্গত: student.add_mark("Physics", 78) আর student.gpa(). লক্ষ করুন, GPA-কে একটি attribute হিসেবে জমা না রেখে প্রতিবার নম্বর থেকে হিসাব করা ভালো; জমা রাখলে নতুন নম্বর যোগের পর সেটি পুরোনো হয়ে যেতে পারে — আরেকটি invariant ভাঙার পথ।
object ছাপলে কেন অদ্ভুত লেখা আসে — __repr__-এর ঝলক
__repr__ ছাড়া একটি class-এর object ছাপলে এমন কিছু দেখবেন:
<__main__.Account object at 0x7f3a2c1b9d50>
অর্থাৎ: __main__ module-এর Account class-এর একটি object, মেমরির এই ঠিকানায়। ঠিকানাটি প্রতিবার চালালে বদলায় আর কোনো কাজের তথ্য দেয় না। তাই ডিবাগ করার জন্য প্রায় প্রতিটি class-এ একটি __repr__ লেখা ভালো অভ্যাস — দ্বিতীয় প্রোগ্রামের মতো এক লাইনের, যা object-কে পরিচিত রূপে দেখায়: Wallet('Rahim', 500). __repr__ আর তার ভাই __str__-এর পার্থক্য, ==-এর জন্য __eq__, len()-এর জন্য __len__ — এসব নিয়েই পরের পর্ব।
C++-এর class-এর মতোই ধারণা, কিন্তু কয়েকটি বড় পার্থক্য: Python-এ this-এর মতো লুকানো pointer নেই, self স্পষ্টভাবে লিখতে হয়; attribute আগে থেকে ঘোষণা করতে হয় না — __init__-এ assign করলেই তৈরি হয়, আর object-এর ভেতরে একটি dict-এ থাকে। public/private নেই, আছে শুধু _ প্রচলন; destructor-এর ওপর নির্ভর করে সম্পদ ছাড়ার (RAII) বদলে Python-এ with ব্যবহার হয় (পর্ব ৩৬)। আর __init__ C++-এর constructor-এর মতো দেখালেও object তৈরি করে না, শুধু সাজায়। তুলনার জন্য দেখুন C++-এর class ও object.
- class আর object-এর পার্থক্য? — class হলো নকশা (ডেটা ও আচরণের বর্ণনা); object হলো সেই নকশায় তৈরি একটি নির্দিষ্ট instance, যার নিজের attribute-এর মান আছে।
selfকী? — যে object-এর ওপর method ডাকা হলো, সেটি; Python নিজে প্রথম argument হিসেবে পাঠায়:obj.m(x)মানেCls.m(obj, x).__init__কি constructor? — ঠিক নয়; object বানায়__new__,__init__বানানো object-কে শুরুর মান দিয়ে সাজায় (initializer).- instance attribute আর class attribute-এর পার্থক্য? — প্রথমটি প্রতিটি object-এর নিজের (
self.x); দ্বিতীয়টি class-এ একবার রাখা, সবাই ভাগ করে; পড়ার সময় আগে instance, পরে class খোঁজা হয়। @classmethodআর@staticmethod-এর পার্থক্য? — classmethod প্রথম argument হিসেবে class (cls) পায়, বিকল্প constructor-এ কাজে লাগে; staticmethod কিছুই পায় না।
৯ · AI-এর যুগে — AI-এর লেখা class যাচাই করা
AI-কে “একটি wallet সিস্টেম লিখে দাও” বললে প্রায় সবসময় একগাদা class আসে। কোড চলে, দেখতেও “পেশাদার” লাগে — কিন্তু প্রায়ই সেগুলো অন্য ভাষার অভ্যাসে লেখা, বা invariant রক্ষার কোনো ব্যবস্থাই থাকে না। AI-এর প্রশিক্ষণের ডেটায় Java-ধাঁচের কোড প্রচুর, তাই এই ভুলগুলো বারবার দেখা যায়:
| AI প্রায়ই যা করে | সমস্যা | কী চাইবেন |
|---|---|---|
প্রতিটি attribute-এর জন্য get_owner(), set_owner() | Java-র অভ্যাস; কোড দ্বিগুণ, কোনো লাভ নেই | সরাসরি acc.owner; নিয়ন্ত্রণ লাগলে পরে @property (পর্ব ৩২) |
একটি __init__ আর একটি run() method-এর class | ছদ্মবেশী ফাংশন — “Stop Writing Classes” | একটি সাধারণ ফাংশন |
class-এ items = [] বা cache = {} | সব object একই list/dict ভাগ করে; ডেটা মিশে যায় | __init__-এ self.items = [] |
public balance, আর withdraw-এ কোনো পরীক্ষা নেই | invariant নেই; যে কেউ ঋণাত্মক ব্যালান্স বসাতে পারে | _balance, method-এ পরীক্ষা আর ValueError |
__init__-এর বাইরে, পরের কোনো method-এ প্রথম attribute তৈরি | কিছু object-এ attribute থাকে, কিছুতে থাকে না → AttributeError | সব attribute __init__-এ |
সব কিছুতে __private | অপ্রয়োজনীয় name mangling; ডিবাগ কঠিন | একটি আন্ডারস্কোর |
AI-কে class লিখতে বলার সময় নকশার শর্তগুলো আগেই দিয়ে দিন। নিচের কাঠামোটি কপি করে নিজের কাজ বসান:
AI-এর লেখা class পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১class আর object-এর ধারণা কোথা থেকে এল? Simula 67 আর Smalltalk কীভাবে আজকের Python-এর OOP-কে প্রভাবিত করেছে?
গল্পটি শুরু হয় নরওয়েতে। ১৯৬০-এর দশকে অসলোর Norwegian Computing Center-এ Ole-Johan Dahl আর Kristen Nygaard বাস্তব জগতের সিস্টেম — জাহাজ, বন্দর, গ্রাহকের সারি — কম্পিউটারে simulation করার জন্য একটি ভাষা বানাচ্ছিলেন। simulation-এ স্বাভাবিকভাবেই অনেক “জিনিস” থাকে, প্রত্যেকের নিজের অবস্থা আর আচরণ: প্রতিটি জাহাজের নিজের গতি, নিজের অবস্থান। তাঁদের প্রথম ভাষা Simula I-এর পরে আসে Simula 67, যা প্রথম “class” আর “object” ধারণাকে একটি সাধারণ-উদ্দেশ্যের ভাষায় নিয়ে আসে। সেখানে ছিল class, তা থেকে বানানো object, এক class থেকে আরেক class বানানো (subclass — আজকের inheritance), আর virtual procedure — আজকের method overriding-এর পূর্বসূরি। এই অবদানের জন্য দুজনেই ২০০১ সালে কম্পিউটার বিজ্ঞানের সর্বোচ্চ সম্মান Turing Award পান।
দ্বিতীয় ধারা আসে আমেরিকার Xerox PARC গবেষণাগার থেকে। ১৯৭০-এর দশকে Alan Kay-এর নেতৃত্বে একটি দল (যাতে ছিলেন Dan Ingalls, Adele Goldberg প্রমুখ) তৈরি করেন Smalltalk, যার সংস্করণ Smalltalk-80 পরে বাইরের জগতে ছড়ায়। Kay নিজেই “object-oriented” শব্দটির প্রবর্তক হিসেবে পরিচিত। তাঁর ভাবনায় OOP-এর কেন্দ্রে ছিল message: object-গুলো একে অপরকে বার্তা পাঠায়, আর প্রতিটি object নিজে ঠিক করে বার্তার উত্তরে কী করবে। একটি পরে বহুল উদ্ধৃত ইমেইলে তিনি লিখেছিলেন, তাঁর কাছে OOP মানে শুধু messaging, অবস্থার স্থানীয় সংরক্ষণ ও সুরক্ষা আর আড়াল, আর সবকিছুর চরম late binding. Smalltalk-এ সংখ্যা থেকে class পর্যন্ত সবই object — ঠিক যেমন Python-এ। এই কাজের জন্য Kay ২০০৩ সালে Turing Award পান।
এই দুই ধারা থেকে দুই রকম ভাষা বেরিয়েছে। Simula-র সরাসরি উত্তরসূরি C++ — Bjarne Stroustrup নিজে বলেছেন, Simula-র class ধারণা তিনি C-তে আনতে চেয়েছিলেন, আর শুরুতে ভাষাটির নাম ছিল “C with Classes”. সেখানে জোর compile-এর সময়ের নিরাপত্তা আর গতিতে, private দিয়ে কঠোর আড়াল। Smalltalk-এর উত্তরসূরিরা — Objective-C, Ruby, এবং অনেকাংশে Python — dynamic: চালানোর সময় object-কে প্রশ্ন করা যায়, নতুন attribute জোড়া যায়, সবকিছুই object.
Python দুই দিক থেকেই নিয়েছে। Guido van Rossum জানিয়েছেন, Python-এর class-ব্যবস্থার অনেক কিছু তিনি নিয়েছিলেন Modula-3 আর C++ থেকে। ১৯৯১ সালে প্রকাশিত প্রথম পাবলিক সংস্করণেই (0.9.0) inheritance-সহ class ছিল। আবার “সবই object”, dynamic attribute, পর্ব ৩৩-এর duck typing — এগুলো Smalltalk-এর আত্মার কাছাকাছি।
মূল উপলব্ধি: OOP একটি ভাষার আবিষ্কার নয়, দুটি দর্শনের মিশ্রণ — Simula-র “বাস্তব জিনিসের মডেল, class দিয়ে” আর Smalltalk-এর “স্বাধীন object, বার্তা দিয়ে কথা বলে”। Python-এর class এই দুইয়ের একটি ব্যবহারিক, নমনীয় মিলন।
প্র ০২C++ আর Java-তে this লুকানো থাকে, অথচ Python প্রতিটি method-এ self স্পষ্টভাবে লেখায়। এত বছরের সমালোচনার পরও Python কেন এটি বদলায়নি?
Python-এর যে বৈশিষ্ট্য নিয়ে নতুনদের অভিযোগ সবচেয়ে বেশি, তার একটি এই স্পষ্ট self. প্রতিটি method-এ self লিখতে হয়, প্রতিটি attribute-এর আগে self. — দেখতে বাড়তি মনে হয়। Python-এর অফিসিয়াল Design FAQ-তে “Why must 'self' be used explicitly in method definitions and calls?” নামে একটি প্রশ্ন আছে, আর সেখানে বলা হয়েছে, ধারণাটি নেওয়া হয়েছিল Modula-3 থেকে।
২০০৮ সালে লেখক ও প্রশিক্ষক Bruce Eckel প্রস্তাব দেন, Python 3-এ self parameter-টি ঐচ্ছিক করা হোক। এর উত্তরে Guido van Rossum তাঁর ব্লগে একটি লেখা লেখেন, শিরোনাম “Why explicit self has to stay”. তাঁর যুক্তিগুলো এই পর্বের সাথে সরাসরি যুক্ত:
- method আসলে ফাংশন:
acc.withdraw(200)আরAccount.withdraw(acc, 200)একই — এই সরল, স্বচ্ছ সমতা স্পষ্টself-এর ওপর দাঁড়িয়ে। method-এ কোনো জাদু নেই; এটি একটি সাধারণ ফাংশন, যার প্রথম argument object. - পরে ফাংশন জোড়া: class তৈরির পরে বাইরে লেখা একটি ফাংশন
Account.report = reportলিখে method বানানো যায়।selfলুকানো থাকলে বাইরের ফাংশন কীভাবে জানবে সে method, আর কোন নামটি object? - decorator-এর সমস্যা:
@classmethod,@staticmethodআর নিজের লেখা decorator ফাংশনকে বদলে দেয়। প্রথম parameter লুকানো থাকলে কোন ক্ষেত্রে সেটি object, কোন ক্ষেত্রে class, কোন ক্ষেত্রে কিছুই না — তা পার্সারকে আলাদা নিয়মে জানতে হতো। Guido দেখান, এতে ভাষার একটি সরল নিয়ম জটিল ব্যতিক্রমে ভরে যেত।
আরও কয়েকটি ব্যবহারিক সুবিধা আছে। self.balance দেখেই বোঝা যায় এটি attribute, local ভ্যারিয়েবল নয়; C++-এ অনেক দল এই পার্থক্য বোঝাতে m_balance বা balance_ জাতীয় নামকরণের নিয়ম বানায়, Python-এ সেটি ভাষাতেই আছে। আর Python-এর দর্শন — PEP 20, Tim Peters-এর লেখা “The Zen of Python” — বলে, “Explicit is better than implicit”.
দাম আছে, অবশ্যই — কয়েকটি বাড়তি অক্ষর, আর নতুনদের একটি সাধারণ ভুল: def withdraw(amount) লিখে self ভুলে যাওয়া, যা ডাকার সময় TypeError: ... takes 1 positional argument but 2 were given দেয়। (এই error বার্তা পড়ে বিভ্রান্ত হবেন না: “2 were given”-এর একটি হলো Python-এর নিজে পাঠানো object.)
মূল উপলব্ধি: স্পষ্ট self কোনো পুরোনো অবশেষ নয়; এটি Python-এর “method = ফাংশন + object” মডেলের সরাসরি প্রকাশ। এটি বোঝার পর self আর বাড়তি মনে হয় না, বরং method ডাকের পুরো রহস্য খুলে দেয়।
প্র ০৩Python-এ সত্যিকারের private নেই — “we're all consenting adults”. এটা কি দুর্বলতা? কঠোর private থাকলে কী লাভ, কী ক্ষতি?
“We're all consenting adults here” — কথাটি Python সম্প্রদায়ে বহু বছর ধরে চলে আসছে, আর প্রায়ই এটি Guido van Rossum-এর মুখে শোনা গেছে বলে উল্লেখ করা হয়। এর মানে: ভাষা প্রোগ্রামারকে শিশুর মতো আগলে রাখবে না; সতর্ক করবে, কিন্তু শেষ সিদ্ধান্ত প্রোগ্রামারের।
কঠোর private-এর পক্ষে যুক্তি শক্ত। বড় দলে, বা যখন আপনার লাইব্রেরি হাজার অচেনা মানুষ ব্যবহার করেন, তখন compiler-এর নিশ্চয়তা মূল্যবান: আপনি জানেন ভেতরের ডেটা বাইরের কেউ বদলাতে পারবে না, তাই ভেতরের নকশা নির্ভয়ে বদলাতে পারেন। নিরাপত্তা-সংবেদনশীল কোডে এটি একটি অতিরিক্ত বাধা।
তবে কয়েকটি বাস্তবতা আছে। প্রথমত, “private” কখনো নিরাপত্তা নয়. C++-এ pointer দিয়ে, Java-তে reflection দিয়ে private ডেটা পড়া ও বদলানো যায়। একই প্রক্রিয়ার ভেতরের কোড থেকে ডেটা লুকানোর কোনো আসল উপায় নেই; গোপন তথ্য (পাসওয়ার্ড, চাবি) রক্ষা করতে হয় অন্যভাবে (পর্ব ৫০)। তাই private-এর আসল কাজ হলো ইচ্ছা প্রকাশ — “এটি ইন্টারফেসের অংশ নয়”।
দ্বিতীয়ত, নমনীয়তার দাম. টেস্ট লেখার সময়, ডিবাগ করার সময়, বা একটি লাইব্রেরির বাগ নিজে সাময়িকভাবে পাশ কাটাতে ভেতরের ডেটা দেখা খুব কাজের। Python-এ obj._internal দেখা সহজ; কঠোর ভাষায় সেটি কঠিন বা অসম্ভব।
তৃতীয়ত, Python দলগত শৃঙ্খলার জন্য অন্য হাতিয়ার দেয়: _ প্রচলন, যা প্রায় সবাই মানে; linter (pylint-এর “protected-access” সতর্কতা) যা বাইরের _name ব্যবহার ধরে; @property (পর্ব ৩২) যা পড়ার সুযোগ দিয়ে লেখা আটকায়; আর দলের code review.
একটি মাঝামাঝি উদাহরণ JavaScript: বহু বছর সেখানে কোনো private ছিল না, প্রোগ্রামাররা _name প্রচলন বা closure ব্যবহার করতেন। ES2022-এ #name দিয়ে সত্যিকারের private field যোগ হয়। অর্থাৎ বিষয়টি নিয়ে ভাষাগুলোর মধ্যে আজও বিতর্ক চলে; কোনো একটি উত্তর সবার জন্য ঠিক নয়।
মূল উপলব্ধি: private-এর মূল উদ্দেশ্য হলো ইন্টারফেস আর ভেতরের খুঁটিনাটি আলাদা করা, ডেটা চুরি ঠেকানো নয়। Python এই কাজটি প্রচলন আর বিশ্বাস দিয়ে করে — কম বাধা, বেশি দায়িত্ব।
প্র ০৪type(acc) দেয় Account, আর type(Account) দেয় type. class-ও যদি object হয়, তাহলে class কে বানায়? metaclass কী, আর কখন দরকার?
এই পর্বের শুরুতে বলেছি, class Account: লাইন চললে একটি class object তৈরি হয়। প্রতিটি object কোনো না কোনো class-এর instance — acc হলো Account-এর instance. তাহলে Account object-টি কোন class-এর instance? উত্তর: type-এর। যে class-এর instance নিজেরাই class, তাকে বলে metaclass, আর Python-এর ডিফল্ট metaclass হলো type. এমনকি type(type)-ও type — শৃঙ্খলটি এখানে নিজের ভেতরে বন্ধ হয়।
এর মানে class statement আসলে একটি ফাংশন-ডাকের সুন্দর রূপ। Python class-এর শরীর চালিয়ে একটি namespace (dict) পায়, তারপর মোটামুটি type("Account", (), namespace) ডাকে। আপনি নিজেও সরাসরি লিখতে পারেন: Point = type("Point", (), {"x": 0}) — এটি class Point: x = 0-এর সমান। type-এর এই দুই রূপ (এক argument দিলে type জানায়, তিনটি দিলে নতুন class বানায়) প্রথমে অদ্ভুত লাগে, কিন্তু এটি “class-ও object” ধারণার সরাসরি ফল।
ইতিহাসটিও শিক্ষণীয়। Python 2.2 (২০০১)-এর আগে ভাষায় দুই জগৎ ছিল: Python-এ লেখা class (“classic class”) আর C-তে লেখা built-in type (int, list) — দুটি আলাদা নিয়মে চলত, built-in type থেকে নিজের class বানানো যেত না। Python 2.2-তে Guido “type ও class একীকরণ” (PEP 252 আর 253) করেন — “new-style class”, যার শিকড়ে object আর যার metaclass type. Python 3-এ পুরোনো classic class পুরোপুরি বাদ যায়; এখন সব class-ই new-style.
নিজের metaclass বানানো যায় — class Meta(type): ... আর class Account(metaclass=Meta): — যাতে class তৈরির মুহূর্তেই কিছু করা যায়: সব method-এর নাম যাচাই, class-কে কোনো তালিকায় নিবন্ধন, ইত্যাদি। Django-র মতো framework-এর ORM ঐতিহাসিকভাবে metaclass দিয়ে model class-কে ডেটাবেস টেবিলে রূপ দিয়েছে। তবে metaclass নিয়ে একটি বিখ্যাত উক্তি প্রায়ই Tim Peters-এর নামে উদ্ধৃত হয়: এটি এমন গভীর জাদু, যা নিয়ে ৯৯% ব্যবহারকারীর কখনো মাথা ঘামানোর দরকার নেই; যদি ভাবেন আপনার দরকার কি না, তাহলে দরকার নেই।
আধুনিক Python এই প্রয়োজন আরও কমিয়েছে: class decorator (Python 2.6/3.0 থেকে), আর Python 3.6-এ PEP 487-এর __init_subclass__, যা দিয়ে metaclass ছাড়াই subclass তৈরির মুহূর্তে কাজ করা যায়। পর্ব ৩২-এর @dataclass-ও metaclass নয়, একটি class decorator.
মূল উপলব্ধি: “সবই object” কথাটি class পর্যন্ত গড়ায় — class হলো type-এর instance, আর class statement আসলে type(...) ডাকের সুন্দর রূপ। metaclass জানা থাকা ভালো, কিন্তু প্রতিদিনের কোডে প্রায় কখনো দরকার পড়ে না।
প্র ০৫Jack Diederich-এর “Stop Writing Classes” বক্তৃতা কেন এত বিখ্যাত? class কখন ভালো নকশা, আর কখন শুধু অপ্রয়োজনীয় জটিলতা?
২০১২ সালে PyCon US সম্মেলনে Python-এর অভিজ্ঞ ডেভেলপার Jack Diederich একটি ছোট বক্তৃতা দেন, শিরোনাম “Stop Writing Classes”. বক্তৃতাটি অনলাইনে বিপুলভাবে ছড়িয়ে পড়ে, আর আজও Python-এর নকশা-আলোচনায় এর উল্লেখ আসে। তিনি বাস্তব কোডবেস থেকে উদাহরণ দেখান, যেখানে অপ্রয়োজনীয় class, অপ্রয়োজনীয় module আর অসংখ্য নিজস্ব exception class সরিয়ে কোড অনেক ছোট আর পরিষ্কার করা গেছে।
তাঁর সবচেয়ে উদ্ধৃত নিয়মটি সহজ: যে class-এ মাত্র দুটি method, আর তার একটি __init__ — সেটি আসলে একটি ফাংশন। যেমন Greeting("Rina").greet()-এর বদলে greet("Rina"). class বানালে আপনি তিনটি জিনিস যোগ করেন — একটি নতুন নাম, একটি অবস্থা, আর দুই ধাপের ব্যবহার (আগে বানাও, পরে ডাকো) — অথচ কোনো সুবিধা পান না, কারণ object-টি একবার ব্যবহার করেই ফেলে দেওয়া হয়। আরেকটি পরামর্শ: “ভবিষ্যতে লাগতে পারে” ভেবে আগেভাগে কাঠামো বানাবেন না; প্রয়োজন এলে তখন বানানো সহজ।
এই ভাবনা Python-এর মূল দর্শনের সাথে মেলে। PEP 20 বলে, “Simple is better than complex” আর “Flat is better than nested”. Java-তে ভাষার কাঠামোর কারণেই সব কোড class-এ রাখতে হয়, তাই সেখানে “utility class” — কয়েকটি static method-এর বাক্স — স্বাভাবিক। Python-এ module নিজেই সেই বাক্স; একটি ফাইলে কয়েকটি ফাংশন রাখলেই হলো।
কিন্তু বক্তৃতাটির ভুল ব্যাখ্যাও আছে: “class খারাপ” — এটি তাঁর বক্তব্য ছিল না। তিনি নিজেও মেনেছেন, ভালো class-এর জায়গা আছে। এই পর্বের ভাষায় class তখনই যুক্তিসঙ্গত যখন:
- রক্ষা করার মতো একটি invariant আছে (wallet-এর ব্যালান্স, বইয়ের ধার-অবস্থা);
- একই নকশার অনেক instance, প্রত্যেকের নিজের অবস্থা (প্রতিটি ছাত্র, প্রতিটি রিকশা);
- একই ডেটার ওপর কয়েকটি সম্পর্কিত আচরণ (জমা, তোলা, ইতিহাস);
- পর্ব ৩০-এর dunder method দিয়ে Python-এর সাথে স্বাভাবিকভাবে মিশতে হবে (
len(),==,for).
আর যখন শুধু ডেটা একসাথে রাখতে হয়, তখন dict, namedtuple বা @dataclass (পর্ব ৩২) — যেগুলো প্রায় কোনো বাড়তি কোড ছাড়াই গোছানো রূপ দেয়।
মূল উপলব্ধি: class একটি হাতিয়ার, লক্ষ্য নয়। প্রশ্নটি সবসময় “এখানে class লিখতে পারি কি না” নয়, বরং “class এখানে কোন সমস্যার সমাধান করছে?” — উত্তর যদি invariant, অনেক instance বা সম্পর্কিত আচরণ না হয়, তাহলে সম্ভবত একটি ফাংশনই যথেষ্ট।
অনুশীলন
বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — অনুমান করা, পড়া আর মিলিয়ে নেওয়াই আসল কাজ।
-
selfভুলে গেলে: কেউ লিখলেনclass Meter:যার ভেতরেdef add_trip(km): ...(কোনোselfনেই)।m = Meter()তারপরm.add_trip(2)ডাকলে কী ঘটবে, আর error বার্তায় “2 were given” কেন লেখা থাকবে?আসবে
TypeError: Meter.add_trip() takes 1 positional argument but 2 were given. কারণm.add_trip(2)আসলেMeter.add_trip(m, 2)— Python নিজেm-কে প্রথম argument হিসেবে পাঠিয়েছে, আর আপনি দিয়েছেন 2; মোট দুটি। কিন্তু ফাংশনটি নেয় মাত্র একটি (km). সমাধান:def add_trip(self, km):. এই error দেখলেই প্রথমেselfআছে কি না দেখুন। -
খোঁজার ক্রম অনুমান:
class Rickshaw:যার ভেতরেbase_fare = 30.r1, r2 = Rickshaw(), Rickshaw(); তারপরr1.base_fare = 40; তারপরRickshaw.base_fare = 35. এখনr1.base_fare,r2.base_fareআরvars(r2)কী হবে?r1.base_fare= 40 —r1-এর নিজের খাতায় লেখা আছে, তাই class-এ যাওয়ার দরকারই হয় না; পরে class বদলালেওr1-এর কিছু হয় না।r2.base_fare= 35 —r2-এর খাতায় কিছু নেই, তাই class থেকে পড়ে, আর class-এর মান এখন 35.vars(r2)={}— খালি খাতা। শিক্ষা: class attribute বদলালে যেসব object নিজের মান রাখেনি, শুধু তারাই নতুন মান দেখে। -
invariant খুঁজুন: লাইব্রেরির একটি
Bookclass-এ attribute আছেtitleআরborrower(কারও কাছে না থাকলেNone), আর methodborrow(name)ওgive_back(). এই class-এর invariant কী কী? প্রতিটি method কোন অবস্থায়ValueErrorতুলবে? আর কোন লাইন লিখলে বাইরের কেউ invariant ভাঙতে পারে?মূল invariant: একটি বই একই সময়ে সর্বোচ্চ একজনের কাছে থাকে, আর
borrowerহয়None, নয়তো একটি অ-খালি নাম।borrow(name)তুলবেValueErrorযদিborrowerআগে থেকেইNoneনা হয় (বই অন্যের কাছে), বা নাম খালি হয়।give_back()তুলবে যদি বইটি কারও কাছে না থাকে (borrower is None) — যে বই ধার দেওয়াই হয়নি তা ফেরত হয় না। বাইরে থেকেbook.borrower = "Jamil"লিখলে আগের ধারগ্রহীতার তথ্য নিঃশব্দে মুছে যায় — invariant ভাঙে। প্রতিরোধ: নাম_borrowerকরা (সংকেত) আর পর্ব ৩২-এরpropertyদিয়ে শুধু পড়ার সুযোগ দেওয়া। -
একটি ছোট class লিখুন (এবং AI-কে যাচাই করুন): একটি
Studentclass লিখুন:__init__(self, name)-এ নাম আর একটি খালি dictself.marks;add_mark(subject, mark), যা ০–১০০-এর বাইরে হলেValueErrorতোলে; আরaverage(), যা গড় নম্বর দেয় (কোনো নম্বর না থাকলে 0). দুজন ছাত্র বানিয়ে প্রমাণ করুন তাদের নম্বর মেশে না। তারপর কোনো AI-কে একই কাজ দিন আর চেকলিস্ট দিয়ে তার কোড মেলান।একটি সম্ভাব্য সমাধান:
class Student: def __init__(self, name): self.name = name self.marks = {} # প্রতিটি ছাত্রের নিজের dict def add_mark(self, subject, mark): if not 0 <= mark <= 100: raise ValueError(f"অবৈধ নম্বর: {mark}") self.marks[subject] = mark def average(self): if not self.marks: return 0 return sum(self.marks.values()) / len(self.marks) rina, jamil = Student("রিনা"), Student("জামিল") rina.add_mark("Physics", 80) rina.add_mark("Math", 90) print(rina.average(), jamil.average()) # 85.0 0self.marks = {}__init__-এ থাকায় প্রতিটি ছাত্র নিজের dict পায় — জামিলের গড় 0, রিনার নম্বর সেখানে যায়নি। গড়টি attribute হিসেবে জমা না রেখে প্রতিবার হিসাব করা হচ্ছে, তাই নতুন নম্বর যোগ করলেও সেটি কখনো পুরোনো হয় না। AI যদিmarks = {}class-স্তরে রাখে, বাget_name()/set_name()যোগ করে, বাadd_mark-এ সীমা পরীক্ষা না করে — চেকলিস্ট সেগুলো ধরবে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমPython-এ acc = Account("Rahim", 500) চললে যে ক্রমে কাজ হয়, সেই ক্রমে সাজান (▲▼ চেপে সরান)
- 📞 class object
Account-কে ফাংশনের মতো ডাকা হয় - 🧱
__new__একটি নতুন, খালি object বানায় - 👉
__init__ডাকা হয়,self= সেই খালি object - 📝
self.owner,self.balanceobject-এর__dict__-এ লেখা হয় - 🏷️ সাজানো object-এ
accনামটি লাগে
Python-এ acc যদি Account class-এর একটি object হয়, তাহলে acc.deposit(100) লেখা কোনটির সমান?
- deposit(Account, 100)
- Account.deposit(100)
- Account.deposit(acc, 100)
- self.deposit(acc, 100)
Python class-এ __init__ method-এর কাজ কী?
- মেমরিতে নতুন object তৈরি করা
- সদ্য তৈরি object-কে শুরুর মান দিয়ে সাজানো
- object মুছে ফেলার আগে পরিষ্কার করা
- নতুন object return করা
class Team: players = [] সংজ্ঞায়িত (কোনো __init__ নেই)। a, b = Team(), Team() তারপর a.players.append("Shakib"). এখন b.players কী?
- ['Shakib']
- []
- AttributeError
- None
Python class Account-এর ভেতরে লেখা self.__pin = 1234 নামটি object-এ আসলে কোন নামে রাখা হয়?
- __pin
- _pin
- কোনো নামেই না — বাইরে থেকে পাওয়া অসম্ভব
- _Account__pin
Python-এ @classmethod-এর সবচেয়ে প্রচলিত ব্যবহার কোনটি?
- method-কে private করা
- বিকল্প constructor, যেমন
from_stringদিয়ে object বানানো - কোনো স্বয়ংক্রিয় argument ছাড়া একটি সহায়ক ফাংশন রাখা
- method-কে দ্রুত চালানো
class P: pass সংজ্ঞায়িত। p = P() তারপর p.x = 5 লিখলে কী হয়?
- AttributeError, কারণ x আগে ঘোষণা করা হয়নি
- P class-এর সব object-এ x = 5 হয়ে যায়
- p-এর নিজের __dict__-এ নতুন attribute x তৈরি হয়
- SyntaxError, কারণ খালি class-এ attribute রাখা যায় না
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- class = ডেটা + আচরণ + invariant. class হলো নকশা, প্রতিটি ডাক
Account(...)একটি নতুন, আলাদা object বানায়। Python-এ class নিজেও একটি object (type-এর instance). __new__object বানায়,__init__সাজায় (initializer), কিছু ফেরত দেয় না। সব instance attribute__init__-এ তৈরি করুন।obj.m(x)=Cls.m(obj, x)—selfহলো সেই object, Python নিজে পাঠায়। অবস্থা বদলানোর প্রতিটি method invariant পরীক্ষা করে, ভাঙলেValueError.- পড়া: আগে instance-এর
__dict__, পরে class; লেখা: সবসময় instance-এ। class attribute-এ শুধু ভাগ করার মতো immutable মান — class-এ রাখা list সবাই ভাগ করে। _x= “হাত দেবেন না” সংকেত,__x= name mangling (_Cls__x);@classmethodবিকল্প constructor দেয়। আর একটি ফাংশন যথেষ্ট হলে class লিখবেন না।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩০ · dunder method — Python-এর data modelপরবর্তী পর্ব
__repr__,__eq__,__len__— নিজের object-কে Python-এর সাথে মেশানো। - Python প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — ৮টি মডিউল, কোন পর্ব কখন।
- পর্ব ২০ · scope, *args/**kwargs, closure আর default-এর ফাঁদঝালিয়ে নিনclosure বনাম class, আর mutable default-এর ফাঁদ — এই পর্বের class attribute-এর ফাঁদের যমজ।
- C++ প্রোগ্রামিং · class ও objectতুলনা করুনconstructor,
thisআরprivate— একই ধারণা, কঠোর রূপে।