[Tutor] self.name is calling the __set__ method of another class

Cameron Simpson cs at cskk.id.au
Mon Apr 29 21:03:36 EDT 2019


On 29Apr2019 23:25, Arup Rakshit <ar at zeit.io> wrote:
>In the following code, class attributes name and email is set to the 
>instances of NonBlank.
>
>class NonBlank:
>    def __init__(self, storage_name):
>        self.storage_name = storage_name
>
>    def __set__(self, instance, value):
>        if not isinstance(value, str):
>            raise TypeError("%r must be of type 'str'" % self.storage_name)
>        elif len(value) == 0:
>            raise ValueError("%r must not be empty" % self.storage_name)
>        instance.__dict__[self.storage_name] = value
>
>class Customer:
>    name = NonBlank('name')
>    email = NonBlank('email')
>
>    def __init__(self, name, email, fidelity=0):
>        self.name = name
>        self.email = email
>        self.fidelity = fidelity
>
>    def full_email(self):
>        return '{0} <{1}>'.format(self.name, self.email)
>
>if __name__ == '__main__':
>    cus = Customer('Arup', 99)
>
>Running this code throws an error:
>
>Traceback (most recent call last):
>  File "/Users/aruprakshit/python_playground/pycon2017/decorators_and_descriptors_decoded/customer.py", line 25, in <module>
>    cus = Customer('Arup', 99)
>  File "/Users/aruprakshit/python_playground/pycon2017/decorators_and_descriptors_decoded/customer.py", line 18, in __init__
>    self.email = email
>  File "/Users/aruprakshit/python_playground/pycon2017/decorators_and_descriptors_decoded/customer.py", line 7, in __set__
>    raise TypeError("%r must be of type 'str'" % self.storage_name)
>TypeError: 'email' must be of type 'str'
>Process terminated with an exit code of 1
>
>Now I am not getting how the __set__() method from NonBlank is being 
>called inside the __init__() method. Looks like some magic is going on 
>under the hood. Can anyone please explain this how self.name and 
>self.email assignment is called the __set__ from NonBlank? What is the 
>name of this concept?

As Steven has mentioned, it looks like NonBlank is a descriptor, which 
defined here:

  https://docs.python.org/3/glossary.html#term-descriptor

So NonBlank has a __set__ method. The above text says:

   When a class attribute is a descriptor, its special binding behavior 
   is triggered upon attribute lookup. Normally, using a.b to get, set 
   or delete an attribute looks up the object named b in the class 
   dictionary for a, but if b is a descriptor, the respective descriptor 
   method gets called.

So when your new Customer object runs its __init_ method and goes:

  self.name = name

Since Customer.name is a descriptor, this effectively calls:

  NonBlank.__set__(self, name)

which in turn does some type and value checking and then directly 
modifies self.__dict__ to effect the assignment.

So yes, some magic is occurring - that is what the Python dunder methods 
are for: to provide the mechanism for particular magic actions.

Descriptors are rarely used directly, however the @property decorator is 
quite ommon, where you define methodlike functions which look like 
attributes. Untested example:

  class Foo:
    def __init__(self):
      self.timestamp = time.time()
    @property
    def age(self):
      return time.time() - self.timestamp

which you'd access directly as:

  foo = Foo()
  print("age =", foo.age)

@property arranges this using descriptors: in the example above it 
arranges that the class "age" attribute is a descriptor with a __get__ 
method.

Cheers,
Cameron Simpson <cs at cskk.id.au>


More information about the Tutor mailing list