在Python中何时以及如何使用内置函数property()

85

在我看来,除了一些语法糖之外,property()没有任何好处。

当然,能够编写a.b=2而不是a.setB(2)很好,但隐藏了a.b=2不是一个简单的赋值这一事实,似乎会导致麻烦。可能会发生一些意想不到的结果,例如a.b=2实际上会使a.b变成1,或者引发异常,或者出现性能问题,或者只是让人感到困惑。

你能给我举个好用的具体例子吗?(将其用于修补有问题的代码不算)


13
你理解错了,.setB() 是为那些没有真正属性的语言提供的补丁。Python、C# 和其他一些语言有属性,解决方案是一样的。 - liori
5
我看过Java程序员更糟糕的做法,那就是为每个属性编写getter和setter方法——仅仅是以防万一,因为稍后重构所有内容会很麻烦。 - John La Rooy
4
在不支持属性的语言中(或者至少是一些等效的可能性,例如Python的__getattr__/__setattr__,这些远先于属性),“Java人”(包括C++!-)几乎没有其他办法来保护封装。 - Alex Martelli
8个回答

149

在依赖于getter和setter的语言中,比如Java,它们不应该也不会执行除其名称所表示外的任何操作。如果x.getB()返回当前逻辑属性b的值之外的任何内容,或者x.setB(2)做除了使x.getB()返回2外其他什么工作都是惊人的。

然而,没有关于这种期望行为的语言强制的保证,即编译器对以getset开头的方法的实现体施加的约束:相反,这取决于常识、社交习惯、"风格指南"和测试。

在具有属性(一组包括但不限于Python的语言)的语言中,x.b访问和x.b = 2等赋值的行为与例如Java中的getter和setter方法完全相同:相同的期望,相同的缺乏语言强制保证。

属性的第一个优势是语法和可读性。例如,必须编写:

x.setB(x.getB() + 1)

不使用显而易见的方法

x.b += 1
在支持属性的语言中,强制用户通过复杂的模板代码去获取类的属性是没有任何好处的,这样做只会影响代码可读性而不会带来任何益处。
在Python中,使用属性(或其他描述符)代替getter和setter还有一个巨大的好处:如果以后重构类时不再需要底层的setter和getter,你可以(而且不会破坏该类的公开API)简单地删除这些方法和依赖它们的属性,使 b 成为x类的一个普通“存储”属性,而不是一个通过计算获取和设置的“逻辑”属性。
在Python中,直接完成事情(如果可行)而不是通过方法进行操作是一项重要的优化措施,系统地使用属性使您能够在可能的情况下执行此优化(始终直接公开“正常存储属性”,仅对访问和/或设置需要计算的属性使用方法和属性)。
因此,如果您使用getter和setter而不是属性,除了影响用户代码的可读性之外,还会不必要地浪费计算机周期(和在这些周期中电脑消耗的能源;-),再次强调,完全没有任何好处。
你唯一反对属性的论点可能是:例如,“一个外部用户通常不会期望分配产生任何副作用”;但是你忽略了同样的用户(在类似Java这样的语言中,getter和setter很普遍)也不会期望调用一个setter时产生可观测的“副作用”(甚至更少地期望getter产生副作用;-)。这是合理的期望,作为类的作者,你需要尝试适应它们-无论您的setter和getter是直接使用还是通过属性使用,都没有区别。如果您有具有重要可观测副作用的方法,请不要将它们命名为getThissetThat,也不要通过属性使用它们。
批评属性“隐藏实现”的抱怨完全是不合理的:大部分面向对象编程就是关于实现信息隐藏的--使类负责向外界提供逻辑接口,并在内部尽可能好地实现它。这方面,getter和setter与属性一样,都是实现这个目标的工具。只是属性更擅长这个任务(在支持它们的语言中;-)。

