2025, Sep 27 07:33

NumPy के dtype=object एरे में कस्टम ऑब्जेक्ट ऑपरेटर कैसे काम करते हैं

NumPy dtype=object एरे में कस्टम __add__, __mul__, __radd__, __rmul__ कैसे काम करते हैं; Python ints के साथ मिक्सिंग और चेन ऑपरेशंस में सेमान्टिक्स कैसे रखें।

Python में कस्टम संख्यात्मक व्यवहार बनाने की शुरुआत अक्सर __add__ और __mul__ जैसे dunder मेथड्स से होती है। पेच तब आता है जब उन ऑब्जेक्ट्स को NumPy के ndarray के भीतर रखा जाता है और उन्हें एरे और स्केलर के साथ गणनाओं में इस्तेमाल किया जाता है। क्या ndarrays तब भी आपकी क्लास के ऑपरेटरों का पालन करते हैं? क्या आप कस्टम ऑब्जेक्ट्स को Python ints के साथ मिला सकते हैं? संक्षिप्त जवाब, जो नीचे दिखाया गया है, यह है कि dtype=object होने पर NumPy हर तत्व के लिए आपकी क्लास के मेथड्स को सौंप देता है, और कुछ छोटे जोड़ के साथ आप मिक्स्ड और चेन की गई ऑपरेशंस को एकसमान व्यवहार दे सकते हैं।

समस्या की रूपरेखा

उद्देश्य एक ऐसी कस्टम क्लास का उपयोग करना है जो + और * को ओवरराइड करती हो, और ये ऑपरेटर स्केलर-स्केलर ऑपरेशन में भी सही काम करें और तब भी जब इंस्टेंस एक सामान्य ndarray में रखे गए हों। अपेक्षित व्यवहार में शामिल हैं: कस्टम ऑब्जेक्ट्स वाले एरे के बीच ऑपरेशंस, एरे और एकल कस्टम ऑब्जेक्ट के बीच, कस्टम ऑब्जेक्ट्स के एरे और Python ints के एरे के बीच, साथ ही उल्टे क्रम वाले ऑपरेशन जैसे int * custom_object। अंत में, चेन की गई एक्सप्रेशन शुरू से अंत तक वही कस्टम सेमान्टिक्स बनाए रखें।

व्यवहार दिखाने वाला न्यूनतम उदाहरण

नीचे दी गई क्लास डेमो में नतीजों को स्पष्ट बनाने के लिए केवल + और * के अर्थ उलट देती है। एरे को dtype=object के साथ स्पष्ट रूप से बनाया गया है ताकि तत्व Python ऑब्जेक्ट ही रहें।

import numpy as np
class Quark:
    def __init__(self, payload):
        self.payload = payload
    def __add__(self, other):
        return self.payload * other.payload
    def __mul__(self, other):
        return self.payload + other.payload
x = Quark(5)
Y = Quark(3)
box = np.array([x, y], dtype=object)
res = box * Quark(10)
print(res)  # [15 13], जैसा अपेक्षित था
res = box * np.array([y, x], dtype=object)
print(res)  # [8 8], यह भी अपेक्षित है

जब दोनों ओपरेन्ड कस्टम ऑब्जेक्ट्स के एरे हों, या कस्टम ऑब्जेक्ट्स वाला एरे किसी एकल कस्टम ऑब्जेक्ट के साथ जोड़ा जाए, तो प्रति-तत्व परिणाम क्लास के dunder मेथड्स का ही उपयोग करता है। यही मुख्य बात है: एलिमेंट-वाइज डिस्पैच एरे में रखे हर ऑब्जेक्ट के लिए कस्टम ऑपरेटर को कॉल करता है।

असल में हो क्या रहा है

