[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