关于python:使用@property而不是getter和setter

Using @property versus getters and setters

下面是一个纯Python特定的设计问题:

1
2
3
4
5
6
7
class MyClass(object):
    ...
    def get_my_attr(self):
        ...

    def set_my_attr(self, value):
        ...

1
2
3
4
5
6
7
8
9
class MyClass(object):
    ...        
    @property
    def my_attr(self):
        ...

    @my_attr.setter
    def my_attr(self, value):
        ...

python让我们用任何一种方式来做。如果您要设计一个Python程序,您将使用哪种方法,为什么?


首选属性。他们就是为了这个。

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

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


在Python中,您不使用getter、setter或properties只是为了好玩。首先只使用属性,然后,只有在需要时才迁移到属性,而不必使用类更改代码。

确实有很多扩展名为.pY的代码,它使用GETTER和SETTER和继承和无意义的类,例如一个简单的元组,但是它是用Python编写C++或Java的人编写的代码。

这不是Python代码。


使用属性可以让您从正常的属性访问开始,然后根据需要用getter和setter备份它们。


简短的回答是:房地产大获全胜。总是。

有时需要有能手和二传手,但即使那样,我也会把他们"藏"到外面的世界。在python中有很多方法可以做到这一点(getattrsetattr__getattribute__等),但是非常简洁和干净的方法是:

1
2
3
4
5
6
7
8
9
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的主题。


[TL;Dr?对于代码示例,可以跳到末尾。]好的。

实际上,我更喜欢使用不同的成语,这对于一次性使用来说有点复杂,但是如果您有更复杂的用例,这就更好了。好的。

先来点背景。好的。

属性是有用的,因为它们允许我们以编程的方式处理设置和获取值,但仍然允许以属性的形式访问属性。我们可以将"get"转换为"calculations"(本质上),也可以将"set"转换为"events"。假设我们有下面的类,我用JAVA和SETTER对Java进行了编码。好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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()

你可能想知道为什么我没有在对象的__init__方法中调用defaultXdefaultY。原因是,对于我们的情况,我想假设someDefaultComputation方法返回的值随时间而变化,例如时间戳,并且每当xy未设置(在本例中,"not set"表示"set to none")时,我想要x的(或y)默认计算的值。好的。

所以这是跛脚的,因为上面描述了很多原因。我将使用属性重写它:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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]这是额外的噪声,会使代码混乱。好的。

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

第三,与Java类的示例不同,吸气剂不能接受其他参数。现在我可以听到你已经说了,好吧,如果它采用参数,它不是一个getter!从官方的角度来说,这是真的。但是在实际意义上,我们没有理由不能参数化一个命名的属性——比如x——并为一些特定的参数设置它的值。好的。

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

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

例如。我们能得到的最接近的方法是重写赋值以暗示一些特殊的语义:好的。

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

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

但是即使我们这样做了,我们仍然不能用属性来支持它,因为我们无法获取值,因为我们根本无法将参数传递给getter。所以我们必须归还所有东西,引入不对称。好的。

Java风格的GETT/SETTER让我们处理这个问题,但是我们回到了需要吸气剂/设定器。好的。

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

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

  • 用户不需要在函数的基础上定义额外的变量,因此我们不需要在代码中使用__init__setattr。变量的存在只是因为我们已经创建了这个新的样式属性。好的。

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

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

  • 我们可以参数化属性。好的。

在代码方面,我们需要一种方法来编写:好的。

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

并且能够做到:好的。

1
2
3
4
5
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。好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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这里是一个类方法,valueUberProperty的值,我添加了isSet,因为None可能是一个真正的值,这允许我们以一种干净的方式声明那里确实是"没有价值"。另一种方式是某种形式的哨兵。好的。

这基本上给了我们一个可以做我们想做的事情的对象,但是我们如何把它放到我们的课堂上呢?好吧,房产使用装修师,我们为什么不能呢?让我们看看它的外观(从这里开始,我将坚持只使用一个"属性",x)。好的。

