I would like to understand how the built-in function property
works. What confuses me is that property
can also be used as a decorator, but it only takes arguments when used as a built-in function and not when used as a decorator.
This example is from the documentation:
class C: def __init__(self): self._x = None def getx(self): return self._x def setx(self, value): self._x = value def delx(self): del self._x x = property(getx, setx, delx, "I'm the 'x' property.")
property
‘s arguments are getx
, setx
, delx
and a doc string.
In the code below property
is used as a decorator. The object of it is the x
function, but in the code above there is no place for an object function in the arguments.
class C: def __init__(self): self._x = None @property def x(self): """I'm the 'x' property.""" return self._x @x.setter def x(self, value): self._x = value @x.deleter def x(self): del self._x
How are the x.setter
and x.deleter
decorators created in this case?
Advertisement
Answer
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__)