डेटाक्लासेस और टाइप हिंट्स
@dataclass आपके लिए क्या लिखता है, field(default_factory=...) क्यों ज़रूरी है, frozen=True क्या देता है, फ़ंक्शन पर टाइप हिंट्स, और यह कि प्रोग्राम चलते समय हिंट्स कुछ नहीं करते लेकिन टाइप चेकर के लिए सब कुछ हैं।
आप कैसे सीखना चाहते हैं?
वह समस्या जिसे हम हल कर रहे हैं
पिछले अध्याय की 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__ में और __eq__ में।
चौथा फ़ील्ड जोड़ना हो तो इन सब जगहों पर जोड़ना पड़ेगा। कहीं एक जगह भूल गए तो कोई एरर नहीं आएगा — बस __repr__ या __eq__ चुपचाप अधूरा रह जाएगा।
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 का False आना, और एक ही नाम बार-बार लिखना — एक साथ ख़त्म।
मेथड पहले की तरह ही लिखे जाते हैं:
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डिफ़ॉल्ट वैल्यू भी दी जा सकती है। नियम अध्याय 20 वाला ही है: डिफ़ॉल्ट वाले फ़ील्ड बाद में।
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 नाम से सेव करके mypy main.py चलाएँ:
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'>}म्यूटेबल डिफ़ॉल्ट — तीसरी बार
अध्याय 21 में यह फ़ंक्शन के डिफ़ॉल्ट आर्ग्युमेंट में था। अध्याय 26 में क्लास-लेवल एट्रिब्यूट में। अब 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() कॉल करो”। यह अध्याय 21 के 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'वजह अध्याय 16 से ही आती है: जो बदल सकता है, उसे हैश करना सुरक्षित नहीं। सेट में रखने के बाद वह बदल जाए तो उसे फिर कभी ढूँढा नहीं जा सकेगा।
frozen=True बता देता है कि “यह नहीं बदलेगा”, इसलिए पायथन हैश देने को तैयार हो जाता है।
नियम आसान है: जो ऑब्जेक्ट बनने के बाद बदलना नहीं चाहिए, उसे frozen कीजिए। इससे एक गारंटी मिलती है, और साथ में उसे सेट में या डिक्शनरी की key के रूप में इस्तेमाल करने की सुविधा।
फ़ंक्शन पर हिंट्स
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वीं लाइन में totals("pen") लिखा है — तो mypy कहता है:
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हिंट्स का सबसे बड़ा फ़ायदा यहीं है। नतीजा None है या नहीं, यह देखे बिना इस्तेमाल करें — मान लीजिए 8वीं लाइन print(find(["pen"], "pen").upper()) बन जाए — तो mypy कहता है:
main.py:8: error: Item "None" of "str | None" has no attribute "upper" [union-attr]यह असल में AttributeError: 'NoneType' object has no attribute 'upper' है — असल काम में सबसे ज़्यादा दिखने वाले एरर्स में से एक। लेकिन यहाँ यह प्रोग्राम चलाने से पहले ही पकड़ा जा रहा है, और वह इनपुट हाथ में आए बिना जिस पर None आता है।
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अध्याय 25 की वह लिस्ट याद है — अपने बनाए ऑब्जेक्ट सीधे 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 सेट में जाकर एक हो गईं।
पिछले अध्याय की OrderLine से यही लाइन चलाते तो 2 आता। कोई एरर नहीं होता: उस क्लास में __eq__ नहीं था, इसलिए डिफ़ॉल्ट हैश था, लेकिन दो अलग ऑब्जेक्ट कभी बराबर नहीं होते थे — सेट दोनों को रख लेता।
list[OrderLine] सिर्फ़ सजावट नहीं है। टाइप चेकर इसी से जानता है कि line.total() सही है। line.totl() लिखें तो वह पकड़ लेगा — कोड चलाए बिना।
"Order" कोट्स के अंदर है। जब add मेथड के सिग्नेचर में Order नाम लिखा जा रहा है, तब तक क्लास की डेफ़िनिशन पूरी नहीं हुई है — नाम अभी बना ही नहीं। कोट्स में लिखने पर पायथन उसे उसी समय नहीं ढूँढता।
इस कोर्स के वर्ज़न, पायथन 3.12 में, क्लास के अंदर अपनी ही क्लास का नाम हिंट में लिखने के दो तरीक़े हैं: नाम को कोट्स में लिखिए, या फ़ाइल की सबसे पहली लाइनों में from __future__ import annotations लिखिए।
जब कुछ टूटता है
ValueError: mutable default ... use default_factory = [] या = {} लिखा गया है। field(default_factory=list) इस्तेमाल कीजिए।
TypeError: non-default argument follows default argument डिफ़ॉल्ट वाला फ़ील्ड ज़रूरी फ़ील्ड से पहले रखा गया है। अध्याय 20 का वही नियम: ज़रूरी फ़ील्ड पहले।
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"। या फ़ाइल की शुरुआत में from __future__ import annotations लिखिए।
चरण 4 / 6 — अनुमान
अपनी समझ की जाँच करें
सात लाइनों की एक dataclass। दोनों लाइनें क्या प्रिंट करेंगी?
from dataclasses import dataclass
@dataclass
class Book:
title: str
pages: int
print(Book("one", 300))
print(Book("one", 300) == Book("one", 300))- A<__main__.Book object at 0x...> False
- BBook(title='one', pages=300) True
- CBook(title='one', pages=300) False
- DBook('one', 300) True
कोई ऑब्जेक्ट नहीं बनाया गया, सिर्फ़ क्लास लिखी गई है। क्या होगा?
from dataclasses import dataclass
@dataclass
class Shelf:
books: list = []- Aकुछ नहीं होता; समस्या पहला ऑब्जेक्ट बनाने पर दिखेगी
- Bएक `ValueError` — क्लास डिफ़ाइन होते समय ही
- Cएक `TypeError`
- Dकुछ नहीं होता; सारे इंस्टेंस एक ही लिस्ट साझा करेंगे
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`
उत्तर देने के लिए अकाउंट आवश्यक है
अपने उत्तर जाँचने के लिए साइन इन करें
प्रश्न ऊपर दिए गए हैं, और मन में उत्तर सोचना ही मुख्य कार्य है। सही उत्तर, व्याख्या और तीन-स्तरीय संकेत देखने के लिए साइन इन करें।
आपकी बारी
अध्याय 26 की 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 नहीं — यह अनुवाद एकतरफ़ा है, और वापसी का रास्ता आपको ख़ुद लिखना होता है। लेकिन अध्याय 26 की जो कमी तब दूर नहीं हो पाई थी — ग़लत नाम सिर्फ़ चलाते समय पकड़ा जाता था — टाइप चेकर उसे चलाने से पहले ही पकड़ लेता है।
चरण 6 / 6
चुनौती — अध्याय की क्विज़
सरल से कठिन — दस प्रश्न, अंतिम वाले जानबूझकर चुनौतीपूर्ण बनाए गए हैं।
क्विज़ देने के लिए साइन इन करें