4
另外,看看C++的运算符重载。同样的“一个外部用户不会希望…”的论点也适用于此。有一些情况下,你就是不想让用户知道。在这些情况下,你需要确保不会让用户感到意外。 - Oren S
5
@Oren,说得好,Python的运算符重载与C++的非常相似(由于赋值不是运算符,因此无法重载,等等,但是总体概念相似)。避免例如__add__改变self和/或执行与加法毫不相关的操作取决于编写者的礼貌、纪律和常识 - 这并不意味着当其被精心而明智地使用时运算符重载是一件坏事(尽管Java的设计者不同意,他们故意将它留出了_他们的_语言!)。 - Alex Martelli
“…你可以(不破坏类的公开API)简单地消除那些方法和依赖它们的属性,使b成为x类的一个普通的“存储”属性,而不是通过计算获得和设置的“逻辑”属性。” - ??? 我不理解这个,我知道如果你改变组成属性的底层内部属性,属性提供的公共“接口”就不必改变,但getter/setter也是如此。两者都只是在值之上的抽象层,这个值应该是对象内部的。你想说什么? - Adam Parkin
8
如果客户端程序调用了“GetTaxRate()”,那么您就无法再删除该方法而不破坏向后兼容性。但是,您可以在不破坏向后兼容性的情况下删除“@property”,因为直接访问和“@property”使用相同的语法。 - AlexLordThorsen

35

这个想法是让你在真正需要它们之前避免编写getter和setter。

因此,首先你要编写:

class MyClass(object):
    def __init__(self):
        self.myval = 4

显然,您现在可以编写myobj.myval = 5

但是以后,您决定需要一个setter,因为您想同时进行一些巧妙的操作。但是您不想更改使用类的所有代码-因此,您将setter包装在@property装饰器中,所有内容都可以正常工作。


3
你的例子正是我所说的“用它来修复有问题的代码”的意思…也许“有问题的”不是这里应该使用的正确词语,但在我看来,这是一种补丁。 - olamundo
我支持@olamundo的想法:我经常看到变量变成@property以在setter/getter中添加一些逻辑而不破坏兼容性。这故意给@property带来了副作用,对未来的程序员在同一代码上是令人困惑的。 - Lorenzo Belli

15
但是隐藏事实,即a.b=2不是一个简单的赋值,看起来像是惹麻烦的做法。
然而,你并没有隐藏这个事实;其实一开始根本就不存在这个事实。这是Python语言——一种高级语言,不是汇编语言。其中的很少几个“简单”语句可以归结为单个CPU指令。把赋值简化到一步操作是一种错误的理解。
当你说x.b=c时,你应该想到的只是“发生了什么事情,现在x.b应该是c”。

我同意你说的,但是我仍然认为这是在某种程度上“隐藏”内部工作,因为外部用户通常不会预料到赋值操作会产生任何副作用。 - olamundo
5
没错,Noam。然而,面向对象编程(OOP)的核心是拥有一些黑盒子对象,除了它们所呈现的接口外,我们应该假定在表面下会发生一些奇怪、神奇的事情 ;) - Lee B
2
a.b = 2 和 x = 2 的赋值操作本质上并没有太大的区别。如果它们的实现方式不同,那也没关系,面向对象编程的整个意义就在于隐藏实现细节。如果 a.b = 2 将某些东西设置为 490,那么这是一个不好的副作用,即使使用 a.setB(2) 也无法解决。如果你认为当你看到 a.b = 2 时它应该是一个非常快的操作,那么你应该意识到你正在使用 Python,而且已经比 C 慢了至少一个数量级。 - Clueless

5
基本的原因就是它更好看,更符合Python语言的特点,尤其是对于库来说。something.getValue()看起来不如something.value好看。
在Plone(一个相当大的CMS)中,你过去需要使用document.setTitle()完成很多事情,比如存储该值、重新索引等等。只需要使用document.title = 'something'更好看。不管怎样,你知道幕后正在发生很多事情。

