# Python-এ class ও OOP: __init__, self, মেথড, উত্তরাধিকার ও dataclass

URL: https://skillaria.top/bn/guides/python/classes-python

Python-এর OOP-এর ভিত্তি class: class ফিল্ড ও মেথড বর্ণনা করে, আর অবজেক্ট (ইনস্ট্যান্স) তৈরি হয় ক্লাসকে ডেকে; __init__ ফিল্ড ভরে, self বোঝায় বর্তমান অবজেক্টকে। উত্তরাধিকার প্যারেন্টের আচরণ নেয় এবং কিছু মেথড বদলায়, আর @dataclass ডেটা-প্রধান ক্লাসের জন্য __init__, __repr__ ও __eq__ নিজে বানিয়ে দেয়।

Python-এর class হলো একটি নকশা: সে বর্ণনা করে অবজেক্ট কী ডেটা রাখবে এবং তার সঙ্গে কী করা যাবে। অবজেক্ট (ইনস্ট্যান্স) হলো সেই নকশায় বানানো নির্দিষ্ট একটি জিনিস। Python OOP-এর তিনটি স্তম্ভ: মেথডসহ class, উত্তরাধিকার, এবং `__init__` ও `__repr__`-এর মতো বিশেষ মেথড। একটি সূত্র ধরে সবটা দেখা যাক — খরচের হিসাব।

## class ছাড়া ডিকশনারি কেন যথেষ্ট নয়

ডেটা dictionary-তেও রাখা যায়: `{"title": "Coffee", "amount": 250}`। সমস্যা হলো, dictionary জানে না কোন key সঠিক। বানানে ভুল `expense.get("amout", 0)` কোনো ত্রুটি দেয় না — শূন্য ফিরে আসে, আর রিপোর্ট নীরবে ভুল বলতে শুরু করে।

Class-এর অবজেক্টে ফিল্ড আগে থেকে ঘোষিত থাকে। না থাকা অ্যাট্রিবিউটে প্রবেশ করলে সঙ্গে সঙ্গে `AttributeError` আসে, আর আধুনিক Python সঠিক নামের পরামর্শও দেয়। তাছাড়া class ডেটা এবং তার ওপর কাজ এক জায়গায় রাখে: একটি খরচ নিজেই কর সহ যোগফল হিসাব করতে পারে, রিপোর্ট নিজেই মোট বের করে।

## Python-এ class কীভাবে বানাবেন

Class ঘোষণা হয় `class` শব্দে, নাম PEP 8 অনুযায়ী CamelCase-এ লেখা হয়। সবচেয়ে সরল একটি class:

```
class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

coffee = Expense("Coffee", 250)
taxi = Expense("Taxi", 300)
print(coffee.title, coffee.amount)   # Coffee 250
```

`Expense("Coffee", 250)` কল একটি নতুন ইনস্ট্যান্স বানায়। `coffee` ও `taxi` একই ধরনের দুটি আলাদা অবজেক্ট, প্রতিটির নিজস্ব ফিল্ড-মান। অন্তর্ভুক্তি যাচাই করতে `isinstance(coffee, Expense)` — ফল `True`।

### কনস্ট্রাক্টর __init__: class-এর প্রাথমিক সেটআপ

`__init__`-কে প্রায়ই কনস্ট্রাক্টর বলা হয়, যদিও নির্ভুলভাবে এটি ইনিশিয়ালাইজার। অবজেক্টটি বানায় `__new__` মেথড, আর `__init__` তারপর ডাকা হয় এবং আর্গুমেন্টগুলো ফিল্ডে সাজায়। সেখান থেকে কিছু ফেরত দিতে হয় না (আসলে `None` ফেরত দিতেই হয়)।

`__init__`-এর ভেতরে ইনপুট যাচাই করা ভালো, যাতে প্রোগ্রামে ভুল অবজেক্ট না আসে:

```
class Expense:
    def __init__(self, title, amount):
        if amount < 0:
            raise ValueError("পরিমাণ ঋণাত্মক হতে পারে না")
        self.title = title
        self.amount = amount
```

### Python-এর self কী

`self` হলো সেই অবজেক্টের রেফারেন্স, যার সঙ্গে মেথডটি এখন কাজ করছে। যখন লেখেন `coffee.with_tax()`, Python আসলে `Expense.with_tax(coffee)` ডাকে এবং অবজেক্টটি প্রথম আর্গুমেন্ট হিসেবে পাঠায়। তাই মেথড ঘোষণায় `self` প্রথমে থাকে, আর ডাকার সময় তা লেখা হয় না।

