मुख्य सामग्री पर जाएँ

Python Descriptors Tutorial

जानें कि Python Descriptors क्या होते हैं, इन्हें कब और क्यों इस्तेमाल करना चाहिए।
· 13 मि॰ पढ़ें

AI के साथ खोजें

Python descriptors, या सामान्य रूप से कहें तो descriptors, आपको ऐसा शक्तिशाली तरीका देते हैं जिससे आप पुन: प्रयोज्य कोड लिख सकें जो कक्षाओं (classes) के बीच साझा किया जा सके। वे इनहेरिटेंस की अवधारणा जैसे लग सकते हैं, लेकिन तकनीकी रूप से वे अलग हैं। ये गुण (attribute) एक्सेस को इंटरसेप्ट करने का एक सामान्य-उद्देश्य तरीका हैं। प्रॉपर्टीज, स्टैटिक मेथड्स, क्लास मेथड्स, सुपर मेथड्स आदि के पीछे descriptors ही मैकेनिज़्म है।

Descriptors को Python संस्करण 2.2 में जोड़ा गया था, और तब से इन्हें जादुई चीज़ों के रूप में माना गया है जिन्होंने पारंपरिक कक्षाओं को एक नया स्टाइल दिया। ये ऐसी कक्षाएँ हैं जो आपको किसी दूसरी कक्षा में प्रबंधित प्रॉपर्टीज (managed properties) लागू करने देती हैं। विशेष रूप से, वे __get__(), __set__() और __delete__() मेथड के लिए इंटरफेस लागू करती हैं, जो कई कारणों से उन्हें रोचक बनाते हैं। उदाहरण के लिए, Python में आपने पहले जो class decorator और property decorator देखे होंगे।

सरल शब्दों में, कोई भी कक्षा जो किसी ऑब्जेक्ट के लिए __get__(), __set()__, या __delete()__ मेथड लागू करती है, उसे "Descriptor" कहा जाता है। सीधे Python के आधिकारिक प्रलेखन से उद्धृत करें तो, एक descriptor वह ऑब्जेक्ट एट्रिब्यूट है जिसमें binding behavior होता है, यानी जिसका एट्रिब्यूट एक्सेस descriptor प्रोटोकॉल के मेथड्स द्वारा ओवरराइड किया गया हो। ये मेथड्स हैं __get__(), __set__(), और __delete__() (स्रोत).

Descriptors के संदर्भ में binding behavior का अर्थ है कि किसी दिए गए वेरिएबल, ऑब्जेक्ट या डेटा सेट के लिए मान कैसे सेट किया जा सकता है, क्वेरी (get) किया जा सकता है, या हटाया जा सकता है—इस पूरे इंटरैक्शन को उसी डेटा के टुकड़े से बाइंड किया जाता है, क्योंकि यह केवल उसी डेटा पर लागू होता है जिस पर आपने इसे सेट किया है।

Descriptors को आगे डेटा और नॉन-डेटा descriptors में वर्गीकृत किया जा सकता है। यदि आपके द्वारा लिखा गया descriptor केवल __get__() मेथड रखता है, तो यह नॉन-डेटा descriptor है; दूसरी ओर, __set__() और __delete__() मेथड्स वाली इम्प्लीमेंटेशन को डेटा descriptor कहा जाता है। नॉन-डेटा descriptors केवल रीडेबल होते हैं जबकि डेटा descriptors दोनों रीडेबल और राइटेबल होते हैं।

यह ध्यान देने योग्य है कि descriptors किसी कक्षा (class) को असाइन किए जाते हैं, न कि कक्षा के instance को। कक्षा को संशोधित करने से descriptor स्वयं ओवरराइट या डिलीट होता है, न कि उसके कोड को ट्रिगर करता है (IBM Developer).

अंत में, descriptor कक्षा केवल इन तीन मेथड्स तक सीमित नहीं है, यानी इसमें get, set, और delete मेथड के अलावा अन्य कोई भी एट्रिब्यूट हो सकते हैं।

