2025, Nov 01 05:02
Python dataclass में optional इनपुट के साथ अनिवार्य फ़ील्ड बनाने का सही तरीका
Python dataclass में optional इनपुट रखते हुए फ़ील्ड को इंस्टेंस पर अनिवार्य बनाएं. __init__ बनाम __post_init__, टाइपिंग व mypy चेतावनी से बचने का सरल तरीका.
किसी फ़ील्ड को सिर्फ़ निर्माण के समय वैकल्पिक रखना और इंस्टेंस पर उसे अनिवार्य बनाए रखना, साधारण वैल्यू ऑब्जेक्ट्स बनाते समय अक्सर आवश्यक होता है। सामान्य Python क्लासों में यह आसान है, लेकिन @dataclass के साथ सावधानी न बरतें तो टाइप से जुड़ी चेतावनियाँ आ सकती हैं। नीचे समस्या का संक्षिप्त विवरण और उसे सुलझाने का साफ़ तरीका दिया है।
बेसलाइन: @dataclass के बिना
एक सीधी-सी क्लास, अगर दूसरी निर्देशांक छूट जाए, तो उसे पहली के बराबर सेट कर देती है:
class Coord:
def __init__(self, a: int, b: int | None = None):
self.a = a
self.b = b if b is not None else a
इससे हर बनाए गए इंस्टेंस में b रनटाइम पर int ही रहता है, क्योंकि initialization के दौरान None को बदल दिया जाता है।
कोशिश @dataclass के साथ—जहाँ चेतावनियाँ मिलती हैं
इसे @dataclass के साथ लिखना पहले नज़र में आसान लगता है:
from dataclasses import dataclass
@dataclass
class Coord:
a: int
b: int | None
def __post_init__(self):
if self.b is None:
self.b = self.a
कोड रनटाइम पर ठीक चलता है, मगर अब b को वैकल्पिक के रूप में annotate किया गया है। स्टैटिक एनालिसिस, Coord(1, 2).b + 0 जैसे निर्दोष कोड को भी इस तरह चिह्नित कर सकती है मानो वह None पर काम कर रहा हो, और यूँ गलत-धनात्मक चेतावनी दे देती है। इसका एक रास्ता यह है कि सार्वजनिक फ़ील्ड और केवल कंस्ट्रक्टर में आने वाले इनपुट को अलग-अलग एट्रिब्यूट्स में बाँट दें, लेकिन यह अनावश्यक रूप से विस्तारवादी लगता है।
ऐसा क्यों होता है
@dataclass में कोई अंतर्निर्मित सुविधा नहीं है जो कह सके: “यह फ़ील्ड सिर्फ़ __init__ के लिए वैकल्पिक है, लेकिन इंस्टेंस पर अनिवार्य।” __post_init__ का उपयोग करके आप जेनरेटेड कंस्ट्रक्टर के चलने के बाद मानों को समायोजित कर सकते हैं, लेकिन इससे फ़ील्ड का घोषित टाइप नहीं बदलता; इसलिए टूलिंग इसे वैकल्पिक ही मानती रहती है।
व्यावहारिक समाधान
सबसे सरल और विश्वसनीय उपाय है @dataclass का उपयोग बनाए रखते हुए अपना खुद का __init__ लिखना। यदि आप __init__ परिभाषित करते हैं, तो डेकोरेटर उसे जेनरेट नहीं करेगा, और फिर भी dataclasses की बाकी सुविधाएँ मिलती रहेंगी।
from dataclasses import dataclass
@dataclass
class Coord:
a: int
b: int
def __init__(self, a: int, b: int | None = None):
self.a = a
self.b = self.a if b is None else b
यह वांछित निर्माण-व्यवहार बनाए रखता है, इंस्टेंस पर optional टाइप से बचाता है, और @dataclass के लाभ—जैसे स्वतः निर्मित __eq__, __repr__, वैकल्पिक रिच तुलना और हैशिंग, तथा वैकल्पिक स्वतः स्लॉट जेनरेशन—को सुरक्षित रखता है। कोई विशेष फ़्लैग देने की ज़रूरत नहीं; __init__ उपलब्ध कराना काफी है।
यह जानना क्यों उपयोगी है
अक्सर मान लिया जाता है कि __post_init__ कंस्ट्रक्टर से जुड़ी हर ज़रूरत संभाल सकता है, पर वह मुख्यतः छोटे-छोटे समायोजनों के लिए है जब जेनरेटेड __init__ अधिकांशतः उपयुक्त हो। जब कंस्ट्रक्टर के सिग्नेचर या अर्थ-вिधान को अलग होना पड़े, तो सीधे __init__ लिखना ही न्यूनतम और स्पष्ट तरीका है, और dataclass की बाकी मशीनरी जस की तस रहती है।
मुझे किसी तरह अपने आप init फ़ंक्शन लिखने का ख्याल ही नहीं आया था। यह बिल्कुल कारगर समाधान है, क्योंकि यह फिर भी कच्ची क्लास के मुकाबले वे फायदे देता है जिनका आपने ज़िक्र किया—जैसे तुलना और स्ट्रिंग प्रतिनिधित्व।
निष्कर्ष
यदि कोई फ़ील्ड इंस्टेंस पर अनिवार्य की तरह व्यवहार करे पर निर्माण के समय वैकल्पिक हो, तो उसे __post_init__ के जरिए ज़बरदस्ती न करें। @dataclass के भीतर अपना __init__ परिभाषित करें और बाकी काम डेकोरेटर पर छोड़ दें। इससे टाइपिंग सटीक रहती है, optional एनोटेशनों से झूठी चेतावनियाँ नहीं मिलतीं, और dataclasses के सारे उपयोगी लाभ भी बने रहते हैं।
यह लेख StackOverflow पर पूछे गए प्रश्न (लेखक: Dominik Kaszewski) और ShadowRanger के उत्तर पर आधारित है।