James Franco
James Franco

Reputation: 4706

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.

Upvotes: 1

Views: 219

Answers (2)

Martijn Pieters
Martijn Pieters

Reputation: 1121654

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.

Upvotes: 2

BrenBarn
BrenBarn

Reputation: 251373

Does object mean an instance of a class ?

Yes.

How is the definition def __get__(self, obj, objtype): being called with m.x.

Because of just what the documentation you quoted says:

For objects, the machinery is in object.__getattribute__() which transforms b.x into type(b).__dict__['x'].__get__(b, type(b)).

object.__getattribute__ is mechanism that implements what happens when you do something like m.x. So, explicitly:

m.x
type(m).__dict__['x'].__get__(m, type(m))
MyClass.__dict__['x'].__get__(m, MyClass)
RevealAccess(10, 'var "x"').__get__(m, MyClass)

so the last line calls the __get__ method of the RevealAccess class.

Upvotes: 1

Related Questions