Question

In a process of coding I have been faced with the need to change behavior of object's property (but NOT a class property). And I found that already initialized object cannot be patched with descriptor. So why?

code examples

class A(object):
    pass

class D(object):
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        pass
    def __get__(self, obj, objtype=None):
        return 5

A.x = D()
A.x
Out[12]: 5 # work!
a = A()
a.y = D()
a.y
Out[14]: <__main__.D at 0x13a8d90> # not work!
Was it helpful?

Solution

From the documentation.

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

That is, the attribute lookup on an instance (b) is converted into a descriptor call on the class (type(b)). Descriptors operate at the class level.

As for why this is true, it's because descriptors are basically a way to do method-like work (i.e., call a method) on attribute lookup. And methods are essentially class-level behavior: you generally define the methods you want on a class, and you don't add extra methods to individual instances. Doing descriptor lookup on an instance would be like defining a method on an instance.

Now, it is possible to assign new methods to instances, and it's also possible to get descriptors to work on instances of a particular class. You just have to do extra work. As the documentation quote above says, the machinery is in object.__getattribute__, so you can override it by defining a custom __getattribute__ on your class:

class Foo(object):
    def __getattribute__(self, attr):
        myDict = object.__getattribute__(self, '__dict__')
        if attr in myDict and hasattr(myDict[attr], '__get__'):
            return myDict[attr].__get__(self, type(self))
        else:
            return super(Foo, self).__getattribute__(attr)

class D(object):
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        pass
    def __get__(self, obj, objtype=None):
        return 5

And then:

>>> f = Foo()
>>> f.x = D()
>>> f.x
5

So if you feel the need to do this, you can make it happen. It's just not the default behavior, simply because it's not what descriptors were designed to do.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top