এই পর্বে যা শিখবেন
- কেন Python-এ getter/setter আগে থেকে লেখা হয় না, আর uniform access principle কী
@property, setter, deleter আরcached_propertyদিয়ে হিসাব করা ও যাচাই করা attribute@dataclassকী কোড তৈরি করে, mutable default-এর ফাঁদ,__post_init__,asdictআরreplacefrozen,order,slots,kw_only— কখন কোনটি; dataclass বনাম namedtuple, আর কখন dataclass নয়- descriptor আর attribute খোঁজার ক্রম, metaclass-এর এক অনুচ্ছেদ, আর AI-এর লেখা class-কোড যাচাই
১ · সাধারণ attribute দিয়ে শুরু — getter/setter আগে থেকে নয়
পর্ব ২৯-এ আপনি class আর instance বানিয়েছেন, আর self.balance = 500 লিখে একটি attribute রেখেছেন। বাইরে থেকে পড়তে acc.balance, বদলাতে acc.balance = 700. সহজ, পরিষ্কার।
Java বা C++ থেকে যাঁরা আসেন, তাঁরা এখানে একটু অস্বস্তি বোধ করেন। সেখানে শেখানো হয়: field সবসময় private থাকবে, আর বাইরে দেওয়া হবে getBalance() আর setBalance() method. যুক্তিটা ভালো — আজ না হোক কাল যদি যাচাই লাগে (ব্যালান্স ঋণাত্মক হবে না), তখন শুধু setter-এর ভেতরটা বদলালেই চলবে, যারা class ব্যবহার করে তাদের কোড বদলাতে হবে না। কিন্তু field সরাসরি খোলা রাখলে পরে যাচাই যোগ করতে গেলে সব জায়গার acc.balance = x বদলে acc.setBalance(x) লিখতে হতো। সেই ভয়েই আগে থেকে getter আর setter লিখে রাখা হয় — যদিও শতকরা নব্বই ভাগ ক্ষেত্রে তারা শুধু মান পড়ে আর বসায়, আর কিছুই করে না।
Python এই সমস্যাটিই অন্যভাবে সমাধান করেছে। এখানে আপনি সাধারণ attribute দিয়ে শুরু করেন। পরে যেদিন যাচাই বা হিসাব দরকার হবে, সেদিন সেই attribute-কে একটি property বানিয়ে দেন — আর ডাকার কোডে acc.balance যেমন ছিল, তেমনই থাকে। Python-এর কাছে acc.balance লেখাটা “একটি ঘর থেকে মান আনো” না “একটি ফাংশন চালাও” — তা ডাকার জায়গা থেকে বোঝাই যায় না। এই ধারণার একটি নাম আছে: uniform access principle.
একটি চায়ের দোকানে গিয়ে আপনি বলেন, “মামা, এক কাপ দুধ চা।” চা আগে থেকে ফ্লাস্কে বানানো ছিল, নাকি এখন নতুন করে বানানো হলো — আপনাকে জানতে হয় না, আপনার চাওয়ার ভঙ্গিও বদলায় না। দোকানি একদিন ঠিক করলেন, এখন থেকে প্রতিটি কাপ অর্ডার পেয়ে টাটকা বানাবেন, আর চিনি বেশি চাইলে একটা সীমা টানবেন। ক্রেতাদের কিছুই নতুন শিখতে হলো না। attribute আর property-র সম্পর্ক ঠিক এমন: “চাওয়ার ভঙ্গি” (acc.balance) একই থাকে, ভেতরের ব্যবস্থা বদলায়।
☕ Java-ধাঁচ — আগে থেকে সব method
প্রতিটি field-এর জন্য দুটি method, যদিও বেশিরভাগই শুধু পড়ে আর বসায়। ডাকার কোড লম্বা আর ভাঙা ভাঙা।
acc.set_balance(acc.get_balance() + 200)🐍 Python-ধাঁচ — attribute, দরকারে property
শুরুতে সাধারণ attribute. যাচাই লাগলে @property — ডাকার কোড একই থাকে, শুধু class-এর ভেতরটা বদলায়।
acc.balance += 200Python-এ attribute হলো class-এর প্রকাশ্য প্রতিশ্রুতি, আর property হলো সেই প্রতিশ্রুতি না ভেঙে ভেতরের নিয়ম বদলানোর উপায়। তাই “পরে লাগতে পারে” ভেবে আজ getter/setter লিখবেন না। আজ যা দরকার, তা-ই লিখুন — পরের পরিবর্তনের পথ Python খোলা রেখেছে।
২ · @property — দেখতে attribute, ভেতরে হিসাব
প্রথম ব্যবহার হলো এমন মান, যা আলাদা করে রাখার দরকার নেই, কারণ অন্য মান থেকে হিসাব করা যায়। একজন শিক্ষার্থীর প্রতিটি বিষয়ের grade point রাখা আছে; GPA হলো তাদের গড়। GPA আলাদা attribute হিসেবে রাখলে বিপদ — নতুন একটি বিষয়ের নম্বর যোগ হলো, অথচ কেউ GPA আবার হিসাব করতে ভুলে গেল, তখন দুটি তথ্য পরস্পরবিরোধী। সমাধান: GPA রাখবেনই না, প্রতিবার চাইলে হিসাব করবেন — কিন্তু ডাকার ভঙ্গি হবে attribute-এর মতো।
class Student:
def __init__(self, name, points):
self.name = name
self.points = points # প্রতিটি বিষয়ের grade point
@property
def gpa(self):
return round(sum(self.points) / len(self.points), 2)
s = Student("সুমি", [4.0, 3.5, 5.0])
print(s.gpa) # 4.17 — বন্ধনী নেই, তবু হিসাব চলল
s.points.append(3.0)
print(s.gpa) # 3.88 — সবসময় হালনাগাদ@property একটি decorator (পর্ব ৩৫-এ decorator-এর ভেতরটা দেখবেন)। এটি gpa ফাংশনটিকে মুড়িয়ে একটি property object বানায় আর class-এ gpa নামে বসায়। এরপর s.gpa লিখলে Python সেই ফাংশন চালিয়ে ফল দেয়। এমন attribute-কে বলে computed attribute. লক্ষ করুন, s.gpa() লিখলে উল্টো error আসবে — কারণ s.gpa ইতিমধ্যেই একটি সংখ্যা, আর সংখ্যাকে ডাকা যায় না।
শুধু getter থাকলে property read-only: s.gpa = 5.0 লিখলে Python থামিয়ে দেয় — AttributeError: property 'gpa' of 'Student' object has no setter. এটা আসলে সুবিধা: হিসাব করা মান কেউ ভুল করে হাতে বসিয়ে দিতে পারে না। দোকানের একটি অর্ডারের total, একটি আয়তক্ষেত্রের area, একজন মানুষের জন্মতারিখ থেকে age — এগুলো read-only property-র আদর্শ উদাহরণ।
property-কে হালকা রাখুন। s.gpa দেখে পাঠক ভাবে এটি একটি সস্তা কাজ — একটি মান পড়া। ভেতরে যদি ডেটাবেস থেকে ডেটা আনা, ফাইল পড়া বা ইন্টারনেটে অনুরোধ পাঠানো থাকে, তবে লুপের ভেতরে s.gpa একশো বার লিখলে প্রোগ্রাম রহস্যজনকভাবে ধীর হবে। ভারী বা ঝুঁকিপূর্ণ কাজ হলে সাধারণ method রাখুন (s.fetch_results()) — বন্ধনী দেখেই পাঠক বুঝবে “এখানে কিছু ঘটছে।” আর property যেন কখনো কোনো side effect না ঘটায়; পড়লেই কিছু বদলে যাওয়া খুব বিভ্রান্তিকর।
cached_property — ভারী হিসাব একবারই
কখনো হিসাবটা সত্যিই ভারী, অথচ ফল বদলায় না — যেমন একটি বড় রিপোর্টের পরিসংখ্যান, যা object তৈরির পর আর পাল্টায় না। প্রতিবার হিসাব করা অপচয়। Python 3.8 থেকে functools module-এ আছে cached_property: প্রথমবার পড়লে ফাংশন চলে, ফলটি object-এর নিজের মধ্যে জমা হয়, পরের বার থেকে সরাসরি সেই জমা মান আসে।
from functools import cached_property
class SalesReport:
def __init__(self, sales):
self.sales = sales
@cached_property
def summary(self):
print("হিসাব চলছে...") # শুধু প্রথমবার ছাপবে
return {"মোট": sum(self.sales), "সর্বোচ্চ": max(self.sales)}সতর্কতা: জমা মান আর নিজে থেকে হালনাগাদ হয় না। পরে sales-এ নতুন বিক্রি যোগ হলেও summary পুরোনোটাই দেখাবে, যতক্ষণ না আপনি del report.summary লিখে জমা মান মুছে দেন। তাই এটি শুধু সেই হিসাবের জন্য, যার ভিত্তি তৈরির পর বদলায় না। এটি কেন এভাবে কাজ করে, তা অংশ ৭-এর attribute খোঁজার ক্রম দেখলে পরিষ্কার হবে। (ফাংশনের ফল মনে রাখার সাধারণ কৌশল memoization নিয়ে আরও পর্ব ৩৮-এ।)
৩ · setter দিয়ে যাচাই — ভুল মান ঢুকতেই পারবে না
এবার সবচেয়ে গুরুত্বপূর্ণ ব্যবহার। একটি ব্যাংক হিসাবের নিয়ম: ব্যালান্স কখনো ঋণাত্মক হবে না। এমন “সবসময় সত্য থাকতে হবে” নিয়মকে বলে invariant. সাধারণ attribute এই নিয়ম রক্ষা করতে পারে না — যে কেউ acc.balance = -5 লিখে দিতে পারে। setter যোগ করলে প্রতিটি বসানোর চেষ্টা একটি যাচাইয়ের দরজা পার হয়:
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance # এখানেও setter চলে!
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError(f"ব্যালান্স ঋণাত্মক হতে পারে না: {value}")
self._balance = valueতিনটি জিনিস লক্ষ করুন। এক, আসল মান রাখা হয়েছে _balance নামে — শুরুতে আন্ডারস্কোর মানে “এটি ভেতরের জিনিস, বাইরে থেকে ছোঁবেন না” (পর্ব ২২-এর private name রীতি)। property আর আসল ঘরের নাম এক হলে self.balance = value আবার setter-কেই ডাকত, আর অসীম recursion হতো। দুই, @balance.setter — আগের property object-এর .setter method দিয়ে একই নামে একটি নতুন property বানানো হয়, যাতে getter আর setter দুটোই আছে। তাই দুটি ফাংশনের নামই balance হতে হবে। তিন, __init__-এর ভেতরে self.balance = balance লেখায় শুরুর মানও যাচাই হয় — BankAccount("রহিম", -100) তৈরিই হবে না।
acc.balance = -5 লেখার পর ঠিক কী কী ঘটে, ধাপে ধাপে দেখুন:
1acc.balance = -5 # বা = 9002@balance.setter3def balance(self, value):4 if value < 0:5 raise ValueError(...)6 self._balance = value
| খোঁজা | |
| value | |
| acc._balance |
- দেখতে সাধারণ বসানো। ডাকার কোড জানেই না balance একটি property. Python প্রথমে instance-এর ভেতরে নয়, বরং তার class-এ দেখে
balanceনামে বিশেষ কিছু আছে কি না। - class-এ পাওয়া গেল একটি property. এটি একটি “data descriptor” (অংশ ৭) — তাই instance-এর নিজস্ব ঘরে সরাসরি লেখার বদলে Python property-কেই দায়িত্ব দেয়।
- setter চলে। Python আসলে ডাকে
setter(acc, -5)—selfহলো acc,valueহলো -5. আপনার লেখা একটি সাধারণ assignment গোপনে একটি ফাংশন-কলে পরিণত হলো। - যাচাই।
-5 < 0সত্য। এই লাইনটিই সেই দরজা, যা পুরো class-এর invariant রক্ষা করে। - raise — আর কিছু বদলায়নি। ValueError ছোড়া হলো, ফাংশন সেখানেই শেষ; ৬ নম্বর লাইন চলেইনি। তাই
_balanceএখনো 700. ভুল মান ঢোকার আগেই আটকে গেল — পরে খুঁজে বের করার ঝামেলা নেই। - উল্টো পথ: বৈধ মান।
acc.balance = 900হলে শর্ত মিথ্যা, তাই ৬ নম্বর লাইন চলে আর আসল ঘরে 900 বসে।acc.balance += 200লিখলেও একই: আগে getter মান পড়ে, তারপর যোগফল নিয়ে setter চলে।
একই কৌশলে বয়সের সীমা (0 <= age <= 150), মোবাইল নম্বরের দৈর্ঘ্য, পরীক্ষার নম্বর ০ থেকে ১০০-র মধ্যে — যেকোনো input validation এক জায়গায় রাখা যায়। নিয়ম বদলালে শুধু setter বদলাবে।
deleter — খুব কম লাগে
তৃতীয় একটি অংশও আছে: @balance.deleter, যা চলে del acc.balance লিখলে। বাস্তবে এটি কদাচিৎ দরকার হয় — যেমন জমা রাখা কোনো মান মুছে দিতে, বা মুছে ফেলা নিষেধ করতে। নামটা জেনে রাখুন; লেখার সম্ভাবনা কম।
property আসলে পুরোনো জিনিস। @balance.setter লেখাটা চিনিমাখা রূপ। এর নিচে আছে balance = property(fget, fset, fdel, doc) — একটি built-in class, যা চারটি ফাংশন নিয়ে একটি object বানায়। decorator রূপটি এসেছে পরে, যাতে পড়তে সুবিধা হয়।
৪ · @dataclass — ডেটা-প্রধান class-এর একঘেয়ে কোড বাদ
অনেক class-এর মূল কাজ শুধু কয়েকটি তথ্য একসাথে রাখা: দোকানের একটি পণ্য (নাম, দাম, স্টক), একটি অর্ডার, একজন গ্রাহক, একটি জেলার তথ্য। এমন class হাতে লিখতে গেলে একই কাজ বারবার করতে হয়। __init__-এ প্রতিটি নাম তিনবার লিখতে হয় (parameter, self.x, = x). ছাপলে যেন <Product object at 0x...> না দেখায়, তার জন্য __repr__. দুটি পণ্য সমান কি না দেখতে __eq__ (পর্ব ৩০-এর dunder method). পাঁচটি field হলে প্রায় পঁচিশ লাইন — আর একটি নতুন field যোগ করলে তিন জায়গায় বদলাতে হয়, একটিতে ভুলে গেলেই বাগ। এমন বারবার লেখা ছাঁচের কোডকে বলে boilerplate.
Python 3.7 থেকে dataclasses module-এর @dataclass এই কাজ নিজে করে দেয়। আপনি শুধু বলেন কোন কোন field, আর প্রতিটির type hint কী; বাকিটা decorator লিখে দেয়। ধাপে ধাপে দেখুন, আপনার চার লাইন থেকে কী কী তৈরি হয়:
- আপনার class
4 লাইনশুধু field - field খোঁজা
annotationsক্রমসহ - __init__
তৈরিমান বসানো - __repr__
তৈরিপড়ার মতো ছাপা - __eq__
তৈরিমান দিয়ে তুলনা
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
stock: int = 0
Product.__annotations__ {'name': str, 'price': float, 'stock': int} # ক্রম: name → price → stock (লেখার ক্রমেই) # stock-এর default 0; বাকি দুটির default নেই # type hint ছাড়া লেখা class variable field নয়
def __init__(self, name: str, price: float,
stock: int = 0):
self.name = name
self.price = price
self.stock = stock
# __post_init__ থাকলে এখানে সেটি ডাকা হয়
def __repr__(self):
return (f"Product(name={self.name!r}, "
f"price={self.price!r}, stock={self.stock!r})")
Product(name='চা', price=20, stock=0)
def __eq__(self, other):
if other.__class__ is not self.__class__:
return NotImplemented
return ((self.name, self.price, self.stock) ==
(other.name, other.price, other.stock))
__hash__ = None # eq আছে, frozen নেই → unhashable
- আপনি লেখেন শুধু field. প্রতিটি লাইন একটি field: নাম, type hint, চাইলে default. কোনো method নেই, তবু class-টি পুরোপুরি ব্যবহারযোগ্য হবে।
- decorator field খোঁজে। class তৈরি হওয়ার পর
@dataclassতার__annotations__পড়ে — অর্থাৎ type hint দেওয়া নামগুলো, লেখার ক্রমে। type hint ছাড়া কোনো নাম (যেমনshop_name = "করিম স্টোর") field হিসেবে গণ্য হয় না, সাধারণ class attribute থাকে। type hint চালানোর সময় যাচাই হয় না —price-এ string দিলেও dataclass আপত্তি করবে না (পর্ব ৩৭)। __init__তৈরি। field-এর ক্রমেই parameter. default থাকলে parameter-এও default. তাই default ছাড়া field default-ওয়ালা field-এর পরে লিখলে class তৈরিই হয় না —TypeError: non-default argument 'x' follows default argument 'y'— ঠিক ফাংশনের নিয়মের মতো (পর্ব ১৩)।__repr__তৈরি। ছাপলে class-এর নাম আর প্রতিটি field-এর নাম-মান। ডিবাগিংয়ে এটি বিশাল সুবিধা: log-এProduct(name='চা', price=20, stock=0)দেখে সঙ্গে সঙ্গে বোঝা যায় কী আছে।__eq__তৈরি — আর hash বন্ধ। একই class-এর দুটি object-এর সব field সমান হলে তারা সমান। কিন্তু সমতা মান-নির্ভর হলে আর object বদলানো গেলে hash নিরাপদ নয় (পর্ব ১৬), তাই dataclass__hash__-কেNoneকরে দেয় — এমন object dict-এর চাবি বা set-এর সদস্য হতে পারে না। চাবি বানাতে হলে অংশ ৬-এরfrozen=True.
আরও একটি জিনিস বিনামূল্যে আসে: Python 3.10 থেকে dataclass __match_args__-ও বানায়, তাই পর্ব ১১-এর match-এ case Product(name, price): ধরনের class pattern সরাসরি কাজ করে।
মনে রাখবেন, dataclass কোনো জাদুর নতুন ধরনের object বানায় না। ফল একটি সাধারণ Python class — আপনি নিজের method যোগ করতে পারেন, property রাখতে পারেন, inheritance করতে পারেন। decorator শুধু আপনার হয়ে কয়েকটি dunder method লিখে দিয়েছে। কোনো একটি method আপনি নিজে লিখে দিলে (যেমন নিজের __repr__), dataclass সেটিকে স্পর্শ করে না।
৫ · default মান, mutable default-এর ফাঁদ আর __post_init__
পর্ব ২০-এ দেখেছেন, def add(item, cart=[]) লিখলে সেই একটি list সব ডাকায় ভাগ হয়ে যায় — বিখ্যাত mutable default ফাঁদ। dataclass-এ ঠিক একই বিপদ হতো: items: list = [] লিখলে সব object একই list ভাগ করত, আর একজন গ্রাহকের কার্টে চাল যোগ করলে সবার কার্টে চাল দেখা যেত। dataclass এই ভুলটি করতে দেয় না — class তৈরির সময়েই থামিয়ে দেয়। ধাপে ধাপে দেখুন:
1@dataclass2class Cart: items: list = []3@dataclass4class Cart: items: list = field(default_factory=list)5a, b = Cart(), Cart()6a.items.append("চাল")
| Cart | |
| a.items | |
| b.items |
- ফাঁদটি class তৈরির সময়েই ধরা পড়ে।
@dataclassfield-গুলো পড়ার সময় দেখে, default মানটি hashable নয় — list, dict বা set-এর মতো mutable জিনিস (Python 3.11 থেকে যেকোনো unhashable default এভাবে আটকায়; আগে শুধু list, dict, set). সঙ্গে সঙ্গেValueError, আর বার্তাতেই সমাধান লেখা: “use default_factory”. - default নয়, একটি “কারখানা”.
field(default_factory=list)মানে: “default হিসেবে কোনো list দিও না; প্রতিটি নতুন object-এর জন্যlist()ডেকে নতুন একটি খালি list বানাও।” default_factory-তে যেকোনো ফাংশন দেওয়া যায় যা কোনো argument ছাড়া ডাকা যায় —dict,set, বা নিজের লেখা ফাংশন। - দুটি object, দুটি আলাদা list. তৈরি হওয়া
__init__প্রতিবার কারখানা ডাকে, তাই a আর b-এর list দুটি আলাদা object —a.items is b.itemsমিথ্যা (পর্ব ১৯-এর alias-ভাবনা)। - একজনের কার্ট বদলালে অন্যজনেরটা অক্ষত। a-তে চাল ঢুকল, b খালিই রইল। পর্ব ২০-এর ফাংশনের ফাঁদের সমাধান ছিল
Noneদিয়ে শুরু করে ভেতরে নতুন list বানানো; dataclass-এ সেই কাজটিই পরিষ্কারভাবে করেdefault_factory.
__post_init__ — যাচাই আর হিসাব করা field
dataclass নিজে __init__ লেখে, তাহলে যাচাইয়ের কোড কোথায় রাখবেন? এর জন্য আছে __post_init__: class-এ এই নামে একটি method থাকলে তৈরি হওয়া __init__ সব field বসানোর পর এটিকে ডাকে। দাম ঋণাত্মক কি না পরীক্ষা, নাম থেকে বাড়তি ফাঁকা জায়গা ছাঁটা, বা অন্য field থেকে একটি field হিসাব করা — সব এখানে। হিসাব করা field-কে field(init=False) দিলে সেটি __init__-এর parameter হয় না, কিন্তু repr আর তুলনায় থাকে।
এবার একটি দোকানের পণ্যের পুরো উদাহরণ চালিয়ে দেখুন — field, default_factory, __post_init__, আর দুটি সহায়ক ফাংশন asdict ও replace:
from dataclasses import dataclass, field, asdict, replace
@dataclass
class Product:
name: str
price: float
tags: list[str] = field(default_factory=list)
with_vat: float = field(init=False)
def __post_init__(self):
if self.price < 0:
raise ValueError(f"দাম ঋণাত্মক হতে পারে না: {self.price}")
self.with_vat = round(self.price * 1.15, 2)
tea = Product("চা", 20)
tea.tags.append("পানীয়")
print(tea)
print(Product("বিস্কুট", 10))
print(tea == Product("চা", 20, ["পানীয়"]))
print(asdict(replace(tea, price=30)))
try:
Product("ডিম", -5)
except ValueError as err:
print("ValueError:", err)প্রথম লাইন ছাপে Product(name='চা', price=20, tags=['পানীয়'], with_vat=23.0) — repr নিজে তৈরি হয়েছে, আর with_vat (১৫% VAT ধরে) __post_init__-এ হিসাব হয়েছে। বিস্কুটের tags খালি [] — default_factory তাকে নিজস্ব নতুন list দিয়েছে, চায়ের list ভাগ করেনি; তার with_vat 11.5. তৃতীয় লাইন True: তৈরি হওয়া __eq__ সব field মিলিয়ে দেখেছে। replace(tea, price=30) একটি নতুন Product বানায় (মূল tea অক্ষত), আর যেহেতু নতুন object-এর জন্য আবার __init__ ও __post_init__ চলে, VAT-সহ দামও ঠিকঠাক 34.5 হয়ে গেছে। asdict সেটিকে একটি সাধারণ dict-এ বদলেছে — {'name': 'চা', 'price': 30, 'tags': ['পানীয়'], 'with_vat': 34.5} — যা পর্ব ২৬-এর মতো JSON-এ লেখার জন্য সুবিধাজনক। শেষে ঋণাত্মক দামে object তৈরিই হয়নি: ValueError: দাম ঋণাত্মক হতে পারে না: -5.
__post_init__ শুধু জন্মের সময় চলে। তৈরির পর কেউ tea.price = -50 লিখলে কোনো যাচাই হবে না, কারণ সাধারণ dataclass-এর field সাধারণ attribute. প্রতিটি বদলানোর সময়েও নিয়ম রক্ষা করতে চাইলে দুটি পথ: object-টিকে frozen=True করুন (বদলানোই যাবে না, নতুন মান চাইলে replace, যা আবার যাচাই চালায়), অথবা সেই field-এর জন্য property বা অংশ ৭-এর descriptor ব্যবহার করুন।
৬ · frozen, order, slots, kw_only — আর কখন dataclass নয়
@dataclass-কে বন্ধনীতে বিকল্প দিয়ে আচরণ বদলানো যায়। সবচেয়ে কাজের চারটি:
| বিকল্প | কী করে | কখন ব্যবহার করবেন |
|---|---|---|
frozen=True | তৈরির পর কোনো field বদলানো যায় না (FrozenInstanceError); সাথে __hash__ তৈরি হয় | স্থানাঙ্ক, টাকার পরিমাণ, কনফিগারেশন — যেকোনো “মান”, যা dict-এর চাবি বা set-এর সদস্য হবে |
order=True | <, <=, >, >= তৈরি; field-গুলোকে ক্রমানুসারে tuple-এর মতো তুলনা করে | যখন “স্বাভাবিক ক্রম” আছে, যেমন (বছর, মাস) দিয়ে সংস্করণ সাজানো |
slots=True | Python 3.10 থেকে; প্রতিটি object-এর __dict__ বাদ দিয়ে __slots__ ব্যবহার করে | লক্ষ লক্ষ ছোট object থাকলে মেমরি বাঁচাতে; ভুল বানানে নতুন attribute বসানোও আটকায় |
kw_only=True | Python 3.10 থেকে; __init__-এর সব parameter keyword-only | অনেক field থাকলে Order(id=7, total=450) লিখতে বাধ্য করা — ক্রম গুলিয়ে ফেলার ভয় নেই |
frozen — অপরিবর্তনীয় মান, dict-এর চাবি
৬৪টি জেলার সদর দপ্তরের স্থানাঙ্ক দিয়ে একটি ম্যাপ বানাচ্ছেন। স্থানাঙ্ক একটি “মান” — ময়মনসিংহের অবস্থান বদলায় না, আর দুটি স্থানাঙ্কের সংখ্যা মিললে তারা একই জায়গা। পর্ব ১৫-এ এমন রেকর্ডের জন্য tuple আর namedtuple দেখেছেন। frozen dataclass একই কাজ করে, তবে এটি tuple নয় — তাই Coordinate(1, 2) == (1, 2) মিথ্যা, index দিয়ে ভুল ঘর পড়ারও সুযোগ নেই।
frozen=True দিলে dataclass এমন একটি __setattr__ বসায়, যা যেকোনো বসানোর চেষ্টায় FrozenInstanceError ছোড়ে — বার্তা cannot assign to field 'lat'. আর object যেহেতু আর বদলাবে না, তাই field থেকে hash হিসাব করা নিরাপদ — dataclass __hash__ তৈরি করে দেয়, আর object hashable হয়। নিচের কোডে property-র setter আর frozen dataclass একসাথে চালিয়ে দেখুন:
from dataclasses import dataclass
class BankAccount:
def __init__(self, balance=0):
self.balance = balance # এখানেও setter চলে
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError(f"ব্যালান্স ঋণাত্মক হতে পারে না: {value}")
self._balance = value
acc = BankAccount(500)
acc.balance += 200
try:
acc.balance = -5
except ValueError as err:
print(err, "| এখনো", acc.balance)
@dataclass(frozen=True)
class Coordinate:
lat: float
lon: float
hq = {Coordinate(24.75, 90.41): "ময়মনসিংহ", Coordinate(22.70, 90.37): "বরিশাল"}
print(hq[Coordinate(22.70, 90.37)], Coordinate(22.70, 90.37))প্রথম অংশে acc.balance += 200 getter দিয়ে 500 পড়েছে, তারপর setter দিয়ে 700 বসিয়েছে। acc.balance = -5 setter-এর যাচাইয়ে আটকেছে, আর আউটপুট ব্যালান্স ঋণাত্মক হতে পারে না: -5 | এখনো 700 — ব্যালান্স অক্ষত, ঠিক অংশ ৩-এর স্টেপারের মতো। দ্বিতীয় অংশে frozen Coordinate dict-এর চাবি হয়েছে। খোঁজার সময় একেবারে নতুন একটি Coordinate(22.70, 90.37) বানানো হয়েছে, তবু চাবি মিলেছে, কারণ hash আর == দুটোই field-এর মান দিয়ে হিসাব হয়। আউটপুট বরিশাল Coordinate(lat=22.7, lon=90.37) — float 22.70 ছাপায় 22.7 হয়ে যায়। (স্থানাঙ্কগুলো আনুমানিক।) এখানে hq[...].lat = 0 বা কোনো চাবির lat বদলানোর চেষ্টা করলে FrozenInstanceError আসত — আর সেটাই চাবিকে নিরাপদ রাখে।
frozen-এর সীমাও tuple-এর মতো অগভীর (পর্ব ১৫): একটি field-এ list রাখলে সেই list-এর ভেতরটা বদলানো যায়, আর list নিজে unhashable বলে hash() চাইলেই TypeError আসে। frozen dataclass-এর field-এ tuple বা frozenset রাখুন।
order, slots, kw_only — এক লাইনে করে
@dataclass(order=True) দিলে দুটি object তুলনা হয় field-গুলোর tuple হিসেবে, লেখার ক্রমে — তাই কোন field আগে লিখছেন, সেটাই ঠিক করে কোনটি দিয়ে আগে সাজানো হবে। প্রায়ই sorted(products, key=lambda p: p.price) বেশি স্পষ্ট; order=True তখনই দিন, যখন class-টির সত্যিই একটি স্বাভাবিক ক্রম আছে। slots=True object-কে হালকা করে আর p.prise = 10 (বানান ভুল) লিখলে নতুন attribute তৈরি না করে AttributeError দেয়। kw_only=True দিলে Product("চা", 20) আর চলে না, লিখতে হয় Product(name="চা", price=20).
dataclass বনাম namedtuple বনাম সাধারণ class
| প্রশ্ন | NamedTuple (পর্ব ১৫) | dataclass | সাধারণ class |
|---|---|---|---|
| বদলানো যায়? | না | হ্যাঁ (frozen=True দিলে না) | হ্যাঁ |
| tuple-এর মতো index/unpacking? | হ্যাঁ | না | না |
| tuple-এর সাথে সমান হতে পারে? | হ্যাঁ — P(1, 2) == (1, 2) | না | না |
__init__/__repr__/__eq__ | নিজে তৈরি | নিজে তৈরি | হাতে লিখতে হয় |
| যাচাই, property, method | সীমিত | সহজে (__post_init__) | পুরো স্বাধীনতা |
| সবচেয়ে মানানসই | ছোট স্থির রেকর্ড, ফাংশন থেকে কয়েকটি মান | ডেটা-প্রধান object | আচরণ-প্রধান object |
কখন dataclass নয়
dataclass-এর নামেই তার উদ্দেশ্য: ডেটা-প্রধান class. যে class-এর আসল পরিচয় তার আচরণ — একটি ডেটাবেস সংযোগ, একটি ফাইল ডাউনলোডার, একটি গেমের ইঞ্জিন — সেখানে মান দিয়ে তুলনা (__eq__) বা সব field ছাপানো (__repr__) অর্থহীন, এমনকি ক্ষতিকর হতে পারে (repr-এ পাসওয়ার্ড ছেপে যেতে পারে — এমন field-এ field(repr=False)). যে class-এর ভেতরের অবস্থা কঠোর নিয়মে বাঁধা, যেমন ব্যাংক হিসাব, যেখানে ব্যালান্স শুধু deposit আর withdraw দিয়েই বদলাবে, সেখানে সব field খোলা রেখে এমন __init__ বানানো, যা যেকোনো মান নেয়, ভুল বার্তা দেয়। সহজ পরীক্ষা: “দুটি object-এর সব মান সমান হলে কি তারা সত্যিই সমান?” উত্তর হ্যাঁ হলে dataclass; না হলে সাধারণ class.
property Python-এ এসেছে ২০০১ সালে Python 2.2-তে, “type আর class একীকরণ”-এর বড় পরিবর্তনের অংশ হিসেবে — একই সময়ে descriptor ব্যবস্থাও এসেছে। @ চিহ্নের decorator রূপ এসেছে Python 2.4-এ, আর @x.setter লেখার সুবিধা Python 2.6-এ। dataclass এসেছে অনেক পরে, ২০১৮ সালে Python 3.7-এ, Eric V. Smith-এর PEP 557 থেকে; এর পেছনে তৃতীয় পক্ষের attrs লাইব্রেরির (Hynek Schlawack) বহু বছরের অভিজ্ঞতা ছিল। ভাবনার প্রশ্ন ২-এ গল্পটি বিস্তারিত।
৭ · descriptor — property-র ভেতরের যন্ত্র
এতক্ষণ যা দেখলেন, তার নিচে একটি সাধারণ ব্যবস্থা কাজ করছে। descriptor হলো এমন যেকোনো object, যার class-এ __get__, __set__ বা __delete__ method আছে, আর যাকে অন্য একটি class-এর attribute হিসেবে রাখা হয়েছে। তখন সেই নামে attribute পড়লে-লিখলে Python সরাসরি মান না নিয়ে descriptor-এর method ডাকে। property ঠিক এমনই একটি object: তার __get__ আপনার getter ডাকে, __set__ আপনার setter.
এই কৌশল নিজেও কাজে লাগানো যায়। ধরুন একটি দোকানের সফটওয়্যারে দশটি class-এ বিশটি field আছে যাদের ধনাত্মক হতেই হবে — দাম, পরিমাণ, ওজন। প্রতিটির জন্য আলাদা property লিখলে একই যাচাই বিশবার। একটি descriptor লিখে সব জায়গায় ব্যবহার করুন:
class PositiveNumber:
def __set_name__(self, owner, name): # class তৈরির সময় নাম জানায়
self.private = "_" + name
def __get__(self, obj, objtype=None):
if obj is None: # class থেকে পড়লে descriptor নিজেই
return self
return getattr(obj, self.private)
def __set__(self, obj, value):
if value <= 0:
raise ValueError(f"{self.private[1:]} ধনাত্মক হতে হবে: {value}")
setattr(obj, self.private, value)
class OrderLine:
price = PositiveNumber() # একবার লেখা যাচাই,
quantity = PositiveNumber() # বহু জায়গায় ব্যবহার
def __init__(self, price, quantity):
self.price, self.quantity = price, quantityOrderLine(120, 0) লিখলে আসবে ValueError: quantity ধনাত্মক হতে হবে: 0. __set_name__ (Python 3.6 থেকে) descriptor-কে জানিয়ে দেয় সে কোন নামে বসানো হয়েছে, তাই একই class দুটি field-এর জন্য আলাদা আসল ঘর (_price, _quantity) ব্যবহার করে। Django-র model field, SQLAlchemy-র column — বড় লাইব্রেরিগুলো ঠিক এই কৌশলেই চলে।
attribute খোঁজার ক্রম
descriptor-এর দুই রকম আছে। যার __set__ বা __delete__ আছে, সে data descriptor (property, PositiveNumber). যার শুধু __get__ আছে, সে non-data descriptor — যেমন সাধারণ ফাংশন (এভাবেই s.method একটি bound method হয়ে ওঠে) আর cached_property. এই পার্থক্য ঠিক করে, obj.x লিখলে Python কোন ক্রমে খোঁজে:
- class-এ খোঁজা
MROনামটি আছে? - data descriptor
__get__সবার আগে - instance-এর ঘর
__dict__নিজস্ব মান - non-data / class attr
methodতারপর - কিছুই নেই
AttributeErrorবা __getattr__
line = OrderLine(120, 3) line.price # Python: type(line).__mro__ = (OrderLine, object) # এদের __dict__-এ 'price' খোঁজো OrderLine.__dict__['price'] → PositiveNumber object
# পাওয়া object-এর class-এ __set__ আছে? হ্যাঁ → PositiveNumber.__get__(desc, line, OrderLine) → 120 # instance-এর __dict__ দেখাই হলো না। # property-ও এভাবে জেতে।
line.__dict__ {'_price': 120, '_quantity': 3} line._price → 120 # এখানেই পাওয়া গেল # cached_property-র জমা মানও এখানে বসে, # তাই পরের বার ফাংশন আর চলে না।
line.total # class-এ একটি ফাংশন (শুধু __get__) → function.__get__ → bound method OrderLine.shop # সাধারণ class attribute → "করিম স্টোর" # সব instance ভাগ করে
line.discount # class-এ __getattr__ থাকলে সেটি ডাকা হয়; # না থাকলে — AttributeError: 'OrderLine' object has no attribute 'discount'
- আগে class-এ খোঁজ। অবাক লাগতে পারে, কিন্তু
line.priceলিখলে Python প্রথমে instance নয়, তার class আর পূর্বপুরুষদের (পর্ব ৩১-এর MRO ক্রমে) দেখে — সেখানেpriceনামে কিছু আছে কি না। - data descriptor সবার উপরে। পাওয়া জিনিসটি যদি data descriptor হয় (property, PositiveNumber), তবে তার
__get__-এর ফলই উত্তর। instance-এর নিজস্ব__dict__-এ একই নামে কিছু থাকলেও সেটি উপেক্ষিত। এ কারণেই property-কে “পাশ কাটিয়ে”acc.__dict__['balance'] = -5লিখলেওacc.balanceপড়ার সময় property-ই জেতে। - তারপর instance-এর নিজস্ব ঘর। data descriptor না থাকলে Python instance-এর
__dict__দেখে — সাধারণ attribute এখানেই থাকে।cached_propertyশুধু__get__রাখে বলে, প্রথমবার হিসাবের পর ফল এই dict-এ একই নামে লিখে দেয়; পরের বার ধাপ ৩-এই উত্তর মেলে, descriptor আর ডাকাই হয় না। চালাক নকশা। - তারপর non-data descriptor আর class attribute. instance-এ না পেলে class-এর সেই জিনিস: ফাংশন হলে তার
__get__একটি bound method বানায় (তাইselfআপনাআপনি আসে), সাধারণ মান হলে সেটিই — সব instance-এর ভাগের class attribute. - কোথাও নেই। class-এ
__getattr__লেখা থাকলে শেষ সুযোগ হিসেবে সেটি ডাকা হয়; না থাকলেAttributeError. বার্তায় ঠিক কোন নাম পাওয়া যায়নি, তা লেখা থাকে — বানান ভুল খোঁজার প্রথম সূত্র।
এই ক্রম মুখস্থ করার দরকার নেই, কিন্তু একবার বুঝলে Python-এর অনেক “জাদু” পরিষ্কার হয়ে যায়: property কেন instance-এর মানকে হারায়, method কেন self পায়, classmethod আর staticmethod কীভাবে কাজ করে (দুটোই descriptor), আর __slots__ কীভাবে __dict__ ছাড়া attribute রাখে (প্রতিটি slot একটি descriptor).
৮ · metaclass — এক অনুচ্ছেদে, আর কেন প্রায় কখনো লাগে না
পর্ব ০৫-এ শিখেছেন, Python-এ সবকিছুই object — class নিজেও। তাহলে class-টি কোন class-এর object? উত্তর: type. type(5) হলো int, আর type(int) হলো type; আপনার Product-এর ক্ষেত্রেও type(Product) হলো type. যে class অন্য class বানায়, তাকে বলে metaclass, আর type হলো ডিফল্ট metaclass — “class-এর class”. নিজের metaclass লিখে class তৈরির প্রক্রিয়াটাই বদলানো যায়: প্রতিটি নতুন subclass-কে কোথাও নিবন্ধন করা, নির্দিষ্ট method আছে কি না যাচাই করা। কিন্তু এটি Python-এর সবচেয়ে গভীর আর সবচেয়ে কম দরকারি অংশ। Zen of Python-এর লেখক Tim Peters-এর বিখ্যাত কথা: “Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't.” (“metaclass এমন গভীর জাদু, যা নিয়ে ৯৯% ব্যবহারকারীর কখনো মাথা ঘামানোর দরকার নেই। আপনার দরকার কি না ভাবছেন মানেই দরকার নেই।”) যে কাজগুলোর জন্য আগে metaclass লাগত, তার বেশিরভাগ এখন সহজ পথে হয়: Python 3.6-এর __init_subclass__ (PEP 487), যা প্রতিটি নতুন subclass তৈরির সময় parent class-এ একটি সাধারণ method ডাকে; আর class decorator — @dataclass নিজেই যার সবচেয়ে বড় উদাহরণ, কোনো metaclass ছাড়াই পুরো class বদলে দেয়।
class PaymentMethod:
registry = {}
def __init_subclass__(cls, **kwargs): # প্রতিটি subclass তৈরির সময়
super().__init_subclass__(**kwargs)
PaymentMethod.registry[cls.__name__.lower()] = cls
class Bkash(PaymentMethod): ...
class Nagad(PaymentMethod): ...
print(list(PaymentMethod.registry)) # ['bkash', 'nagad']metaclass ছাড়াই প্রতিটি নতুন পেমেন্ট পদ্ধতি নিজে থেকে তালিকায় উঠে গেল। পর্ব ৩৩-এ যে ABC দেখবেন, তা ভেতরে ABCMeta নামের একটি metaclass ব্যবহার করে — অর্থাৎ metaclass আপনি ব্যবহার করবেন অনেক, লিখবেন প্রায় কখনোই না।
C++-এ property নেই, তাই private member আর getBalance()/setBalance() জোড়াই প্রচলিত — আর তার কারণ ঠিক অংশ ১-এর সমস্যা: public member থেকে পরে method-এ যেতে হলে সব ডাকার কোড বদলাতে হয়। Python-এর dataclass অনেকটা একটি C++ aggregate struct, যার সাথে C++20-এর = default করা operator== আর operator<=> জুড়ে দেওয়া হয়েছে; frozen dataclass-কে const member-ওয়ালা struct ভাবতে পারেন। পার্থক্য: C++-এ যাচাই compile-এর সময় type দিয়ে হয়, Python-এ চালানোর সময় setter বা __post_init__ দিয়ে। access control আর encapsulation-এর বিস্তারিত দেখতে C++ কোর্সের class ও object পর্ব দেখুন।
- Python-এ getter/setter-এর বদলে কী ব্যবহার হয়? — সাধারণ attribute; যাচাই বা হিসাব লাগলে
@propertyআর@x.setter, যাতে ডাকার কোড বদলায় না। - শুধু getter-ওয়ালা property-তে মান বসালে কী হয়? —
AttributeError(property has no setter). - dataclass-এ
items: list = []কেন চলে না? — mutable default সব object ভাগ করত; dataclass class তৈরির সময়েইValueErrorদেয়, সমাধানfield(default_factory=list). - সাধারণ dataclass hashable নয় কেন, frozen কেন hashable? —
eq=Trueকিন্তু বদলানো যায় এমন object-এর hash অনিরাপদ, তাই__hash__ = None; frozen হলে field থেকে hash তৈরি হয়। - descriptor কী, একটি উদাহরণ? —
__get__/__set__/__delete__আছে এমন class-attribute object; property, classmethod, staticmethod, সাধারণ ফাংশন সবই descriptor.
৯ · AI-এর যুগে — AI-এর লেখা class-কোড যাচাই
AI মডেলগুলো শিখেছে বিপুল পরিমাণ Java, C# আর পুরোনো Python কোড থেকে। তাই Python class লিখতে বললে তারা প্রায়ই অন্য ভাষার অভ্যাস নিয়ে আসে, বা একঘেয়ে কোড হাতে লিখে দেয় যা Python নিজেই তৈরি করতে পারত। কোড চলে, কিন্তু লম্বা, ভঙ্গুর আর কখনো কখনো বাগভরা। সবচেয়ে প্রচলিত সমস্যাগুলো:
| AI প্রায়ই যা লেখে | সমস্যা | ভালো রূপ |
|---|---|---|
get_name(), set_name() প্রতিটি field-এর জন্য | Java-অভ্যাস; কিছুই যাচাই করে না, শুধু কোড বাড়ায় | সাধারণ attribute; দরকার হলে পরে property |
হাতে লেখা __init__, __repr__, __eq__ (৩০ লাইন) | নতুন field যোগে তিন জায়গায় বদল; একটিতে ভুললেই বাগ | @dataclass |
items: list = [] বা def __init__(self, items=[]) | dataclass-এ ValueError; সাধারণ class-এ সব object একই list ভাগ করে | field(default_factory=list) বা None দিয়ে শুরু |
property-র ভেতরে self.balance = value | setter নিজেকেই ডাকে — RecursionError | আসল মান self._balance-এ |
| ছোট সমস্যায় metaclass | পড়তে কঠিন, অন্য metaclass-এর সাথে সংঘাত | __init_subclass__ বা class decorator |
| dict-এর চাবি হিসেবে সাধারণ dataclass | TypeError: unhashable type | frozen=True |
AI-কে ডেটা-রাখার class লিখতে বলার সময় এই কাঠামোটি কপি করে ব্যবহার করুন:
AI-এর লেখা class-কোড পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১property কি Python-এর একটি বিশেষ ভাষা-বৈশিষ্ট্য, নাকি সাধারণ একটি ব্যবস্থার উপর বানানো একটি class? descriptor ব্যবস্থা কেন এল, আর Raymond Hettinger-এর “Descriptor HowTo Guide” কী দেখায়?
অনেকে ভাবেন property হলো ভাষার ভেতরে গাঁথা একটি বিশেষ নিয়ম, যেমন if বা for. আসলে তা নয়। property একটি সাধারণ built-in class, আর সেটি কাজ করে একটি সাধারণ ব্যবস্থার উপর — descriptor protocol. যে object-এর __get__, __set__ বা __delete__ আছে, তাকে class-এর attribute হিসেবে রাখলে attribute পড়া-লেখার সময় Python সেই method ডাকে। property-র __get__ শুধু আপনার দেওয়া getter ডাকে, __set__ setter ডাকে। ব্যস।
এই ব্যবস্থা এসেছিল ২০০১ সালে Python 2.2-তে, যখন Guido van Rossum “type আর class একীকরণ” (PEP 252, PEP 253) করছিলেন। তার আগে built-in type (যেমন list) আর ব্যবহারকারীর লেখা class ছিল দুই আলাদা জগৎ; list-এর subclass বানানো যেত না। নতুন নকশায় একটি একক নিয়ম দরকার ছিল, যা দিয়ে method, class method, static method, property আর __slots__ — সবকিছু ব্যাখ্যা করা যায়। descriptor সেই একক নিয়ম। আপনি যে প্রতিটি সাধারণ method ডাকেন, সেটিও descriptor: ফাংশন object-এর __get__ আছে, তাই s.method লিখলে সে self-কে আগে থেকে বেঁধে একটি bound method দেয়। এ কারণেই self আপনাকে হাতে পাঠাতে হয় না।
Raymond Hettinger, যিনি Python-এর core developer আর collections, itertools-এর মতো module-এর পেছনের মানুষ, এই বিষয়ে “Descriptor HowTo Guide” লিখেছেন, যা এখন Python-এর অফিসিয়াল ডকুমেন্টেশনের অংশ। গাইডটির সবচেয়ে শিক্ষণীয় অংশ হলো, সেখানে property, classmethod, staticmethod আর এমনকি ফাংশন-থেকে-method হওয়ার প্রক্রিয়া বিশুদ্ধ Python-এ কয়েক লাইনে নতুন করে লিখে দেখানো হয়েছে। CPython-এ এগুলো C-তে লেখা গতির জন্য, কিন্তু আচরণ সেই Python সংস্করণের মতোই। অর্থাৎ ভাষার একটি বড় অংশ আসলে “সাধারণ object আর একটি নিয়ম” — কোনো গোপন জাদু নয়।
এই নকশার একটি বড় লাভ হলো লাইব্রেরি-লেখকেরা ভাষা না বদলেই নতুন ধরনের attribute বানাতে পারেন। Django-র ORM, SQLAlchemy, বিভিন্ন validation লাইব্রেরি descriptor দিয়ে এমন field বানায়, যা পড়লে ডেটাবেস থেকে মান আনে বা লিখলে যাচাই করে। খরচও আছে: প্রতিটি attribute পড়ায় Python-কে class-এ খুঁজতে হয়, descriptor কি না দেখতে হয় — যদিও CPython এই খোঁজা cache করে দ্রুত রাখে।
মূল উপলব্ধি: ভালো ভাষা-নকশায় বিশেষ নিয়ম কম, সাধারণ নিয়ম বেশি। property, method, classmethod — সবই একটি ছোট protocol-এর প্রয়োগ। protocol-টি বুঝলে একসাথে অনেকগুলো “জাদু” বোঝা হয়ে যায়।
প্র ০২dataclass Python-এ এল ২০১৮ সালে, অথচ attrs লাইব্রেরি তার কয়েক বছর আগে থেকেই একই কাজ করছিল। PEP 557 কেন লেখা হলো, attrs থেকে কী নেওয়া হলো, আর standard library কেন সরাসরি attrs-কেই নিয়ে নিল না?
“শুধু ডেটা রাখার class”-এর boilerplate সমস্যা Python-এ বহু পুরোনো। namedtuple (২০০৮) আংশিক সমাধান দিয়েছিল, কিন্তু সেটি tuple — immutable, index-নির্ভর, আর tuple-এর সাথে সমান হয়ে যায়। ২০১৫ সালে Hynek Schlawack প্রকাশ করেন attrs লাইব্রেরি (তাঁর আগের একটি ছোট প্রজেক্ট characteristic-এর উত্তরসূরি)। attrs একটি class decorator দিয়ে __init__, __repr__, তুলনা, hash, যাচাই (validator), রূপান্তর (converter) — সব তৈরি করে দিত। অল্প দিনেই এটি Python জগতের সবচেয়ে বেশি ব্যবহৃত লাইব্রেরিগুলোর একটি হয়ে ওঠে।
Python 3.6-এ ভ্যারিয়েবল annotation (PEP 526) আসার পর একটি নতুন সুযোগ তৈরি হলো: field-এর তালিকা আলাদা করে না লিখে, class-এর ভেতরের type hint থেকেই পড়া যায়। Eric V. Smith এই ধারণা নিয়ে PEP 557 লেখেন, আর dataclasses module চালু হয় Python 3.7-এ, ২০১৮ সালে। PEP-এ স্পষ্টভাবে attrs-এর প্রতি কৃতজ্ঞতা জানানো হয়েছে; Hynek Schlawack নিজেও আলোচনায় অংশ নিয়েছিলেন। নকশার মূল ভাবনাগুলো — class decorator, কোনো base class বা metaclass নয়, তৈরি হওয়া method আসলেই সাধারণ method, frozen, field() দিয়ে প্রতিটি field-এর আলাদা নিয়ন্ত্রণ — attrs থেকেই অনুপ্রাণিত।
তাহলে attrs-কেই কেন standard library-তে নেওয়া হলো না? কয়েকটি কারণ। standard library-র মুক্তির গতি ধীর (প্রতি বছরে একটি বড় version), আর একবার কিছু ঢুকলে তা বদলানো কঠিন; attrs তখনো দ্রুত বদলাচ্ছিল, আর তার রক্ষণাবেক্ষণকারীরা সেই স্বাধীনতা হারাতে চাননি। attrs-এর বৈশিষ্ট্য অনেক বেশি — validator, converter, বহু বিকল্প; standard library চেয়েছিল একটি ছোট, সহজ, স্থির মূল অংশ, যা সাধারণ ক্ষেত্রের ৮০% সমাধান করে। ফলাফল একটি স্বাস্থ্যকর ভাগাভাগি: dataclass সবার জন্য, আর যাঁদের বেশি দরকার, তাঁদের জন্য attrs (এখন attrs.define) বা Pydantic-এর মতো লাইব্রেরি।
পরে Python 3.11-এর PEP 681 (dataclass_transform) একটি মজার স্বীকৃতি দেয়: অনেক তৃতীয় পক্ষের লাইব্রেরি dataclass-এর মতো আচরণ করে, তাই type checker যেন সেগুলোকেও চিনতে পারে, তার একটি মানসম্মত উপায় তৈরি হলো।
মূল উপলব্ধি: standard library প্রায়ই নতুন ধারণা আবিষ্কার করে না; তৃতীয় পক্ষের লাইব্রেরিতে যা বছরের পর বছর টিকে প্রমাণিত হয়েছে, তার সরল ও স্থির রূপটি গ্রহণ করে। “ভালো যথেষ্ট আর সবার কাছে আছে” অনেক সময় “সবচেয়ে শক্তিশালী কিন্তু আলাদা ইনস্টল”-এর চেয়ে বেশি কাজে আসে।
প্র ০৩“uniform access principle” কে প্রথম স্পষ্ট করে বলেছিলেন, এটি ঠিক কী দাবি করে, আর Java, C# ও Python এই নীতির প্রতি কীভাবে আলাদা আলাদা সাড়া দিয়েছে? এর কোনো খরচ আছে কি?
নীতিটি স্পষ্ট করে বলেছিলেন Bertrand Meyer, Eiffel ভাষার স্রষ্টা, তাঁর বিখ্যাত বই Object-Oriented Software Construction-এ (প্রথম সংস্করণ ১৯৮৮)। তাঁর বক্তব্য সংক্ষেপে: একটি module যে সেবা দেয়, তা এমন একটি একক লেখার রূপে পাওয়া উচিত, যা দেখে বোঝা যায় না সেবাটি জমা রাখা মান থেকে আসছে, নাকি হিসাব করে। অর্থাৎ account.balance লেখা ব্যবহারকারীর জানার দরকার নেই ব্যালান্স একটি ঘরে রাখা, নাকি প্রতিবার লেনদেনের তালিকা যোগ করে বের করা হয়। Eiffel-এ field আর argument-ছাড়া ফাংশন একই রূপে ডাকা হয়, তাই ভাষাটি নিজেই এই নীতি মেনে চলে।
Java এই নীতি ভাষায় রাখেনি। field পড়া a.balance, method ডাকা a.getBalance() — দুটি আলাদা রূপ। ফলে বাস্তবায়ন বদলালে (ঘর থেকে হিসাবে) সব ব্যবহারকারীর কোড বদলায়। এর প্রতিক্রিয়ায় জন্ম নিয়েছে একটি সংস্কৃতি: সব field private, আর আগে থেকেই getter/setter — JavaBeans রীতি সেটিকে আনুষ্ঠানিক করেছে, আর IDE সেগুলো এক ক্লিকে তৈরি করে। নীতিটি ভাষা না দিলেও প্রোগ্রামাররা অভ্যাস দিয়ে পূরণ করছেন, বিশাল পরিমাণ বাড়তি কোডের বিনিময়ে। পরে Lombok-এর মতো টুল আর Java 16-এর record এই বোঝা কিছুটা কমিয়েছে।
C# শুরু থেকেই property রেখেছে: a.Balance একটি field-ও হতে পারে, property-ও। Python-ও একই পথে, কিন্তু আরও এক ধাপ এগিয়ে: যেহেতু সাধারণ attribute থেকে property-তে যাওয়ায় ডাকার কোড বদলায় না, তাই আগে থেকে কিছু লেখার দরকারই নেই। Ruby, Kotlin, Swift-ও কোনো না কোনো রূপে এই নীতি মেনেছে।
খরচ আছে, আর সেটা লুকানো খরচ। order.total দেখে মনে হয় একটি সস্তা পড়া, অথচ ভেতরে হয়তো হাজারটি লাইন যোগ হচ্ছে বা ডেটাবেসে প্রশ্ন যাচ্ছে। লুপে ব্যবহার করলে ধীরগতি এমন জায়গা থেকে আসে, যেখানে কেউ খোঁজে না। তাই প্রচলিত পরামর্শ: property হবে সস্তা, দ্রুত আর side effect মুক্ত; ভারী কাজের জন্য বন্ধনীওয়ালা method, যাতে লেখার রূপই সতর্ক করে দেয়।
মূল উপলব্ধি: ভালো interface বাস্তবায়নের বিবরণ লুকায়, যাতে ভেতরটা বদলালে বাইরে কিছু ভাঙে না। কিন্তু খরচের বিবরণ পুরোপুরি লুকালে পাঠক ভুল ধারণা পায় — তাই কোনটি সস্তা আর কোনটি দামি, সেই সংকেত interface-এ রেখে দেওয়াও নকশার অংশ।
প্র ০৪সাধারণ @dataclass-এর object hashable নয়, অথচ frozen=True দিলে hashable হয়ে যায়। dataclass ঠিক কোন নিয়মে __hash__ ঠিক করে, আর frozen-এর “অপরিবর্তনীয়তা” কতটা সত্যি?
শুরুতে মনে করুন hash-এর চুক্তি (পর্ব ১৬): দুটি object == হলে তাদের hash সমান হতে হবে, আর dict-এ থাকা অবস্থায় একটি চাবির hash কখনো বদলাবে না। সাধারণ class-এ __eq__ না লিখলে সমতা মানে একই object (identity), আর hash আসে object-এর পরিচয় থেকে — নিরাপদ। কিন্তু dataclass ডিফল্টভাবে eq=True: সমতা নির্ভর করে field-এর মানের উপর। এখন hash-ও মান থেকে না এলে দুটি সমান object ভিন্ন hash পেত — চুক্তি ভাঙত। আবার মান থেকে hash এলে, আর object বদলানো গেলে, dict-এ রাখার পর একটি field বদলালেই চাবি “হারিয়ে” যেত।
তাই dataclass একটি ছোট সিদ্ধান্ত-টেবিল মানে। eq=True আর frozen=False (ডিফল্ট) হলে __hash__ = None — object unhashable, আর hash() চাইলে TypeError: unhashable type. eq=True আর frozen=True হলে field-গুলোর tuple থেকে __hash__ তৈরি হয়, কারণ মান আর বদলাবে না। eq=False হলে hash যেমন ছিল তেমনই (identity-ভিত্তিক)। আর যাঁরা নিজের দায়িত্বে নিয়ম ভাঙতে চান, তাঁদের জন্য unsafe_hash=True — নামেই সতর্কবার্তা।
frozen কীভাবে প্রয়োগ হয়? dataclass class-এ একটি __setattr__ আর __delattr__ বসায়, যা field বসানো বা মোছার চেষ্টায় FrozenInstanceError ছোড়ে (এটি AttributeError-এর subclass). কিন্তু Python-এ সত্যিকারের অপরিবর্তনীয়তা নেই — object.__setattr__(obj, "lat", 0) লিখলে এই পাহারা পাশ কাটানো যায়। মজার ব্যাপার, frozen dataclass-এর __post_init__-এ হিসাব করা field বসাতে এই কৌশলই প্রচলিত। এছাড়া frozen অগভীর: একটি field-এ list থাকলে তার ভেতরটা বদলানো যায়, আর hash হিসাব করতে গেলেই TypeError আসে (list নিজে unhashable) — ঠিক tuple-এর ভেতরে list-এর মতো।
আরও একটি সূক্ষ্মতা: frozen প্রতিটি বসানোয় একটি পরীক্ষা যোগ করে, তাই __init__ সামান্য ধীর হয়; বেশিরভাগ প্রোগ্রামে তা নগণ্য। বিনিময়ে পাওয়া যায় নিরাপদে ভাগ করার নিশ্চয়তা, thread-এর মধ্যে নিশ্চিন্তে পাঠানো, আর dict/set-এ ব্যবহারের অধিকার।
মূল উপলব্ধি: hashability কোনো আলাদা বৈশিষ্ট্য নয়, এটি সমতা আর পরিবর্তনশীলতার যৌথ ফল। মান দিয়ে সমতা চাইলে এবং চাবি হিসেবে ব্যবহার করতে চাইলে, object-কে বদলানো বন্ধ করতেই হবে — dataclass শুধু এই যুক্তিটি আপনার হয়ে প্রয়োগ করে।
প্র ০৫metaclass এত শক্তিশালী হলে Tim Peters কেন বলেছিলেন ৯৯% মানুষের এ নিয়ে ভাবার দরকার নেই? PEP 487-এর __init_subclass__ কোন সমস্যার সমাধান করল?
Python-এ class Product: ... লেখা একটি কার্যকর statement. Python class-এর body চালিয়ে নামগুলোর একটি dict বানায়, তারপর metaclass-কে ডাকে — ডিফল্টভাবে type(name, bases, namespace) — আর সেটি class object তৈরি করে। নিজের metaclass দিলে এই তৈরির মুহূর্তে আপনি যা খুশি করতে পারেন: method যোগ বা বাদ, নাম বদল, যাচাই, নিবন্ধন। Python 3-এ এটি লেখা হয় class Product(metaclass=MyMeta) রূপে (PEP 3115). standard library-তে ABCMeta (abstract class), EnumType (Enum; আগে নাম ছিল EnumMeta) metaclass ব্যবহার করে, আর Django-র model-ও দীর্ঘদিন metaclass দিয়ে field সংগ্রহ করেছে।
তাহলে সমস্যা কোথায়? প্রথমত, metaclass পড়া কঠিন — class-এর আচরণ এমন জায়গা থেকে আসে, যা class-এর কোডে চোখে পড়ে না। দ্বিতীয়ত, metaclass উত্তরাধিকারে ছড়ায়: আপনার class-এর সব subclass একই metaclass পায়। তৃতীয়ত, metaclass conflict: দুটি ভিন্ন লাইব্রেরির base class যদি দুটি ভিন্ন metaclass ব্যবহার করে, তবে দুটিকে একসাথে inherit করলে TypeError: metaclass conflict — সমাধানে তৃতীয় একটি metaclass লিখতে হয়। Tim Peters — যিনি Zen of Python আর Timsort-এর জন্য পরিচিত — comp.lang.python-এ এই প্রসঙ্গে বলেছিলেন: “Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't (the people who actually need them know with certainty that they need them, and don't need an explanation about why).”
বাস্তবে metaclass-এর বেশিরভাগ ব্যবহার ছিল দুটি সাধারণ কাজে: প্রতিটি subclass তৈরির সময় কিছু করা (নিবন্ধন, যাচাই), আর class-এর attribute-দের নিজেদের নাম জানানো (যেমন descriptor-কে জানানো যে সে price নামে বসেছে)। ২০১৬ সালে Martin Teichmann-এর PEP 487 Python 3.6-এ ঠিক এই দুটির জন্য সহজ hook যোগ করে: __init_subclass__, যা parent class-এ লেখা একটি সাধারণ classmethod-এর মতো, প্রতিটি নতুন subclass তৈরির সময় ডাকা হয়; আর __set_name__, যা অংশ ৭-এর PositiveNumber-এ দেখেছেন। এর আগেই Python 2.6/3.0-তে class decorator এসেছিল (PEP 3129), যা তৈরি হয়ে যাওয়া class হাতে নিয়ে বদলাতে পারে — dataclass এর সেরা প্রমাণ। এই তিনটি মিলে metaclass-এর বেশিরভাগ ব্যবহারের প্রয়োজন মিটিয়েছে, metaclass conflict ছাড়াই।
মূল উপলব্ধি: সবচেয়ে শক্তিশালী হাতিয়ার সবসময় সেরা হাতিয়ার নয়। যে সমাধান সবচেয়ে কম জাদু ব্যবহার করে আর পাঠকের চোখের সামনে থাকে, সেটিই সাধারণত দীর্ঘমেয়াদে ভালো। ভাষা যখন কোনো প্রচলিত জটিল কৌশলের সহজ বিকল্প দেয়, তখন সেটিই নিন।
অনুশীলন
এগুলোর বেশিরভাগে নতুন কোড লিখতে হবে না — অনুমান করা, হাতে চালানো আর মিলিয়ে নেওয়াই আসল কাজ।
-
কী ঘটবে? একটি
Rectangleclass-এwidthআরheightসাধারণ attribute, আরareaশুধু getter-ওয়ালা property (width * height).r = Rectangle(3, 4)-এর পর পরপর চালানো হলো: (ক)r.area; (খ)r.width = 10, তারপরr.area; (গ)r.area(); (ঘ)r.area = 50. প্রতিটির ফল লিখুন।(ক)
12. (খ)40— area জমা রাখা নেই, প্রতিবার নতুন width দিয়ে হিসাব হয়; এটাই computed property-র সুবিধা। (গ)TypeError: 'int' object is not callable—r.areaইতিমধ্যেই 40, আর সংখ্যাকে ডাকা যায় না। (ঘ)AttributeError: property 'area' of 'Rectangle' object has no setter— setter নেই, তাই read-only. -
ভুল খুঁজুন: কেউ লিখেছে
@dataclass class Order: order_id: int = 0; items: dict = {}; customer: str(তিনটি field আলাদা লাইনে)। এই class তৈরির সময় কী কী সমস্যা হবে? ঠিক করে লিখুন। ঠিক করার পরOrder(customer="রহিম") == Order(customer="রহিম")কী দেবে, আর কেন?দুটি সমস্যা। (১)
items: dict = {}— mutable default, dataclassValueError: mutable default <class 'dict'> for field items is not allowed: use default_factoryদেবে। (২)customer-এর default নেই, অথচ সে default-ওয়ালা field-গুলোর পরে —TypeError: non-default argument 'customer' follows default argument. dataclass field-গুলো ক্রমে পরীক্ষা করে বলে প্রথম যে সমস্যা ধরা পড়ে, সেই error-ই আগে দেখবেন; একটি ঠিক করলে পরেরটি আসবে। ঠিক রূপ: আগেcustomer: str, তারপরorder_id: int = 0, তারপরitems: dict = field(default_factory=dict). (অথবাkw_only=Trueদিলে ক্রমের সমস্যা থাকে না।) তুলনাটিTrue— তৈরি হওয়া__eq__তিনটি field মিলিয়ে দেখে, দুটি খালি dict সমান। -
Python-ধাঁচে রূপান্তর (ভাবনা): AI একটি
Employeeclass দিয়েছে, যাতেget_name,set_name,get_salary,set_salaryআছে; শুধুset_salaryপরীক্ষা করে বেতন ৮,০০০ টাকার কম কি না। কোন method-গুলো বাদ দেবেন, কোনটি property হবে, আর ডাকার কোডe.set_salary(e.get_salary() + 2000)কেমন হবে? এটি কি dataclass হওয়া উচিত?get_name/set_nameকিছুই যাচাই করে না, তাই দুটিই বাদ —nameসাধারণ attribute.salary-র যাচাই আছে, তাই সেটি@property+@salary.setter, আসল মানself._salary-এ, আর__init__-এself.salary = salaryযাতে শুরুর মানও যাচাই হয়। ডাকার কোড হবেe.salary += 2000. dataclass চাইলে করা যায় (মূলত ডেটা রাখে), কিন্তু সাধারণ dataclass field-এ property বসানো একটু জটিল; সহজ পথ হলো__post_init__-এ শুরুর যাচাই আর frozen রেখেreplaceদিয়ে নতুন বেতন — অথবা যাচাই-নির্ভর হলে সাধারণ class-ই ভালো। এখানে কোনো একক “ঠিক” উত্তর নেই; যুক্তিটাই আসল। -
নিজে লিখুন (একমাত্র কোড লেখার কাজ): একটি
@dataclassStudentলিখুন:name: str,marks: list[int](default খালি list). একটি read-only propertyaverageদিন, যা marks খালি হলে 0 আর না হলে গড় (দুই দশমিকে) দেয়।__post_init__-এ কোনো mark 0-এর কম বা 100-এর বেশি হলেValueError. তারপর দেখান দুটি নতুন Student-এর marks list আলাদা।from dataclasses import dataclass, field @dataclass class Student: name: str marks: list[int] = field(default_factory=list) def __post_init__(self): for m in self.marks: if not 0 <= m <= 100: raise ValueError(f"নম্বর 0-100 এর মধ্যে হবে: {m}") @property def average(self): return round(sum(self.marks) / len(self.marks), 2) if self.marks else 0 a, b = Student("সুমি"), Student("রনি") a.marks.append(90) print(a.average, b.marks) # 90.0 []property dataclass-এর field নয় (type hint নেই), তাই repr বা
__eq__-এ আসে না — হিসাব করা মান হিসেবে এটাই ঠিক। লক্ষ করুন,a.marks.append(150)লিখলে এখন কোনো যাচাই হবে না, কারণ__post_init__শুধু তৈরির সময় চলে; প্রতিবার যাচাই চাইলে নম্বর যোগের জন্য একটিadd_markmethod বানান।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি @dataclass class লেখা থেকে Product("চা", 20) দিয়ে object তৈরি পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 📝 class-এর body চলে, type hint-গুলো
__annotations__-এ জমা হয় - 🔍
@dataclassannotation পড়ে field-এর তালিকা বানায় (mutable default থাকলে এখানেই ValueError) - 🏗️
__init__,__repr__,__eq__তৈরি হয়ে class-এ বসে - 📦
Product("চা", 20)ডাকলে তৈরি হওয়া__init__প্রতিটি field-এ মান বসায় - ✅
__init__-এর শেষে__post_init__চলে — যাচাই আর হিসাব করা field
একটি Python class-এ gpa শুধু @property দিয়ে লেখা getter, কোনো setter নেই। s.gpa = 4.0 চালালে কী হয়?
- instance-এ gpa নামে নতুন একটি attribute তৈরি হয়
- কিছুই ঘটে না, পুরোনো মান থাকে
- AttributeError
- TypeError
Python-এ @dataclass দেওয়া একটি class-এ items: list = [] লিখলে কী হয়?
- সব object একই list ভাগ করে, কোনো error নেই
- class তৈরির সময়েই ValueError আসে
- প্রতিটি object নিজে থেকেই আলাদা খালি list পায়
- প্রথম object তৈরির সময় TypeError আসে
Python-এ @dataclass(frozen=True) দেওয়া একটি class সম্পর্কে কোনটি সত্য?
- এটি tuple-এর subclass হয়ে যায়
- এটি dict-এর চাবি হতে পারে না
- এর কোনো field-এর ভেতরের কিছুই কখনো বদলানো যায় না
- field বসানোর চেষ্টায় FrozenInstanceError আসে, আর object hashable হয়
শুধু @dataclass (কোনো বিকল্প ছাড়া) দেওয়া একটি class-এর object নিয়ে hash(obj) চালালে কী হয়?
- TypeError: unhashable type
- object-এর পরিচয় (id) থেকে একটি hash আসে
- field-গুলোর মান থেকে একটি hash আসে
- None ফেরত আসে
Python obj.x পড়ার সময় কোন ক্রমে অগ্রাধিকার দেয়?
- instance-এর __dict__ → data descriptor → non-data descriptor
- non-data descriptor → data descriptor → instance-এর __dict__
- data descriptor → instance-এর __dict__ → non-data descriptor
- class attribute → instance-এর __dict__ → data descriptor
Python dataclass-এ field বসানোর পর যাচাই বা হিসাব করা field বসানোর জন্য কোন method লেখা হয়, যা তৈরি হওয়া __init__ নিজে ডাকে?
- __new__
- __post_init__
- __init_subclass__
- __set_name__
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- সাধারণ attribute দিয়ে শুরু: getter/setter আগে থেকে নয়; যাচাই বা হিসাব লাগলে
@property— ডাকার কোড বদলায় না (uniform access principle). - setter-এ invariant রক্ষা: আসল মান
_x-এ,__init__-এও setter চালান; ভারী অথচ স্থির হিসাবের জন্যcached_property. - @dataclass field থেকে
__init__/__repr__/__eq__লেখে; mutable default-এfield(default_factory=list); যাচাই__post_init__-এ; নতুন কপিreplace, dict-এasdict. - frozen=True মানে বদলানো যায় না আর hashable (চাবি হতে পারে);
order,slots,kw_onlyপ্রয়োজনমতো; আচরণ-প্রধান class-এ dataclass নয়। - descriptor (
__get__/__set__) property ও method-এর ভিত্তি; খোঁজার ক্রম: data descriptor → instance__dict__→ non-data descriptor. metaclass প্রায় কখনো নয় —__init_subclass__বা class decorator.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩৩ · ABC, duck typing আর Protocolপরবর্তী পর্বabstract class-এর চুক্তি, duck typing আর inheritance ছাড়া interface.
- Python প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — ৮টি মডিউল, কোন পর্ব কখন।
- পর্ব ১৫ · tuple আর unpackingফিরে দেখুনnamedtuple আর NamedTuple — dataclass-এর হালকা, immutable আত্মীয়।