.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "auto_examples/oop/descriptors.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note Click :ref:`here ` to download the full example code or to run this example in your browser via Binder .. rst-class:: sphx-glr-example-title .. _sphx_glr_auto_examples_oop_descriptors.py: ================= 3.12 Descriptors ================= .. GENERATED FROM PYTHON SOURCE LINES 8-11 Descriptors are another way to control, what happens when a value of an attribute is set or accessed. We do it while making a class with at least one of ``__get__``, ``__set__`` and ``__del__`` methods. .. GENERATED FROM PYTHON SOURCE LINES 13-40 .. code-block:: default class MyDescriptor(object): """ Basic descriptor to set and get value. """ def __init__(self, initval=None): print("__init__ of MyDescriptor called with initial value: ", initval) self.__set__(self, initval) def __get__(self, instance, owner): print(instance, owner) print('Getting self.val: ', self.val) return self.val def __set__(self, instance, value): print('Setting self.val to ', value) self.val = value class Model(object): temp = MyDescriptor(37) # Descriptor is attached at class definition time body = Model() .. rst-class:: sphx-glr-script-out .. code-block:: none __init__ of MyDescriptor called with initial value: 37 Setting self.val to 37 .. GENERATED FROM PYTHON SOURCE LINES 41-44 When we created an instance of `Model` class, the `MyDescriptor` was initiated. When `MyDescriptor` was initiated, its ``__init__`` method was called and got the string printed. .. GENERATED FROM PYTHON SOURCE LINES 44-47 .. code-block:: default print(body.temp) # a function call from `MyDescriptor` class is hiding here .. rst-class:: sphx-glr-script-out .. code-block:: none <__main__.Model object at 0x7f136954a7c0> Getting self.val: 37 37 .. GENERATED FROM PYTHON SOURCE LINES 48-50 Above when we ran `body.temp`, the ``__get__`` method of `MyDescriptor` class for `temp` attribute ran. .. GENERATED FROM PYTHON SOURCE LINES 50-53 .. code-block:: default body.temp = 38 # a function call is hiding here .. rst-class:: sphx-glr-script-out .. code-block:: none Setting self.val to 38 .. GENERATED FROM PYTHON SOURCE LINES 54-57 .. code-block:: default print(body.temp) .. rst-class:: sphx-glr-script-out .. code-block:: none <__main__.Model object at 0x7f136954a7c0> Getting self.val: 38 38 .. GENERATED FROM PYTHON SOURCE LINES 58-61 Thus we see when we get the value of attribute ``temp``, the method ``__get__`` in `MyDescriptor` gets executed and similarly when we set a value to attribute `temp`, the method ``__set__`` in `MyDescriptor` gets executed. .. GENERATED FROM PYTHON SOURCE LINES 63-64 In ``__get__`` method of `MyDescriptor` class, instance is `body` and and owner is `Model`. .. GENERATED FROM PYTHON SOURCE LINES 66-68 If we want to know what attributes are stored in ``__dict__`` of class and instance, we can do as following .. GENERATED FROM PYTHON SOURCE LINES 70-73 .. code-block:: default print(body.__dict__) .. rst-class:: sphx-glr-script-out .. code-block:: none {} .. GENERATED FROM PYTHON SOURCE LINES 74-76 The ``__get__`` and ``__set__`` of descriptor can be applied only on those attributes which are present in ``__dict__`` of owner class i.e. `Model` in this case. .. GENERATED FROM PYTHON SOURCE LINES 78-85 .. code-block:: default # alternative to print(Model.__dict__) for key, val in Model.__dict__.items(): print(key, ': ', val) .. rst-class:: sphx-glr-script-out .. code-block:: none __module__ : __main__ temp : <__main__.MyDescriptor object at 0x7f136957de80> __dict__ : __weakref__ : __doc__ : None .. GENERATED FROM PYTHON SOURCE LINES 86-87 alternative to print(MyDescriptor.__dict__) .. GENERATED FROM PYTHON SOURCE LINES 87-91 .. code-block:: default for key, val in MyDescriptor.__dict__.items(): print(key, ': ', val) .. rst-class:: sphx-glr-script-out .. code-block:: none __module__ : __main__ __doc__ : Basic descriptor to set and get value. __init__ : __get__ : __set__ : __dict__ : __weakref__ : .. GENERATED FROM PYTHON SOURCE LINES 92-94 We can call `get` from class and its instance but `set` can and should only be called from instance. If we do it from class, this means overriding descriptor. .. GENERATED FROM PYTHON SOURCE LINES 96-100 .. code-block:: default print(Model.temp) .. rst-class:: sphx-glr-script-out .. code-block:: none None Getting self.val: 38 38 .. GENERATED FROM PYTHON SOURCE LINES 101-105 .. code-block:: default Model.temp = "useless" print(Model.temp) .. rst-class:: sphx-glr-script-out .. code-block:: none useless .. GENERATED FROM PYTHON SOURCE LINES 106-109 .. code-block:: default print(body.temp) .. rst-class:: sphx-glr-script-out .. code-block:: none useless .. GENERATED FROM PYTHON SOURCE LINES 110-112 This means we should do some type checking before assigning a value to an attribute. Consider `descriptor` from another angle below. .. GENERATED FROM PYTHON SOURCE LINES 115-133 .. code-block:: default class LazyDescriptor(object): def __init__(self, name, inival): self._val = inival self.name = name def __get__(self, instance, owner): print('get in descriptor called') instance.__dict__[self.name] = self._val return self._val class Model(object): temp = LazyDescriptor("temp", 37) body = Model() .. GENERATED FROM PYTHON SOURCE LINES 134-137 .. code-block:: default print(body.temp) .. rst-class:: sphx-glr-script-out .. code-block:: none get in descriptor called 37 .. GENERATED FROM PYTHON SOURCE LINES 138-139 Above we we ran `body.temp`, the ``__get__`` method of descriptor was called. .. GENERATED FROM PYTHON SOURCE LINES 139-142 .. code-block:: default print(body.temp) .. rst-class:: sphx-glr-script-out .. code-block:: none 37 .. GENERATED FROM PYTHON SOURCE LINES 143-145 So the first time we referenced temp, it called the descriptor but not the second time. Let's look at the `__dict__` for better understanding. .. GENERATED FROM PYTHON SOURCE LINES 147-151 .. code-block:: default body = Model() print(body.__dict__) .. rst-class:: sphx-glr-script-out .. code-block:: none {} .. GENERATED FROM PYTHON SOURCE LINES 152-155 .. code-block:: default print(body.temp) .. rst-class:: sphx-glr-script-out .. code-block:: none get in descriptor called 37 .. GENERATED FROM PYTHON SOURCE LINES 156-159 .. code-block:: default print(body.__dict__) .. rst-class:: sphx-glr-script-out .. code-block:: none {'temp': 37} .. GENERATED FROM PYTHON SOURCE LINES 160-163 .. code-block:: default print(body.temp) .. rst-class:: sphx-glr-script-out .. code-block:: none 37 .. GENERATED FROM PYTHON SOURCE LINES 164-167 .. code-block:: default print(body.__dict__) .. rst-class:: sphx-glr-script-out .. code-block:: none {'temp': 37} .. GENERATED FROM PYTHON SOURCE LINES 168-174 So when we tried to access the value of `x` for the first time, the `key` was not in ``object.__dict__`` so the descriptor's ``__get__`` was called but when it is already present, the ``__get__`` from descriptor was not called. This is because of order in which python looks for attributes of objects. For complete sequence of rules `see this link `_. We can achieve exactly same by another way as well. .. GENERATED FROM PYTHON SOURCE LINES 177-199 .. code-block:: default class LazyProperty(object): def __init__(self, val): self._val = val self.name = val.__name__ def __get__(self, instance, owner): print("get in descriptor called") result = self._val(instance) instance.__dict__[self.name] = result return result class Model(object): @LazyProperty def temp(self): return 42 body = Model() .. GENERATED FROM PYTHON SOURCE LINES 200-203 .. code-block:: default print(body.temp) .. rst-class:: sphx-glr-script-out .. code-block:: none get in descriptor called 42 .. GENERATED FROM PYTHON SOURCE LINES 204-207 .. code-block:: default print(body.temp) .. rst-class:: sphx-glr-script-out .. code-block:: none 42 .. GENERATED FROM PYTHON SOURCE LINES 208-212 Usage cases ------------- Suppose we define a class which takes the `name`, `weight` and `height` as input/for initiation and has a method to calculate body mass index i.e. `bmi`. .. GENERATED FROM PYTHON SOURCE LINES 214-229 .. code-block:: default class Insan: def __init__(self, name, weight, height): self.name = name self.weight = weight # in kg self.height = height # in meters def bmi(self): return self.weight / self.height ** 2 ali = Insan('ali', 78, 1.7) ali.bmi() .. rst-class:: sphx-glr-script-out .. code-block:: none 26.989619377162633 .. GENERATED FROM PYTHON SOURCE LINES 230-231 The problem with the above code is that one can assign negative values to weight. .. GENERATED FROM PYTHON SOURCE LINES 233-237 .. code-block:: default ali.weight = -10 ali.bmi() .. rst-class:: sphx-glr-script-out .. code-block:: none -3.4602076124567476 .. GENERATED FROM PYTHON SOURCE LINES 238-240 Definitely it is wrong and we should perform some checks before setting the new value. We can do it by using `property` .. GENERATED FROM PYTHON SOURCE LINES 243-270 .. code-block:: default class Insan: def __init__(self, name, weight, height): self.name = name self._weight = weight # in kg self.height = height # in meters @property def weight(self): return self._weight @weight.setter def weight(self, value): if value < 0: raise ValueError('weight cannot be negative.') self._weight = value def bmi(self): return self.weight / self.height ** 2 ali = Insan('ali', 78, 1.7) # uncomment following line # ali.weight = -80 # ValueError ali.bmi() .. rst-class:: sphx-glr-script-out .. code-block:: none 26.989619377162633 .. GENERATED FROM PYTHON SOURCE LINES 271-272 Thus upon negative weight, it threw error. But `height` can still be assigned a negative value. .. GENERATED FROM PYTHON SOURCE LINES 272-277 .. code-block:: default ali = Insan('ali', 78, 1.7) ali.height = -1.8 print(ali.height) .. rst-class:: sphx-glr-script-out .. code-block:: none -1.8 .. GENERATED FROM PYTHON SOURCE LINES 278-279 Let's make use of `property` once more. .. GENERATED FROM PYTHON SOURCE LINES 282-315 .. code-block:: default class Insan: def __init__(self, name, weight, height): self.name = name self._weight = weight # in kg self._height = height # in meters @property def weight(self): return self._weight @weight.setter def weight(self, value): if value < 0: raise ValueError('weight cannot be negative.') self._weight = value @property def height(self): return self._height @height.setter def height(self, value): if value < 0: raise ValueError('height cannot be negative.') self._height = value ali = Insan('Ali', 78, 1.7) # uncomment following line # ali.weight = -80 # ValueError .. GENERATED FROM PYTHON SOURCE LINES 316-324 But we are repeating our code. Both the properties are essentially doing same thing i.e. throwing errors on negative value assignment, so remember our code should be DRY (do not repeat yourself) To helps us, python has the concept of `descriptors`. We can define a descriptor which can have ``set``, ``get`` and ``del`` methods. The following code defines the descriptor `NonNegative`. Then inside class `Insan`, we define class attributes and bind them with the descriptor thus making sure that these attributes will always be non-negative otherwise an error will be thrown. .. GENERATED FROM PYTHON SOURCE LINES 326-366 .. code-block:: default class NonNegative: def __init__(self, name): # the name attribute is needed because when the NonNegative object is # created , the assignment to attribute named weight/height hasn't # happen yet. Thus, we need to explicitly pass the name weight/height to the # initializer of the object to use as the key for the instance's __dict__. self.name = name def __get__(self, instance, owner): # we need to reach into the __dict__ object directly, because the # builtins would be intercepted # by the descriptor protocols too and cause the RecursionError. return instance.__dict__[self.name] # getattr(instance, self._name) def __set__(self, instance, value): if value < 0: raise ValueError("{} Cannot be negative.".format(self.name)) # instead of using builtin function getattr and setattr, we need to reach # into the __dict__ object directly, because the builtins would be intercepted # by the descriptor protocols too and cause the RecursionError. instance.__dict__[self.name] = value # setattr(instance, self._name, value) class Insan: weight = NonNegative('weight') height = NonNegative('height') def __init__(self, name, weight, height): self.name = name self.weight = weight # in kg self.height = height # in meters def bmi(self): return self.weight / self.height ** 2 ali = Insan('Ali', 78, 1.7) ali.bmi() .. rst-class:: sphx-glr-script-out .. code-block:: none 26.989619377162633 .. GENERATED FROM PYTHON SOURCE LINES 367-368 Now we can not assign negative values to attributes ``weight`` and ``height`` of class ``Insan``. .. GENERATED FROM PYTHON SOURCE LINES 370-374 .. code-block:: default # uncomment following line # ali.weight = -80 # ValueError: Cannot be negative .. GENERATED FROM PYTHON SOURCE LINES 375-379 .. code-block:: default # uncomment following line # ali.height = -1.8 # ValueError: Cannot be negative .. GENERATED FROM PYTHON SOURCE LINES 380-386 .. code-block:: default ### In python 3.6+ # The `descriptor` definition in python 3.6+ is more flexible. .. GENERATED FROM PYTHON SOURCE LINES 387-420 .. code-block:: default class NonNegative: def __get__(self, instance, owner): return instance.__dict__[self.name] def __set__(self, instance, value): if value < 0: raise ValueError('{} Cannot be negative.'.format(self.name)) instance.__dict__[self.name] = value # __set_name__ is called at the time the owning class owner is created. # The descriptor has been assigned to name. With this protocol, we can now # remove the __init__ and bind the attribute name to the descriptor def __set_name__(self, owner, name): self.name = name class Insan: weight = NonNegative() height = NonNegative() def __init__(self, name, weight, height): self.name = name self.weight = weight # in kg self.height = height # in meters def bmi(self): return self.weight / self.height ** 2 ali = Insan('Ali', 78, 1.7) ali.bmi() .. rst-class:: sphx-glr-script-out .. code-block:: none 26.989619377162633 .. GENERATED FROM PYTHON SOURCE LINES 421-425 .. code-block:: default # uncomment following line # ali.weight = -80 # ValueError: Cannot be negative .. GENERATED FROM PYTHON SOURCE LINES 426-430 .. code-block:: default # uncomment following line # ali.height = -1.8 # ValueError: Cannot be negative .. GENERATED FROM PYTHON SOURCE LINES 431-434 Let's say, we want to calculate a new quantity say `bmi` which is multiplication of `BMI` with `temperature` in Celsius. We can define a property to convert the temperature into Celsius, in case the temperature is provided in Fahrenheit. .. GENERATED FROM PYTHON SOURCE LINES 436-476 .. code-block:: default class NonNegative: def __get__(self, instance, owner): return instance.__dict__[self.name] def __set__(self, instance, value): if value < 0: raise ValueError('{} Cannot be negative.'.format(self.name)) instance.__dict__[self.name] = value def __set_name__(self, owner, name): self.name = name class Insan: weight = NonNegative() height = NonNegative() def __init__(self, name, weight, height, temp_f): self.name = name self.weight = weight # in kg self.height = height # in meters self.fahrenheit = temp_f @property def celsius(self): return 5 * (self.fahrenheit - 32) / 9.0 @celsius.setter def celsius(self, val): self.fahrenheit = 32 + 9 * val / 5.0 def bmit(self): return self.weight / self.height ** 2 * self.celsius ali = Insan('Ali', 78, 1.7, 98.2) ali.bmit() .. rst-class:: sphx-glr-script-out .. code-block:: none 992.6182237600924 .. GENERATED FROM PYTHON SOURCE LINES 477-480 .. code-block:: default print(ali.celsius) .. rst-class:: sphx-glr-script-out .. code-block:: none 36.77777777777778 .. GENERATED FROM PYTHON SOURCE LINES 481-483 But we can also define it as `descriptor` as follows. Furthermore we are also performing non-negative check in this descriptor as well. .. GENERATED FROM PYTHON SOURCE LINES 486-534 .. code-block:: default class Celsius: def __get__(self, instance, owner): return 5 * (instance.fahrenheit - 32) / 9 def __set__(self, instance, value): if value < 0: raise ValueError('{} Cannot be negative.'.format(self.name)) instance.fahrenheit = 32 + 9 * value / 5 def __set_name__(self, owner, name): self.name = name class NonNegative: def __get__(self, instance, owner): return instance.__dict__[self.name] def __set__(self, instance, value): if value < 0: raise ValueError('{} Cannot be negative.'.format(self.name)) instance.__dict__[self.name] = value def __set_name__(self, owner, name): self.name = name class Insan: weight = NonNegative() height = NonNegative() celsius = Celsius() def __init__(self, name, weight, height, temp_f): self.name = name self.weight = weight # in kg self.height = height # in meters self.fahrenheit = temp_f # temperature in fahrenheit def bmit(self): return self.weight / self.height ** 2 * self.celsius ali = Insan('Ali', 78, 1.7, 98.2) ali.bmit() .. rst-class:: sphx-glr-script-out .. code-block:: none 992.6182237600924 .. GENERATED FROM PYTHON SOURCE LINES 535-538 .. code-block:: default print(ali.fahrenheit) .. rst-class:: sphx-glr-script-out .. code-block:: none 98.2 .. GENERATED FROM PYTHON SOURCE LINES 539-542 .. code-block:: default print(ali.celsius) .. rst-class:: sphx-glr-script-out .. code-block:: none 36.77777777777778 .. GENERATED FROM PYTHON SOURCE LINES 543-547 .. code-block:: default # uncomment following line # ali.celsius = -30 # ValueError .. GENERATED FROM PYTHON SOURCE LINES 548-552 Caveat -------- Because the descriptors are linked with class and not with instance, so when we create a new instance, the values get overridden by new instance if they are not linked with instance. .. GENERATED FROM PYTHON SOURCE LINES 554-593 .. code-block:: default class Descriptor: def __init__(self): self.__temp = 0 def __get__(self, instance, owner): return self.__temp def __set__(self, instance, value): if isinstance(float(value), float): print(value) else: raise TypeError("Body Temperature must be float or integer") if value < 20: raise ValueError("Body Temperature can never be less than 20") self.__temp = value def __set_name__(self, owner, name): self.name = name class Model: temp = Descriptor() def __init__(self, name, weight, temp): self._name = name self.weight = weight self.temp = temp def __str__(self): return "{0} with weight {1} has body temperature {2} Celsius.".format(self._name, self.weight, self.temp) body1 = Model("Ali", 80, 40) print(body1) .. rst-class:: sphx-glr-script-out .. code-block:: none 40 Ali with weight 80 has body temperature 40 Celsius. .. GENERATED FROM PYTHON SOURCE LINES 594-597 .. code-block:: default print(body1.__dict__) .. rst-class:: sphx-glr-script-out .. code-block:: none {'_name': 'Ali', 'weight': 80} .. GENERATED FROM PYTHON SOURCE LINES 598-602 .. code-block:: default body2 = Model("Hasan", 75, 37) print(body2) .. rst-class:: sphx-glr-script-out .. code-block:: none 37 Hasan with weight 75 has body temperature 37 Celsius. .. GENERATED FROM PYTHON SOURCE LINES 603-606 .. code-block:: default print(body1) .. rst-class:: sphx-glr-script-out .. code-block:: none Ali with weight 80 has body temperature 37 Celsius. .. GENERATED FROM PYTHON SOURCE LINES 607-608 The solution is to bind the attribute with instance in descriptor as shown below. .. GENERATED FROM PYTHON SOURCE LINES 610-649 .. code-block:: default class Descriptor: def __init__(self): self.__temp = 0 def __get__(self, instance, owner): return instance.__dict__[self.name] def __set__(self, instance, value): if isinstance(float(value), float): print(value) else: raise TypeError("Body Temperature must be float or integer") if value < 20: raise ValueError("Body Temperature can never be less than 20") instance.__dict__[self.name] = value def __set_name__(self, owner, name): self.name = name class Model: temp = Descriptor() def __init__(self, name, weight, temp): self.name = name self.weight = weight self.temp = temp def __str__(self): return "{0} with weight {1} has body temperature {2} Celsius.".format(self.name, self.weight, self.temp) body1 = Model("Ali", 80, 40) print(body1) .. rst-class:: sphx-glr-script-out .. code-block:: none 40 Ali with weight 80 has body temperature 40 Celsius. .. GENERATED FROM PYTHON SOURCE LINES 650-653 .. code-block:: default print(body1.__dict__) .. rst-class:: sphx-glr-script-out .. code-block:: none {'name': 'Ali', 'weight': 80, 'temp': 40} .. GENERATED FROM PYTHON SOURCE LINES 654-658 .. code-block:: default body2 = Model("Hasan", 75, 37) print(body2) .. rst-class:: sphx-glr-script-out .. code-block:: none 37 Hasan with weight 75 has body temperature 37 Celsius. .. GENERATED FROM PYTHON SOURCE LINES 659-662 .. code-block:: default print(body1) .. rst-class:: sphx-glr-script-out .. code-block:: none Ali with weight 80 has body temperature 40 Celsius. .. GENERATED FROM PYTHON SOURCE LINES 663-667 Using WeakKeyDictionary ------------------------- Usually the attributes from descriptors are saved in ``WeakKeyDictionary``. The above code can be implemented using ``WeakKeyDictionary`` as shown below .. GENERATED FROM PYTHON SOURCE LINES 669-710 .. code-block:: default from weakref import WeakKeyDictionary class Descriptor: def __init__(self): self.data = WeakKeyDictionary() def __get__(self, instance, owner): return self.data[instance] def __set__(self, instance, value): if isinstance(float(value), float): print(value) else: raise TypeError("Body Temperature must be float or integer") if value < 20: raise ValueError("Body Temperature can never be less than 20") self.data[instance] = value def __set_name__(self, owner, name): self.name = name class Model: temp = Descriptor() def __init__(self, name, weight, temp): self.name = name self.weight = weight self.temp = temp def __str__(self): return "{0} with weight {1} has body temperature {2} Celsius.".format(self.name, self.weight, self.temp) body1 = Model("Ali", 80, 40) print(body1) .. rst-class:: sphx-glr-script-out .. code-block:: none 40 Ali with weight 80 has body temperature 40 Celsius. .. GENERATED FROM PYTHON SOURCE LINES 711-715 .. code-block:: default body2 = Model("Hasan", 75, 37) print(body2) .. rst-class:: sphx-glr-script-out .. code-block:: none 37 Hasan with weight 75 has body temperature 37 Celsius. .. GENERATED FROM PYTHON SOURCE LINES 716-719 .. code-block:: default print(body1) .. rst-class:: sphx-glr-script-out .. code-block:: none Ali with weight 80 has body temperature 40 Celsius. .. GENERATED FROM PYTHON SOURCE LINES 722-732 **References:** The material in this lesson is inspired from following posts * `talk on descriptors `_ * `Python course eu website `_ * `Encapsulation with descriptors `_ * `Some great answers on stackoverflow `_ * `A post by Daw Ran Liou `_ * `DataCamp `_ .. rst-class:: sphx-glr-timing **Total running time of the script:** ( 0 minutes 0.021 seconds) .. _sphx_glr_download_auto_examples_oop_descriptors.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: binder-badge .. image:: images/binder_badge_logo.svg :target: https://mybinder.org/v2/gh/AtrCheema/python-seekho/master?urlpath=lab/tree/notebooks/auto_examples/oop/descriptors.ipynb :alt: Launch binder :width: 150 px .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: descriptors.py ` .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: descriptors.ipynb ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_