`self` কোনো কীওয়ার্ড নয়, একটি রীতি: প্যারামিটারের নাম যেকোনো হতে পারে, কিন্তু কেউ তা করে না। নতুনদের সাধারণ ভুল — মেথড ঘোষণায় `self` ভুলে যাওয়া। তখন ডাকলে আসে `TypeError: Expense.with_tax() takes 0 positional arguments but 1 was given`।

## মেথড ও বিশেষ মেথড

মেথড হলো class-এর ভেতরে ঘোষিত ফাংশন। সে `self`-এর মাধ্যমে অবজেক্টের ফিল্ড দেখে:

```
class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

    def with_tax(self, rate=0.2):
        return self.amount * (1 + rate)

    def __repr__(self):
        return f"Expense({self.title!r}, {self.amount})"

coffee = Expense("Coffee", 250)
print(coffee.with_tax())   # 300.0
print(coffee)              # Expense('Coffee', 250)
```

`__repr__` না থাকলে আউটপুট হতো `<__main__.Expense object at 0x7f...>` — ডিবাগের জন্য অকেজো। দুই পাশে দুটি আন্ডারস্কোরযুক্ত মেথডকে বলে বিশেষ (dunder) মেথড: Python নিজে সেগুলো ডাকে। `__str__` ব্যবহারকারীর জন্য সুন্দর আউটপুট দেয়, `__eq__` `==` তুলনা নিয়ন্ত্রণ করে, `__len__` `len(obj)` নিয়ন্ত্রণ করে।

### @classmethod ও @staticmethod

সাধারণ মেথড ইনস্ট্যান্সের সঙ্গে কাজ করে। `@classmethod` সাজানো মেথড প্রথম আর্গুমেন্ট হিসেবে পায় class নিজে (`cls`) — এটি প্রায়ই বিকল্প কনস্ট্রাক্টর হিসেবে ব্যবহার হয়। `@staticmethod` পায় না অবজেক্ট বা class — এটি সাধারণ ফাংশন, যা যৌক্তিকভাবে class-এর সঙ্গে সম্পর্কিত।

```
class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

    @classmethod
    def from_csv(cls, line):
        title, amount = line.split(";")
        return cls(title, float(amount))

    @staticmethod
    def is_valid_amount(value):
        return value >= 0

e = Expense.from_csv("Lunch;520")
print(e.amount)                    # 520.0
print(Expense.is_valid_amount(-1)) # False
```

### ধ্বংসকারী __del__

Python-এ `__del__` মেথড আছে, যেটি অবজেক্ট মুছে ফেলার আগে garbage collector ডাকে। রিসোর্স ছাড়ানোর জন্য একে নির্ভর করা ঠিক নয়: কখন ডাকা হবে তা নিশ্চিত নয়। ফাইল ও সংযোগের জন্য `with` কনটেক্সট ম্যানেজার বা স্পষ্ট `close()` মেথড ব্যবহার করুন।

## ক্লাস-অ্যাট্রিবিউট ও ইনস্ট্যান্স-অ্যাট্রিবিউট

`self.name = ...` দিয়ে যে ফিল্ড হয়, সেটি নির্দিষ্ট অবজেক্টের। Class-এর শরীরে সরাসরি ঘোষিত ভেরিয়েবল হলো class-অ্যাট্রিবিউট: সবার মধ্যে ভাগ করা হয়। ধ্রুবক সাধারণত এখানে রাখা হয়।

```
class Expense:
    CURRENCY = "BDT"          # class-অ্যাট্রিবিউট, ধ্রুবক

    def __init__(self, title, amount):
        self.title = title    # ইনস্ট্যান্স-অ্যাট্রিবিউট
        self.amount = amount

a = Expense("Coffee", 250)
b = Expense("Tea", 150)
print(a.CURRENCY, Expense.CURRENCY)   # BDT BDT
```

একটি বিপজ্জনক ফাঁদ — পরিবর্তনযোগ্য class-অ্যাট্রিবিউট। Class-এর শরীরে `rows = []` লিখে `self.rows.append(...)` করলে তালিকাটি সব অবজেক্টের মধ্যে ভাগ হয়ে যায়। পরিবর্তনযোগ্য ফিল্ড সব সময় `__init__`-এর ভেতরে বানান।

