অধ্যায় 26

ক্লাস — তথ্য আর আচরণ একসাথে

class, __init__ আর self, মেথড, __repr__ দিয়ে ছাপার রূপ, __eq__ দিয়ে সমতা, ক্লাস-স্তর আর ইনস্ট্যান্স-স্তরের পার্থক্য, আর কখন ক্লাস লাগে না।

38 মিনিটPython 3.12
  1. 1সমস্যা
  2. 2বোঝা
  3. 3উদাহরণ
  4. 4অনুমান
  5. 5নিজে করা
  6. 6কঠিন করা

যে সমস্যাটা আমরা সমাধান করছি

এতদিন একটা জিনিসের কয়েকটা বৈশিষ্ট্য একসাথে রাখতে আমরা ডিকশনারি ব্যবহার করেছি।

python
order = {"name": "pen", "price": 15.0, "quantity": 3}

print(order["nmae"])
text
KeyError: 'nmae'

ত্রুটিটা ধরা পড়েছে, ভালো কথা — কিন্তু কেবল ওই লাইনটা চলার সময়। একটা শর্তের ভিতরে লুকোনো থাকলে সেটা মাসখানেক পরেও প্রথমবার দেখা দিতে পারত।

আর সমস্যাটা কেবল বানান নয়। একটা ডিকশনারিতে যেকোনো কী বসানো যায়, তাই কোথাও লেখা নেই একটা অর্ডার-লাইনে আসলে কী কী থাকা উচিত। মোট হিসাব করার ফাংশনটা অন্য কোথাও, আর তাকে ডাকার আগে ডিকশনারিটায় সব ক্ষেত্র আছে কিনা কেউ যাচাই করে না। তথ্য এক জায়গায়, তা নিয়ে কী করা যায় সেটা আরেক জায়গায়, আর কোনটা কোনটার সাথে যায় তা কেবল আপনার মনে।

python
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())
text
pen
45.0

একটা ক্লাস হলো একটা ছাঁচ: সে বলে দেয় এই ধরনের জিনিসে কী কী থাকে, আর তাকে নিয়ে কী করা যায়।

এই অধ্যায় শেষে আপনি পারবেন

  • class লিখতে, আর __init__ ও self কী করে তা বলতে
  • মেথড লিখতে, আর তারা কেন সাধারণ ফাংশন থেকে আলাদা তা বলতে
  • __repr__ দিয়ে ছাপার উপযোগী একটা রূপ দিতে
  • ক্লাস-স্তরের আর ইনস্ট্যান্স-স্তরের বৈশিষ্ট্যের পার্থক্য বলতে — আর একটা পরিচিত ফাঁদ এড়াতে
  • __eq__ দিয়ে দুটো বস্তুকে তুলনাযোগ্য করতে
  • বলতে কখন ক্লাস লাগে না

পূর্বশর্ত: JSON আর CSV।


self আসলে কী

self কোনো জাদুর শব্দ নয় — সে কেবল প্রথম প্যারামিটার, আর তাতে বসে যে বস্তুটার উপর মেথডটা ডাকা হয়েছে।

line.total() লেখার মানে আসলে «OrderLine-এর total মেথডটা চালাও, আর self-এ line-কে দাও»। সেজন্যই ভিতরে self.price লিখলে ওই নির্দিষ্ট লাইনটার দাম পাওয়া যায়।

ভুলে গেলে বার্তাটা প্রথমে বিভ্রান্তিকর লাগে:

python
class OrderLine:
    def __init__(self, name):
        self.name = name

    def shout():
        return "hello"


line = OrderLine("pen")
print(line.shout())
text
TypeError: OrderLine.shout() takes 0 positional arguments but 1 was given

«একটা দেওয়া হয়েছে» — কিন্তু বন্ধনীটা তো খালি! দেওয়া হয়েছে line-কেই, নীরবে, আর shout সেটা নেওয়ার জায়গা রাখেনি।

__init__-টাও একটা সাধারণ মেথড, কেবল নামটা বিশেষ: বস্তুটা তৈরি হওয়ার পরপরই পাইথন তাকে ডাকে। ভিতরে self.name = name লিখে বৈশিষ্ট্যগুলো বসানো হয়।

বানান ভুল এখন সাথে সাথেই ধরা পড়ে

python
class OrderLine:
    def __init__(self, name):
        self.name = name


line = OrderLine("pen")
print(line.nmae)
text
AttributeError: 'OrderLine' object has no attribute 'nmae'

ডিকশনারির KeyError-এর সাথে পার্থক্যটা সূক্ষ্ম কিন্তু বাস্তব: ডিকশনারিতে যেকোনো কী বসানো যায়, তাই order["nmae"] = 5 লিখলে কোনো অভিযোগ আসত না — একটা নতুন কী তৈরি হয়ে যেত। বস্তুতেও নতুন বৈশিষ্ট্য বসানো যায়, কিন্তু __init__ পড়লেই দেখা যায় কী কী থাকার কথা।

আর ভিতরে কী আছে তা দেখাও যায়:

python
class OrderLine:
    def __init__(self, name):
        self.name = name


line = OrderLine("pen")

print(type(line))
print(isinstance(line, OrderLine))
print(line.__dict__)
text
<class '__main__.OrderLine'>
True
{'name': 'pen'}

