মৌলিক

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

হালনাগাদ: · Skillaria.Top

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 callableproperty বন্ধনী ছাড়া পড়ুন: r.total

সারসংক্ষেপ

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

সাধারণ প্রশ্ন

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-এ কি প্রাইভেট ফিল্ড আছে?

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

ভিডিওতে বিষয়টি শিখুন

“Python 3.15: от новичка до профи” কোর্সে বিষয়টি অনুশীলনসহ বিস্তারিত শেখানো হয়েছে।

কোর্সে যান