অবজেক্টের ফিল্ড দেখতে `vars(obj)` বা `obj.__dict__`, আর স্ট্রিং থেকে নাম দিয়ে অ্যাট্রিবিউট পড়তে `getattr(obj, "amount")`।

## Class-এর property: @property

Property হলো এমন মেথড, যা বাইরে থেকে বন্ধনী ছাড়া সাধারণ ফিল্ডের মতো পড়া যায়। হিসাবকৃত মানের জন্য এটি সুবিধাজনক: মোট হিসাব আলাদা রাখতে হয় না, আর হালনাগাদ করতে ভোলার ঝুঁকি থাকে না।

```
class Report:
    def __init__(self):
        self.rows = []

    def add(self, expense):
        self.rows.append(expense)

    @property
    def total(self):
        return sum(e.amount for e in self.rows)

r = Report()
r.add(Expense("Coffee", 250))
r.add(Expense("Taxi", 300))
print(r.total)   # 550
```

লেখা নিয়ন্ত্রণ করতে property-তে যোগ হয় সেটার, `@total.setter` দিয়ে। সেটার ছাড়া `r.total = 0` লিখলে আসবে `AttributeError`।

এনক্যাপসুলেশন নিয়ে একটি কথা। Python-এ কঠোর অর্থে প্রাইভেট ফিল্ড নেই। একটি আন্ডারস্কোর (`_cache`) একটি চুক্তি «বাইরে থেকে ছোঁবেন না», আর দুটি আন্ডারস্কোর (`__secret`) নাম বিকৃতি (name mangling) চালু করে, যাতে উত্তরাধিকারীর কোনো ফিল্ড ভুলবশত সেটিকে ঢেকে না দেয়।

## Python-এ উত্তরাধিকার

উত্তরাধিকারে একটি সন্তান class গড়া যায় প্যারেন্টের ওপর: সন্তান সব ফিল্ড ও মেথড পায়, আর দরকারি মেথডগুলো বদলাতে পারে। প্যারেন্ট class লেখা হয় বন্ধনীর ভেতরে।

```
class MonthlyReport(Report):
    def __init__(self, month):
        super().__init__()        # প্যারেন্টের __init__ ডাকা
        self.month = month

    def add(self, expense):       # মেথড বদলানো
        print(f"[{self.month}] + {expense}")
        super().add(expense)      # প্যারেন্টের আচরণ বজায় রাখা

m = MonthlyReport("October")
m.add(Expense("Coffee", 250))   # [October] + Expense('Coffee', 250)
print(m.total)                  # 250 — property উত্তরাধিকারসূত্রে পাওয়া
```

`super()` প্যারেন্টের মেথড ডাকার একটি মধ্যস্থ অবজেক্ট ফেরত দেয়। সন্তানের `__init__`-এ `super().__init__()` ভুলে গেলে এটি সাধারণ ভুল: `rows` ফিল্ড তৈরি হয় না, এবং প্রথম `add`-এই `AttributeError` আসে।

`issubclass(MonthlyReport, Report)` দিয়ে সম্পর্ক যাচাই করা যায়। Python 3-এর সব class অন্তর্নিহিতভাবে `object`-এর উত্তরাধিকারী — সেখান থেকেই আসে মৌলিক `__repr__`, `__eq__` ইত্যাদি। Python বহু-উত্তরাধিকারও সমর্থন করে (`class C(A, B)`); মেথড খোঁজার ক্রম দেখায় `C.__mro__`।

### Abstract class

Abstract class একটি ইন্টারফেস ঠিক করে: সন্তানদের কোন মেথড বাধ্যতামূলকভাবে বানাতে হবে। এর ইনস্ট্যান্স বানানো যায় না। এজন্য আছে `abc` মডিউল:

```
from abc import ABC, abstractmethod

class Exporter(ABC):
    @abstractmethod
    def export(self, report): ...

class CsvExporter(Exporter):
    def export(self, report):
        return "\n".join(f"{e.title};{e.amount}" for e in report.rows)

Exporter()   # TypeError: Can't instantiate abstract class Exporter ...
```

যে সন্তান সব abstract মেথড বানায়নি, সেও abstract থেকে যায় এবং ইনস্ট্যান্স বানানোর সময় একই ত্রুটি দেয়।

## উত্তরাধিকারের বদলে কম্পোজিশন