dtype=object होने पर, ndarrays से जुड़ी गणनाएँ Python के सामान्य ऑपरेटर रेज़ॉल्यूशन नियमों के तहत ऑपरेशन को तत्व-दर-तत्व लागू करती हैं। व्यवहार में इसका अर्थ है कि हर तत्व का __add__ या __mul__ बुलाया जाता है, और मिश्रित प्रकारों की स्थिति में, यदि बाएँ ओपरेन्ड का मेथड ऑपरेशन को संभाल नहीं पाता, तो __radd__ और __rmul__ जैसे रिवर्स मेथड काम में आते हैं। यह वही है जो आप उदाहरण चलाकर देखेंगे और नीचे दिए गए परिणामों से पुष्ट होता है।

Python ints के साथ मिश्रित ऑपरेशंस को संभालना

यदि आप कस्टम ऑब्जेक्ट्स के एरे को ints के एरे के साथ मिलाते हैं, तो सीधी कॉल ValueError दे सकती है, क्योंकि कस्टम मेथड एक कस्टम इंस्टेंस की अपेक्षा करता है। आप अपनी क्लास को अधिक मज़बूत बना सकते हैं ताकि वह आपके कस्टम प्रकार के साथ-साथ सामान्य संख्याएँ भी स्वीकार करे।

class Quark:
    def __init__(self, payload):
        self.payload = payload
    def __add__(self, other):
        if isinstance(other, Quark):
            return self.payload * other.payload
        return self.payload * other
    def __mul__(self, other):
        if isinstance(other, Quark):
            return self.payload + other.payload
        return self.payload + other

इस बदलाव के साथ, ऑब्जेक्ट एरे को int एरे के साथ जोड़ना ठीक से काम करता है।

res = box * np.array([1, 2], dtype=object)  # [6 5]

हालाँकि, ओपरेन्ड्स का क्रम उलटने पर असफलता संभव है, क्योंकि पहले int का ऑपरेटर चलता है और उसे कस्टम प्रकार के साथ संयोजन करना नहीं आता। यहीं पर रिवर्स dunder मेथड्स मायने रखते हैं।

res = np.array([1, 2], dtype=object) * box  # रिवर्स मेथड्स के बिना त्रुटि

__radd__ और __rmul__ लागू करने से वही लॉजिक पुनः उपयोग होता है और यह असमानता दूर हो जाती है।

class Quark:
    def __init__(self, payload):
        self.payload = payload
    def __add__(self, other):
        if isinstance(other, Quark):
            return self.payload * other.payload
        return self.payload * other
    def __mul__(self, other):
        if isinstance(other, Quark):
            return self.payload + other.payload
        return self.payload + other
    def __radd__(self, other):
        return self.__add__(other)
    def __rmul__(self, other):
        return self.__mul__(other)

अब array * array और array * scalar दोनों ही क्रम की परवाह किए बिना काम करते हैं।

res = np.array([1, 2], dtype=object) * box  # [6 5]

यही सममिति मिश्रित ऑब्जेक्ट/संख्या एरे को भी सहजता से जोड़ने देती है।

u = np.array([x, 2], dtype=object)
v = np.array([10, y], dtype=object)
out = u + v  # [50 6]

चेन की गई अभिव्यक्तियों में कस्टम व्यवहार बनाए रखना

चेन ऑपरेशंस में एक और बारीकी सामने आती है। यदि आपके ऑपरेटर साधारण Python संख्याएँ लौटाते हैं, तो चेन का अगला चरण आपके कस्टम अर्थों के बजाय डिफ़ॉल्ट संख्यात्मक सेमान्टिक्स अपनाएगा। इस अभिव्यक्ति को देखें, जहाँ बाएँ हिस्से का ऑपरेशन int लौटाता है:

res = 2 * Quark(3) * 5  # 25, लेकिन बदले हुए सेमान्टिक्स के तहत अपेक्षित परिणाम 10 है

पहला हिस्सा, 2 * Quark(3), बदले हुए नियमों में 5 देता है, लेकिन उसके बाद 5 * 5 सामान्य int गुणा बन जाता है और 25 लौटाता है। समाधान यह है कि ऑपरेटरों से अपना ही प्रकार लौटाएँ, ताकि चेन आगे भी आपके कस्टम मेथड्स पर ही डिस्पैच होती रहे।

