3

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?

Vahan
  • 51
  • 6
  • You've defined three functions with the same name, and only one (the last one defined) actually ends up on the class. A property is an instance of the `property` class and it's that class that has the set and get and delete methods, not the class it's attached to. – kindall Jul 10 '23 at 16:09
  • @kindall That's not quite true. Yes, the property class is the one which uses the get, set, and delete methods, but the methods themselves need to be defined by the user _somewhere_ (usually in the class that uses the property), so that they can be passed to the `property` instance's respective `self.fget`, `self.fset`, and `self.fdel` attributes: either via class constructor (which has the signature of `property(fget=None, fset=None, fdel=None, doc=None)`), or via the `getter`, `setter`, and `deleter` instance methods. It's these latter methods that are called with the decorator syntax. – Vahan Jul 10 '23 at 22:51
  • 1
    The `@property` decorator sets `x` all in one go, without going through an intermediate assignment of intermediate values (i.e. the setter/deleter functions) that would overwrite `x`. [This question is a direct duplicate](https://stackoverflow.com/questions/62952223/how-does-property-decorator-work-internally-using-syntactic-sugar-in-python). – metatoaster Jul 11 '23 at 02:47

2 Answers2

1

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.

Vahan
  • 51
  • 6
1

You can't do this just because the local variable x is overridden by the next function, as you said.

The next code is working:

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_setter(self, value):
        print("setter of x called")
        self._x = value
    x = x.setter(x_setter)

    def x_deleter(self):
        print("deleter of x called")
        del self._x
    x = x.deleter(x_deleter)

The difference between the decorator method to this method, is that the decorator get anonymous function as an argument and the name doesn't matter, but in the straight forward assignment it's failed when you override your variable.