使用@属性与getter和setter

与经典的getter+setter相比,@property表示法有什么优势?程序员应该在哪些特定情况下选择使用其中一个而不是另一个?

具有属性:

class MyClass(object):@propertydef my_attr(self):return self._my_attr
@my_attr.setterdef 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
468051 次浏览

简短的答案是:房地产轻松获胜。总是。

有时需要getter和setter,但即使这样,我也会将它们“隐藏”给外界。在Python中有很多方法可以做到这一点(getattrsetattr__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的主题。

对我来说,使用属性更直观,更适合大多数代码。

比较

o.x = 5ox = o.x

vs.

o.setX(5)ox = o.getX()

对我来说很明显,哪个更容易阅读。属性也允许私有变量更容易。

使用属性可以让您从普通属性访问开始,然后必要时用getter和setter备份它们

我觉得属性是关于让您仅在实际需要它们时才编写getter和setter的开销。

Java编程文化强烈建议永远不要访问属性,而是通过getter和setter,并且只访问那些实际需要的属性。总是编写这些明显的代码片段有点冗长,并注意到70%的时间它们永远不会被一些不平凡的逻辑所取代。

在Python中,人们实际上关心这种开销,因此您可以接受以下实践:

  • 一开始不要使用getter和setter,如果它们不需要
  • 使用@property来实现它们,而无需更改其余代码的语法。

偏好房产.这就是他们在那里的原因。

原因是所有属性在Python中都是公共的。以下划线或两个开始的名称只是警告给定属性是一个实现细节,在未来的代码版本中可能不会保持不变。它并不阻止您实际获取或设置该属性。因此,标准属性访问是正常的Pythonic访问属性的方式。

属性的好处是它们在语法上与属性访问相同,因此你可以在不更改客户端代码的情况下从一个属性更改到另一个属性。你甚至可以在不更改使用它的代码的情况下,在一个版本的类中使用属性(例如,用于按合约代码或调试)和不用于生产。同时,你不必为所有内容编写getter和setter,以防以后可能需要更好地控制访问。

在Python中,你不会仅仅为了好玩而使用getter或setter或属性。你首先只是使用属性,然后,仅在需要时,最终迁移到属性,而无需使用你的类更改代码。

确实有很多扩展名为. py的代码在任何地方都使用getter和setter,继承和无意义的类,例如简单的元组,但它是使用Python编写C++或Java的代码。

这不是Python代码。

我认为两者都有自己的位置。使用@property的一个问题是很难使用标准类机制扩展子类中getter或setter的行为。问题是实际的getter/setter函数隐藏在属性中。

您实际上可以掌握函数,例如

class C(object):_p = 1@propertydef p(self):return self._p@p.setterdef p(self, val):self._p = val

你可以以C.p.fgetC.p.fset的身份访问getter和setter函数,但是你不能轻易地使用普通的方法继承(例如超)设施来扩展它们。在深入研究了超级的复杂性之后,你可以确实以这种方式使用了超级:

# Using super():class D(C):# Cannot use super(D,D) here to define the property# since D is not yet defined in this scope.@propertydef p(self):return super(D,D).p.fget(self)
@p.setterdef p(self, val):print 'Implement extra functionality here for D'super(D,D).p.fset(self, val)
# Using a direct reference to Cclass E(C):p = C.p
@p.setterdef p(self, val):print 'Implement extra functionality here for E'C.p.fset(self, val)

然而,使用Super()是相当笨拙的,因为必须重新定义属性,并且您必须使用稍微违反直觉的Super(cls, cls)机制来获取p的未绑定副本。

在大多数情况下,我宁愿两者都不使用。属性的问题是它们使类不那么透明。特别是,如果您要从setter引发异常,这是一个问题。例如,如果您有一个Account.email属性:

class Account(object):@propertydef email(self):return self._email
@email.setterdef 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方法。

请注意,我并不是说没有属性有用的情况,只是如果你能让你的类足够简单和透明,以至于你不需要它们,你可能会更好。

[太长别读?你可以跳到最后查看代码示例]

我实际上更喜欢使用不同的习惯用法,这对于一次性使用有点牵扯,但如果您有更复杂的用例则很好。

先说一点背景。

属性很有用,因为它们允许我们以编程的方式处理设置和获取值,但仍然允许将属性作为属性访问。我们可以将'get'转换为'计算'(本质上),我们可以将'set'转换为'事件'。所以假设我们有以下类,我用类似Java的getter和setter编写了它。

class Example(object):def __init__(self, x=None, y=None):self.x = xself.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()

你可能想知道为什么我没有在对象的__init__方法中调用defaultXdefaultY。原因是,对于我们的例子,我想假设someDefaultComputation方法返回的值随时间变化,比如时间戳,每当x(或y)未设置时(为了这个例子的目的,“未设置”意味着“设置为无”)我想要x(或y)的默认计算值。

因此,由于上面描述的许多原因,这是蹩脚的。我将使用属性重写它:

class Example(object):def __init__(self, x=None, y=None):self._x = xself._y = y
@propertydef x(self):return self.x or self.defaultX()
@x.setterdef x(self, value):self._x = value
@propertydef y(self):return self.y or self.defaultY()
@y.setterdef y(self, value):self._y = value
# default{XY} as before.

我们获得了什么?我们获得了将这些属性称为属性的能力,即使在幕后,我们最终运行方法。

当然,属性的真正力量在于我们通常希望这些方法除了获取和设置值之外还能做一些事情(否则使用属性就没有意义了)。我在getter示例中这样做了。我们基本上运行一个函数体来获取默认值。这是一种非常常见的模式。

我们失去了什么,我们不能做什么?

在我看来,主要的烦恼是,如果你定义了一个getter(就像我们在这里所做的那样),你还必须定义一个setter。

另一个烦恼是我们仍然必须初始化__init__中的xy值。(当然,我们可以使用setattr()添加它们,但这是更多的额外代码。)

第三,与类似Java的示例不同,getter不能接受其他参数。现在我可以听到你已经说过,好吧,如果它接受参数,它就不是getter!在官方意义上,这是真的。但在实际意义上,我们没有理由不能参数化命名属性-就像x-并为某些特定参数设置其值。

如果我们能做这样的事情就好了:

e.x[a,b,c] = 10e.x[d,e,f] = 20

例如。我们可以得到的最接近的方法是覆盖赋值以暗示一些特殊的语义学:

e.x = [a,b,c,10]e.x = [d,e,f,30]

当然,确保我们的setter知道如何提取前三个值作为字典的键并将其值设置为数字或其他东西。

但即使我们这样做了,我们仍然无法用属性支持它,因为没有办法获得值,因为我们根本无法将参数传递给getter。所以我们不得不返回所有内容,引入不对称性。

Java风格的getter/setter确实允许我们处理这个问题,但我们又需要getter/setter。

在我看来,我们真正想要的是满足以下要求的东西:

  • 用户仅为给定属性定义一个方法,并且可以指示有属性是只读还是读写。属性未通过此测试如果属性可写。

  • 用户不需要在函数下面定义一个额外的变量,所以我们不需要代码中的__init__setattr。变量只是因为我们创建了这个新样式属性而存在。

  • 属性的任何默认代码都在方法主体本身中执行。

  • 我们可以将属性设置为属性并将其引用为属性。

  • 我们可以参数化属性。

在代码方面,我们想要一种编写方式:

def x(self, *args):return defaultX()

然后能够做到:

print e.x     -> The default at time T0e.x = 1print e.x     -> 1e.x = Noneprint e.x     -> The default at time T1

诸如此类。

对于可参数化属性的特殊情况,我们还需要一种方法来做到这一点,但仍然允许默认的分配情况工作。

现在到了重点(耶!重点!)。我想出的解决方案如下。

我们创建一个新对象来替换属性的概念。该对象旨在存储设置为它的变量的值,但也维护一个知道如何计算默认值的代码句柄。它的工作是存储集合value,如果该值未设置,则运行method

让我们称之为UberProperty

class UberProperty(object):
def __init__(self, method):self.method = methodself.value = Noneself.isSet = False
def setValue(self, value):self.value = valueself.isSet = True
def clearValue(self):self.value = Noneself.isSet = False

我假设这里的method是一个类方法,valueUberProperty的值,我添加了isSet,因为None可能是一个实值,这允许我们以一种干净的方式声明确实“没有值”。另一种方式是某种哨兵。

这基本上给了我们一个可以做我们想做的事情的对象,但是我们如何实际上将它放在我们的类上?好吧,属性使用装饰器;为什么我们不能呢?让我们看看它会是什么样子(从这里开始,我将坚持使用单个“属性”,x)。

class Example(object):
@uberPropertydef x(self):return defaultX()

当然,这实际上还没有起作用。我们必须实现uberProperty和确保它同时处理get和set。

让我们从get开始。

我的第一次尝试是简单地创建一个新的UberProperty对象并返回它:

def uberProperty(f):return UberProperty(f)

当然,我很快发现这是行不通的:Python从不将可调用对象绑定到对象,我需要对象来调用函数。即使在类中创建装饰器也不起作用,因为尽管现在我们有了类,我们仍然没有对象可以使用。

所以我们需要在这里做更多的事情。我们知道一个方法只需要表示一次,所以让我们继续保留我们的装饰器,但修改UberProperty以只存储method引用:

class UberProperty(object):
def __init__(self, method):self.method = method

它也是不可调用的,所以目前没有任何工作。

我们如何完成图片?好吧,当我们使用新的装饰器创建示例类时,我们最终会得到什么:

class Example(object):
@uberPropertydef x(self):return defaultX()
print Example.x     <__main__.UberProperty object at 0x10e1fb8d0>print Example().x   <__main__.UberProperty object at 0x10e1fb8d0>

在这两种情况下,我们都会返回UberProperty,这当然不是可调用的,所以这没有多大用处。

我们需要的是某种方法,在类创建后,装饰器创建的UberProperty实例动态绑定到类的对象,然后该对象返回给该用户使用。嗯,是的,这是第一个电话,伙计。

让我们先写下我们希望找到的结果。我们将UberProperty绑定到一个实例,所以一个明显的返回是BoundUberProperty。这是我们实际维护x属性状态的地方。

class BoundUberProperty(object):def __init__(self, obj, uberProperty):self.obj = objself.uberProperty = uberPropertyself.isSet = False
def setValue(self, value):self.value = valueself.isSet = True
def getValue(self):return self.value if self.isSet else self.uberProperty.method(self.obj)
def clearValue(self):del self.valueself.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修改为:

@uberPropertydef 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.9858132013-05-31 00:05:13.9862902013-05-312013-05-31 00:05:13.986310

(哎呀,我工作到很晚。)

请注意,我在这里使用了getValuesetValueclearValue。这是因为我还没有链接到自动返回这些的方法。

但我认为这是一个很好的地方,因为我累了。你也可以看到我们想要的核心功能已经到位;其余的是装饰。重要的可用性装饰,但这可以等到我有一个改变来更新帖子。

我将在下一篇文章中通过解决这些问题来完成示例:

  • 我们需要确保UberObject的__init__总是被子类调用。

    • 所以我们要么强制它在某个地方被调用,要么阻止它被实现。
    • 我们将看到如何使用元类来做到这一点。
  • 我们需要确保我们处理某人“别名”的常见情况一个函数,比如:

      class Example(object):@uberPropertydef x(self):...
    y = x
  • We need e.x to return e.x.getValue() by default.

    • What we'll actually see is this is one area where the model fails.
    • It turns out we'll always need to use a function call to get the value.
    • But we can make it look like a regular function call and avoid having to use e.x.getValue(). (Doing this one is obvious, if you haven't already fixed it out.)
  • We need to support setting e.x directly, as in e.x = <newvalue>. We can do this in the parent class too, but we'll need to update our __init__ code to handle it.

  • Finally, we'll add parameterized attributes. It should be pretty obvious how we'll do this, too.

Here's the code as it exists up to now:

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 = objself.uberProperty = uberPropertyself.isSet = False
def setValue(self, value):self.value = valueself.isSet = True
def getValue(self):return self.value if self.isSet else self.uberProperty.method(self.obj)
def clearValue(self):del self.valueself.isSet = False
def uberProperty(f):return UberProperty(f)
class Example(UberObject):
@uberPropertydef x(self):return datetime.datetime.now()

[1]我可能会落后于这种情况。

我很惊讶没有人提到属性是描述符类的绑定方法,Adam DonohueNeilenMarais在他们的帖子中完全理解了这个想法——getter和setter是函数,可以用来:

  • 验证
  • 改变数据
  • 鸭型(强迫型到另一型)

这提供了一种智能的方式来隐藏实现细节和代码垃圾,如正则表达式、类型转换、try…除了块、断言或计算值。

一般来说,在对象上执行CRUD通常可能相当平凡,但考虑将持久化到关系数据库的数据示例。ORM可以在绑定到属性类中定义的fget、fset、fdel的方法中隐藏特定SQL白话的实现细节,该方法将管理在OO代码中如此丑陋的可怕if… elif… else梯子-公开简单而优雅的self.variable = something并为开发人员使用 ORM消除细节。

如果一个人认为属性只是束缚和纪律语言(即Java)的一些沉闷的痕迹,他们就错过了描述符的要点。

在复杂的项目中,我更喜欢使用带有显式setter函数的只读属性(或getter):

class MyClass(object):...@propertydef 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()

@property和传统的getter和setter都有其优点。这取决于您的用例。

@property的优点

  • 在更改数据访问的实现时,您不必更改接口。当你的项目很小时,你可能想使用直接属性访问来访问类成员。例如,假设你有一个类型Foo的对象foo,它有一个成员num。然后你可以简单地用num = foo.num获得这个成员。随着你的项目的发展,你可能会觉得需要对简单的属性访问进行一些检查或调试。然后你可以用@property类来做到这一点。数据访问接口保持不变,这样就不需要修改客户端代码。

    引用自PEP-8

    对于简单的公共数据属性,最好只公开属性名称,而不要使用复杂的访问器/修改器方法。请记住,如果你发现简单的数据属性需要增长函数行为,Python为未来的增强提供了一条简单的途径。在这种情况下,使用属性将函数实现隐藏在简单的数据属性访问语法后面。

  • 在Python中使用@property进行数据访问被视为Pythonic

传统getter和setter的优点

  • 传统的getter和setter允许比简单的属性访问更复杂的数据访问。例如,当你设置一个类成员时,有时你需要一个标志来指示你想在哪里强制执行此操作,即使看起来不完美。虽然如何增加像foo.num = num这样的直接成员访问并不明显,但你可以使用额外的force参数轻松增加传统的setter:

    def Foo:def set_num(self, num, force=False):...
  • Traditional getters and setters make it explicit that a class member access is through a method. This means:

    • What you get as the result may not be the same as what is exactly stored within that class.

    • Even if the access looks like a simple attribute access, the performance can vary greatly from that.

    Unless your class users expect a @property hiding behind every attribute access statement, making such things explicit can help minimize your class users surprises.

  • As mentioned by @NeilenMarais and in this post, extending traditional getters and setters in subclasses is easier than extending properties.

  • Traditional getters and setters have been widely used for a long time in different languages. If you have people from different backgrounds in your team, they look more familiar than @property. Also, as your project grows, if you may need to migrate from Python to another language that doesn't have @property, using traditional getters and setters would make the migration smoother.

Caveats

  • Neither @property nor traditional getters and setters makes the class member private, even if you use double underscore before its name:

    class Foo:def __init__(self):self.__num = 0
    @propertydef num(self):return self.__num
    @num.setterdef 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: 0print(foo.get_num())    # output: 0print(foo._Foo__num)    # output: 0

这是从“有效的Python:编写更好的Python的90种具体方法”(神奇的书。我强烈推荐它)摘录。

要记住的事情

使用简单的公共属性定义新的类接口并避免定义setter和getter方法。

当属性被定义时,使用@属性定义特殊行为如果需要,可以访问您的对象。

遵循最少惊讶的规则,避免奇怪的副作用@属性方法。

确保@属性方法快速;对于缓慢或复杂的工作-特别是涉及I/O或引起副作用-使用正常方法。

@属性的一个高级但常见的用法是转换将一个简单的数值属性转换为动态计算。这非常有用,因为它允许您迁移所有现有的一个类有新的行为,而不需要任何调用站点要重写(如果有调用代码,这尤其重要你无法控制)。@属性也提供了一个重要的权宜之计随着时间的推移改进界面。

我特别喜欢@属性,因为它可以让你增加随着时间的推移,朝着更好的数据模型发展。
@属性是一个工具帮助您解决在实际代码中遇到的问题。不要过度使用它。当你发现自己反复扩展@属性时方法,可能是时候重构你的类而不是进一步你的代码糟糕的设计铺平。

使用@属性给出现有的实例属性新功能。

向更好的数据取得增量进展使用@属性建模。

考虑重构一个类和所有调用当你发现自己使用@属性太重时。