উত্তরাধিকার শিখে মানুষ স্তরের পর স্তর সিঁড়ি গড়তে চায়: মৌলিক খরচ, টাকার খরচ, ডলারের খরচ — এভাবে পাঁচ স্তর। Python-এ এমনটা কম লেখা হয়। উত্তরাধিকার বোঝায় «হয়» সম্পর্ক, আর কম্পোজিশন «ধারণ করে»: রিপোর্ট কোনো তালিকার ধরন নয়, সে তালিকাটিকে `rows` ফিল্ডে ধারণ করে।

ব্যবহারিক নিয়ম: অন্য অবজেক্টের সামর্থ্য শুধু কাজে লাগাতে হলে সেটিকে ফিল্ড হিসেবে রাখুন, উত্তরাধিকারী হবেন না। ডেটার জন্য প্রায়ই dataclass যথেষ্ট, আর যুক্তি সাধারণ ফাংশনে রাখা যায়।

## Python-এর dataclass: বাড়তি কোড ছাড়া class

যে class মূলত ডেটা ধরে রাখে, তার `__init__`, `__repr__` ও `__eq__` হাতে লেখা ক্লান্তিকর। `dataclasses` মডিউলের `@dataclass` ডেকোরেটর ফিল্ডের টাইপ-অ্যানোটেশন থেকে সেগুলো বানিয়ে দেয়:

```
from dataclasses import dataclass, field

@dataclass
class Expense:
    title: str
    amount: float
    tags: list[str] = field(default_factory=list)

    def __post_init__(self):
        if self.amount < 0:
            raise ValueError("পরিমাণ ঋণাত্মক হতে পারে না")

e = Expense("Coffee", 250)
print(e)                           # Expense(title='Coffee', amount=250, tags=[])
print(e == Expense("Coffee", 250)) # True
```

আগে `__init__`-এ যে যাচাই লেখা হতো, তা যায় `__post_init__`-এ — এটি তৈরি হওয়া ইনিশিয়ালাইজারের ঠিক পরেই ডাকা হয়। পরিবর্তনযোগ্য ডিফল্ট মান শুধু `field(default_factory=list)` দিয়ে দেওয়া যায়: `tags: list = []` লিখলে dataclass তা নিষেধ করে এবং `ValueError` দেয়।

### frozen ও slots

`frozen=True` অবজেক্টকে অপরিবর্তনীয় করে, আর `slots=True` (Python 3.10 থেকে) `__slots__` বানিয়ে মেমোরি বাঁচায়। অপরিবর্তনীয় অবজেক্টের ফিল্ড বদলাতে গেলে আসে `FrozenInstanceError`। নতুন মানসহ অবজেক্ট পেতে কপি বানানো হয়:

```
from dataclasses import dataclass, replace

@dataclass(frozen=True, slots=True)
class Expense:
    title: str
    amount: float

e = Expense("Coffee", 250)
# e.amount = 300   -> dataclasses.FrozenInstanceError
e2 = replace(e, amount=300)
print(e, e2)   # Expense(title='Coffee', amount=250) Expense(title='Coffee', amount=300)
```

Python 3.13 থেকে একই কাজ করে সাধারণ ফাংশন `copy.replace()`। অপরিবর্তনীয় dataclass-অবজেক্ট সেটে রাখা যায় এবং dictionary-র key হিসেবে ব্যবহার করা যায় — তাদের `__hash__` আছে।

## ক্লাসে নতুন সুবিধা: Self, @override, __slots__

ক্লাসের টাইপিংয়ের জন্য দুটি কাজের টুল আছে। `typing.Self` (Python 3.11, PEP 673) বোঝায় «ফেরত আসবে একই class-এর অবজেক্ট» — এটি উত্তরাধিকারী মেথডেও কাজ করে। `@override` ডেকোরেটর (Python 3.12, PEP 698) চিহ্নিত করে যে মেথডটি প্যারেন্টের মেথড বদলাবে: বানানে ভুল থাকলে mypy বা pyright জানায়, এমন কোনো মৌলিক মেথড নেই।

```
from typing import Self, override

class Report:
    def __init__(self):
        self.rows = []

    def add(self, e) -> Self:
        self.rows.append(e)
        return self              # চেইনে ডাকা যায়

class MonthlyReport(Report):
    @override
    def add(self, e) -> Self:
        return super().add(e)
```

