डेटाक्लासेस और टाइप हिंट्स
@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आपको क्या सुविधा देता है - फ़ंक्शंस पर टाइप हिंट्स (type hints) लिखना —
list[str],dict[str, float],int | None - यह समझना कि हिंट्स रनटाइम पर कुछ नहीं करते, लेकिन चेकर के तहत सब कुछ करते हैं
asdictकी मदद से डेटाक्लास को JSON में बदलना
ज़रूरी शर्तें: क्लासेस (Classes)।
@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)Runtime पर Hints कुछ नहीं करते
यह शुरुआत में ही स्पष्ट होना चाहिए, क्योंकि इसके नाम से कुछ और लग सकता है।
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 लिखा गया था और सीधे एक टेक्स्ट स्ट्रिंग चली गई। पायथन हिंट्स की जाँच नहीं करता — यह सिर्फ़ उन्हें रिकॉर्ड करता है, और उनके उपयोग का काम किसी और पर छोड़ देता है।
एरर बहुत बाद में आया, जब किसी चीज़ ने उस वैल्यू के साथ गणना करने की कोशिश की। इसके बीच में वह प्रिंट हुआ, लॉग हुआ, और संभवतः किसी फ़ाइल में लिखा भी गया।
यहीं पर टाइप चेकर (type checker) की अहमियत समझ आती है। mypy एक अलग प्रोग्राम है जो कोड को बिना चलाए पढ़ता है:
main.py:10: error: Argument 2 to "Book" has incompatible type "str"; expected "int" [arg-type]लाइन नंबर, कौन सा तर्क (argument), क्या दिया गया और क्या अपेक्षित था — और इनमें से किसी के लिए भी प्रोग्राम को चलाने की आवश्यकता नहीं पड़ी।
पूरी बात दो वाक्यों में: हिंट्स रनटाइम पर कुछ नहीं करते और चेकर के तहत सब कुछ करते हैं। हिंट लिखना चेकर और कोड पढ़ने वाले के लिए एक वादा है, इंटरप्रेटर के लिए नहीं।
रिकॉर्ड किए गए हिंट्स को देखा भी जा सकता है:
def shout(text: str) -> str:
return text.upper()
print(shout("pen"))
print(shout.__annotations__)PEN
{'text': <class 'str'>, 'return': <class 'str'>}परिवर्तनीय डिफ़ॉल्ट (Mutable Default), तीसरी बार
इक्कीसवें अध्याय में यह फ़ंक्शन का डिफ़ॉल्ट तर्क था। छब्बीसवें अध्याय में यह क्लास एट्रिब्यूट था। अब डेटाक्लास फ़ील्ड में — और इस बार पायथन खुद इसे रोकता है:
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 रोक दिया गया — जैसे टुपल (tuple) के साथ होता है।
और अब यह सेट (set) में जा सकता है। पिछले अध्याय की अंतिम समस्या याद करें: __eq__ लिखने से हैश छिन जाता है। यहाँ दो "समान" पॉइंट्स सेट में मिलकर एक बन गए, बिना किसी एरर के।
frozen के बिना:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
print({Point(1, 2)})TypeError: unhashable type: 'Point'कारण सोलहवें अध्याय से आता है: जो बदल सकता है उसे हैश करना सुरक्षित नहीं है — सेट में रखने के बाद उसे बदल दें तो वह फिर कभी नहीं मिल पाएगा। frozen=True कहता है "यह नहीं बदलेगा", इसलिए पायथन हैश प्रदान करने के लिए तैयार हो जाता है।
नियम सीधा है: जो चीज़ बनने के बाद बदलने वाली नहीं है, उसे फ़्रीज़ (freeze) कर दें। इससे एक वादा बनता है, और उसके साथ सेट का सदस्य या डिक्शनरी की की (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: 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' है — जो व्यवहार में किसी भी अन्य एरर से ज़्यादा देखा जाता है — बस अंतर यह है कि यह प्रोग्राम चलने से पहले ही पकड़ में आ गया, बिना उस विशेष इनपुट का सामना किए जो इसका कारण बनता।
डेटाक्लास से 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 यहाँ एक पुल का काम करता है, और यह नेस्टेड डेटाक्लासेस को भी बदल देता है।
पूर्ण उदाहरण
पिछले अध्याय का ऑर्डर, डेटाक्लासेस और हिंट्स के साथ दोबारा बनाया गया।
"""छब्बीसवें अध्याय का ऑर्डर, डेटाक्लासेस और टाइप हिंट्स के साथ।"""
import json
from dataclasses import dataclass, field, asdict
TAX_RATE = 0.15
@dataclass(frozen=True)
class OrderLine:
"""फ़्रोज़न: बनने के बाद लाइन कभी नहीं बदलती, इसलिए यह सेट में रह सकती है।"""
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:
"""फ़्रोज़न नहीं है: समय के साथ इसमें और लाइन्स जोड़ी जाती हैं।"""
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 होने का मतलब है कि उसकी मात्रा कम से कम एक है।
एक फ़्रोज़न है और दूसरा नहीं, जानबूझकर। एक बार बनने के बाद लाइन में बदलाव नहीं होना चाहिए, जबकि एक ऑर्डर में नई लाइनें जुड़ती रहती हैं। सवाल हर बार वही है: क्या यह चीज़ बनने के बाद बदलती है?
यह 1 इस बात का प्रमाण है कि फ़्रीज़िंग ने काम किया। दो समान 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 वाला डेटाक्लास सेट में नहीं जा सकता।
ग़लत टाइप दिया गया और कुछ नहीं हुआ जैसा कि अपेक्षित था — हिंट्स रनटाइम पर कुछ नहीं करते। mypy चलाएं।
mypy मेरी फ़ाइल को नहीं देख रहा है उसे पाथ दें — mypy main.py। और अगर पहला रन बहुत सारे एरर दिखाए, तो mypy --ignore-missing-imports से शुरू करें।
TypeError: Object of type X is not JSON serializable asdict(obj) भेजें।
क्लास के अंदर उसी के नाम के लिए NameError नाम को कोट्स में रखें: -> "Order"।
चरण 4 / 6 — अनुमान
अपनी समझ की जाँच करें
A seven-line dataclass. What do the two lines print?
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
No object is made, only the class is written. What happens?
from dataclasses import dataclass
@dataclass
class Shelf:
books: list = []- AA `ValueError` — while the class is being defined
- BNothing; the problem appears when the first object is made
- CNothing; every instance will share one list
- DA `TypeError`
frozen=True is set. What happens?
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- AA set of one prints, then a `FrozenInstanceError`
- BA set of two prints, then a `FrozenInstanceError`
- C`TypeError: unhashable type` — at the set
- DA set of one prints, then `p.x` becomes `5`
उत्तर देने के लिए अकाउंट आवश्यक है
अपने उत्तर जाँचने के लिए साइन इन करें
प्रश्न ऊपर दिए गए हैं, और मन में उत्तर सोचना ही मुख्य कार्य है। सही उत्तर, व्याख्या और तीन-स्तरीय संकेत देखने के लिए साइन इन करें।
आपकी बारी
छब्बीसवें अध्याय की Book और Shelf को डेटाक्लासेस के रूप में दोबारा लिखें।
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 से जो वापस आता है वह एक डिक्शनरी है, डेटाक्लास नहीं — अनुवाद एकतरफ़ा होता है, और वापसी का सफ़र आपको खुद कोड करना होता है। लेकिन जिस कमी को छब्बीसवाँ अध्याय पूरा नहीं कर सका था — केवल रनटाइम पर पकड़ी जाने वाली स्पेलिंग की ग़लती — चेकर उसे कोड चलने से पहले ही पकड़ लेता है।
Step 6 of 6
चुनौती — the chapter quiz
सरल से कठिन — दस प्रश्न, अंतिम वाले जानबूझकर चुनौतीपूर्ण बनाए गए हैं।
Sign in to take the quiz