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

时间:2018-10-19 20:29:45

标签: 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)

(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

区别从何而来?第一个示例的值范围检查在哪里?

2 个答案:

答案 0 :(得分:2)

两个版本的代码的结果几乎完全相同。两种情况下,您最后拥有的属性描述符在功能上都是相同的。描述符中的唯一区别在于,如果您真的尝试过(通过Location.longitude.fset.__name__)可以访问的函数名称,并且如果出了问题,可能会在异常回溯中看到。

唯一的不同是完成之后,get_fooset_foo方法的存在。使用@property时,不会有那些使名称空间混乱的方法。如果您自己手动构建property对象,它们将保留在类名称空间中,因此,如果您确实愿意,可以直接调用它们,而不是通过property对象使用普通的属性访问。

通常,@property语法更好,因为它隐藏了通常不需要的方法。我能想到的,您可能想要公开它们的唯一原因是,如果您希望将方法作为回调传递给其他函数(例如some_function(*args, callback=foo.set_longitude))。虽然您可以只将lambda用于回调(lambda x: setattr(foo, "longitude", x)),所以我认为仅在这种极端情况下,不应该使用多余的getter和setter方法来污染一个不错的API。

答案 1 :(得分:2)

总是要使用装饰器。其他语法没有优势,只有劣势。

装饰点

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

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

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

执行起来很像

def functionname(...):
    # ...

functionname = decorator(functionname)

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

Python之所以添加此功能,是因为当函数主体为 long 时,您不容易看到该函数已被装饰器包装。您必须向下滚动到函数定义才能看到它,而当您想了解某个函数的几乎所有其他内容恰好位于顶部时,这并不是很有用。参数,名称和文档字符串就在那里。

根据原始的PEP 318 – Decorators for Functions and Methods规范:

  

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

     

[...]

     

使用较长的方法时,这变得不太可读。对于从概念上讲是一个声明的函数,将函数命名三遍似乎也比pythonic少。

设计目标下:

  

新语法应该

     
      
  • [...]
  •   
  • 从当前隐藏的功能的末尾移到您的脸部最前面
  •   

因此使用

@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_latitudelatitudeset_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和{{1 }}属性:

.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()继承的getter和一个新的setter。

TLDR

使用装饰器语法。

  • 这是自我记录
  • 避免命名空间污染
  • 它使从属性继承的访问器更干净,更直接