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

标签 python properties python-decorators

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

(a) 属性(property)类别

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)

(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



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

是否有任何官方指南使用哪种“风格”?

一个比另一个有什么真正的优势吗?

我试过的

py_compile + uncompyle6

我已经编译了两个:
>>> import py_compile
>>> py_compile.compile('test.py')

然后用 uncompyle6 反编译两者.但这恰好返回了我开始的内容(格式略有不同)

导入 + 显示

我试过
import test  # (a)
import test2  # (b)
dis.dis(test)
dis.dis(test2)

我对 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

而第一个更大:
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

这种差异从何而来?第一个示例的值范围检查在哪里?

最佳答案

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

装饰器的要点

那是因为装饰器语法是专门为避免其他语法而发明的。您找到的有关 name = property(...) 的任何示例多样性通常出现在早于装饰器的代码中。

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

@decorator
def functionname(...):
    # ...

执行得很像
def functionname(...):
    # ...

functionname = decorator(functionname)

没有 functionname被分配两次(def functionname(...) 部分创建一个函数对象并正常分配给 functionname,但使用装饰器创建函数对象并直接传递给装饰器对象)。

Python 增加了这个特性是因为当你的函数体很长的时候,你很难看出这个函数已经被装饰器包裹了。您必须向下滚动函数定义才能看到它,当您想了解的有关函数的几乎所有其他信息都在顶部时,这并不是很有帮助;参数、名称、文档字符串就在那里。

来自原文 PEP 318 – Decorators for Functions and Methods规范:

The current method of applying a transformation to a function or method places the actual transformation after the function body. For large functions this separates a key component of the function's behavior from the definition of the rest of the function's external interface.

[...]

This becomes less readable with longer methods. It also seems less than pythonic to name the function three times for what is conceptually a single declaration.



在设计目标下:

The new syntax should

  • [...]
  • move from the end of the function, where it's currently hidden, to the front where it is more in your face


所以使用
@property
def latitude(self):
    # ...

@latitude.setter
def latitude(self, latitude):
    # ...


def get_latitude(self):
    # ...

def set_latitude(self, latitude):
    # ...

latitude = property(get_latitude, set_latitude)

没有命名空间污染

接下来,因为@property装饰器将您装饰的函数对象替换为装饰结果(一个 property 实例),您还可以避免命名空间污染。无 @property@<name>.setter@<name>.deleter ,你必须在你的类定义中添加 3 个额外的、单独的名称,然后没有人会使用:
>>> [n for n in sorted(vars(Location)) if n[:2] != '__']
['get_latitude', 'get_longitude', 'latitude', 'longitude', 'set_latitude', 'set_longitude']

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

当然,您可以使用 del get_latitude, set_latitude紧跟在 latitude = property(...) 之后分配,但这是更多额外的代码来执行,没有真正的目的。

令人困惑的方法名称

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

这可能会导致回溯中的一些困惑;在访问器方法之一中引发的异常导致回溯 get_latitudeset_latitude在名称中,而前一行使用了 object.latitude . Python 属性新手可能并不总是清楚这两者是如何连接的,特别是如果他们错过了 latitude = property(...)再往下线;看上面。

访问访问器,如何继承

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

但是property对象,当在类上访问时,已经通过 .fget 为您提供了对访问器的引用。 , .fset.fdel属性:
>>> Location.latitude
<property object at 0x10d1c3d18>
>>> Location.latitude.fget
<function Location.get_latitude at 0x10d1c4488>
>>> Location.latitude.fset
<function Location.set_latitude at 0x10d195ea0>

您可以重复使用 @<name>.getter/@<name>.setter/@<name>.deleter子类中的语法,而不必记住创建新的 property目的!

使用旧语法,尝试仅覆盖其中一个访问器是司空见惯的:
class SpecialLocation(Location):
    def set_latitude(self, latitude):
        # ...

然后想知道为什么它不会被继承的 property 捡起来目的。

使用装饰器语法,您将使用:
class SpecialLocation(Location):
    @Location.latitude.setter
    def latitude(self, latitude):
        # ...

SpecialLocation然后给子类一个新的 property()具有从 Location 继承的 getter 的实例,并使用新的二传手。

TLDR

使用装饰器语法。
  • 它是自我记录的
  • 避免命名空间污染
  • 它使从属性继承访问器更清晰、更直接
  • 关于python - 与属性类相比,使用属性装饰器是否有优势?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52899509/

    相关文章:

    python - Django 中的非全局中间件

    python - 如何从 Path 切换到 os.path,反之亦然?

    python - 如何使用 Pandas 获取单元格的值并存储到变量中?

    python - 使用 Flask-Login 临时覆盖来自特定重定向路由的登录消息

    c# - 为什么 FSharpOption 的某些属性(例如 IsSome 和 IsNone)在 C# 中不可见?

    c# - 如何从对象列表创建 MVC HtmlHelper 表

    python - 用 super() 装饰子类的 __init__ 方法

    ios - 与 Objective-C 中的属性相比,公共(public)实例变量访问速度快多少

    python - python @cache 装饰器是如何工作的?

    用模拟函数替换真实返回值和函数实现的 Pythonic 方法