使用@property 与 getter 和 setter [重复]
- 2024-12-03 08:45:00
- admin 原创
- 166
问题描述:
与传统的 getter+setter 相比,该符号有什么优势@property
?程序员在哪些特定情况/情况下应该选择使用其中一种而不是另一种?
具有以下属性:
class MyClass(object):
@property
def my_attr(self):
return self._my_attr
@my_attr.setter
def my_attr(self, value):
self._my_attr = value
无属性:
class MyClass(object):
def get_my_attr(self):
return self._my_attr
def set_my_attr(self, value):
self._my_attr = value
解决方案 1:
更喜欢属性。这就是它们存在的目的。
原因是 Python 中的所有属性都是公共的。名称以一个或两个下划线开头只是一个警告,表示给定的属性是一个实现细节,在未来版本的代码中可能不会保持不变。它不会阻止您实际获取或设置该属性。因此,标准属性访问是访问属性的正常 Python 方式。
属性的优点在于,它们在语法上与属性访问相同,因此您可以在不更改客户端代码的情况下从一个属性更改为另一个属性。您甚至可以拥有一个使用属性的类版本(例如,用于按合同编写代码或调试),以及一个不使用属性的类版本用于生产,而无需更改使用它的代码。同时,您不必为所有内容编写 getter 和 setter,以防以后需要更好地控制访问。
解决方案 2:
在 Python 中,您不能仅仅为了好玩而使用 getter、setter 或属性。您首先只使用属性,然后,仅在需要时,最终迁移到属性,而无需使用类更改代码。
确实有很多以 .py 为扩展名的代码在任何地方都使用了 getter 和 setter 以及继承和无意义的类,例如一个简单的元组就可以完成这些事情,但这些代码是人们使用 Python 用 C++ 或 Java 编写的。
那不是 Python 代码。
解决方案 3:
使用属性可让您从常规属性访问开始,然后根据需要使用 getter 和 setter 支持它们。
解决方案 4:
简短的回答是:房地产毫无疑问胜出。永远如此。
有时需要 getter 和 setter,但即使如此,我也会将它们“隐藏”到外部世界。在 Python 中有很多方法可以做到这一点(getattr
、setattr
、__getattribute__
等...,但一种非常简洁明了的方法是:
def set_email(self, value):
if '@' not in value:
raise Exception("This doesn't look like an email address.")
self._email = value
def get_email(self):
return self._email
email = property(get_email, set_email)
这是一篇简短的文章,介绍 Python 中的 getter 和 setter 主题。
解决方案 5:
[ TL;DR? 您可以跳到最后查看代码示例。]
我实际上更喜欢使用不同的习语,这对于一次性使用来说有点复杂,但如果您有一个更复杂的用例,那就很好了。
首先介绍一些背景。
属性很有用,因为它们允许我们以编程方式处理设置和获取值,但仍然允许将属性作为属性访问。我们可以将“获取”转换为“计算”(本质上),并且可以将“设置”转换为“事件”。假设我们有以下类,我用类似 Java 的 getter 和 setter 对其进行了编码。
class Example(object):
def __init__(self, x=None, y=None):
self.x = x
self.y = y
def getX(self):
return self.x or self.defaultX()
def getY(self):
return self.y or self.defaultY()
def setX(self, x):
self.x = x
def setY(self, y):
self.y = y
def defaultX(self):
return someDefaultComputationForX()
def defaultY(self):
return someDefaultComputationForY()
您可能想知道为什么我没有在对象的方法中调用defaultX
and 。原因是,对于我们的情况,我想假设方法返回随时间变化的值,比如时间戳,并且每当(或) 未设置时(其中,出于本示例的目的,“未设置”表示“设置为 None”),我想要(或) 的默认计算值。defaultY
`__init__someDefaultComputation
xy
x`y
因此,由于上述许多原因,这是很蹩脚的。我将使用属性重写它:
class Example(object):
def __init__(self, x=None, y=None):
self._x = x
self._y = y
@property
def x(self):
return self.x or self.defaultX()
@x.setter
def x(self, value):
self._x = value
@property
def y(self):
return self.y or self.defaultY()
@y.setter
def y(self, value):
self._y = value
# default{XY} as before.
我们得到了什么?尽管在幕后我们最终运行的是方法,但我们获得了将这些属性作为属性引用的能力。
当然,属性的真正威力在于,我们通常希望这些方法除了获取和设置值之外还能做一些事情(否则使用属性就没有意义了)。我在我的 getter 示例中就是这么做的。我们基本上是在运行一个函数体,以便在未设置值时获取默认值。这是一种非常常见的模式。
但我们失去了什么?我们又不能做什么?
在我看来,主要的烦恼是如果你定义了一个 getter(就像我们在这里所做的那样),那么你还必须定义一个 setter。[1] 这会产生额外的噪音,使代码变得混乱。
另一个烦恼是我们仍然必须初始化中的x
和值。(当然,我们可以使用添加它们,但那是额外的代码。)y
`__init__`setattr()
第三,与 Java 类示例不同,getter 不能接受其他参数。现在我听到你可能已经在说,好吧,如果它接受参数,它就不是 getter!从官方意义上讲,这是真的。但从实际意义上讲,我们没有理由不能参数化命名属性(例如x
)并为某些特定参数设置其值。
如果我们可以做类似的事情就好了:
e.x[a,b,c] = 10
e.x[d,e,f] = 20
例如。我们能做到的最接近的就是覆盖赋值以暗示一些特殊的语义:
e.x = [a,b,c,10]
e.x = [d,e,f,30]
当然,还要确保我们的设置器知道如何提取前三个值作为字典的键并将其值设置为数字或其他值。
但即使我们这样做了,我们仍然无法通过属性来支持它,因为我们无法将参数传递给 getter,所以无法获取值。所以我们必须返回所有内容,从而引入不对称。
Java 风格的 getter/setter 确实允许我们处理这个问题,但我们又回到了需要 getter/setter 的状态。
在我看来,我们真正想要的是满足以下要求的东西:
用户只需为给定的属性定义一个方法,并可在该方法中指示该属性是只读还是读写。如果属性是可写的,则属性未通过此测试。
用户无需在函数底层定义额外的变量,因此代码中不需要或
__init__
。setattr
变量的存在只是因为我们创建了这个新式属性。属性的任何默认代码都在方法主体本身中执行。
我们可以将属性设置为属性,并将其作为属性引用。
我们可以将属性参数化。
在代码方面,我们想要一种编写方式:
def x(self, *args):
return defaultX()
然后可以执行以下操作:
print e.x -> The default at time T0
e.x = 1
print e.x -> 1
e.x = None
print e.x -> The default at time T1
等等。
我们还希望找到一种方法来处理可参数化属性的特殊情况,但仍允许默认赋值情况工作。您将在下面看到我如何解决这个问题。
现在进入正题(耶!正题!)。我想到的解决方案如下。
我们创建一个新对象来代替属性的概念。该对象旨在存储为其设置的变量的值,但也维护知道如何计算默认值的代码句柄。它的工作是存储设置value
或运行(method
如果未设置该值)。
我们称之为UberProperty
。
class UberProperty(object):
def __init__(self, method):
self.method = method
self.value = None
self.isSet = False
def setValue(self, value):
self.value = value
self.isSet = True
def clearValue(self):
self.value = None
self.isSet = False
我假设method
这里是类方法,value
是的值UberProperty
,并且我添加了isSet
因为None
可能是一个真实值,这使我们能够以一种干净的方式声明确实“没有值”。另一种方法是某种类型的标记。
这基本上给了我们一个可以做我们想做的事情的对象,但我们如何将它放在我们的类上呢?好吧,属性使用装饰器;为什么我们不能呢?让我们看看它看起来会是什么样子(从这里开始,我将坚持只使用一个“属性” x
)。
class Example(object):
@uberProperty
def x(self):
return defaultX()
当然,这实际上还不起作用。我们必须实现uberProperty
并确保它能够处理获取和设置。
让我们从 gets 开始。
我的第一次尝试是简单地创建一个新的 UberProperty 对象并返回它:
def uberProperty(f):
return UberProperty(f)
当然,我很快发现这行不通:Python 从不将可调用函数绑定到对象,而我需要对象才能调用该函数。即使在类中创建装饰器也行不通,因为虽然现在我们有了类,但我们仍然没有可以使用的对象。
所以我们需要在这里做更多的事情。我们知道一个方法只需要表示一次,所以让我们继续保留我们的装饰器,但修改UberProperty
为仅存储method
引用:
class UberProperty(object):
def __init__(self, method):
self.method = method
它也无法调用,所以目前什么都不起作用。
我们如何完成这幅图?好吧,当我们使用新的装饰器创建示例类时,我们最终会得到什么:
class Example(object):
@uberProperty
def x(self):
return defaultX()
print Example.x <__main__.UberProperty object at 0x10e1fb8d0>
print Example().x <__main__.UberProperty object at 0x10e1fb8d0>
在这两种情况下,我们得到的UberProperty
当然是不可调用的,所以这没有多大用处。
我们需要的是某种方式,UberProperty
在创建类之后将装饰器创建的实例动态绑定到该类的对象,然后再将该对象返回给该用户以供使用。嗯,是的,伙计,这很明智__init__
。
首先,让我们写下我们希望查找的结果。我们将一个绑定UberProperty
到一个实例,因此显然要返回的是 BoundUberProperty。这是我们实际维护属性状态的地方x
。
class BoundUberProperty(object):
def __init__(self, obj, uberProperty):
self.obj = obj
self.uberProperty = uberProperty
self.isSet = False
def setValue(self, value):
self.value = value
self.isSet = True
def getValue(self):
return self.value if self.isSet else self.uberProperty.method(self.obj)
def clearValue(self):
del self.value
self.isSet = False
现在我们来看一下表示;如何将这些内容放到对象上?有几种方法,但最容易解释的方法是使用__init__
方法进行映射。到调用时,__init__
我们的装饰器已经运行,因此只需查看对象__dict__
并更新属性值为类型的任何属性UberProperty
。
现在,超级属性很酷,我们可能想要经常使用它们,因此创建一个基类来为所有子类执行此操作是有意义的。我想你知道基类将被称为什么。
class UberObject(object):
def __init__(self):
for k in dir(self):
v = getattr(self, k)
if isinstance(v, UberProperty):
v = BoundUberProperty(self, v)
setattr(self, k, v)
我们添加这个,将我们的示例改为继承自UberObject
,然后......
e = Example()
print e.x -> <__main__.BoundUberProperty object at 0x104604c90>
修改后x
为:
@uberProperty
def x(self):
return *datetime.datetime.now()*
我们可以运行一个简单的测试:
print e.x.getValue()
print e.x.getValue()
e.x.setValue(datetime.date(2013, 5, 31))
print e.x.getValue()
e.x.clearValue()
print e.x.getValue()
我们得到了想要的输出:
2013-05-31 00:05:13.985813
2013-05-31 00:05:13.986290
2013-05-31
2013-05-31 00:05:13.986310
(哎呀,我工作到很晚。)
请注意,我在这里使用了getValue
、setValue
和clearValue
。这是因为我还没有链接自动返回这些内容的方法。
但我觉得现在就到此为止吧,因为我累了。你还可以看到我们想要的核心功能已经到位;其余的只是表面文章。重要的可用性表面文章,但可以等到我有变化时再更新帖子。
我将在下一篇文章中通过解决以下问题来完成这个例子:
我们需要确保 UberObject
__init__
总是被子类调用。
+ 因此我们要么强制在某处调用它,要么阻止它被实现。
+ 我们将了解如何使用元类来实现这一点。
我们需要确保能够处理常见的情况,即有人将某个函数“别名”为其他函数,例如:
class Example(object):
@uberProperty
def x(self):
...
y = x
我们需要默认
e.x
返回。e.x.getValue()
+ 我们实际上会看到,这是模型失败的一个领域。
+ 事实证明我们总是需要使用函数调用来获取值。
+ 但我们可以让它看起来像一个常规的函数调用,从而避免使用`e.x.getValue()`。(如果你还没有修复它,那么这样做是显而易见的。)
我们需要支持设置
e.x directly
,如e.x = <newvalue>
。我们也可以在父类中执行此操作,但我们需要更新__init__
代码来处理它。最后,我们将添加参数化属性。如何做到这一点应该也很明显。
以下是迄今为止的代码:
import datetime
class UberObject(object):
def uberSetter(self, value):
print 'setting'
def uberGetter(self):
return self
def __init__(self):
for k in dir(self):
v = getattr(self, k)
if isinstance(v, UberProperty):
v = BoundUberProperty(self, v)
setattr(self, k, v)
class UberProperty(object):
def __init__(self, method):
self.method = method
class BoundUberProperty(object):
def __init__(self, obj, uberProperty):
self.obj = obj
self.uberProperty = uberProperty
self.isSet = False
def setValue(self, value):
self.value = value
self.isSet = True
def getValue(self):
return self.value if self.isSet else self.uberProperty.method(self.obj)
def clearValue(self):
del self.value
self.isSet = False
def uberProperty(f):
return UberProperty(f)
class Example(UberObject):
@uberProperty
def x(self):
return datetime.datetime.now()
[1] 我可能还没有弄清楚现在是否仍然如此。
解决方案 6:
对我来说,使用属性更直观,并且更适合大多数代码。
比较
o.x = 5
ox = o.x
对阵
o.setX(5)
ox = o.getX()
对我来说很明显哪个更容易阅读。此外,属性允许私有变量更容易。
解决方案 7:
我认为两者都有其适用之处。使用的一个问题@property
是,很难使用标准类机制扩展子类中的 getter 或 setter 的行为。问题是实际的 getter/setter 函数隐藏在属性中。
您实际上可以掌握这些功能,例如
class C(object):
_p = 1
@property
def p(self):
return self._p
@p.setter
def p(self, val):
self._p = val
C.p.fget
您可以通过和访问 getter 和 setter 函数C.p.fset
,但无法轻松使用常规方法继承(例如 super)功能来扩展它们。在深入了解 super 的复杂性之后,您确实可以通过以下方式使用 super:
# Using super():
class D(C):
# Cannot use super(D,D) here to define the property
# since D is not yet defined in this scope.
@property
def p(self):
return super(D,D).p.fget(self)
@p.setter
def p(self, val):
print 'Implement extra functionality here for D'
super(D,D).p.fset(self, val)
# Using a direct reference to C
class E(C):
p = C.p
@p.setter
def p(self, val):
print 'Implement extra functionality here for E'
C.p.fset(self, val)
但是,使用 super() 非常笨重,因为必须重新定义属性,并且必须使用稍微违反直觉的 super(cls,cls) 机制来获取 p 的未绑定副本。
解决方案 8:
我觉得属性就是让你仅在真正需要它们时才承担编写 getter 和 setter 的开销。
Java 编程文化强烈建议永远不要授予对属性的访问权限,而是通过 getter 和 setter,并且只使用那些真正需要的。总是编写这些显而易见的代码片段有点冗长,并且请注意,70% 的时间它们从未被一些非平凡的逻辑所取代。
在 Python 中,人们实际上关心这种开销,以便您可以采用以下做法:
如果不需要,请不要首先使用 getter 和 setter
使用
@property
来实现它们而不改变其余代码的语法。
解决方案 9:
在大多数情况下,我都不想使用这两种方法。属性的问题在于它们会使类变得不那么透明。特别是,如果您要从 setter 引发异常,这将是一个问题。例如,如果您有一个 Account.email 属性:
class Account(object):
@property
def email(self):
return self._email
@email.setter
def email(self, value):
if '@' not in value:
raise ValueError('Invalid email address.')
self._email = value
那么该类的用户就不会想到给该属性赋值会导致异常:
a = Account()
a.email = 'badaddress'
--> ValueError: Invalid email address.
结果,异常可能无法处理,并且在调用链中传播得太高而无法正确处理,或者导致向程序用户呈现非常无用的回溯(不幸的是,这在 python 和 java 的世界中太常见了)。
我也会避免使用 getter 和 setter:
因为提前定义所有属性非常耗时,
使代码量不必要地变长,这使得理解和维护代码更加困难,
如果仅根据需要为属性定义它们,则类的接口将会改变,从而损害该类的所有用户
我更喜欢在明确定义的地方(例如在验证方法中)执行复杂的逻辑,而不是属性和 getter/setter:
class Account(object):
...
def validate(self):
if '@' not in self.email:
raise ValueError('Invalid email address.')
或类似的 Account.save 方法。
请注意,我并不是说属性在任何情况下都是没用的,只是说如果你能让你的类足够简单和透明以至于你不需要它们,那么你可能会更好。
解决方案 10:
我很惊讶没有人提到属性是描述符类的绑定方法,Adam Donohue和NeilenMarais在他们的文章中明确表达了这个想法 —— getter 和 setter 是函数,可用于:
证实
修改数据
鸭式(强制类型为另一种类型)
这提供了一种巧妙的方法来隐藏实现细节和代码垃圾,如正则表达式、类型转换、尝试……除了块、断言或计算值。
一般来说,对一个对象进行 CRUD 可能通常相当平凡,但请考虑将持久保存到关系数据库中的数据示例。ORM 可以将特定 SQL 语言的实现细节隐藏在与 fget、fset、fdel 绑定的方法中,这些方法在属性类中定义,该属性类将管理可怕的 if .. elif .. else 阶梯,这些阶梯在 OO 代码中非常丑陋 - 暴露简单而优雅,并为使用ORM 的self.variable = something
开发人员消除细节。
如果人们仅仅认为属性是束缚与纪律语言(例如 Java)的一些乏味的痕迹,那么他们就忽略了描述符的要点。
解决方案 11:
在复杂的项目中,我更喜欢使用具有显式 setter 函数的只读属性(或 getter):
class MyClass(object):
...
@property
def my_attr(self):
...
def set_my_attr(self, value):
...
在长期项目中,调试和重构比编写代码本身花费的时间更多。使用这种方法有几个缺点@property.setter
,使得调试更加困难:
1) python 允许为现有对象创建新属性。这使得以下印刷错误很难追踪:
my_object.my_atttr = 4.
如果你的对象是一个复杂的算法,那么你将花费相当多的时间来尝试找出它不收敛的原因(请注意上面一行中多余的“t”)
2)setter 有时可能会演变成一个复杂而缓慢的方法(例如访问数据库)。对于另一个开发人员来说,弄清楚为什么以下函数非常慢可能相当困难。他可能会花很多时间分析do_something()
方法,而这my_object.my_attr = 4.
实际上是导致速度变慢的原因:
def slow_function(my_object):
my_object.my_attr = 4.
my_object.do_something()
解决方案 12:
传统的 getter 和 setter 都有各自@property
的优势。这取决于您的用例。
优点@property
您不必在更改数据访问实现的同时更改接口。当您的项目较小时,您可能希望使用直接属性访问来访问类成员。例如,假设您有一个
foo
类型为的对象Foo
,它有一个成员num
。然后您可以简单地使用来获取此成员num = foo.num
。随着项目的增长,您可能会觉得需要对简单的属性访问进行一些检查或调试。然后您可以@property
在类中使用来执行此操作。数据访问接口保持不变,因此无需修改客户端代码。
引自PEP-8:
对于简单的公共数据属性,最好只公开属性名称,而不要使用复杂的访问器/变量器方法。请记住,如果您发现简单的数据属性需要增加功能行为,Python 会提供一条通往未来增强的简单途径。在这种情况下,使用属性将功能实现隐藏在简单的数据属性访问语法后面。
在 Python 中使用
@property
进行数据访问被视为符合Pythonic 规范:
+ 它可以增强你作为 Python(而非 Java)程序员的自我认同。
+ 如果面试官认为 Java 风格的 getter 和 setter 是反模式,它可以帮助您的面试。
传统 getter 和 setter 的优点
传统的 getter 和 setter 允许比简单的属性访问更复杂的数据访问。例如,当您设置类成员时,有时您需要一个标志来指示您希望强制执行此操作的位置,即使某些东西看起来并不完美。虽然如何增强直接成员访问并不明显
foo.num = num
,但您可以使用附加force
参数轻松增强传统的 setter:
def Foo:
def set_num(self, num, force=False):
...
传统的 getter 和 setter明确指出类成员访问是通过方法进行的。这意味着:
+ 您得到的结果可能与该类中存储的内容不同。
+ 即使访问看起来像简单的属性访问,其性能也会有很大差异。除非您的类用户期望`@property`在每个属性访问语句后面都隐藏一些东西,否则明确地将这些事情做出来可以帮助最大限度地减少您的类用户的意外。
正如@NeilenMarais和这篇文章所提到的,在子类中扩展传统的 getter 和 setter 比扩展属性更容易。
传统的 getter 和 setter 长期以来已在不同语言中广泛使用。如果您的团队中有来自不同背景的人,他们看起来比 更熟悉
@property
。此外,随着项目的发展,如果您可能需要从 Python 迁移到另一种没有 的语言@property
,使用传统的 getter 和 setter 将使迁移更加顺畅。
注意事项
@property
即使在名称前使用双下划线,传统的 getter 和 setter 都不会将类成员变为私有的:
class Foo:
def __init__(self):
self.__num = 0
@property
def num(self):
return self.__num
@num.setter
def num(self, num):
self.__num = num
def get_num(self):
return self.__num
def set_num(self, num):
self.__num = num
foo = Foo()
print(foo.num) # output: 0
print(foo.get_num()) # output: 0
print(foo._Foo__num) # output: 0
解决方案 13:
以下是《有效的 Python:编写更好的 Python 的 90 种具体方法》(非常棒的书。我强烈推荐)的摘录。
要记住的事情
✦使用简单的公共属性定义新的类接口,避免定义 setter 和 getter 方法。
✦ 如有必要,使用@property 定义在访问对象属性时的特殊行为。
✦ 遵循最小意外规则,避免@property 方法中出现奇怪的副作用。
✦ 确保@property 方法速度快;对于缓慢或复杂的工作(尤其是涉及 I/O 或引起副作用的工作),请改用普通方法。
@property 的一个高级但常见的用途是将曾经简单的数字属性转换为动态计算。这非常有用,因为它允许您迁移类的所有现有用法以获得新行为,而无需重写任何调用站点(如果有您无法控制的调用代码,这一点尤其重要)。@property 还提供了一个重要的权宜之计,可以随着时间的推移改进界面。
我特别喜欢 @property,因为它可以让你随着时间的推移逐步获得更好的数据模型。
@property 是一种帮助你解决实际代码中遇到的问题的工具。不要过度使用它。当你发现自己反复扩展 @property 方法时,可能是时候重构你的类了,而不是进一步掩盖代码的糟糕设计。
✦使用@property为现有实例属性赋予新功能。
✦使用@property逐步实现更好的数据模型。
✦ 当你发现自己过多使用@property时,请考虑重构一个类和所有调用站点。