आइए IBM Developer पेज से प्रेरित होकर get, set, और delete मेथड्स को थोड़ी और विस्तार से समझें:

  • self वह descriptor instance है जो आप बनाते हैं (Real Python).
  • object वह ऑब्जेक्ट instance है जिससे आपका descriptor जुड़ा है (Real Python).
  • type उस ऑब्जेक्ट का प्रकार है जिससे descriptor जुड़ा है (Real Python).
  • value वह मान है जो descriptor के एट्रिब्यूट को असाइन किया जाता है। get(self, object, type) set(self, object, value) delete(self, object)
  • __get__() एट्रिब्यूट तक पहुँचता है या जब आप कुछ जानकारी निकालना चाहते हैं तब चलता है। यह एट्रिब्यूट का मान लौटाता है या यदि अनुरोधित एट्रिब्यूट मौजूद नहीं है तो AttributeError अपवाद उठाता है।

  • __set__() किसी एट्रिब्यूट असाइनमेंट ऑपरेशन में कॉल होता है जो एट्रिब्यूट का मान सेट करता है। यह कुछ नहीं लौटाता, लेकिन AttributeError अपवाद उठा सकता है।

  • __delete__() डिलीट ऑपरेशन को नियंत्रित करता है, यानी जब आप किसी ऑब्जेक्ट से एट्रिब्यूट हटाना चाहते हैं। यह भी कुछ नहीं लौटाता।

अब descriptors के उद्देश्य को कुछ उदाहरणों की मदद से समझते हैं!

Descriptors का उद्देश्य

आइए एक कक्षा car परिभाषित करें जिसके तीन एट्रिब्यूट हैं: make, model, और fuel_cap। आप कक्षा के एट्रिब्यूट्स को इनिशियलाइज़ करने के लिए __init__() मेथड का उपयोग करेंगे। फिर आप मैजिक फ़ंक्शन __str__() का उपयोग करेंगे, जो बस उन तीन एट्रिब्यूट्स का आउटपुट लौटाएगा जिन्हें आप ऑब्जेक्ट बनाते समय कक्षा को पास करेंगे।

ध्यान दें कि __str__() मेथड ऑब्जेक्ट का स्ट्रिंग प्रतिनिधित्व लौटाता है। इसे तब कॉल किया जाता है जब कक्षा के ऑब्जेक्ट पर print() या str() फ़ंक्शन चलाया जाता है।

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car1 = Car("BMW","X7",40)
print(car1)
BMW model X7 with a fuel capacity of 40 ltr.

तो जैसा कि आप ऊपर के आउटपुट से देख सकते हैं, सब कुछ बढ़िया है!

अब कार की फ्यूल कैपेसिटी को नकारात्मक 40 में बदलते हैं।

car2 = Car("BMW","X7",-40)
print(car2)
BMW model X7 with a fuel capacity of -40 ltr.

रुकिए, कुछ गड़बड़ है, है न? कार की फ्यूल कैपेसिटी कभी नकारात्मक नहीं हो सकती। फिर भी Python इसे बिना किसी त्रुटि के इनपुट के रूप में स्वीकार कर लेता है। ऐसा इसलिए क्योंकि Python एक डायनेमिक प्रोग्रामिंग भाषा है जो स्पष्ट रूप से टाइप-चेकिंग को सपोर्ट नहीं करती।

इस समस्या से बचने के लिए, __init__() मेथड में एक if कंडीशन जोड़ते हैं और जाँचते हैं कि दी गई फ्यूल कैपेसिटी वैध है या अवैध। यदि दर्ज की गई फ्यूल कैपेसिटी अवैध है, तो ValueError अपवाद उठाएँ।

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car1 = Car("BMW","X7",40)
print(car1)
BMW model X7 with a fuel capacity of 40 ltr.
car2 = Car("BMW","X7",-40)
print(car2)
----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-22-1c3d23be72f7> in <module>
----> 1 car2 = Car("BMW","X7",-40)
      2 print(car2)


<ipython-input-20-1e154783588d> in __init__(self, make, model, fuel_cap)
      5         self.fuel_cap = fuel_cap
      6         if self.fuel_cap < 0:
