Sheng Wang
Sheng Wang

Reputation: 33

The different behavior for Class.foo and instance.foo when using property in Python

##### Case 1, use property #####
class Case1:
    # ignore getter and setter for property
    var = property(getter, setter, None, None)

##### Case 2, use equivalent methods ####
class Descriptor:
    def __get__(self, obj, type=None):
        return None
    def __set__(self, obj, val):
        pass

class Case2:
     var = Descriptor()

My question is:

When I use 'property' to control the access of one variable,

instance.var will correctly return the real value, while Class.var will return the property object itself (e.g. property object at 0x7fc5215ac788)

But when I use equivalent methods (e.g. descriptor) and override __get__ and __set__ methods, both instance.var and Class.var can return the real value instead of the object itself.

So why they behave so differently?

I guess it is because some of default functions implemented in the my descriptor make the magic, so what are they?


update:

The reason for the above question is that __get__ function implemented in the property will determine if it is called by instance or Class, and when it is called by Class, it will return the object itself (i.e. self).

But as __set__ function does not have type or cls parameter, and based on my test, Class.var = 5 cannot be caught by __set__ function. Therefore, I wonder what hooks we can use to customize the class variable level assignment Class.var = value?

Upvotes: 1

Views: 109

Answers (1)

bruno desthuilliers
bruno desthuilliers

Reputation: 77892

When you do MyClass.some_descriptor, there's (obviously) no instance to be passed to the descriptor, so it is invoked with obj=None:

>>> class Desc(object):
...     def __get__(self, obj, cls=None):
...         print "obj : {} - cls : {}".format(obj, cls)
...         return 42
... 
>>> class Foo(object):
...    bar = Desc()
... 
>>> Foo.bar
obj : None - cls : <class '__main__.Foo'>
42
>>> Foo().bar
obj : <__main__.Foo object at 0x7fd285cf4a90> - cls : <class '__main__.Foo'>
42
>>> 

In most cases (and specially with the generic property descriptor) the goal is to compute the return value based on instance attributes so there's not much you can return without the instance. In this case, most authors choose to return the descriptor instance itself so it can be correctly identified for what it is when inspecting the class.

If you want this behaviour (which makes sense for most descriptors), you just have to test obj against None and return self:

>>> class Desc2(object):
...     def __get__(self, obj, cls=None):
...         if obj is None:
...             return self
...         return 42
... 
>>> Foo.baaz = Desc2()
>>> Foo.baaz
<__main__.Desc2 object at 0x7fd285cf4b10>
>>> Foo().baaz
42
>>> 

And that's all the "magic" involved .

Now if you wonder why this is not the default: there are use cases for returning something else for a descriptor looked up on a class - methods for example (yes, Python functions are descriptors - their __get__ method returns a method object, which is actually a callable wrapper around the instance (if any), class and function):

>>> Foo.meth = lambda self: 42
>>> Foo.meth
<unbound method Foo.<lambda>>
>>> Foo().meth
<bound method Foo.<lambda> of <__main__.Foo object at 0x7fd285cf4bd0>>
>>> Foo.meth(Foo())
42

Upvotes: 1

Related Questions