Search code examples
pythonpython-3.xpropertiesdecorator

Why can't I assign a property to self in Python?


I want to create an attribute in class that connected to another attribute. for example biz is bar+1.

The next code is working:

class Foo:
     def __init__(self, bar, biz=None):
         self.bar = bar

     biz = property(lambda self: self.bar + 1)

print(Foo(0).biz+1)

and print 2. But when I move the biz declaretion into the initializer, for example to apply a condition, I get an error:

class Foo:
     def __init__(self, bar, biz=None):
         self.bar = bar
         if biz is None:
            self.biz = property(lambda self: self.bar + 1)
         else:
            self.biz = biz

print(Foo(0).biz+1)

return TypeError: unsupported operand type(s) for +: 'property' and 'int'

Why python relate to the attribute as 'property' type and not as 'int'?

EDIT: I found that if I use self.__class__.biz=property(...) it's working, but I'm still asking why I can't use property locally?


Solution

  • Properties are implemented using descriptors. As per the docs:

    A descriptor is what we call any object that defines __get__(), __set__(), or __delete__().

    Optionally, descriptors can have a __set_name__() method. This is only used in cases where a descriptor needs to know either the class where it was created or the name of class variable it was assigned to. (This method, if present, is called even if the class is not a descriptor.)

    Descriptors get invoked by the dot operator during attribute lookup. If a descriptor is accessed indirectly with vars(some_class)[descriptor_name], the descriptor instance is returned without invoking it.

    Descriptors only work when used as class variables. When put in instances, they have no effect.

    The main motivation for descriptors is to provide a hook allowing objects stored in class variables to control what happens during attribute lookup.

    Traditionally, the calling class controls what happens during lookup. Descriptors invert that relationship and allow the data being looked-up to have a say in the matter.

    Descriptors are used throughout the language. It is how functions turn into bound methods. Common tools like classmethod(), staticmethod(), property(), and functools.cached_property() are all implemented as descriptors.

    In the first case, you set the property on the class level, which is why it works. This is also why it works when you do self.__class__.biz=property(...). However, you should not modify the class definition from an instance constructor, as creating multiple instances will overwrite the property being set on the class.

    To achieve your goal, I would instead write the property like this:

    class Foo:
        def __init__(self, bar, biz=None):
            self.bar = bar
            self._biz = biz
    
        @property
        def biz(self):
            if self._biz is None:
                return self.bar + 1
            else:
                return self._biz