----> 7             raise ValueError("Fuel Capacity can never be less than zero")
      8
      9     def __str__(self):


ValueError: Fuel Capacity can never be less than zero

ऊपर के आउटपुट से आप देख सकते हैं कि अभी के लिए सब ठीक काम कर रहा है क्योंकि यदि फ्यूल कैपेसिटी शून्य से कम है तो प्रोग्राम ValueError उठाता है।

हालाँकि, एक और समस्या हो सकती है, यानी यदि दर्ज की गई fuel capacity एक फ्लोट वैल्यू या स्ट्रिंग हो। केवल फ्यूल कैपेसिटी ही नहीं, कार का make और model भी इन्टीजर वैल्यू हो सकता है। इन सभी मामलों में, प्रोग्राम अपवाद उठाने में विफल रहेगा।

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car(-40,"X7",40)
print(car2)
-40 model X7 with a fuel capacity of 40 ltr.

ऊपर के मामले को संभालने के लिए, आप एक और if कंडीशन जोड़ने या टाइप-चेकिंग के लिए isinstance मेथड का उपयोग करने के बारे में सोच सकते हैं।

इस बार त्रुटि को संभालने के लिए बिल्ट-इन मेथड isinstance का उपयोग करते हैं।

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if isinstance(self.make, str):
            print(self.make)
        else:
            raise ValueError("Make of the car can never be an integer")

        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40)
print(car2)
BMW
BMW model X7 with a fuel capacity of 40 ltr.
car2 = Car(-40,"X7",40)
print(car2)
----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-34-75b08cba454f> in <module>
----> 1 car2 = Car(-40,"X7",40)
      2 print(car2)


<ipython-input-31-175690bf3b98> in __init__(self, make, model, fuel_cap)
      7             print(self.make)
      8         else:
----> 9             raise ValueError("Make of the car can never be an integer")
     10
     11         if self.fuel_cap < 0:


ValueError: Make of the car can never be an integer

बहुत बढ़िया! आप इस त्रुटि को भी संभाल पाए।

लेकिन, यदि आप बाद में फ्यूल कैपेसिटी एट्रिब्यूट को स्पष्ट रूप से नकारात्मक 40 में बदलना चाहें तो? इस स्थिति में यह काम नहीं करेगा, क्योंकि टाइप-चेकिंग केवल एक बार __init__() मेथड में होगी। जैसा कि आप जानते हैं, __init__() मेथड एक कंस्ट्रक्टर है, और यह केवल एक बार तब कॉल होता है जब आप कक्षा का ऑब्जेक्ट बनाते हैं। इसलिए, कस्टम टाइप-चेकिंग बाद में विफल हो जाएगी।

इसे एक उदाहरण से समझते हैं।

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if isinstance(self.make, str):
            print(self.make)
        else:
            raise ValueError("Make of the car can never be an integer")

        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40)
print(car2)
BMW
BMW model X7 with a fuel capacity of 40 ltr.
car2.make = -40
print(car2)
-40 model X7 with a fuel capacity of 40 ltr.

और हो गया! आप टाइप-चेकिंग से बाहर निकलने में सफल हो गए।

अब ऐसे सोचिए, यदि कार के कई और एट्रिब्यूट—जैसे माइलेज, कीमत, एक्सेसरीज़ आदि—भी हों जिन्हें टाइप-चेकिंग की ज़रूरत है और आप यह भी चाहते हैं कि इनमें से कुछ एट्रिब्यूट्स पर केवल read एक्सेस हो। क्या यह परेशान करने वाला नहीं होगा?

ऊपर बताई गई सभी समस्याओं के समाधान के लिए, Python के पास Descriptors हैं जो मदद करते हैं!

जैसा कि आपने ऊपर सीखा, कोई भी कक्षा जो किसी descriptor प्रोटोकॉल के ऑब्जेक्ट के लिए __get__(), __set()__, या __delete()__ मैजिक मेथड्स लागू करती है, उसे Descriptors कहा जाता है। वे यह अतिरिक्त नियंत्रण भी देते हैं कि किसी एट्रिब्यूट को कैसे काम करना चाहिए—जैसे क्या उसे read या write एक्सेस होना चाहिए।

