Python装饰器用于属性的setter和deleter的机制是什么?

huangapple go评论82阅读模式
英文:

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

问题

Python属性的主题在这里得到了广泛的讨论,Python文档提供了一个纯Python实现,链接在这里。然而,我仍然不完全清楚装饰器功能本身的机制。更具体地说,对于具有相同名称的getter和setter x,在传递给@x.setter装饰器之前,setter函数对象x不会重写绑定到x的属性对象(从而使装饰器调用变得没有意义)。

考虑以下示例:

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

根据我对装饰器的理解(如果我理解错误,请纠正我),在getter定义中的@property后面跟着def x(self): ...等同于def x(self): ...后面跟着x = property(x)。然而,当我尝试用类构造函数调用语法的等效方式替换所有三个装饰器(同时保持函数名称相同)时,它停止工作,如下所示:

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)

这会导致在第14行(x = x.setter(x))出现AttributeError: 'function' object has no attribute 'setter'错误。这似乎是预期的,因为setter的def x(self, value)...应该覆盖上面的x = property(x)

我漏掉了什么?

英文:

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?

答案1

得分: 1

根据@kindall指出的,答案似乎在于装饰器:事实上,使用装饰器时,Python似乎不会将原始函数名称绑定到命名空间,而只是创建原始函数对象,然后在其上调用装饰器函数,仅绑定最终结果。这在答案这里中提到过,更好地解答在这里,两者都引用了PEP318,其中解释了:

@dec2
@dec1
def func(arg1, arg2, ...):
    pass

... 等同于:

def func(arg1, arg2, ...):
    pass
func = dec2(dec1(func))

尽管没有中间创建名为 func 的变量。

正如这里所建议的那样,可以使用 dis 模块来“反汇编”代码并查看实际执行的内容。以下是从对原始问题中的第一个示例代码运行的 dis 命令的输出摘录。 (这看起来像Python读取和解释类主体的部分:

<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

我们可以看到(根据我的理解)对于每个装饰的函数定义:

  • 内部函数被加载为代码对象:LOAD_CONST (<code object x at ...>
  • 转换为函数对象:MAKE_FUNCTION
  • 直接传递给装饰器调用而不绑定:PRECALL,然后是 CALL
  • 最后以最终形式绑定/存储:STORE_NAME

最后,这是一个看起来不太漂亮但有效的解决方案,尝试模仿这个装饰器行为,同时不使用装饰器并保留相同的原始函数名称(如最初在原始问题中寻求的):

from types import FunctionType

class C(object):
    def __init__(self):
        self._x = None

    x = property(
        FunctionType(
            code=compile(
                '''
def _(self):
    print("getter of x called")
    return self._x
                ''',
                '<string>',
                'exec').co_consts[0],
            globals=globals(),
        )
    )

    x = x.setter(
        FunctionType(
            code=compile(
                '''
def _(self, value):
    print("setter of x called")
    self._x = value
                ''',
                '<string>',
                'exec').co_consts[0],
            globals=globals(),
        )
    )

    x = x.deleter(
        FunctionType(
            code=compile(
                '''
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

希望有实际的CPython知识或良好的来源,可以编写或指向最接近Python底层工作的纯Python模拟装饰器行为的代码。

英文:

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 &lt;filename&gt;) 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 &lt;code object C at 0x00000212AAA1DB80, file     &quot;PracticeRun6/property1.py&quot;, line 1&gt;:
1           0 RESUME                   0
            2 LOAD_NAME                0 (__name__)
            4 STORE_NAME               1 (__module__)
            6 LOAD_CONST               0 (&#39;C&#39;)
            8 STORE_NAME               2 (__qualname__)

2          10 LOAD_CONST               1 (&lt;code object __init__ at 0x00000212AACA5BD0, file &quot;PracticeRun6/property1.py&quot;, line 2&gt;)
            12 MAKE_FUNCTION            0
            14 STORE_NAME               3 (__init__)

5          16 LOAD_NAME                4 (property)

6          18 LOAD_CONST               2 (&lt;code object x at 0x00000212AAA234B0, file &quot;PracticeRun6/property1.py&quot;, line 5&gt;)
            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 (&lt;code object x at 0x00000212AAA235A0, file &quot;PracticeRun6/property1.py&quot;, line 11&gt;)
            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 (&lt;code object x at 0x00000212AA952CD0, file &quot;PracticeRun6/property1.py&quot;, line 16&gt;)
            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 (&lt;code object x at ...&gt;
  • 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&quot;&quot;&quot;
def _(self):
    print(&quot;getter of x called&quot;)
    return self._x
                &quot;&quot;&quot;,
                &#39;&lt;string&gt;&#39;,
                &#39;exec&#39;).co_consts[0],
            globals=globals(),
        )
    )

    x = x.setter(
        FunctionType(
            code=compile(
                r&quot;&quot;&quot;
def _(self, value):
    print(&quot;setter of x called&quot;)
    self._x = value
                &quot;&quot;&quot;,
                &#39;&lt;string&gt;&#39;,
                &#39;exec&#39;).co_consts[0],
            globals=globals(),
        )
    )

    x = x.deleter(
        FunctionType(
            code=compile(
                r&quot;&quot;&quot;
def _(self):
    print(&quot;deleter of x called&quot;)
    del self._x
                &quot;&quot;&quot;,
                &#39;&lt;string&gt;&#39;,
                &#39;exec&#39;).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.

答案2

得分: 1

以下是您要翻译的部分:

无法仅因为局部变量 x 被下一个函数覆盖而这样做,正如您所说。

下面的代码有效:

class C(object):
    def __init__(self):
        self._x = None

    def x(self):
        &quot;&quot;&quot;我是 &#39;x&#39; 属性。&quot;&quot;&quot;
        print(&quot;调用 x 的 getter&quot;)
        return self._x
    x = property(x)
    
    def x_setter(self, value):
        print(&quot;调用 x 的 setter&quot;)
        self._x = value
    x = x.setter(x_setter)

    def x_deleter(self):
        print(&quot;调用 x 的 deleter&quot;)
        del self._x
    x = x.deleter(x_deleter)

装饰器方法与这种方法之间的区别在于装饰器将匿名函数作为参数,并且名称无关紧要,但在直接赋值的情况下,当覆盖变量时会失败。

英文:

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):
        &quot;&quot;&quot;I&#39;m the &#39;x&#39; property.&quot;&quot;&quot;
        print(&quot;getter of x called&quot;)
        return self._x
    x = property(x)
    
    def x_setter(self, value):
        print(&quot;setter of x called&quot;)
        self._x = value
    x = x.setter(x_setter)

    def x_deleter(self):
        print(&quot;deleter of x called&quot;)
        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.

huangapple
  • 本文由 发表于 2023年7月10日 20:48:34
  • 转载请务必保留本文链接:https://go.coder-hub.com/76653905.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定