与属性类相比,使用属性装饰器是否有优势?

Mar*_*oma 5 python properties python-decorators

我可以看到两种在Python中具有属性的非常相似的方法

(a)财产类别

class Location(object):

    def __init__(self, longitude, latitude):
        self.set_latitude(latitude)
        self.set_longitude(longitude)

    def set_latitude(self, latitude):
        if not (-90 <= latitude <= 90):
            raise ValueError('latitude was {}, but has to be in [-90, 90]'
                             .format(latitude))
        self._latitude = latitude

    def set_longitude(self, longitude):
        if not (-180 <= longitude <= 180):
            raise ValueError('longitude was {}, but has to be in [-180, 180]'
                             .format(longitude))
        self._longitude = longitude

    def get_longitude(self):
        return self._latitude

    def get_latitude(self):
        return self._longitude

    latitude = property(get_latitude, set_latitude)
    longitude = property(get_longitude, set_longitude)
Run Code Online (Sandbox Code Playgroud)

(b)物业装饰员

class Location(object):

    def __init__(self, longitude, latitude):
        self.latitude = latitude
        self.longitude = latitude

    @property
    def latitude(self):
        """I'm the 'x' property."""
        return self._latitude

    @property
    def longitude(self):
        """I'm the 'x' property."""
        return self._longitude

    @latitude.setter
    def latitude(self, latitude):
        if not (-90 <= latitude <= 90):
            raise ValueError('latitude was {}, but has to be in [-90, 90]'
                             .format(latitude))
        self._latitude = latitude

    @longitude.setter
    def longitude(self, longitude):
        if not (-180 <= longitude <= 180):
            raise ValueError('longitude was {}, but has to be in [-180, 180]'
                             .format(longitude))
        self._longitude = longitude
Run Code Online (Sandbox Code Playgroud)

这两段代码是否相同(例如字节码)?他们表现出同样的行为吗?

是否有任何"风格"使用的官方指南?

一个是否有任何真正的优势?

我试过的

py_compile + uncompyle6

我编译了两个:

>>> import py_compile
>>> py_compile.compile('test.py')
Run Code Online (Sandbox Code Playgroud)

然后用uncompyle6反编译.但那刚刚回归我的开始(格式有点不同)

import + dis

我试过了

import test  # (a)
import test2  # (b)
dis.dis(test)
dis.dis(test2)
Run Code Online (Sandbox Code Playgroud)

我对以下输出感到非常困惑test2:

Disassembly of Location:
Disassembly of __init__:
 13           0 LOAD_FAST                2 (latitude)
              2 LOAD_FAST                0 (self)
              4 STORE_ATTR               0 (latitude)

 14           6 LOAD_FAST                2 (latitude)
              8 LOAD_FAST                0 (self)
             10 STORE_ATTR               1 (longitude)
             12 LOAD_CONST               0 (None)
             14 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

而第一个更大:

Disassembly of Location:
Disassembly of __init__:
 13           0 LOAD_FAST                0 (self)
              2 LOAD_ATTR                0 (set_latitude)
              4 LOAD_FAST                2 (latitude)
              6 CALL_FUNCTION            1
              8 POP_TOP

 14          10 LOAD_FAST                0 (self)
             12 LOAD_ATTR                1 (set_longitude)
             14 LOAD_FAST                1 (longitude)
             16 CALL_FUNCTION            1
             18 POP_TOP
             20 LOAD_CONST               0 (None)
             22 RETURN_VALUE

Disassembly of set_latitude:
 17           0 LOAD_CONST               3 (-90)
              2 LOAD_FAST                1 (latitude)
              4 DUP_TOP
              6 ROT_THREE
              8 COMPARE_OP               1 (<=)
             10 JUMP_IF_FALSE_OR_POP    18
             12 LOAD_CONST               1 (90)
             14 COMPARE_OP               1 (<=)
             16 JUMP_FORWARD             4 (to 22)
        >>   18 ROT_TWO
             20 POP_TOP
        >>   22 POP_JUMP_IF_TRUE        38

 18          24 LOAD_GLOBAL              0 (ValueError)
             26 LOAD_CONST               2 ('latitude was {}, but has to be in [-90, 90]')
             28 LOAD_ATTR                1 (format)
             30 LOAD_FAST                1 (latitude)
             32 CALL_FUNCTION            1
             34 CALL_FUNCTION            1
             36 RAISE_VARARGS            1

 19     >>   38 LOAD_FAST                1 (latitude)
             40 LOAD_FAST                0 (self)
             42 STORE_ATTR               2 (latitude)
             44 LOAD_CONST               0 (None)
             46 RETURN_VALUE

Disassembly of set_longitude:
 22           0 LOAD_CONST               3 (-180)
              2 LOAD_FAST                1 (longitude)
              4 DUP_TOP
              6 ROT_THREE
              8 COMPARE_OP               1 (<=)
             10 JUMP_IF_FALSE_OR_POP    18
             12 LOAD_CONST               1 (180)
             14 COMPARE_OP               1 (<=)
             16 JUMP_FORWARD             4 (to 22)
        >>   18 ROT_TWO
             20 POP_TOP
        >>   22 POP_JUMP_IF_TRUE        38

 23          24 LOAD_GLOBAL              0 (ValueError)
             26 LOAD_CONST               2 ('longitude was {}, but has to be in [-180, 180]')
             28 LOAD_ATTR                1 (format)
             30 LOAD_FAST                1 (longitude)
             32 CALL_FUNCTION            1
             34 CALL_FUNCTION            1
             36 RAISE_VARARGS            1

 24     >>   38 LOAD_FAST                1 (longitude)
             40 LOAD_FAST                0 (self)
             42 STORE_ATTR               2 (longitude)
             44 LOAD_CONST               0 (None)
             46 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