class Quark:
    def __init__(self, payload):
        self.payload = payload
    def __add__(self, other):
        if isinstance(other, Quark):
            return Quark(self.payload * other.payload)
        return Quark(self.payload * other)
    def __mul__(self, other):
        if isinstance(other, Quark):
            return Quark(self.payload + other.payload)
        return Quark(self.payload + other)
    def __radd__(self, other):
        return self.__add__(other)
    def __rmul__(self, other):
        return self.__mul__(other)

इस बदलाव से चेन कस्टम प्रकार के भीतर ही रहती है और इच्छित ऑपरेटर सेमान्टिक्स बनाए रखती है।

res = 2 * Quark(3) * 5
print(res.payload)  # 10

इंटरएक्टिव आउटपुट में बेहतर दिखने के लिए, स्ट्रिंग रिप्रेज़ेंटेशन के माध्यम से अंतर्निहित मान दिखा सकते हैं।

def __repr__(self):
    return str(self.payload)

इसके बाद, ऑब्जेक्ट को प्रिंट करने पर पेलोड सीधे दिखेगा।

res = 2 * Quark(3) * 5  # 10

समाधान का सार और कार्यशील मॉडल

मुख्य बात यह है कि ऑब्जेक्ट एरे ऑपरेशन एलिमेंट-वाइज करते हैं और संग्रहित ऑब्जेक्ट्स के Python ऑपरेटर मेथड्स को कॉल करते हैं। __add__, __mul__, __radd__ और __rmul__ ऐसे दें जो आपके कस्टम प्रकार के साथ-साथ साधारण संख्याएँ भी स्वीकार करें, और इन ऑपरेटरों से अपना कस्टम प्रकार लौटाएँ—तो स्केलर, एरे, मिक्स्ड-टाइप, रिवर्स्ड और चेन की गई सभी ऑपरेशंस में व्यवहार एकसमान रहता है। यह मानक ndarrays पर लागू होता है, और np.asarray से रैप या री-रैप करने पर भी, जब dtype=object बना रहता है, वही व्यवहार कायम रहता है।

यह क्यों महत्वपूर्ण है

इस डिस्पैच मॉडल को समझने से आप ऐसी कस्टम स्केलर-जैसी क्लासें डिज़ाइन कर सकते हैं जो बिना एरे कंटेनर बदले या एरे-स्तरीय हुक्स पर निर्भर हुए NumPy के साथ घुल-मिल जाएँ। यह तब खास काम आता है जब एरे का निर्माण या उपयोग-पैटर्न आपके नियंत्रण में न हों, और तत्व अलग-अलग संदर्भों में दिख सकते हों—अलग-थलग स्केलर, एरे, या Python संख्याओं के साथ मिश्रित। सही dunder मेथड्स के सेट के साथ, वही सेमान्टिक्स लगातार और पूर्वानुमेय रूप से लागू होते हैं।

मुख्य बातें

यदि आपको NumPy ndarrays के भीतर कस्टम अंकगणित चाहिए, तो अपने ऑब्जेक्ट्स को dtype=object एरे में रखें, ताकि हर ऑपरेशन ऑब्जेक्ट के अपने मेथड्स को बुलाए। मिश्रित और उल्टे ओपरेन्ड क्रम को कवर करने के लिए फ़ॉरवर्ड और रिवर्स दोनों ऑपरेटर लागू करें। चेन की गई अभिव्यक्तियों में वही सेमान्टिक्स बनाए रखने के लिए, अंकगणितीय मेथड्स से साधारण Python प्रिमिटिव की बजाय अपना कस्टम प्रकार लौटाएँ। साफ-सुथरे इंटरएक्टिव आउटपुट के लिए, __repr__ दें जो अंतर्निहित मान दिखाए। इन बातों के साथ, आपको मनचाहा व्यवहार पाने के लिए मानक ndarrays ही पर्याप्त हैं।

यह लेख StackOverflow के प्रश्न (लेखक: orange) और Aadvik के उत्तर पर आधारित है।