Search code examples
pythonpropertiesdecoratorgetter-setterdescriptor

What are the mechanics of Python decorator for property setter and deleter?


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?


Solution

  • 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:

    • the inner function is loaded as a code object: LOAD_CONST (<code object x at ...>
    • made into a function object: MAKE_FUNCTION
    • passed straight to the decorator call without being bound: PRECALL followed by CALL
    • and finally bound/stored in final form: 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.