1
2
3
4
5
class Example(object):

    @uberProperty
    def x(self):
        return defaultX()

当然,这实际上还不起作用。我们必须实施UberProperty和确保它同时处理get和set。好的。

让我们从GET开始。好的。

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

1
2
def uberProperty(f):
    return UberProperty(f)

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

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

1
2
3
4
class UberProperty(object):

    def __init__(self, method):
        self.method = method

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

我们如何完成这张照片?那么,当我们使用新的decorator创建示例类时,最终会得到什么呢?好的。

1
2
3
4
5
6
7
8
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绑定到一个实例上,所以一个明显的返回内容就是boundberproperty。在这里,我们将实际维护x属性的状态。好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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__被称为decorator时,我们已经运行了,所以只需要查看对象的__dict__并更新属性值为UberProperty类型的任何属性。好的。

现在,Uber属性很酷,我们可能会经常使用它们,所以只创建一个为所有子类这样做的基类是有意义的。我想你知道基类会叫什么。好的。

1
2
3
4
5
6
7
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继承,然后……好的。

1
2
e = Example()
print e.x               -> <__main__.BoundUberProperty object at 0x104604c90>

x修改为:好的。

1
2
3
@uberProperty
def x(self):
    return *datetime.datetime.now()*

我们可以运行一个简单的测试:好的。

1
2
3
4
5
6
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()

我们得到了我们想要的输出:好的。

1
2
3
4
2013-05-31 00:05:13.985813
2013-05-31 00:05:13.986290
2013-05-31
2013-05-31 00:05:13.986310

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

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

但我觉得这是一个好地方,因为我累了。您还可以看到,我们需要的核心功能已经到位;剩下的就是窗饰。重要的可用性窗口穿衣,但这可以等到我有一个改变,以更新帖子。好的。

我将在下一篇文章中通过解决以下问题来完成这个示例:好的。

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

    • 所以我们要么强制在某个地方调用它,要么阻止它被实现。
    • 我们将了解如何使用元类来实现这一点。
  • 我们需要确保我们处理的常见情况是某人的别名其他功能,如:好的。

    1
    2
    3
    4
    5
    6
      class Example(object):
          @uberProperty
          def x(self):
              ...

          y = x
  • 我们需要e.x默认返回e.x.getValue()。好的。

    • 我们实际上会看到,这是一个模型失效的区域。
    • 事实证明,我们总是需要使用函数调用来获取值。
    • 但是我们可以使它看起来像一个常规函数调用,避免使用e.x.getValue()。(如果你还没有把它修好的话,做这个很明显。)
  • 我们需要支持设置e.x directly,就像在e.x = 中一样。我们也可以在父类中这样做,但是我们需要更新我们的__init__代码来处理它。好的。

  • 最后,我们将添加参数化属性。很明显我们也会这么做。好的。

以下是迄今为止存在的代码:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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]我可能会在这件事是否仍然存在的问题上落后。好的。好啊。


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

实际上,您可以使用

1
2
3
4
5
6
7
8
class C(object):
    _p = 1
    @property
    def p(self):
        return self._p
    @p.setter
    def p(self, val):
        self._p = val

您可以访问getter和setter函数,例如C.p.fgetC.p.fset,但是您不能轻松地使用普通方法继承(例如super)工具来扩展它们。在深入研究super的复杂性之后,您确实可以这样使用super:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 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的未绑定副本。


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

比较

1
2
o.x = 5
ox = o.x

VS

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

对我来说很明显,这很容易理解。属性还允许更容易地使用私有变量。


在大多数情况下,我都不想使用。属性的问题是它们使类的透明性降低。尤其是,如果您要从setter中引发异常,则这是一个问题。例如,如果您有account.email属性:

1
2
3
4
5
6
7
8
9
10
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

