I've been reading about descriptors in the Descriptor HowTo Guide, and I'm confused by this sentence:
If an instance’s dictionary has an entry with the same name as a data descriptor, the data descriptor takes precedence.
How can the dictionary contain two items (a normal entry and a data descriptor) with the same name? Or are attributes that are descriptors not stored in __dict__
?
The most accessible technique is to use the property function to define get, set and delete methods associated with an attribute name. The property function builds descriptors for you. A slightly less accessible, but more extensible and reusable technique is to define descriptor classes yourself.
Attributes of a class are function objects that define corresponding methods of its instances. They are used to implement access controls of the classes.
Descriptors are Python objects that implement a method of the descriptor protocol, which gives you the ability to create objects that have special behavior when they're accessed as attributes of other objects.
“Descriptors” are objects that describe some attribute of an object. They are found in the dictionary of type objects.
The data descriptor lives in the class namespace, while the instance attribute lives in the instance namespace (so instance.__dict__
). These are two separate dictionaries, so there is no conflict here.
So for any given attribute lookup for the name foo
on an instance bar
, Python also looks at it's class (type(bar)
, named C
below), in the following order:
C.foo
is looked up. If it is a data descriptor, this is where the lookup ends. C.foo.__get__(bar, C)
is returned. Otherwise, Python will store this result for step 3 (no point in looking this up twice).
If C.foo
did not exist or is a regular attribute, then Python looks for bar.__dict__['foo']
. If it exists, it is returned. Note that this part is never reached if C.foo
is a data descriptor!
If bar.__dict__['foo']
does not exist, but C.foo
exists, then C.foo
is used. If C.foo
is a (non-data) descriptor, thet C.foo.__get__(bar, C)
is returned.
(Note that C.foo
is really C.__dict__['foo']
, but for simplicity sake I've ignored descriptor access on classes in the above).
Perhaps a concrete example helps; here are two descriptors, one is a data descriptor (there is a __set__
method), and the other is not a data descriptor:
>>> class DataDesc(object):
... def __get__(self, inst, type_):
... print('Accessed the data descriptor')
... return 'datadesc value'
... def __set__(self, inst, value):
... pass # just here to make this a data descriptor
...
>>> class OtherDesc(object):
... def __get__(self, inst, type_):
... print('Accessed the other, non-data descriptor')
... return 'otherdesc value'
...
>>> class C(object):
... def __init__(self):
... # set two instance attributes, direct access to not
... # trigger descriptors
... self.__dict__.update({
... 'datadesc': 'instance value for datadesc',
... 'otherdesc': 'instance value for otherdesc',
... })
... datadesc = DataDesc()
... otherdesc = OtherDesc()
...
>>> bar = C()
>>> bar.otherdesc # non-data descriptor, the instance wins
'instance value for otherdesc'
>>> bar.datadesc # data descriptor, the descriptor wins
Accessed the data descriptor
'datadesc value'
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With