The subject of Python properties is covered extensively here, and the Python documentation provides a pure Python implementation here. However, I am still not fully clear on the mechanics of the decorator functionality itself. More specifically, for identically named getters and setters x
, how does the setter function object x
(before being passed to the @x.setter decorator) not end-up rewriting the property object bound to x
(thus making the decorator call meaningless)?
Consider the following example:
class C(object):
def __init__(self):
self._x = None
@property
def x(self):
"""I'm the 'x' property."""
print("getter of x called")
return self._x
@x.setter
def x(self, value):
print("setter of x called")
self._x = value
@x.deleter
def x(self):
print("deleter of x called")
del self._x
From what I understand about decorators (please correct me if I'm wrong), @property
followed by def x(self): ...
in the getter definition is the pure equivalent of def x(self): ...
followed by x = property(x)
. Yet, when I try to replace all three decorators with the class constructor call syntax equivalent (while still keeping the function names identical), it stops working, like so:
class C(object):
def __init__(self):
self._x = None
def x(self):
"""I'm the 'x' property."""
print("getter of x called")
return self._x
x = property(x)
def x(self, value):
print("setter of x called")
self._x = value
x = x.setter(x)
def x(self):
print("deleter of x called")
del self._x
x = x.deleter(x)
... which results in AttributeError: 'function' object has no attribute 'setter'
on line 14 (x = x.setter(x)
).
This seems expected, since def x(self, value)...
for the setter should overwrite x = property(x)
from above.
What am I missing?
As pointed out by @kindall, the answer seems to lie in the decorator: and the fact that with it, Python does not seem to bind the raw function name to the namespace, but simply creates the raw function object, then calls the decorator function on it, and only binds the final result. This is touched upon in the answer here, answered much better here, both citing PEP318 which explains that:
@dec2
@dec1
def func(arg1, arg2, ...):
pass
... is equivalent to:
def func(arg1, arg2, ...):
pass
func = dec2(dec1(func))
though without the intermediate creation of a variable named func
.
As suggested here, this seems to be also directly evidenced by using the dis
module to "disassemble" the code and see what is actually executing. Here is the excerpt from the output of dis command (python -m dis <filename>
) ran on the code from the first example of the original question above. (This looks like the part where Python reads and interprets the class body:
Disassembly of <code object C at 0x00000212AAA1DB80, file "PracticeRun6/property1.py", line 1>:
1 0 RESUME 0
2 LOAD_NAME 0 (__name__)
4 STORE_NAME 1 (__module__)
6 LOAD_CONST 0 ('C')
8 STORE_NAME 2 (__qualname__)
2 10 LOAD_CONST 1 (<code object __init__ at 0x00000212AACA5BD0, file "PracticeRun6/property1.py", line 2>)
12 MAKE_FUNCTION 0
14 STORE_NAME 3 (__init__)
5 16 LOAD_NAME 4 (property)
6 18 LOAD_CONST 2 (<code object x at 0x00000212AAA234B0, file "PracticeRun6/property1.py", line 5>)
20 MAKE_FUNCTION 0
5 22 PRECALL 0
26 CALL 0
6 36 STORE_NAME 5 (x)
11 38 LOAD_NAME 5 (x)
40 LOAD_ATTR 6 (setter)
12 50 LOAD_CONST 3 (<code object x at 0x00000212AAA235A0, file "PracticeRun6/property1.py", line 11>)
52 MAKE_FUNCTION 0
11 54 PRECALL 0
58 CALL 0
12 68 STORE_NAME 5 (x)
16 70 LOAD_NAME 5 (x)
72 LOAD_ATTR 7 (deleter)
17 82 LOAD_CONST 4 (<code object x at 0x00000212AA952CD0, file "PracticeRun6/property1.py", line 16>)
84 MAKE_FUNCTION 0
16 86 PRECALL 0
90 CALL 0
17 100 STORE_NAME 5 (x)
102 LOAD_CONST 5 (None)
104 RETURN_VALUE
We can see (from what I understand) that for each decorated function definition:
LOAD_CONST (<code object x at ...>
MAKE_FUNCTION
PRECALL
followed by CALL
STORE_NAME
.Finally, here is my ugly-looking but working (!) solution that tries to emulate this decorator behavior all while not using decorators and keeping the same raw function names (as initially sought in the original qeustion):
from types import FunctionType
class C(object):
def __init__(self):
self._x = None
x = property(
FunctionType(
code=compile(
r"""
def _(self):
print("getter of x called")
return self._x
""",
'<string>',
'exec').co_consts[0],
globals=globals(),
)
)
x = x.setter(
FunctionType(
code=compile(
r"""
def _(self, value):
print("setter of x called")
self._x = value
""",
'<string>',
'exec').co_consts[0],
globals=globals(),
)
)
x = x.deleter(
FunctionType(
code=compile(
r"""
def _(self):
print("deleter of x called")
del self._x
""",
'<string>',
'exec').co_consts[0],
globals=globals(),
)
)
c = C()
c.x = 120
print(c.x)
del c.x
Hopefully, someone with actual knowledge of CPython, or a good source, can write or point to an actual pure Python emulation of Decorator behavior, that most closely resembles what Python does under the hood.