ক্লাস — তথ্য আর আচরণ একসাথে
class, __init__ আর self, মেথড, __repr__ দিয়ে ছাপার রূপ, __eq__ দিয়ে সমতা, ক্লাস-স্তর আর ইনস্ট্যান্স-স্তরের পার্থক্য, আর কখন ক্লাস লাগে না।
- 1সমস্যা
- 2বোঝা
- 3উদাহরণ
- 4অনুমান
- 5নিজে করা
- 6কঠিন করা
যে সমস্যাটা আমরা সমাধান করছি
এতদিন একটা জিনিসের কয়েকটা বৈশিষ্ট্য একসাথে রাখতে আমরা ডিকশনারি ব্যবহার করেছি।
order = {"name": "pen", "price": 15.0, "quantity": 3}
print(order["nmae"])KeyError: 'nmae'ত্রুটিটা ধরা পড়েছে, ভালো কথা — কিন্তু কেবল ওই লাইনটা চলার সময়। একটা শর্তের ভিতরে লুকোনো থাকলে সেটা মাসখানেক পরেও প্রথমবার দেখা দিতে পারত।
আর সমস্যাটা কেবল বানান নয়। একটা ডিকশনারিতে যেকোনো কী বসানো যায়, তাই কোথাও লেখা নেই একটা অর্ডার-লাইনে আসলে কী কী থাকা উচিত। মোট হিসাব করার ফাংশনটা অন্য কোথাও, আর তাকে ডাকার আগে ডিকশনারিটায় সব ক্ষেত্র আছে কিনা কেউ যাচাই করে না। তথ্য এক জায়গায়, তা নিয়ে কী করা যায় সেটা আরেক জায়গায়, আর কোনটা কোনটার সাথে যায় তা কেবল আপনার মনে।
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 লিখলে ওই নির্দিষ্ট লাইনটার দাম পাওয়া যায়।
ভুলে গেলে বার্তাটা প্রথমে বিভ্রান্তিকর লাগে:
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'ডিকশনারির KeyError-এর সাথে পার্থক্যটা সূক্ষ্ম কিন্তু বাস্তব: ডিকশনারিতে যেকোনো কী বসানো যায়, তাই order["nmae"] = 5 লিখলে কোনো অভিযোগ আসত না — একটা নতুন কী তৈরি হয়ে যেত। বস্তুতেও নতুন বৈশিষ্ট্য বসানো যায়, কিন্তু __init__ পড়লেই দেখা যায় কী কী থাকার কথা।
আর ভিতরে কী আছে তা দেখাও যায়:
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 0x00000213E88A9DF0>শেষের সংখ্যাটা মেমরির ঠিকানা, আর প্রতিবার চালালে আলাদা আসবে। ডিবাগ করার সময় এটা কোনো কাজেই লাগে না।
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দুটো বস্তুর ভিতরে একই জিনিস, তবু == মিথ্যা। কারণ ডিফল্ট সমতা হলো অভিন্নতা — == তখন 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__ লিখলে বস্তুটা আর ডিফল্টভাবে হ্যাশযোগ্য থাকে না, তাই তাকে সেটে বা ডিকশনারির কী হিসেবে রাখা যায় না। পরের অধ্যায়ে দেখব 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 ক্লাস-স্তরেই রয়ে গেছে, আর সেটাই ঠিক — সে অপরিবর্তনীয় আর সব ঝুড়ির জন্য একই। নিয়মটা এক লাইনে: ক্লাস-স্তরে কেবল ধ্রুবক; পরিবর্তনীয় সবকিছু __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 লিখতে ভুলে গেছেন। প্রথম প্যারামিটার হিসেবে দিন।
AttributeError: 'X' object has no attribute 'y' বানান ভুল, নয়তো __init__-এ self.y কখনো বসানো হয়নি। print(obj.__dict__) দিয়ে দেখুন আসলে কী কী আছে।
TypeError: X() takes no arguments __init__ লেখা হয়নি, নয়তো নামটা ভুল বানানে (__int__ নয়, __init__, দুই দিকে দুটো করে আন্ডারস্কোর)।
সব বস্তু একই লিস্ট ভাগ করছে লিস্টটা ক্লাস-স্তরে বসানো হয়েছে। __init__-এর ভিতরে নিয়ে যান।
দুটো বস্তুর ভিতরে একই জিনিস, তবু == মিথ্যা ডিফল্ট সমতা হলো অভিন্নতা। __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`hello` ছাপা হয়
- C`TypeError` — একটি নেয়, কিন্তু শূন্যটি দেওয়া হয়েছে
- D`AttributeError`
দুটো আলাদা ঝুড়ি, দুটো আলাদা জিনিস। কী ছাপা হবে?
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'] ['pen', 'bag']
- B['pen'] ['bag']
- C['pen'] ['pen', 'bag']
- D['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
- BTrue True
- CFalse False
- Dএকটি `TypeError`
উত্তর দিতে অ্যাকাউন্ট লাগবে
উত্তর মিলিয়ে দেখতে সাইন ইন করুন
প্রশ্নগুলো উপরে আছে, আর মাথায় মাথায় উত্তর ভেবে নেওয়াই আসল কাজ। সঠিক উত্তর, ব্যাখ্যা আর তিন ধাপের ইঙ্গিত দেখতে সাইন ইন করুন।
নিজে করুন
দুটো ক্লাস লিখুন।
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__)করুন — কী দেখছেন?
শেষ পরীক্ষাটা গুরুত্বপূর্ণ, কারণ সে ক্লাসের সীমাটা দেখায়। পড়ার সময় বানান ভুল ধরা পড়ে, লেখার সময় নয় — একটা নতুন বৈশিষ্ট্য বসানো সবসময়ই চলে। পরের অধ্যায়ে দেখব টাইপ হিন্ট আর একটা যাচাইকারী এই ফাঁকটাও বন্ধ করতে পারে।
ধাপ ৬ / ৬
কঠিন করা — অধ্যায়ের কুইজ
সহজ থেকে কঠিন — দশটি প্রশ্ন, শেষেরগুলো ইচ্ছে করেই কঠিন।
সাইন ইন করে কুইজ দিন