Kodokon kodokon.com

टाइप हिंट्स और dataclasses: आधुनिक Python को गंभीरता से टाइप करना

समझें कि एनोटेशन रनटाइम पर असल में क्या करते हैं, और frozen, slots तथा default_factory के साथ dataclasses का पूरा लाभ उठाएँ।

10 मिनट · 3 प्रश्न

इस पाठ को Kodokon में खोलें

टाइप एनोटेशन मेटाडेटा हैं, बाधाएँ नहीं: इंटरप्रेटर उन्हें __annotations__ में संग्रहीत करता है और रनटाइम पर कभी नहीं जाँचता। जाँचना mypy या pyright जैसे स्टैटिक एनालाइज़र का काम है, निष्पादन से पहले। इसका सीधा परिणाम: गलत टाइप वाला प्रोग्राम बिना किसी शिकायत के चलता है - और यह जानबूझकर ऐसा ही है। ग्रैजुअल टाइपिंग आपको किसी मौजूदा कोडबेस को थोड़ा-थोड़ा करके, बिना कुछ तोड़े, एनोटेट करने देती है।

PYTHON
from typing import get_type_hints

def add(a: int, b: int) -> int:
    return a + b

print(add("py", "thon"))
print(add.__annotations__)
print(get_type_hints(add))
एनोटेशन कुछ नहीं रोकते: "गलत" कॉल सफल हो जाती है।

Python 3.9 से, बिल्ट-इन टाइप सीधे जेनेरिक हैं: typing.List इम्पोर्ट किए बिना list[int] या dict[str, float] लिखें। 3.10 से यूनियन int | None लिखे जाते हैं। इनपुट टाइप को आउटपुट टाइप से जोड़ने के लिए TypeVar का उपयोग करें - Python 3.12 तो संक्षिप्त सिंटैक्स def first[T](items: list[T]) -> T भी देता है। एक बिंदु जिसे अक्सर अनदेखा किया जाता है: from __future__ import annotations हर एनोटेशन को आलसी बना देता है (स्ट्रिंग के रूप में संग्रहीत), जिससे आगे परिभाषित क्लासों का संदर्भ देना संभव होता है और इम्पोर्ट के समय की लागत घटती है।

PYTHON
from collections.abc import Iterable, Iterator
from typing import TypeVar

T = TypeVar("T")

def dedupe(items: Iterable[T]) -> Iterator[T]:
    seen: set[T] = set()
    for item in items:
        if item not in seen:
            seen.add(item)
            yield item

print(list(dedupe([3, 1, 3, 2, 1])))
TypeVar गारंटी देता है कि dedupe(list[int]) वाकई int देता है।
PYTHON
from dataclasses import dataclass, field

@dataclass(frozen=True, slots=True)
class Point:
    x: float
    y: float

@dataclass
class Basket:
    items: list[str] = field(default_factory=list)
    total: int = 0

    def __post_init__(self) -> None:
        self.total = len(self.items)

point = Point(1.0, 2.0)
print(point, hash(point))
print(Basket(["book", "pen"]))
Point अपरिवर्तनीय है, हैशेबल है और इसका प्रति-इंस्टेंस __dict__ नहीं होता।

तीन पैरामीटर @dataclass को प्रोडक्शन-स्तर का औज़ार बना देते हैं। frozen=True इंस्टेंस को अपरिवर्तनीय बनाता है और, जनरेट की गई समानता के साथ मिलकर, हैशेबल भी - डिक्शनरी की कुंजी के रूप में प्रयोग योग्य। slots=True (Python 3.10+) प्रति-इंस्टेंस __dict__ हटा देता है: कम मेमोरी, तेज़ एट्रिब्यूट एक्सेस, और कोई भी अप्रत्याशित एट्रिब्यूट असाइनमेंट विफल हो जाता है। अंत में kw_only=True स्पष्ट कीवर्ड कॉल को अनिवार्य करता है। __post_init__ वह जगह है जहाँ इनवेरिएंट और व्युत्पन्न फ़ील्ड रखे जाते हैं। किसी साधारण अपरिवर्तनीय समुच्चय के लिए NamedTuple अब भी समझदारी है; जैसे ही व्यवहार या सत्यापन आ जाए, dataclass को प्राथमिकता दें।

ज्ञान जांच

सुनिश्चित करें कि आपको इस पाठ के मुख्य बिंदु याद हैं।

  1. जब कोई फ़ंक्शन (a: int, b: int) -> int एनोटेट हो और आप add('py', 'thon') कॉल करें, तो रनटाइम पर क्या होता है?
    • Python तुरंत एक TypeError उठाता है
    • कॉल सामान्य रूप से चलती है: एनोटेशन रनटाइम पर कभी नहीं जाँचे जाते
    • Python एक RuntimeWarning देता है और फिर कॉल चलाता है
    • मॉड्यूल इम्पोर्ट होने से इनकार कर देता है
  2. किसी dataclass फ़ील्ड के डिफ़ॉल्ट के रूप में items: list[str] = [] क्यों वर्जित है?
    • क्योंकि list[str] किसी फ़ील्ड के लिए मान्य एनोटेशन नहीं है
    • क्योंकि dataclasses केवल बिना डिफ़ॉल्ट वैल्यू वाले फ़ील्ड सहन करते हैं
    • क्योंकि यह म्यूटेबल डिफ़ॉल्ट सभी इंस्टेंस के बीच साझा हो जाता: dataclass ValueError उठाता है और field(default_factory=list) की माँग करता है
    • क्योंकि डिफ़ॉल्ट __post_init__ में सेट किए जाने चाहिए
  3. frozen=True किसी dataclass के साथ क्या करता है?
    • इंस्टेंस अपरिवर्तनीय हो जाते हैं और, जनरेट की गई समानता के साथ, हैशेबल भी - डिक्शनरी की कुंजी के रूप में प्रयोग योग्य
    • क्लास को अब सबक्लास नहीं किया जा सकता
    • फ़ील्ड साझा क्लास एट्रिब्यूट बन जाते हैं
    • हर असाइनमेंट पर एनोटेशन जाँचे जाते हैं