何时以及如何在 Python 中使用内置函数 property()
- 2025-03-10 08:52:00
- admin 原创
- 68
问题描述:
在我看来,除了一些语法糖之外,property() 并没有什么用处。
当然,能够a.b=2
用 代替写出来是件好事a.setB(2)
,但隐藏 ab=2 不是简单赋值的事实似乎会招致麻烦,要么是因为可能会发生一些意想不到的结果,比如a.b=2
实际上会导致a.b
。1
要么是引发异常。要么是性能问题。要么就是让人困惑。
你能给我一个具体的例子来说明如何正确使用它吗?(用它来修补有问题的代码不算;-)
解决方案 1:
在依赖 getter 和 setter 的语言中,比如 Java,它们不应该也不期望做除了它们所说的之外的任何事情——如果x.getB()
除了返回逻辑属性的当前值之外做了其他任何事情b
,或者除了进行返回x.setB(2)
所需的少量内部工作之外做了其他任何事情,那将是令人惊讶的。x.getB()
`2`
但是,语言并没有对这种预期行为做出任何保证get
,例如,编译器对名称以或开头的方法主体强制约束set
:相反,这取决于常识、社会惯例、“风格指南”和测试。
在具有属性的语言(包括但不限于 Python 的一组语言)中,x.b
访问和分配的行为(例如)与 Java 中的 getter 和 setter 方法完全相同:相同的期望,同样缺乏语言强制保证。x.b = 2
属性的第一个好处是语法和可读性。例如,
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 是直接使用还是通过属性使用,都没有区别。如果您的方法具有重要的可观察到的副作用,请不要将它们命名为getThis
, setThat
,也不要通过属性使用它们。
属性“隐藏实现”的抱怨完全没有道理:OOP 的大部分内容都是关于实现信息隐藏的——让一个类负责向外界呈现一个逻辑接口,并尽可能地在内部实现它。getter 和 setter 与属性完全一样,是实现此目标的工具。属性只是在这方面做得更好(在支持它们的语言中;-)。
解决方案 2:
这样做的目的是让你避免在真正需要 getter 和 setter 之前编写它们。
因此,首先你要写:
class MyClass(object):
def __init__(self):
self.myval = 4
显然你现在可以写myobj.myval = 5
。
但后来,你决定确实需要一个 setter,因为你想同时做一些巧妙的事情。但你不想更改使用你的类的所有代码 - 所以你将 setter 包装在@property
装饰器中,这样一切就都正常了。
解决方案 3:
但隐藏 ab=2 不是一个简单的赋值这一事实,看起来似乎是自找麻烦
但你并没有隐瞒这个事实;这个事实从一开始就不存在。这是 Python——一种高级语言;不是汇编。其中的“简单”语句很少归结为单 CPU 指令。将简单性读入作业就是读出不存在的东西。
当你说 xb = c 时,可能你应该想到的是“无论刚刚发生了什么,xb 现在应该是 c”。
解决方案 4:
一个基本原因其实很简单,就是它看起来更好。它更符合 Python 风格。尤其是对于库来说。something.getValue() 看起来不如 something.value 好看
在 plone(一个相当大的 CMS)中,你曾经使用 document.setTitle() 来做很多事情,比如存储值、再次索引等等。只需执行 document.title = 'something' 就更好了。无论如何,你知道幕后发生了很多事情。
解决方案 5:
这是我的一个旧示例。我包装了一个 C 库,该库具有“void dt_setcharge(int atom_handle, int new_charge)”和“int dt_getcharge(int atom_handle)”等函数。我想在 Python 级别执行“atom.charge = atom.charge + 1”。
“属性”装饰器让这一切变得简单。例如:
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)
10 年前,当我编写这个包时,我必须使用 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
解决方案 6:
你说得对,它只是语法糖。根据你对有问题的代码的定义,它可能没有什么用处。
假设您的应用程序中有一个广泛使用的类 Foo。现在这个应用程序已经变得相当大,而且可以说它是一个非常流行的 Web 应用程序。
您发现 Foo 造成了瓶颈。也许可以向 Foo 添加一些缓存来加快速度。使用属性可以让您做到这一点,而无需更改 Foo 之外的任何代码或测试。
是的,这当然是有问题的代码,但你只需快速修复它就可以节省很多钱。
如果 Foo 位于一个拥有数百或数千名用户的库中,该怎么办?这样,当他们升级到 Foo 的最新版本时,您就不必告诉他们进行昂贵的重构了。
发行说明中有关于 Foo 的项目,而不是段落移植指南。
a.b=2
经验丰富的 Python 程序员对以外的内容没有太多期待a.b==2
,但他们知道甚至可能并非如此。类内部发生的事情是它自己的事。
解决方案 7:
许多情况下都需要 getter 和 setter,因为它们对代码是透明的,所以非常有用。如果对象 Something 具有属性 height,则可以分配一个值如 Something.height = 10,但是如果 height 具有 getter 和 setter,那么在分配该值时,您可以在过程中做很多事情,比如验证最小值或最大值,比如因为高度改变而触发事件,根据新的高度值自动设置其他值,所有这些都可能发生在分配 Something.height 值时。请记住,您不需要在代码中调用它们,它们会在您读取或写入属性值时自动执行。在某种程度上,它们就像事件过程,当属性 X 更改值时以及当读取属性 X 值时。
解决方案 8:
当您尝试在重构中用委托替换继承时,它很有用。以下是一个示例。Stack是Vector中的子类。
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__'
代码作为库。
扫码咨询,免费领取项目管理大礼包!