ক্লাস — ডেটা আর আচরণ একসাথে
class, __init__ আর self, মেথড, __repr__ দিয়ে প্রিন্টের রূপ, __eq__ দিয়ে সমতা, ক্লাস-লেভেল আর ইনস্ট্যান্স-লেভেলের পার্থক্য, আর কখন ক্লাস লাগে না।
কীভাবে শিখতে চান?
যে সমস্যাটা আমরা সমাধান করছি
এতদিন একটা জিনিসের কয়েকটা তথ্য একসাথে রাখতে আমরা ডিকশনারি ব্যবহার করেছি।
order = {"name": "pen", "price": 15.0, "quantity": 3}
print(order["nmae"])KeyError: 'nmae'এররটা ধরা পড়েছে, ভালো কথা — কিন্তু কেবল ওই লাইনটা চলার সময়। একটা শর্তের ভিতরে লুকানো থাকলে সেটা মাসখানেক পরে প্রথমবার দেখা দিতে পারত।
আর সমস্যাটা কেবল বানান নয়। একটা ডিকশনারিতে যেকোনো key বসানো যায়, তাই কোথাও লেখা নেই একটা অর্ডার-লাইনে আসলে কী কী থাকা উচিত। মোট হিসাব করার ফাংশনটা অন্য কোথাও, আর তাকে ডাকার আগে ডিকশনারিটায় সব ফিল্ড আছে কিনা কেউ যাচাই করে না। ডেটা এক জায়গায়, সেটা নিয়ে কী করা যায় তা আরেক জায়গায়, আর কোনটা কোনটার সাথে যায় তা কেবল আপনার মাথায়।
class OrderLine:
def __init__(self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity
def total(self):
return round(self.price * self.quantity, 2)
line = OrderLine("pen", 15.0, 3)
print(line.name)
print(line.total())pen
45.0একটা ক্লাস হলো একটা ছাঁচ: সে বলে দেয় এই ধরনের জিনিসে কী কী থাকে, আর তাকে নিয়ে কী করা যায়।
এই অধ্যায় শেষে আপনি পারবেন
classলিখতে, আর__init__ওselfকী করে তা বলতে- মেথড লিখতে, আর মেথড কেন সাধারণ ফাংশন থেকে আলাদা তা বলতে
__repr__দিয়ে প্রিন্ট করার উপযোগী একটা রূপ দিতে- ক্লাস-লেভেল আর ইনস্ট্যান্স-লেভেলের অ্যাট্রিবিউটের পার্থক্য বলতে — আর একটা পরিচিত ফাঁদ এড়াতে
__eq__দিয়ে দুটো অবজেক্টকে তুলনা করার উপযোগী করতে- বলতে কখন ক্লাস লাগে না
আগে যা জানা লাগবে: JSON আর CSV।
self আসলে কী
self কোনো জাদুর শব্দ নয় — সে কেবল প্রথম প্যারামিটার, আর তাতে বসে সেই অবজেক্টটা, যার উপর মেথডটা ডাকা হয়েছে।
line.total() লেখার মানে আসলে “OrderLine-এর total মেথডটা চালাও, আর self-এ line-কে দাও”। সেজন্যই ভিতরে self.price লিখলে ঠিক ওই লাইনটার দাম পাওয়া যায়।
self লিখতে ভুলে গেলে মেসেজটা প্রথমে বিভ্রান্তিকর লাগে:
class OrderLine:
def __init__(self, name):
self.name = name
def shout():
return "hello"
line = OrderLine("pen")
print(line.shout())TypeError: OrderLine.shout() takes 0 positional arguments but 1 was given“একটা দেওয়া হয়েছে” — কিন্তু ব্র্যাকেট তো খালি। দেওয়া হয়েছে line-কেই, চুপচাপ, আর shout সেটা নেওয়ার জায়গা রাখেনি।
__init__-ও একটা সাধারণ মেথড, কেবল নামটা বিশেষ: অবজেক্টটা তৈরি হওয়ার পরপরই পাইথন তাকে ডাকে। ভিতরে self.name = name লিখে অ্যাট্রিবিউটগুলো বসানো হয়।
বানান ভুল হলে এররটা আরও পরিষ্কার
class OrderLine:
def __init__(self, name):
self.name = name
line = OrderLine("pen")
print(line.nmae)AttributeError: 'OrderLine' object has no attribute 'nmae'একটা কথা সৎভাবে বলা দরকার: ক্লাস এই ভুলটা আগেভাগে ধরে না। AttributeError-ও ঠিক KeyError-এর মতো তখনই আসে, যখন লাইনটা চলে। শর্তের ভিতরে লুকানো থাকলে এটাও মাসখানেক পরে দেখা দিতে পারে।
তাহলে লাভটা কোথায়? তিনটা জায়গায়।
- এররটা বেশি কিছু বলে। মেসেজে ক্লাসের নাম আছে —
'OrderLine' object— তাই বোঝা যায় কোন ধরনের জিনিসে ভুল নামটা খোঁজা হয়েছে। - কী কী থাকার কথা, তা এক জায়গায় লেখা।
__init__পড়লেই দেখা যায় একটাOrderLine-এ কোন কোন অ্যাট্রিবিউট আছে। ডিকশনারিতে এমন কোনো তালিকা নেই। - এডিটর সাহায্য করতে পারে। এডিটর ক্লাসটা পড়ে
line.লেখার পর অ্যাট্রিবিউটের নাম সাজেস্ট করে, আর অনেক এডিটর অচেনা নামের নিচে দাগ টেনে দেয় — কোড চালানোর আগেই। ডিকশনারির key-এর বেলায় এডিটর সাধারণত এটা পারে না।
সীমাটাও মনে রাখুন: লেখার সময় বানান ভুল ধরা পড়ে না। order["nmae"] = 5 যেমন চুপচাপ একটা নতুন key বানিয়ে ফেলে, line.nmae = 5-ও তেমনি চুপচাপ একটা নতুন অ্যাট্রিবিউট বানিয়ে ফেলে।
আর অবজেক্টের ভিতরে কী আছে তা দেখাও যায়:
class OrderLine:
def __init__(self, name):
self.name = name
line = OrderLine("pen")
print(type(line))
print(isinstance(line, OrderLine))
print(line.__dict__)<class '__main__.OrderLine'>
True
{'name': 'pen'}__dict__ দেখায় অবজেক্টটার ভিতরে আসলে একটা ডিকশনারিই আছে। ক্লাস সেই ডিকশনারিটাকে সরায়নি — তাকে একটা নাম আর একটা নির্দিষ্ট আকার দিয়েছে।
__repr__ — প্রিন্ট করলে কী দেখা যাবে
__repr__ না লিখলে:
class OrderLine:
def __init__(self, name):
self.name = name
print(OrderLine("pen"))<__main__.OrderLine object at 0x79b769f03830>শেষের সংখ্যাটা মেমরির ঠিকানা, আর প্রতিবার চালালে আলাদা আসবে — আপনার স্ক্রিনে অন্য একটা সংখ্যা দেখবেন। ডিবাগ করার সময় এটা কোনো কাজে লাগে না।
class OrderLine:
def __init__(self, name, price):
self.name = name
self.price = price
def __repr__(self):
return f"OrderLine(name={self.name!r}, price={self.price!r})"
line = OrderLine("pen", 15.0)
print(line)
print([line, line])OrderLine(name='pen', price=15.0)
[OrderLine(name='pen', price=15.0), OrderLine(name='pen', price=15.0)]দ্বিতীয় লাইনটাই আসল সুবিধা: লিস্টের ভিতরে থাকলেও কাজ করে। লিস্ট প্রিন্ট করার সময় পাইথন ভিতরের প্রতিটা জিনিসের __repr__ ডাকে, __str__ নয়। সেজন্যই দুটোর মধ্যে __repr__ আগে লেখা উচিত।
প্রচলিত নিয়ম হলো এমন একটা স্ট্রিং ফেরত দেওয়া যা দেখতে অবজেক্টটা বানানোর কোডের মতো। !r ব্যবহার করলে ভিতরের স্ট্রিংগুলো কোটেশনসহ আসে — ২৪ নম্বর অধ্যায়ের সেই একই কারণে।
সমতা — ডিফল্টটা আপনি যা চান তা নয়
class OrderLine:
def __init__(self, name):
self.name = name
a = OrderLine("pen")
b = OrderLine("pen")
print(a == b)
print(a == a)False
Trueদুটো অবজেক্টের ভিতরে একই জিনিস, তবু == বলছে False। কারণ ডিফল্ট সমতা মানে অভিন্নতা — == তখন is-এর মতোই আচরণ করে, অর্থাৎ জিজ্ঞেস করে “এটা কি ঠিক ওই অবজেক্টটাই?”।
বদলাতে হলে বলে দিতে হয় সমান মানে কী:
class OrderLine:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
a = OrderLine("pen")
b = OrderLine("pen")
print(a == b)
print(a is b)True
Falseএখন == বলছে “একই রকম” আর is বলছে “একই অবজেক্ট” — ২১ নম্বর অধ্যায়ের সেই পার্থক্যটা, এবার আপনার নিজের বানানো টাইপে।
একটা সাবধানতা: __eq__ লিখলে অবজেক্টটা আর ডিফল্টভাবে হ্যাশযোগ্য থাকে না, তাই তাকে সেটে বা ডিকশনারির key হিসেবে রাখা যায় না। পরের অধ্যায়ে দেখব dataclass এই দুটো কাজই এক লাইনে করে দেয়।
ক্লাস-লেভেল আর ইনস্ট্যান্স-লেভেল
class Basket:
TAX_RATE = 0.15
items = []
def add(self, name):
self.items.append(name)
a = Basket()
b = Basket()
a.add("pen")
b.add("bag")
print(a.items)
print(b.items)
print(a.items is b.items)['pen', 'bag']
['pen', 'bag']
Trueদুটো আলাদা ঝুড়ি, অথচ লিস্ট একটাই।
চিনতে পারছেন? এটা ২১ নম্বর অধ্যায়ের মিউটেবল ডিফল্টের ফাঁদটাই, শুধু অন্য জায়গায়। class লাইনের ঠিক নিচে লেখা items = [] একবারই চলে, ক্লাসটা তৈরি হওয়ার সময় — প্রতিটা অবজেক্টের জন্য আলাদা করে নয়। তাই লিস্টটা ক্লাসের, আর সব ইনস্ট্যান্স সেটাকেই ভাগ করে ব্যবহার করে।
সমাধানও একই ধরনের: যা প্রতিটা অবজেক্টের নিজের, তা __init__-এর ভিতরে বানান।
class Basket:
TAX_RATE = 0.15
def __init__(self):
self.items = []
def add(self, name):
self.items.append(name)
a = Basket()
b = Basket()
a.add("pen")
b.add("bag")
print(a.items)
print(b.items)
print(a.TAX_RATE, b.TAX_RATE)['pen']
['bag']
0.15 0.15TAX_RATE ক্লাস-লেভেলেই রয়ে গেছে, আর সেটাই ঠিক — এটা সব ঝুড়ির জন্য একই, আর আমরা এটা বদলাতে চাই না। তবে খেয়াল করুন, পাইথন এটা বদলানো আটকায় না: Basket.TAX_RATE = 0.2 লিখলে কোনো এরর আসবে না। পুরো বড় হাতের নামটা কেবল একটা প্রচলিত নিয়ম — পাঠককে বলে “এটা ধ্রুবক হিসেবে ধরুন, বদলাবেন না”। নিয়মটা এক লাইনে: ক্লাস-লেভেলে কেবল ধ্রুবক; যা বদলায় তার সবকিছু __init__-এ।
কখন ক্লাস লিখবেন না
ক্লাস শেখার পর সবকিছুকে ক্লাস বানানোর একটা ঝোঁক আসে। কয়েকটা লক্ষণ, যেখানে ক্লাসটা বাড়তি:
ক্লাসটায় শুধু __init__ আর একটা মেথড। সেটা আসলে একটা ফাংশন, অকারণে দুই ভাগে লেখা।
কোনো মেথড নেই, কেবল ডেটা। পরের অধ্যায়ের dataclass ঠিক এর জন্য, আর তাতে লাইন অনেক কম লাগে।
অবজেক্টটা কখনো বদলায় না আর একটাই জিনিস ধরে রাখে। একটা টাপল, নয়তো একটা সাধারণ ভ্যালুই যথেষ্ট।
ক্লাসের আসল জায়গা হলো যেখানে ডেটা আর আচরণ সত্যিই একসাথে থাকে: কিছু ডেটা যা বদলায়, আর কয়েকটা কাজ যেগুলো সবসময় সেই ডেটার উপরেই চলে।
একটা সম্পূর্ণ উদাহরণ
"""An order built from two small classes."""
TAX_RATE = 0.15
class OrderLine:
"""One item on an order: what it is, what it costs, how many."""
def __init__(self, name, price, quantity):
if quantity < 1:
raise ValueError(f"quantity must be at least 1: {quantity}")
self.name = name
self.price = price
self.quantity = quantity
def total(self):
"""Price for this line, tax included."""
return round(self.price * self.quantity * (1 + TAX_RATE), 2)
def __repr__(self):
return f"OrderLine({self.name!r}, {self.price!r}, {self.quantity!r})"
class Order:
"""A named collection of lines, and the questions you ask about it."""
def __init__(self, customer):
self.customer = customer
self.lines = []
def add(self, name, price, quantity):
self.lines.append(OrderLine(name, price, quantity))
return self
def total(self):
return round(sum(line.total() for line in self.lines), 2)
def largest(self):
"""The most expensive line, or None for an empty order."""
if not self.lines:
return None
return max(self.lines, key=lambda line: line.total())
def __repr__(self):
return f"Order({self.customer!r}, {len(self.lines)} lines)"
def main():
order = Order("rafi")
order.add("pen", 15.0, 3)
order.add("bag", 850.0, 1)
order.add("ink", 120.0, 2)
for line in order.lines:
print(f"{line.name:<6} {line.total():>9.2f}")
print(f"{'total':<6} {order.total():>9.2f}")
print()
print(order)
print("largest:", order.largest())
try:
order.add("clip", 5.0, 0)
except ValueError as err:
print("rejected:", err)
if __name__ == "__main__":
main()pen 51.75
bag 977.50
ink 276.00
total 1305.25
Order('rafi', 3 lines)
largest: OrderLine('bag', 850.0, 1)
rejected: quantity must be at least 1: 0চারটা জিনিস খেয়াল করার মতো।
__init__ যাচাই করছে। quantity < 1 হলে অবজেক্টটা তৈরিই হয় না। এটাই ক্লাসের সবচেয়ে কাজের সুবিধা: একটা OrderLine থাকা মানেই তার পরিমাণ অন্তত এক — ব্যবহারের প্রতিটা জায়গায় আর আলাদা করে যাচাই করতে হয় না। ডিকশনারি এই নিশ্চয়তা দিতে পারে না।
Order জানে না একটা লাইনের দাম কীভাবে হিসাব হয়। সে line.total() ডাকে, ব্যস। ট্যাক্স কীভাবে লাগে সেটা OrderLine-এর ব্যাপার, আর বদলাতে হলে এক জায়গাতেই বদলাতে হয়।
__repr__ দুটো ক্লাসেই আছে, আর largest()-এর আউটপুটেই তার মূল্য বোঝা যায়। ওটা না থাকলে শেষ লাইনটা হতো একটা মেমরির ঠিকানা — যা থেকে কিছুই জানা যেত না।
add নিজেকেই ফেরত দেয় (return self)। এতে order.add(...).add(...) লেখা সম্ভব হয়। এখানে সেটা ব্যবহার করা হয়নি, আর সেটা ইচ্ছা করেই — সুবিধাটা থাকা আর সেটা দিয়ে প্রতিটা লাইন লম্বা করা এক কথা নয়।
কিছু ভাঙা অবস্থা ও তার সমাধান
TypeError: ... takes 0 positional arguments but 1 was given মেথডটায় self লিখতে ভুলে গেছেন। প্রথম প্যারামিটার হিসেবে self দিন।
AttributeError: 'X' object has no attribute 'y' বানান ভুল, নয়তো __init__-এ self.y কখনো বসানো হয়নি। print(obj.__dict__) দিয়ে দেখুন আসলে কী কী আছে।
TypeError: X() takes no arguments __init__ লেখা হয়নি, নয়তো নামের বানান ভুল (__int__ নয়, __init__ — দুই পাশে দুটো করে আন্ডারস্কোর)।
সব অবজেক্ট একই লিস্ট ভাগ করছে লিস্টটা ক্লাস-লেভেলে বসানো হয়েছে। সেটা __init__-এর ভিতরে নিয়ে যান।
দুটো অবজেক্টের ভিতরে একই জিনিস, তবু == হলো False ডিফল্ট সমতা মানে অভিন্নতা। __eq__ লিখুন — বা পরের অধ্যায়ের dataclass ব্যবহার করুন।
__eq__ লেখার পর সেটে রাখা যাচ্ছে না __eq__ লিখলে ডিফল্ট হ্যাশ চলে যায়। dataclass(frozen=True) দুটোই দেয়।
প্রিন্ট করলে <__main__.X object at 0x...> আসে __repr__ লেখা হয়নি।
ধাপ ৪ / ৬ — অনুমান
যাচাই করুন
shout() কোনো আর্গুমেন্ট ছাড়াই ডাকা হয়েছে। কী হবে?
class OrderLine:
def __init__(self, name):
self.name = name
def shout():
return "hello"
line = OrderLine("pen")
print(line.shout())- A`TypeError` — একটা নেয়, কিন্তু শূন্যটা দেওয়া হয়েছে
- B`TypeError` — শূন্যটা নেয়, কিন্তু একটা দেওয়া হয়েছে
- C`AttributeError`
- D`hello` প্রিন্ট হয়
দুটো আলাদা ঝুড়ি, প্রতিটাতে একটা করে জিনিস। কী প্রিন্ট হবে?
class Basket:
items = []
def add(self, name):
self.items.append(name)
a = Basket()
b = Basket()
a.add("pen")
b.add("bag")
print(a.items)
print(b.items)- A['pen', 'bag'] []
- B['pen'] ['pen', 'bag']
- C['pen'] ['bag']
- D['pen', 'bag'] ['pen', 'bag']
দুটো অবজেক্টের ভিতরে একই নাম। কী প্রিন্ট হবে?
class OrderLine:
def __init__(self, name):
self.name = name
a = OrderLine("pen")
b = OrderLine("pen")
print(a == b)
print(a == a)- AFalse True
- Bএকটা `TypeError`
- CFalse False
- DTrue True
উত্তর দিতে অ্যাকাউন্ট লাগবে
উত্তর মিলিয়ে দেখতে সাইন ইন করুন
প্রশ্নগুলো উপরে আছে, আর মাথায় মাথায় উত্তর ভেবে নেওয়াই আসল কাজ। সঠিক উত্তর, ব্যাখ্যা আর তিন ধাপের ইঙ্গিত দেখতে সাইন ইন করুন।
নিজে করুন
দুটো ক্লাস লিখুন।
Book — title, author আর pages নিয়ে তৈরি হবে। pages এক-এর কম হলে ValueError রেইজ করবে। একটা __repr__ থাকবে, আর একটা __eq__ থাকবে যা টাইটেল ও লেখক মিললে দুটো বইকে সমান বলবে।
Shelf — বই জমা রাখবে, আর দেবে: add(book), total_pages(), longest(), আর by_author(name) যা একটা লিস্ট ফেরত দেবে।
তারপর পাঁচটা পরীক্ষা:
Shelf-এরbooks = []লাইনটা__init__-এর বাইরে,classলাইনের নিচে নিয়ে যান। দুটো আলাদা তাক বানিয়ে একটাতে বই যোগ করুন, তারপর অন্যটা প্রিন্ট করুন। কী দেখছেন?__repr__-টা মুছে দিন, তারপর বইয়ের একটা লিস্ট প্রিন্ট করুন। ডিবাগ করা কতটা কঠিন হলো?__eq__মুছে দিন, তারপর একই টাইটেল-লেখকের দুটো বই তুলনা করুন।- একটা মেথড থেকে
selfসরিয়ে দিন আর তাকে ডাকুন। মেসেজটা কয়টা আর্গুমেন্টের কথা বলছে? - একটা
Bookবানিয়েbook.pagse = 100লিখুন (ইচ্ছা করে বানান ভুল করে)। কোনো এরর আসে কি? তারপরprint(book.__dict__)করুন — কী দেখছেন?
শেষ পরীক্ষাটা গুরুত্বপূর্ণ, কারণ সেটা ক্লাসের সীমা দেখায়। পড়ার সময় ভুল নামে এরর আসে, লেখার সময় আসে না — একটা নতুন অ্যাট্রিবিউট বসানো সবসময়ই চলে। পরের অধ্যায়ে দেখব টাইপ হিন্ট আর একটা টাইপ চেকার এই ফাঁকটাও বন্ধ করতে পারে।
ধাপ ৬ / ৬
কঠিন করা — অধ্যায়ের কুইজ
সহজ থেকে কঠিন — দশটি প্রশ্ন, শেষেরগুলো ইচ্ছে করেই কঠিন।
সাইন ইন করে কুইজ দিন