那么,类的用户不希望为属性赋值会导致异常:

1
2
3
a = Account()
a.email = 'badaddress'
--> ValueError: Invalid email address.

因此,异常可能不被处理,并且在调用链中传播得太高,无法正确处理,或者导致向程序用户呈现非常无益的回溯(这在Python和Java世界中非常普遍)。

我也会避免使用getter和setter:

  • 因为提前为所有属性定义它们非常耗时,
  • 使代码量不必要地变长,这使得理解和维护代码更加困难,
  • 如果只在需要时为属性定义它们,那么类的接口将会改变,从而损害类的所有用户。

我更喜欢在定义良好的地方(如验证方法中)执行复杂的逻辑,而不是属性和getter/setter:

1
2
3
4
5
class Account(object):
    ...
    def validate(self):
        if '@' not in self.email:
            raise ValueError('Invalid email address.')

或类似的account.save方法。

请注意,我并不是想说,在属性有用的情况下没有任何情况,只有当您能够使类变得足够简单和透明,以至于不需要它们时,您才可能获得更好的效果。


令我惊讶的是,没有人提到属性是描述符类的绑定方法,Adam Donohue和Neilenmarais在他们的文章中就得出了这样的想法——getter和setter是函数,可以用来:

  • 验证
  • 修改数据
  • duck类型(强制类型转换为其他类型)

这提供了一种隐藏实现细节和代码类似cruft的正则表达式、类型转换、尝试的智能方法。除了块、断言或计算值。

一般来说,在对象上执行CRUD可能非常普通,但请考虑将持久化到关系数据库的数据示例。ORM可以在绑定到fget、fset、fdel的方法中隐藏特定SQL语言的实现细节,这些方法在属性类中定义,这些属性类将管理糟糕的if.。埃利夫其他一些在OO代码中非常难看的梯子——暴露了简单而优雅的self.variable = something,并为使用ORM的开发人员排除了细节。

如果人们认为属性仅仅是束缚和纪律语言(即Java)的沉闷痕迹,那么它们就失去了描述符的点。


我觉得属性是让你只在你真正需要的时候才得到写getter和setter的开销。

Java编程文化强烈建议永远不要访问属性,而是通过吸气剂和设置器,而只需要那些真正需要的。总是写这些明显的代码片段有点冗长,注意到70%的时间它们从不被一些非平凡的逻辑所取代。

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

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


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

1
2
3
4
5
6
7
8
class MyClass(object):
...        
@property
def my_attr(self):
    ...

def set_my_attr(self, value):
    ...

在长寿命的项目中,调试和重构比编写代码本身要花更多的时间。使用@property.setter有几个缺点,使调试更加困难:

1)python允许为现有对象创建新属性。这使得以下打印错误很难跟踪:

1
my_object.my_atttr = 4.

如果您的对象是一个复杂的算法,那么您将花费相当长的时间试图找出它不收敛的原因(注意上面这行中有一个额外的"t")。

2)setter有时会演变成一种复杂而缓慢的方法(例如,访问数据库)。对于另一个开发人员来说,很难弄清楚为什么下面的函数非常慢。他可能会花很多时间来分析do_something()方法,而my_object.my_attr = 4.实际上是减速的原因:

1
2
3
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:


    For simple public data attributes, it is best to expose just the attribute name, without complicated accessor/mutator methods. Keep in mind that Python provides an easy path to future enhancement, should you find that a simple data attribute needs to grow functional behavior. In that case, use properties to hide functional implementation behind simple data attribute access syntax.

  • 在python中使用@property进行数据访问被视为pythonic:

    • 它可以增强你作为Python(非Java)程序员的自我识别能力。

    • 如果你的面试官认为Java风格的吸引子和定位器是反模式,它可以帮助你的求职面试。

传统吸气剂和setter的优势

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

    1
    2
    3
    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都不会使类成员成为私有的,即使在其名称前面使用双下划线:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    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