__dict__ দেখায় বস্তুটার ভিতরে আসলে একটা ডিকশনারিই আছে। ক্লাস সেই ডিকশনারিটাকে সরায়নি — তাকে একটা নাম আর একটা আকার দিয়েছে।

__repr__ — ছাপলে কী দেখা যাবে

__repr__ না লিখলে:

python
class OrderLine:
    def __init__(self, name):
        self.name = name


print(OrderLine("pen"))
text
<__main__.OrderLine object at 0x00000213E88A9DF0>

শেষের সংখ্যাটা মেমরির ঠিকানা, আর প্রতিবার চালালে আলাদা আসবে। ডিবাগ করার সময় এটা কোনো কাজেই লাগে না।

python
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])
text
OrderLine(name='pen', price=15.0)
[OrderLine(name='pen', price=15.0), OrderLine(name='pen', price=15.0)]

দ্বিতীয় লাইনটাই আসল সুবিধা: লিস্টের ভিতরে থাকলেও কাজ করে। লিস্ট ছাপার সময় পাইথন ভিতরের প্রতিটা জিনিসের __repr__ ডাকে, __str__ নয়। সেজন্যই দুটোর মধ্যে __repr__ আগে লেখা উচিত।

প্রথা হলো এমন একটা লেখা দেওয়া যা দেখতে বস্তুটা বানানোর কোডের মতো। !r ব্যবহার করলে ভিতরের লেখাগুলো উদ্ধৃতিসহ আসে — চতুর্বিংশ অধ্যায়ের সেই একই কারণ।

সমতা — ডিফল্টটা যা আপনি চান তা নয়

python
class OrderLine:
    def __init__(self, name):
        self.name = name


a = OrderLine("pen")
b = OrderLine("pen")

print(a == b)
print(a == a)
text
False
True

দুটো বস্তুর ভিতরে একই জিনিস, তবু == মিথ্যা। কারণ ডিফল্ট সমতা হলো অভিন্নতা — == তখন is-এর মতোই আচরণ করে, অর্থাৎ «এটা কি ঠিক ওই বস্তুটাই?»।

বদলাতে হলে বলে দিতে হয় সমান মানে কী:

python
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)
text
True
False

এখন == বলছে «একই রকম» আর is বলছে «একই বস্তু» — দ্বাদশ অধ্যায়ের সেই পার্থক্যটা, এবার আপনার নিজের ধরনে।

সাবধানতা একটা: __eq__ লিখলে বস্তুটা আর ডিফল্টভাবে হ্যাশযোগ্য থাকে না, তাই তাকে সেটে বা ডিকশনারির কী হিসেবে রাখা যায় না। পরের অধ্যায়ে দেখব dataclass এই দুটো কাজই এক লাইনে করে দেয়।

ক্লাস-স্তর আর ইনস্ট্যান্স-স্তর

python
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)
text
['pen', 'bag']
['pen', 'bag']
True

দুটো আলাদা ঝুড়ি, একটাই লিস্ট।

চিনতে পারছেন? এটা একবিংশ অধ্যায়ের পরিবর্তনীয় ডিফল্টের ফাঁদটাই, নতুন পোশাকে। class লাইনের ঠিক নিচে লেখা items = [] একবারই চলে, ক্লাসটা তৈরি হওয়ার সময় — প্রতিটা বস্তুর জন্য নয়। তাই লিস্টটা ক্লাসের, আর সব ইনস্ট্যান্স সেটাকেই ভাগ করে নেয়।

সমাধানও একই আকারের: যা প্রতিটা বস্তুর নিজের, তা __init__-এর ভিতরে বানান।

python
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)
text
['pen']
['bag']
0.15 0.15

TAX_RATE ক্লাস-স্তরেই রয়ে গেছে, আর সেটাই ঠিক — সে অপরিবর্তনীয় আর সব ঝুড়ির জন্য একই। নিয়মটা এক লাইনে: ক্লাস-স্তরে কেবল ধ্রুবক; পরিবর্তনীয় সবকিছু __init__-এ।

কখন ক্লাস লিখবেন না

ক্লাস শেখার পর সবকিছুকে ক্লাস বানানোর একটা প্রবণতা আসে। কয়েকটা চিহ্ন, যেখানে ক্লাসটা অতিরিক্ত:

ক্লাসটায় একটাই মেথড আর __init__। সেটা আসলে একটা ফাংশন, অকারণে দুই ভাগে লেখা।

কোনো মেথড নেই, কেবল তথ্য। পরের অধ্যায়ের dataclass ঠিক এর জন্য, আর তাতে লাইন অনেক কম।

বস্তুটা কখনো বদলায় না আর একটাই জিনিস ধরে। একটা টাপল, নয়তো একটা মান — যথেষ্ট।

ক্লাসের আসল জায়গা হলো যেখানে অবস্থা আর আচরণ সত্যিই একসাথে থাকে: কিছু তথ্য যা পাল্টায়, আর কয়েকটা কাজ যেগুলো সবসময় সেই তথ্যের উপরেই চলে।


একটা সম্পূর্ণ উদাহরণ

python
"""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()
text
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__ লেখা হয়নি।