How does the @property decorator work in Python?
The property()
function returns a special descriptor object:
>>> property()<property object at 0x10ff07940>
It is this object that has extra methods:
>>> property().getter<built-in method getter of property object at 0x10ff07998>>>> property().setter<built-in method setter of property object at 0x10ff07940>>>> property().deleter<built-in method deleter of property object at 0x10ff07998>
These act as decorators too. They return a new property object:
>>> property().getter(None)<property object at 0x10ff079f0>
that is a copy of the old object, but with one of the functions replaced.
Remember, that the @decorator
syntax is just syntactic sugar; the syntax:
@propertydef foo(self): return self._foo
really means the same thing as
def foo(self): return self._foofoo = property(foo)
so foo
the function is replaced by property(foo)
, which we saw above is a special object. Then when you use @foo.setter()
, what you are doing is call that property().setter
method I showed you above, which returns a new copy of the property, but this time with the setter function replaced with the decorated method.
The following sequence also creates a full-on property, by using those decorator methods.
First we create some functions and a property
object with just a getter:
>>> def getter(self): print('Get!')... >>> def setter(self, value): print('Set to {!r}!'.format(value))... >>> def deleter(self): print('Delete!')... >>> prop = property(getter)>>> prop.fget is getterTrue>>> prop.fset is NoneTrue>>> prop.fdel is NoneTrue
Next we use the .setter()
method to add a setter:
>>> prop = prop.setter(setter)>>> prop.fget is getterTrue>>> prop.fset is setterTrue>>> prop.fdel is NoneTrue
Last we add a deleter with the .deleter()
method:
>>> prop = prop.deleter(deleter)>>> prop.fget is getterTrue>>> prop.fset is setterTrue>>> prop.fdel is deleterTrue
Last but not least, the property
object acts as a descriptor object, so it has .__get__()
, .__set__()
and .__delete__()
methods to hook into instance attribute getting, setting and deleting:
>>> class Foo: pass... >>> prop.__get__(Foo(), Foo)Get!>>> prop.__set__(Foo(), 'bar')Set to 'bar'!>>> prop.__delete__(Foo())Delete!
The Descriptor Howto includes a pure Python sample implementation of the property()
type:
class Property: "Emulate PyProperty_Type() in Objects/descrobject.c" def __init__(self, fget=None, fset=None, fdel=None, doc=None): self.fget = fget self.fset = fset self.fdel = fdel if doc is None and fget is not None: doc = fget.__doc__ self.__doc__ = doc def __get__(self, obj, objtype=None): if obj is None: return self if self.fget is None: raise AttributeError("unreadable attribute") return self.fget(obj) def __set__(self, obj, value): if self.fset is None: raise AttributeError("can't set attribute") self.fset(obj, value) def __delete__(self, obj): if self.fdel is None: raise AttributeError("can't delete attribute") self.fdel(obj) def getter(self, fget): return type(self)(fget, self.fset, self.fdel, self.__doc__) def setter(self, fset): return type(self)(self.fget, fset, self.fdel, self.__doc__) def deleter(self, fdel): return type(self)(self.fget, self.fset, fdel, self.__doc__)
Documentation says it's just a shortcut for creating readonly properties. So
@propertydef x(self): return self._x
is equivalent to
def getx(self): return self._xx = property(getx)
Here is a minimal example of how @property
can be implemented:
class Thing: def __init__(self, my_word): self._word = my_word @property def word(self): return self._word>>> print( Thing('ok').word )'ok'
Otherwise word
remains a method instead of a property.
class Thing: def __init__(self, my_word): self._word = my_word def word(self): return self._word>>> print( Thing('ok').word() )'ok'