On 29Apr2019 23:25, Arup Rakshit <a...@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 <c...@cskk.id.au>
_______________________________________________
Tutor maillist  -  Tutor@python.org
To unsubscribe or change subscription options:
https://mail.python.org/mailman/listinfo/tutor

Reply via email to