3
你是正确的,这只是语法糖。根据你对问题代码的定义,可能没有好的使用方法。
考虑一个广泛用于应用程序的类Foo。现在这个应用程序变得非常庞大,而且假设它是一个非常受欢迎的Web应用程序。
你发现Foo正在引起瓶颈。也许可以添加一些缓存来加快Foo的速度。使用属性将使您能够在不改变Foo之外的任何代码或测试的情况下完成此操作。
当然,这是有问题的代码,但你刚刚快速解决了很多问题,节省了很多钱。
如果Foo在你拥有成百上千用户的库中呢?那么,在升级到最新版本的Foo时,你就不必告诉他们进行昂贵的重构了。
发布说明中有关于Foo的行项目,而不是段落移植指南。
经验丰富的Python程序员并不期望从 a.b=2 中获得太多信息,除了 a.b==2 ,但他们知道即使这也可能不是真的。类内部发生的事情是它自己的事情。

2

这是我以前的一个例子。我封装了一个C库,其中有像“void dt_setcharge(int atom_handle, int new_charge)”和“int dt_getcharge(int atom_handle)”这样的函数。我想在Python级别上执行“atom.charge = atom.charge + 1”。

“property”装饰器使得这变得容易。类似于:

class Atom(object):
    def __init__(self, handle):
        self.handle = handle
    def _get_charge(self):
        return dt_getcharge(self.handle)
    def _set_charge(self, charge):
        dt_setcharge(self.handle, charge)
    charge = property(_get_charge, _set_charge)

十年前我写这个包时,我必须使用__getattr__和__setattr__,虽然实现是可能的,但错误率更高。

class Atom:
    def __init__(self, handle):
        self.handle = handle
    def __getattr__(self, name):
        if name == "charge":
            return dt_getcharge(self.handle)
        raise AttributeError(name)
    def __setattr__(self, name, value):
        if name == "charge":
            dt_setcharge(self.handle, value)
        else:
            self.__dict__[name] = value

1
获取器和设置器在许多情况下都是必需的,它们非常有用,因为它们对代码透明。如果对象Something具有属性height,您可以将值分配为Something.height = 10,但如果height具有getter和setter,则在分配该值时,您可以在过程中执行许多操作,例如验证最小或最大值,例如触发事件,因为高度已更改,自动设置其他值以便于新的高度值,所有这些可能会发生在Something.height值被分配的那一刻。请记住,在您的代码中不需要调用它们,它们在读取或写入属性值的时候自动执行。在某种程度上,它们就像事件过程,当属性X更改值时,以及当读取属性X值时。

0

在重构中尝试使用委托替换继承时,这将非常有用。以下是一个玩具示例。 StackVector 中的一个子类。

class Vector:
    def __init__(self, data):
        self.data = data

    @staticmethod
    def get_model_with_dict():
        return Vector([0, 1])


class Stack:
    def __init__(self):
        self.model = Vector.get_model_with_dict()
        self.data = self.model.data


class NewStack:
    def __init__(self):
        self.model = Vector.get_model_with_dict()

    @property
    def data(self):
        return self.model.data

    @data.setter
    def data(self, value):
        self.model.data = value


if __name__ == '__main__':
    c = Stack()
    print(f'init: {c.data}') #init: [0, 1]

    c.data = [0, 1, 2, 3]
    print(f'data in model: {c.model.data} vs data in controller: {c.data}') 
    #data in model: [0, 1] vs data in controller: [0, 1, 2, 3]

    c_n = NewStack()
    c_n.data = [0, 1, 2, 3]
    print(f'data in model: {c_n.model.data} vs data in controller: {c_n.data}') 
    #data in model: [0, 1, 2, 3] vs data in controller: [0, 1, 2, 3]

请注意,如果您直接访问而不是使用属性,则 self.model.data 不等于 self.data,这超出了我们的预期。
您可以将 __name__=='__main__' 之前的代码视为库。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接