How does the @property decorator work in Python?
The documentation says it's just a shortcut for creating read-only properties. So
@property
def x(self):
return self._x
is equivalent to
def getx(self):
return self._x
x = property(getx)
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:
@property
def foo(self): return self._foo
really means the same thing as
def foo(self): return self._foo
foo = 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 getter
True
>>> prop.fset is None
True
>>> prop.fdel is None
True
Next we use the .setter()
method to add a setter:
>>> prop = prop.setter(setter)
>>> prop.fget is getter
True
>>> prop.fset is setter
True
>>> prop.fdel is None
True
Last we add a deleter with the .deleter()
method:
>>> prop = prop.deleter(deleter)
>>> prop.fget is getter
True
>>> prop.fset is setter
True
>>> prop.fdel is deleter
True
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__)
The first part is simple:
@property
def x(self): ...
is the same as
def x(self): ...
x = property(x)
- which, in turn, is the simplified syntax for creating a
property
with just a getter.
The next step would be to extend this property with a setter and a deleter. And this happens with the appropriate methods:
@x.setter
def x(self, value): ...
returns a new property which inherits everything from the old x
plus the given setter.
x.deleter
works the same way.
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'