Python 3.15-এ একটি সীমাবদ্ধতা তুলে নেওয়া হয়েছে: `tuple`-এর উত্তরাধিকারীদের জন্য এখন খালি নয় এমন `__slots__` অনুমোদিত, আর যেকোনো class-এর `__slots__`-এ `__dict__` ও `__weakref__` লেখা যায়। 3.14-এ tuple-উত্তরাধিকারীর এমন ঘোষণায় আসত `TypeError: nonempty __slots__ not supported for subtype of 'tuple'`।

## class-এর সাধারণ ভুল

| ভুল | লক্ষণ | সমাধান |
| --- | --- | --- |

| মেথডে `self` নেই | `TypeError: ... takes 0 positional arguments but 1 was given` | মেথডের প্রথম প্যারামিটার সব সময় `self` |

| `__init__`-এ ফিল্ডে `self.` ছাড়া | `__init__`-এর পরে মান হারিয়ে যায় | লিখুন `self.amount = amount` |

| class-অ্যাট্রিবিউট হিসেবে তালিকা | ডেটা অবজেক্টের মধ্যে «গড়িয়ে» যায় | `__init__`-এ বা `default_factory` দিয়ে তালিকা বানান |

| `super().__init__()` নেই | প্যারেন্টের ফিল্ডে `AttributeError` | প্যারেন্টের ইনিশিয়ালাইজার ডাকুন |

| property-কে বন্ধনী দিয়ে ডাকা | `TypeError: 'int' object is not callable` | property বন্ধনী ছাড়া পড়ুন: `r.total` |

## সারসংক্ষেপ

Python OOP শুরু হয় সহজভাবে: class ফিল্ড ও মেথড বর্ণনা করে, `__init__` ফিল্ড ভরে, `self` বর্তমান অবজেক্টকে দেখায়। উত্তরাধিকার ও `super()` আচরণ বাড়াতে দেয়, abstract class ইন্টারফেস ঠিক করে, আর `@property` হিসাবকৃত ফিল্ড দেয়। ডেটা-প্রধান class-এর জন্য `@dataclass` নিন, আর গভীর উত্তরাধিকারের বদলে কম্পোজিশন বেছে নিন। কোর্সে class ও dataclass বাস্তবে শেখানো হয়েছে খরচের ট্র্যাকারের উদাহরণে।

## FAQ

**class ও অবজেক্টের পার্থক্য কী Python-এ?**

Class হলো ধরনের বর্ণনা: তার কী ফিল্ড ও মেথড আছে। অবজেক্ট বা ইনস্ট্যান্স তৈরি হয় class-কে ডেকে, যেমন Expense("Coffee", 250), এবং নির্দিষ্ট মানগুলো ধরে রাখে।

**class-এ __init__ লেখা কি বাধ্যতামূলক?**

না। __init__ ঘোষণা না থাকলে object থেকে পাওয়া মেথড ব্যবহৃত হয়, যা কোনো আর্গুমেন্ট নেয় না। dataclass-এ এটি ডেকোরেটর নিজে বানায়।

**__str__ আর __repr__-এর পার্থক্য কী?**

__repr__ হলো ডেভেলপার ও ডিবাগারের জন্য একক অর্থের উপস্থাপনা, আর __str__ হলো print ও f-স্ট্রিংয়ে ব্যবহারকারীর জন্য সুপাঠ্য আউটপুট। __str__ না থাকলে Python __repr__ ব্যবহার করে।

**কখন dataclass, আর কখন সাধারণ class বেছে নেবেন?**

Dataclass তখন ভালো, যখন class মূলত ডেটা ধরে রাখে এবং তৈরি __init__, __repr__ ও তুলনা দরকার। সাধারণ class তখন সুবিধাজনক, যখন ইনিশিয়ালাইজেশন-যুক্তি জটিল বা আচরণ ডেটার চেয়ে গুরুত্বপূর্ণ।

**Python-এ কি প্রাইভেট ফিল্ড আছে?**

কঠোর প্রাইভেসি নেই। নামের শুরুতে একটি আন্ডারস্কোর হলো «বাইরের ব্যবহারের জন্য নয়» চুক্তি, আর দুটি আন্ডারস্কোর নাম বিকৃতি চালু করে, যা উত্তরাধিকারীর দ্বারা ফিল্ড ভুলবশত বদলানো আটকায়।


---
Source: Skillaria.Top (https://skillaria.top)
