ডেটাক্লাস আর টাইপ হিন্ট
@dataclass কী লিখে দেয়, field(default_factory=...) কেন লাগে, frozen=True কী দেয়, ফাংশনে টাইপ হিন্ট, আর হিন্ট চালানোর সময় কিছু না করে যাচাইকারীর নিচে সবকিছু করার মানে।
- 1সমস্যা
- 2বোঝা
- 3উদাহরণ
- 4অনুমান
- 5নিজে করা
- 6কঠিন করা
যে সমস্যাটা আমরা সমাধান করছি
গত অধ্যায়ের Book ক্লাসটা লিখতে গেলে দাঁড়ায় এরকম:
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
def __repr__(self):
return f"Book({self.title!r}, {self.author!r}, {self.pages!r})"
def __eq__(self, other):
return (self.title, self.author, self.pages) == (other.title, other.author, other.pages)
a = Book("one", "rafi", 300)
print(a)
print(a == Book("one", "rafi", 300))Book('one', 'rafi', 300)
Trueতেরো লাইন, আর তার মধ্যে একটাও সিদ্ধান্ত নেই। প্রতিটা ক্ষেত্রের নাম তিনবার লেখা হয়েছে — __init__-এর প্যারামিটারে, self. বসানোর লাইনে, আর __repr__-এ। চতুর্থ একটা ক্ষেত্র যোগ করতে হলে তিন জায়গায় যোগ করতে হবে, আর কোনো একটা ভুলে গেলে ত্রুটি আসবে না — কেবল __repr__-টা অসম্পূর্ণ হবে, নীরবে।
from dataclasses import dataclass
@dataclass
class Book:
title: str
author: str
pages: int
a = Book("one", "rafi", 300)
print(a)
print(a == Book("one", "rafi", 300))Book(title='one', author='rafi', pages=300)
Trueসাত লাইন, আর ক্ষেত্রগুলোর নাম একবার করে।
এই অধ্যায় শেষে আপনি পারবেন
@dataclassলিখতে, আর বলতে সে কী কী তৈরি করে দেয়field(default_factory=...)ব্যবহার করতে, আর বলতে কেন সেটা লাগেfrozen=Trueকী দেয় তা বলতে- ফাংশনে টাইপ হিন্ট লিখতে —
list[str],dict[str, float],int | None - বলতে হিন্ট চালানোর সময় কিছুই করে না, আর যাচাইকারীর নিচে সবকিছু করে
asdictদিয়ে একটা dataclass-কে JSON-এ পাঠাতে
পূর্বশর্ত: ক্লাস।
@dataclass কী লিখে দেয়
উপরের সাত লাইনের বিনিময়ে যা পাওয়া গেল:
- একটা
__init__, যার প্যারামিটারগুলো ক্ষেত্রগুলোর ক্রমেই - একটা
__repr__, যা প্রতিটা ক্ষেত্র নামসহ দেখায় - একটা
__eq__, যা সব ক্ষেত্র মিললে সমান বলে
গত অধ্যায়ের তিনটে সমস্যা — <object at 0x...>, a == b মিথ্যা হওয়া, আর একই কথা তিনবার লেখা — একসাথে শেষ।
মেথড লেখা যায় আগের মতোই:
from dataclasses import dataclass
@dataclass
class OrderLine:
name: str
price: float
quantity: int
def total(self) -> float:
return round(self.price * self.quantity * 1.15, 2)
line = OrderLine("pen", 15.0, 3)
print(line)
print(line.total())OrderLine(name='pen', price=15.0, quantity=3)
51.75ডিফল্ট মানও, আর নিয়মটা বিংশ অধ্যায়ের সেই একই: ডিফল্টওয়ালা ক্ষেত্র পরে।
from dataclasses import dataclass
@dataclass
class Book:
title: str
pages: int = 1
print(Book("one"))
print(Book("one", 300))Book(title='one', pages=1)
Book(title='one', pages=300)হিন্ট চালানোর সময় কিছুই করে না
এটা প্রথমেই পরিষ্কার হওয়া দরকার, কারণ নামটা অন্য কিছু বলে।
from dataclasses import dataclass
@dataclass
class Book:
title: str
pages: int
b = Book("one", "three hundred")
print(b)
print(b.pages + 1)Book(title='one', pages='three hundred')
TypeError: can only concatenate str (not "int") to strpages: int লেখা সত্ত্বেও একটা লেখা দিব্যি ঢুকে গেছে। পাইথন হিন্ট যাচাই করে না — সে সেগুলো সংরক্ষণ করে রাখে, আর কাজে লাগানোর দায়িত্ব অন্য কারো।
ত্রুটিটা এসেছে অনেক পরে, যখন ওই মানটা দিয়ে হিসাব করার চেষ্টা হয়েছে। মাঝখানে সেটা ছাপা হয়েছে, লগে গেছে, হয়তো ফাইলেও লেখা হয়েছে।
এখানেই যাচাইকারীর জায়গা। mypy একটা আলাদা প্রোগ্রাম যে কোডটা না চালিয়ে পড়ে:
main.py:10: error: Argument 2 to "Book" has incompatible type "str"; expected "int" [arg-type]লাইন নম্বর, কোন আর্গুমেন্ট, কী পাওয়া গেছে, কী আশা করা হয়েছিল — আর এই বার্তাটা এসেছে প্রোগ্রামটা একবারও না চালিয়ে।
দুটো বাক্যে গোটা ব্যাপারটা: হিন্ট চালানোর সময় কিছুই করে না, আর যাচাইকারীর নিচে সবকিছু করে। হিন্ট লেখা মানে যাচাইকারীকে আর পাঠককে কথা দেওয়া, ইন্টারপ্রেটারকে নয়।
সংরক্ষিত হিন্টগুলো দেখাও যায়:
def shout(text: str) -> str:
return text.upper()
print(shout("pen"))
print(shout.__annotations__)PEN
{'text': <class 'str'>, 'return': <class 'str'>}পরিবর্তনীয় ডিফল্ট — তৃতীয়বার
একবিংশ অধ্যায়ে এটা ছিল ফাংশনের ডিফল্ট আর্গুমেন্টে। ষড়বিংশে ক্লাস-স্তরের বৈশিষ্ট্যে। এবার dataclass-এ — আর এবার পাইথন নিজেই থামিয়ে দেয়:
from dataclasses import dataclass
@dataclass
class Shelf:
books: list = []ValueError: mutable default <class 'list'> for field books is not allowed: use default_factoryলক্ষ্য করুন, কোনো বস্তু তৈরিই করা হয়নি — ত্রুটিটা এসেছে ক্লাসটা সংজ্ঞায়িত হওয়ার সময়। পাইথন এই ভুলটা এত বেশি দেখেছে যে এখানে সে অনুমতিই দেয় না।
ঠিক পথটা হলো একটা কারখানা দেওয়া — কী বানাতে হবে তা বলা, বানানো জিনিসটা নয়:
from dataclasses import dataclass, field
@dataclass
class Shelf:
name: str
books: list[str] = field(default_factory=list)
a = Shelf("a")
b = Shelf("b")
a.books.append("one")
print(a)
print(b)Shelf(name='a', books=['one'])
Shelf(name='b', books=[])field(default_factory=list) মানে «প্রতিবার list() ডাকো»। একবিংশ অধ্যায়ের if basket is None: basket = []-এর সমান কাজ, কেবল এক লাইনে আর ভুল করার সুযোগ ছাড়াই।
frozen=True
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
p = Point(1, 2)
print(p)
print({p, Point(1, 2)})
p.x = 5Point(x=1, y=2)
{Point(x=1, y=2)}
dataclasses.FrozenInstanceError: cannot assign to field 'x'দুটো জিনিস একসাথে ঘটেছে, আর তারা সম্পর্কিত।
বস্তুটা আর বদলানো যায় না। p.x = 5 থেমে গেছে — টাপলের মতোই।
আর সেটা এখন সেটে যায়। গত অধ্যায়ের শেষ সমস্যাটা মনে আছে: __eq__ লিখলে হ্যাশ চলে যায়। এখানে দুটো «সমান» বিন্দু সেটে একটাই হয়ে বসেছে, আর কোনো ত্রুটি নেই।
frozen ছাড়া হলে:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
print({Point(1, 2)})TypeError: unhashable type: 'Point'কারণটা ষোড়শ অধ্যায় থেকেই আসে: যা বদলাতে পারে, তাকে হ্যাশ করা নিরাপদ নয় — সেটে রাখার পর বদলে গেলে তাকে আর খুঁজে পাওয়া যেত না। frozen=True বলে «এটা বদলাবে না», তাই পাইথন হ্যাশটা দিতে রাজি হয়।
নিয়মটা সহজ: যে বস্তু তৈরি হওয়ার পর বদলানোর কথা নয়, তাকে frozen করুন। তাতে একটা প্রতিশ্রুতি পাওয়া যায়, আর সাথে সেট ও ডিকশনারির কী হিসেবে ব্যবহারের সুযোগ।
ফাংশনে হিন্ট
def totals(rows: list[tuple[str, float]]) -> dict[str, float]:
result: dict[str, float] = {}
for name, amount in rows:
result[name] = result.get(name, 0.0) + amount
return resultlist[str], dict[str, float], tuple[str, float] — ভিতরে কী আছে সেটাও বলা যায়, আর সেটাই হিন্টকে কাজের করে তোলে। list লিখলে কেবল «একটা লিস্ট» জানা যায়; list[tuple[str, float]] লিখলে জানা যায় লুপে কী আসবে।
ভুল কিছু দিলে:
main.py:9: error: Argument 1 to "totals" has incompatible type "str"; expected "list[tuple[str, float]]" [arg-type]«থাকতেও পারে, না-ও পারে» বোঝাতে |:
def find(names: list[str], wanted: str) -> str | None:
for name in names:
if name == wanted:
return name
return None
print(find(["pen"], "pen"))
print(find(["pen"], "bag"))pen
Noneআর এখানেই হিন্টের সবচেয়ে বড় দামটা পাওয়া যায়। ফলটা যাচাই না করে ব্যবহার করলে:
main.py:8: error: Item "None" of "str | None" has no attribute "upper" [union-attr]এটা AttributeError: 'NoneType' object has no attribute 'upper' — সেই বার্তাটা, যা বাস্তবে সবচেয়ে বেশি দেখা যায় — কিন্তু প্রোগ্রামটা চালানোরও আগে, আর সেই নির্দিষ্ট ইনপুটটা না পেয়েই।
dataclass থেকে JSON
import json
from dataclasses import dataclass, asdict
@dataclass
class Book:
title: str
pages: int
print(asdict(Book("one", 300)))
print(json.dumps(asdict(Book("one", 300))))
print(json.dumps(Book("one", 300))){'title': 'one', 'pages': 300}
{"title": "one", "pages": 300}
TypeError: Object of type Book is not JSON serializableপঞ্চবিংশ অধ্যায়ের সেই তালিকাটা মনে আছে — নিজের বানানো বস্তু JSON-এ যায় না। asdict সেই সেতুটা, আর সে ভিতরের dataclass-গুলোকেও নামিয়ে আনে।
একটা সম্পূর্ণ উদাহরণ
গত অধ্যায়ের অর্ডারটাই, এবার dataclass আর হিন্ট দিয়ে।
"""The order of chapter twenty-six, rebuilt with dataclasses and hints."""
import json
from dataclasses import dataclass, field, asdict
TAX_RATE = 0.15
@dataclass(frozen=True)
class OrderLine:
"""Frozen: a line never changes once made, so it can live in a set."""
name: str
price: float
quantity: int
def __post_init__(self) -> None:
if self.quantity < 1:
raise ValueError(f"quantity must be at least 1: {self.quantity}")
def total(self) -> float:
return round(self.price * self.quantity * (1 + TAX_RATE), 2)
@dataclass
class Order:
"""Not frozen: lines are added over time."""
customer: str
lines: list[OrderLine] = field(default_factory=list)
def add(self, name: str, price: float, quantity: int) -> "Order":
self.lines.append(OrderLine(name, price, quantity))
return self
def total(self) -> float:
return round(sum(line.total() for line in self.lines), 2)
def largest(self) -> OrderLine | None:
if not self.lines:
return None
return max(self.lines, key=lambda line: line.total())
def main() -> None:
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.largest())
print(json.dumps(asdict(order)))
print(len({OrderLine("pen", 15.0, 3), OrderLine("pen", 15.0, 3)}))
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
OrderLine(name='bag', price=850.0, quantity=1)
{"customer": "rafi", "lines": [{"name": "pen", "price": 15.0, "quantity": 3}, {"name": "bag", "price": 850.0, "quantity": 1}, {"name": "ink", "price": 120.0, "quantity": 2}]}
1
rejected: quantity must be at least 1: 0পাঁচটা জিনিস দেখার মতো।
__post_init__ যাচাইয়ের জায়গা। @dataclass নিজেই __init__ লিখে দেয়, তাই যাচাই বসানোর মতো জায়গা নেই — __post_init__ সেই ফাঁকটাই পূরণ করে, আর সে চলে ক্ষেত্রগুলো বসানোর ঠিক পরে। গত অধ্যায়ের প্রতিশ্রুতিটা তাই অক্ষত: একটা OrderLine থাকা মানেই তার পরিমাণ অন্তত এক।
একটা frozen, অন্যটা নয় — আর সেটা ইচ্ছাকৃত। একটা লাইন তৈরি হওয়ার পর বদলানোর কথা নয়, কিন্তু একটা অর্ডারে লাইন যোগ হতেই থাকে। প্রতিটা ক্ষেত্রে প্রশ্নটা একই: এই জিনিসটা কি তৈরি হওয়ার পর বদলায়?
1 সংখ্যাটাই frozen-এর প্রমাণ। দুটো অভিন্ন OrderLine সেটে গিয়ে একটাই হয়েছে — গত অধ্যায়ে এই লাইনটা TypeError দিত।
list[OrderLine] কেবল সাজসজ্জা নয়। যাচাইকারী এখান থেকেই জানে line.total() বৈধ, আর line.totl() লিখলে সে ধরে ফেলবে — কোডটা না চালিয়েই।
"Order" উদ্ধৃতির ভিতরে। add-এর ভিতরে যখন Order-কে উল্লেখ করা হচ্ছে, ক্লাসটার সংজ্ঞা তখনো শেষ হয়নি — নামটা তখনো নেই। উদ্ধৃতি দিলে পাইথন সেটাকে পরে মেলায়। নিজের ক্লাসকে নিজের ভিতরে উল্লেখ করতে গেলে এটা লাগবেই।
কিছু ভাঙা অবস্থা ও তার সমাধান
ValueError: mutable default ... use default_factory = [] বা = {} লেখা হয়েছে। field(default_factory=list) নিন।
TypeError: non-default argument follows default argument ডিফল্টওয়ালা ক্ষেত্র আবশ্যকটার আগে বসেছে। বিংশ অধ্যায়ের সেই নিয়মটাই: আবশ্যক আগে।
dataclasses.FrozenInstanceError frozen=True বস্তুতে মান বসানোর চেষ্টা। বদল দরকার হলে dataclasses.replace(obj, x=5) একটা নতুন বস্তু দেয়।
TypeError: unhashable type frozen=True ছাড়া dataclass সেটে যায় না।
ভুল ধরনের মান ঢুকে গেছে আর কিছুই ঘটেনি সেটাই স্বাভাবিক — হিন্ট চালানোর সময় কিছু করে না। mypy চালান।
mypy আমার ফাইলটা দেখছেই না পথটা দিন — mypy main.py। আর প্রথমবার একগাদা বার্তা এলে mypy --ignore-missing-imports দিয়ে শুরু করুন।
TypeError: Object of type X is not JSON serializable asdict(obj) দিয়ে পাঠান।
NameError — নিজের ক্লাসের নাম নিজের ভিতরে নামটা উদ্ধৃতির ভিতরে লিখুন: -> "Order"।
ধাপ ৪ / ৬ — অনুমান
যাচাই করুন
সাত লাইনের একটা dataclass। দুটো লাইন কী ছাপবে?
from dataclasses import dataclass
@dataclass
class Book:
title: str
pages: int
print(Book("one", 300))
print(Book("one", 300) == Book("one", 300))- ABook(title='one', pages=300) True
- BBook(title='one', pages=300) False
- C<__main__.Book object at 0x...> False
- DBook('one', 300) True
কোনো বস্তু বানানো হয়নি, কেবল ক্লাসটা লেখা হয়েছে। কী হবে?
from dataclasses import dataclass
@dataclass
class Shelf:
books: list = []- A`ValueError` — ক্লাসটা সংজ্ঞায়িত হওয়ার সময়েই
- Bকিছুই হয় না; সমস্যাটা প্রথম বস্তু বানালে দেখা দেবে
- Cকিছুই হয় না; সব ইনস্ট্যান্স একই লিস্ট ভাগ করবে
- Dএকটি `TypeError`
frozen=True দেওয়া আছে। কী হবে?
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
p = Point(1, 2)
print({p, Point(1, 2)})
p.x = 5- Aএকটা জিনিসের সেট ছাপা হয়, তারপর `FrozenInstanceError`
- Bদুটো জিনিসের সেট ছাপা হয়, তারপর `FrozenInstanceError`
- C`TypeError: unhashable type` — সেট বানানোর সময়েই
- Dএকটা জিনিসের সেট ছাপা হয়, তারপর `p.x` হয় `5`
উত্তর দিতে অ্যাকাউন্ট লাগবে
উত্তর মিলিয়ে দেখতে সাইন ইন করুন
প্রশ্নগুলো উপরে আছে, আর মাথায় মাথায় উত্তর ভেবে নেওয়াই আসল কাজ। সঠিক উত্তর, ব্যাখ্যা আর তিন ধাপের ইঙ্গিত দেখতে সাইন ইন করুন।
নিজে করুন
ষড়বিংশ অধ্যায়ের Book আর Shelf আবার লিখুন, এবার dataclass দিয়ে।
Book হবে frozen=True, ক্ষেত্র title: str, author: str, pages: int, আর __post_init__-এ pages < 1 হলে ValueError। Shelf-এ থাকবে name: str আর books: list[Book] = field(default_factory=list), সাথে add, total_pages() -> int, longest() -> Book | None আর by_author(name: str) -> list[Book]।
তারপর pip install mypy করে mypy library.py চালান, আর কোনো বার্তা না আসা পর্যন্ত ঠিক করুন।
তারপর ছয়টা পরীক্ষা:
pages-এ একটা লেখা দিন —Book("one", "rafi", "300")। প্রোগ্রামটা চলে? তারপরmypyকী বলে?books-এরfield(default_factory=list)সরিয়ে= []লিখুন। ত্রুটিটা কখন আসে — বস্তু বানানোর সময়, নাকি তার আগে?- দুটো অভিন্ন
Bookবানিয়ে একটা সেটে রাখুন। কয়টা থাকে? তারপরfrozen=Trueসরিয়ে আবার চেষ্টা করুন। longest()-এর ফলটা সরাসরি ব্যবহার করুন —shelf.longest().title— খালি তাক নিয়ে। প্রোগ্রামটা কী বলে, আরmypyআগেই কী বলেছিল?asdict(shelf)করেjson.dumpsদিয়ে লিখুন, তারপরjson.loadsদিয়ে ফেরত আনুন। ফেরত আসা জিনিসটা কিShelf?- একটা মেথডের নাম ভুল বানানে ডাকুন —
book.pagse।mypyকি সেটা ধরে?
শেষ দুটো পরীক্ষা এই অধ্যায়ের সীমা আর শক্তি — দুটোই দেখায়। JSON থেকে ফেরত আসে ডিকশনারি, dataclass নয় — অনুবাদটা একমুখী, আর ফেরার পথটা আপনাকেই লিখতে হয়। কিন্তু ষড়বিংশ অধ্যায়ের যে ফাঁকটা তখন বন্ধ করা যায়নি — বানান ভুল কেবল চালানোর সময় ধরা পড়ত — যাচাইকারী সেটা চালানোর আগেই ধরে ফেলে।
ধাপ ৬ / ৬
কঠিন করা — অধ্যায়ের কুইজ
সহজ থেকে কঠিন — দশটি প্রশ্ন, শেষেরগুলো ইচ্ছে করেই কঠিন।
সাইন ইন করে কুইজ দিন