James Franco James Franco - 1 month ago 8
Python Question

Understanding the descriptor example from "descriptors howto" article

I am trying to wrap my head around the concept of descriptors and I have had no success lately. This article of descriptor How to has really helped and at the same time it has also confused me. I am struggling with this example here
on why

m.x
calls
def __get__(self, obj, objtype):


class RevealAccess(object):
def __init__(self, initval=None, name='var'):
self.val = initval
self.name = name

def __get__(self, obj, objtype):
print('Retrieving', self.name)
return self.val

def __set__(self, obj, val):
print('Updating', self.name)
self.val = val

>>> class MyClass(object):
... x = RevealAccess(10, 'var "x"')
... y = 5
...
>>> m = MyClass()
>>> m.x
Retrieving var "x"
10


I believe the reason i am struggling with this is because I am confused by the following statement in the article


The details of invocation depend on whether obj is an object or a
class. For objects, the machinery is in
object.__getattribute__()

which transforms
b.x
into
type(b).__dict__['x'].__get__(b, type(b))
.
The implementation works through a precedence chain that gives data
descriptors priority over instance variables, instance variables
priority over non-data descriptors, and assigns lowest priority to
__getattr__()
if provided


I am not sure what the author means here by object or class. Does object mean an instance of a class ? I understand that when we do
instance.var
python internally does a
instance.__dict__["var"]
if that is not found then it does
class.__dict__["var"]
. I kind of lost it after that concept. Could anyone explain a little on how this example is working. How is the definition
def __get__(self, obj, objtype):
being called with
m.x
. I would be very grateful if anyone could clear this up.

Answer

Yes, by object the author means an instance, not a class. The distinction comes from where the descriptor lives; the descriptor is defined on the class, so when accessing that descriptor directly on the class a different path is followed from when you access that descriptor on an instance of the class.

In your example, m is an instance. That instance itself has no attribute m ('x' in m.__dict__ is False). Python also looks at type(m) to resolve that attribute, and type(m) here is MyClass. 'x' in MyClass.__dict__ is True, and MyClass.__dict__['x'].__get__ exists, so Python now knows that that object is a descriptor.

So, because there is no m.__dict__['x'], but there is a type(m).__dict__['x'].__get__, that method is called with m and type(m) as arguments, leading to type(m).__dict__['x'].__get__(m, type(m)).

If 'x' in MyClass.__dict__ is true but MyClass.__dict__['x'].__get__ did not exist (so that object is not a descriptor object), then MyClass.__dict__['x'] would have been returned directly.

The situation gets more interesting if you tried to add the x attribute to the instance too. In that case it matters if MyClass.__dict__['x'].__set__ or MyClass.__dict__['x'].__delete__ exist, making the descriptor a data descriptor. Data descriptors always win in the case of a tie. So if MyClass.__dict__['x'].__get__ and at least one of MyClass.__dict__['x'].__set__ or MyClass.__dict__['x'].__delete__ exist, it doesn't matter anymore if m.__dict__['x'] also exists. Python won't even look for it.

However, if there are no __set__ or __delete__ methods on the descriptor object on the class, then m.__dict__['x'] wins and is returned. In that case the descriptor is a regular, non-data descriptor and it loses out to the instance attribute.

Last but not least, if 'x' in type(m).__dict__ is false (there is no object on the class), then m.__dict__['m'] is returned. The descriptor protocol is only applied to objects found on the class, never to attributes on the instance.