这种差异来自哪里?第一个例子的值范围检查在哪里?

Mar*_*ers 9

你总是想使用装饰器。其他语法没有任何优点,只有缺点。

\n\n

装饰器的要点

\n\n

这是因为装饰器语法是专门为了避免其他语法而发明的。您发现的任何此类示例name = property(...)通常都在装饰器出现之前的代码中。

\n\n

装饰器语法是语法糖;表格

\n\n
@decorator\ndef functionname(...):\n    # ...\n
Run Code Online (Sandbox Code Playgroud)\n\n

执行起来很像

\n\n
def functionname(...):\n    # ...\n\nfunctionname = decorator(functionname)\n
Run Code Online (Sandbox Code Playgroud)\n\n

无需functionname分配两次(该def functionname(...)部分创建一个函数对象并正常分配functionname,但使用装饰器时,会创建函数对象并将其直接传递给装饰器对象)。

\n\n

Python 添加此功能是因为当您的函数体很长时,您无法轻易看到该函数已被装饰器包装。您必须向下滚动到函数定义才能看到这一点,当您想了解的有关函数的几乎所有其他内容都位于顶部时,这并不是很有帮助;参数、名称、文档字符串就在那里。

\n\n

来自原始PEP 318 \xe2\x80\x93函数和方法装饰器规范:

\n\n
\n

当前对函数或方法应用转换的方法将实际转换放在函数体之后。对于大型函数,这将函数行为的关键组件与函数外部接口的其余部分的定义分开。

\n\n

[...]

\n\n

对于较长的方法,这会变得不太可读。对于概念上的单个声明来说,将函数命名三次似乎也不太符合 Python 风格。

\n
\n\n

并在设计目标下:

\n\n
\n

新语法应该

\n\n
    \n
  • [...]
  • \n
  • 从当前隐藏的函数末尾移动到更靠近您的前面
  • \n
\n
\n\n

所以使用

\n\n
@property\ndef latitude(self):\n    # ...\n\n@latitude.setter\ndef latitude(self, latitude):\n    # ...\n
Run Code Online (Sandbox Code Playgroud)\n\n

比以下内容更具可读性和自记录性

\n\n
def get_latitude(self):\n    # ...\n\ndef set_latitude(self, latitude):\n    # ...\n\nlatitude = property(get_latitude, set_latitude)\n
Run Code Online (Sandbox Code Playgroud)\n\n

无命名空间污染

\n\n

接下来,因为@property装饰器用装饰结果(实例property)替换了你装饰的函数对象,所以你也避免了命名空间污染。如果没有@propertyand@<name>.setter@<name>.deleter,您必须在类定义中添加3 个额外的单独名称,这样就没有人会使用它们:

\n\n
>>> [n for n in sorted(vars(Location)) if n[:2] != \'__\']\n[\'get_latitude\', \'get_longitude\', \'latitude\', \'longitude\', \'set_latitude\', \'set_longitude\']\n
Run Code Online (Sandbox Code Playgroud)\n\n

想象一个具有 5 个、10 个甚至更多属性定义的类。不太熟悉该项目和自动完成 IDE 的开发人员肯定会对get_latitudelatitude和之间的差异感到困惑set_latitude,并且最终会得到混合样式的代码,并且现在很难摆脱在类级别公开这些方法。

\n\n

当然,您可以在分配del get_latitude, set_latitude后立即使用latitude = property(...),但这仍然是更多没有实际目的的额外代码。

\n\n

令人困惑的方法名称

\n\n

尽管您可以避免在访问器名称前添加前缀get_set_或以其他方式区分名称以从中创建property()对象,但这仍然是几乎所有不使用@property装饰器语法的代码最终命名访问器方法的方式。

\n\n

这可能会导致回溯中出现一些混乱;在其中一个访问器方法中引发的异常会导致名称中包含get_latitude或 的回溯,而前一行使用了. Python 属性新手可能并不总是清楚两者是如何连接的,特别是如果他们错过了下面的线;往上看。set_latitudeobject.latitudelatitude = property(...)

\n\n

访问访问器,如何继承

\n\n

您可能会指出,无论如何您可能需要访问这些功能;例如,当仅重写子类中属性的 getter 或 setter 时,同时继承其他访问器。

\n\n

但是property,当在类上访问该对象时,已经.fget通过和属性为您提供了对访问器的引用.fset.fdel

\n\n
>>> Location.latitude\n<property object at 0x10d1c3d18>\n>>> Location.latitude.fget\n<function Location.get_latitude at 0x10d1c4488>\n>>> Location.latitude.fset\n<function Location.set_latitude at 0x10d195ea0>\n
Run Code Online (Sandbox Code Playgroud)\n\n

并且您可以在子类中重用@<name>.getter//语法,而不必记住创建新对象!@<name>.setter@<name>.deleterproperty

\n\n

使用旧语法,尝试仅覆盖其中一个访问器是很常见的:

\n\n
class SpecialLocation(Location):\n    def set_latitude(self, latitude):\n        # ...\n
Run Code Online (Sandbox Code Playgroud)\n\n

然后想知道为什么它不会被继承的对象拾取property

\n\n

使用装饰器语法,您可以使用:

\n\n
class SpecialLocation(Location):\n    @Location.latitude.setter\n    def latitude(self, latitude):\n        # ...\n
Run Code Online (Sandbox Code Playgroud)\n\n

然后子SpecialLocation类被赋予一个新property()实例,其 getter 继承自Location,并具有新的 setter。

\n\n

总长DR

\n\n

使用装饰器语法。

\n\n
    \n
  • 它是自我记录的
  • \n
  • 它避免了命名空间污染
  • \n
  • 它使得从属性继承访问器变得更干净、更直接
  • \n
\n