अब ऊपर दिए उदाहरण को Python Descriptor मेथड्स जोड़कर बढ़ाते हैं।

class Descriptor:
    def __init__(self):
        self.__fuel_cap = 0
    def __get__(self, instance, owner):    
        return self.__fuel_cap
    def __set__(self, instance, value):
        if isinstance(value, int):
            print(value)
        else:
            raise TypeError("Fuel Capacity can only be an integer")

        if value < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

        self.__fuel_cap = value

    def __delete__(self, instance):
        del self.__fuel_cap
class Car:
    fuel_cap = Descriptor()
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40)
print(car2)
40
BMW model X7 with a fuel capacity of 40 ltr.

यदि Descriptor कक्षा आपको जटिल लग रही है तो चिंता न करें। इसे छोटे-छोटे हिस्सों में तोड़कर समझते हैं कि प्रत्येक मेथड असल में क्या कर रहा है।

  • Descriptor कक्षा के __init__() मेथड में एक लोकल वेरिएबल __fuel_cap को शून्य पर सेट किया गया है। शुरुआत में dunder या डबल अंडरस्कोर का अर्थ है कि वेरिएबल प्राइवेट है। dunder लगाने का उद्देश्य Descriptor कक्षा के फ्यूल कैपेसिटी एट्रिब्यूट को Car कक्षा से अलग पहचानना है।
  • जैसा कि अब आप जानते हैं, __get__() मेथड एट्रिब्यूट को रिट्रीव करने के लिए उपयोग होता है और यह फ्यूल कैपेसिटी वेरिएबल लौटाता है। यह तीन आर्ग्यूमेंट लेता है—descriptor ऑब्जेक्ट, उस कक्षा के instance का संदर्भ जिसमें descriptor ऑब्जेक्ट instance है, यानी car2, और अंत में owner, जो उस कक्षा का संदर्भ है जिससे instance संबंधित है, यानी Car कक्षा। इस मेथड में, आप बस value एट्रिब्यूट यानी fuel_cap लौटाते हैं, जिसका मान __set__() मेथड में सेट होता है।
  • __set__() मेथड तब चालू होता है जब एट्रिब्यूट को कोई मान असाइन किया जाता है, और __get__() मेथड के विपरीत, यह कुछ भी वापस नहीं लौटाता। descriptor ऑब्जेक्ट के अलावा इसमें दो आर्ग्यूमेंट होते हैं—instance, जो __get__() जैसा ही है, और value, जो आप एट्रिब्यूट को असाइन करते हैं। इस मेथड में, आप जाँचते हैं कि fuel_cap एट्रिब्यूट को असाइन किया जाने वाला मान इन्टीजर है या नहीं। यदि नहीं, तो आप TypeError अपवाद उठाते हैं। फिर, इसी मेथड में आप यह भी जाँचते हैं कि मान शून्य से कम तो नहीं; यदि हाँ, तो आप एक और अपवाद उठाते हैं, इस बार ValueError। त्रुटियों की जाँच के बाद, आप fuel_cap एट्रिब्यूट को दिए गए मान के बराबर अपडेट करते हैं।
  • अंत में, __delete__() मेथड तब कॉल होता है जब किसी ऑब्जेक्ट से एट्रिब्यूट हटाया जाता है और यह भी __set__() मेथड की तरह कुछ नहीं लौटाता।

Car कक्षा पहले जैसी ही रहती है। हालाँकि, केवल एक बदलाव यह है कि आप Descriptor() कक्षा का instance fuel_cap के रूप में जोड़ते हैं। ध्यान दें, जैसा कि पहले बताया गया था, descriptor कक्षा का instance किसी कक्षा में class attribute के रूप में जोड़ा जाना चाहिए, न कि instance attribute के रूप में।

जैसे ही आप __init__() मेथड में लोकल वेरिएबल fuel_cap को instance fuel_cap पर सेट करते हैं, यह Descriptor कक्षा के __set__() मेथड को इनवोक करता है।

अब फ्यूल कैपेसिटी को नकारात्मक मान में बदलते हैं और देखते हैं कि प्रोग्राम ValueError अपवाद उठाता है या नहीं।

car2 = Car("BMW","X7",-40)
print(car2)
-40



----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-115-1c3d23be72f7> in <module>
----> 1 car2 = Car("BMW","X7",-40)
      2 print(car2)


<ipython-input-107-3e1f3e97d3c7> in __init__(self, make, model, fuel_cap)
      4         self.make = make
      5         self.model = model
----> 6         self.fuel_cap = fuel_cap
      7
      8     def __str__(self):


<ipython-input-106-0b252695aeed> in __set__(self, instance, value)
     11
     12         if value < 0:
---> 13             raise ValueError("Fuel Capacity can never be less than zero")
     14
     15         self.__fuel_cap = value


ValueError: Fuel Capacity can never be less than zero

यदि आपको यहाँ याद हो, तो टाइप-चेकिंग तब असफल हो गई थी जब आपने बाद में एट्रिब्यूट का मान नकारात्मक संख्या में बदल दिया था, क्योंकि टाइप-चेकिंग केवल एक बार __init__() मेथड में लागू थी। अब fuel_cap का मान स्ट्रिंग में अपडेट करते हैं और देखते हैं कि क्या यह त्रुटि देता है।

car2.fuel_cap = -1
-1



----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-120-dea9dbe96ebe> in <module>
----> 1 car2.fuel_cap = -1


<ipython-input-106-0b252695aeed> in __set__(self, instance, value)
     11
     12         if value < 0:
---> 13             raise ValueError("Fuel Capacity can never be less than zero")
     14
     15         self.__fuel_cap = value


ValueError: Fuel Capacity can never be less than zero
car2.fuel_cap = "BMW"
----------------------------------------

TypeErrorTraceback (most recent call last)

<ipython-input-121-0b316a9872c6> in <module>
----> 1 car2.fuel_cap = "BMW"


<ipython-input-106-0b252695aeed> in __set__(self, instance, value)
      8             print(value)
      9         else:
---> 10             raise TypeError("Fuel Capacity can only be an integer")
     11
     12         if value < 0:


TypeError: Fuel Capacity can only be an integer

बिल्कुल सही! जैसा कि आप देख सकते हैं, बाद में फ्यूल कैपेसिटी एट्रिब्यूट अपडेट करने पर भी यह काम करता है।

Descriptors में एक हल्की समस्या है: जब आप कक्षा का एक नया या दूसरा instance बनाते हैं, तो पिछले instance का मान ओवरराइड हो जाता है। कारण यह है कि descriptors कक्षा से जुड़े होते हैं, instance से नहीं।

इसे नीचे दिए उदाहरण से समझते हैं।

car3 = Car("BMW","X7",48) #created a new instance 'car3' with different values
48

जब आप instance car2 को प्रिंट करेंगे, तो आप देखेंगे कि मान car3 द्वारा ओवरराइड हो गए हैं।

print(car2)
BMW model X7 with a fuel capacity of 48 ltr.

निष्कर्ष

ट्यूटोरियल पूरा करने पर बधाई।

यह ट्यूटोरियल उनके लिए था जो Python में सहज हैं और उन्नत स्तर में महारत हासिल करना चाहते हैं। एक अच्छे अभ्यास के रूप में, आप यह जानने की कोशिश कर सकते हैं कि आज के ट्यूटोरियल में चर्चित instance overriding की समस्या को कैसे सुलझाया जा सकता है।

कृपया इस ट्यूटोरियल से संबंधित कोई भी प्रश्न नीचे टिप्पणी अनुभाग में पूछें।

यदि आप Python के बारे में और सीखना चाहते हैं, तो DataCamp का पूर्ण Python Programming स्किल ट्रैक देखें।

हमारा Python Functions Tutorial देखें।

विषय