使用@property与使用getter和setter方法的区别

796

@property标记与经典的getter+setter相比有哪些优势?在哪些特定情况下程序员应选择使用其中之一?

使用属性:

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
13个回答

661

优先使用属性。它们就是为此而存在的。

原因在于Python中所有属性都是公开的。以下划线或两个下划线开头的名称只是一个警告,表示给定的属性是实现细节,可能在未来版本的代码中发生变化。但这并不妨碍您实际获取或设置该属性。因此,标准属性访问是正常的、符合Python风格的访问属性的方式。

属性的好处是它们在语法上与属性访问完全相同,因此您可以在不更改客户端代码的情况下从其中一个更改为另一个。您甚至可以拥有一个使用属性的类的版本(例如,用于按照约定编写代码或调试),另一个没有使用(用于生产),而不必更改使用它的代码。同时,您也不必为所有属性编写getter和setter,只有在需要对访问进行更好的控制时才需要。


108
Python 中带有双下划线的属性名具有特殊处理方式,这不仅仅是一种惯例。请参阅 http://docs.python.org/py3k/tutorial/classes.html#private-variables。 - 6502
71
它们被处理方式不同,但这并不妨碍你访问它们。PS:AD 30 C0。 - kindall
24
我不同意。结构化代码怎么能等同于意大利面条式代码呢? Python 是一门美丽的语言,但如果支持更好的封装和结构化类之类的简单东西会更好。 - user1441149
82
我同意大多数情况下的观点,但是在使用@property装饰器隐藏慢速方法时要小心。API的使用者期望属性访问像变量访问一样快速,如果过于偏离这个期望,会使你的API难以使用。 - defrex
7
这个问题并不是关于直接属性访问和属性的区别。很明显,代码越多执行起来就需要更多的时间。 - kindall
显示剩余10条评论

175
在Python中,你不会仅仅为了好玩而使用getter、setter或者属性。你首先只是使用属性,然后,只有在必要的时候,才最终迁移到一个属性,而不必更改使用你的类的代码。
确实有很多以 .py 扩展名结尾的代码使用getter和setter,继承和无意义的类,即使一个简单的元组也可以做到,但这些代码来自于使用Python的C++或Java编写的人。
这不是Python代码。

55
当你说“[...] 到处都是毫无意义的类,而简单的元组就足够了”时,类相对于元组的优势在于类实例提供了明确的名称以访问其部分,而元组没有。使用名称比使用元组下标更易读且可以避免错误,特别是当需要将其传递到当前模块之外时。 - Hibou57
18
@Hibou57说:“我不是说类是无用的。但有时候元组已经足够了。问题在于,来自Java或C ++的人别无选择,只能为每个东西创建类,因为其他可能性在这些语言中使用起来非常麻烦。 Java / C ++编程使用Python的另一个典型症状是为没有原因创建抽象类和复杂的类层次结构,在Python中你可以仅使用独立类,不需要这样做,因为有了鸭子类型。” - 6502
43
你可以使用namedtuple来实现,这里是相关文档链接:http://www.doughellmann.com/PyMOTW/collections/namedtuple.html - hugo24
5
自2.6版本以来,它已经在标准库中了...请参见http://docs.python.org/2/library/collections.html - 6502
1
如果你想要一个类似于元组的类,也可以使用定义了__slots__的类。你同样可以定义方法,这样更加节省内存。 - johannestaas
显示剩余6条评论

125

4
似乎很奇怪,因为它确实很奇怪。“成年人之间的事情”是Python中添加属性之前的一个梗,用于回应那些抱怨缺少访问修饰符的人。当属性被添加后,封装变得更加重要。类似的情况还发生在抽象基类上。"Python 一直在反对破坏封装。自由就是奴役。Lambda 应该只适合一行。" - johncip

75

简短的回答是:属性总是更胜一筹。始终如此。

有时候需要使用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的主题。


1
@BasicWolf - 我觉得很明显我是站在那一边的! :) 但我会在我的回答中添加一个段落来澄清。 - mac
21
“always”这个词提示作者试图通过陈述来说服你,而不是通过论证。所以加粗字体的出现也是如此。(我是说,如果你看到大写字母,那么——哇——它一定是正确的。)听着,“property”特性恰好与Java(Python的事实上的劲敌,出于某种原因)不同,因此Python社区的集体思维认为它更好。实际上,属性违反了“显式优于隐式”的规则,但没有人愿意承认。它已经成为语言的一部分,因此现在通过一个自证明的论点被宣称为“Pythonic”。 - Stuart Berg
7
无意冒犯。 :-P 我只是想指出在这种情况下,“Pythonic”规范是不一致的:“明确胜于隐含”直接与使用 property 相冲突。(它 看起来 像是一个简单的赋值,但实际上调用了一个函数。)因此,“Pythonic”本质上是一个无意义的术语,除非用诡辩的定义: “Pythonic规范是我们定义为Pythonic的东西。” - Stuart Berg
3
拥有一个遵循主题的约定集合的想法是非常好的。如果这样的约定集合存在,那么你可以将其用作指导思考的公理集,而不仅仅是一份需要记忆的冗长检查清单,后者显然不太实用。公理可用于推断,帮助你处理以前没有人见过的问题。可惜的是,“属性”功能威胁到使Python公理几乎变得毫无价值的想法。因此,我们只剩下了一份检查清单。 - Stuart Berg
5
我不同意。在大多数情况下,我更喜欢使用属性,但是当您想要强调设置某些内容会产生除修改“self”对象之外的副作用时,显式的setter可能会有所帮助。例如,user.email="..." 看起来似乎只是设置一个属性,不会引发异常,而 user.set_email("...") 明确表明可能存在异常等副作用。 - bluenote10
显示剩余2条评论

70

[简而言之? 你可以直接跳到结尾查看代码示例。]

实际上,我更喜欢使用不同的习语,如果您有更复杂的用例,这种方法会更好一些,但是如果您只需要使用一次,那么这种方法可能过于繁琐。

首先,让我们来了解一下背景。

属性很有用,因为它们允许我们以编程方式处理设置和获取值,但仍允许像属性一样访问属性。 我们可以将“获取”转换为“计算”(本质上),并将“设置”转换为“事件”。 因此,假设我们有以下类,我已经使用类似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()
你可能会想知道为什么我没有在对象的__init__方法中调用defaultXdefaultY。原因是,对于我们的情况,我想假设someDefaultComputation方法返回随时间变化的值,比如一个时间戳,并且每当x(或y)未设置时(在本例中,“未设置”意味着“设置为None”),我希望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] 这是额外的噪音,会使代码变得混乱。
另一个烦恼是我们仍然需要在__init__中初始化x和y的值。 (当然,我们可以使用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]
并且当然要确保我们的 setter 知道如何将前三个值提取为字典的键,并将其值设置为数字或其他内容。但即使我们这样做了,我们仍然无法通过属性支持它,因为我们根本不能传递参数给 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 是一个类方法,valueUberProperty 的值,并且我添加了 isSet ,因为 None 可能是一个真实的值,这样允许我们干净地声明没有真正的“值”。另一种方式是某种特殊的标记。

基本上,这给了我们一个可以完成我们想要的工作的对象,但我们如何将其实际放在我们的类上呢?那么,属性使用装饰器;我们为什么不能用呢?让我们看看它可能是什么样子的(从现在开始,我将仅使用一个“属性”,x)。

class Example(object):

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

当然,这实际上还没有起作用。我们必须实现uberProperty并确保它处理了获取和设置。

让我们从获取开始。

我的第一次尝试是简单地创建一个新的 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

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

请注意,此处使用了getValuesetValueclearValue。这是因为我还没有链接自动返回这些的方式。

但我认为现在停下来是个好时机,因为我有点累了。你也可以看到我们想要的核心功能已经就位。其余的只是装饰性的东西。重要的用法装饰性的东西,但是在我有机会更新帖子之前,这可以等待。

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

  • 我们需要确保UberObject的__init__始终由子类调用。

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

      class Example(object):
          @uberProperty
          def x(self):
              ...
    
          y = x
    
    我们需要默认情况下e.x返回e.x.getValue()
    • 实际上,这是模型失败的一个领域。
    • 结果我们总是需要使用函数调用来获取值。
    • 但我们可以让它看起来像常规的函数调用,避免使用e.x.getValue()。(如果你还没有修复它,那么这很明显。)
    我们需要支持直接设置e.x,例如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] 我可能对这是否仍然是事实落后了。


58
是的,这就是“tldr”。你能否请简要概括一下你在这里想做什么? - poolie
9
@Adam return self.x or self.defaultX() 这段代码存在潜在的风险。当 self.x == 0 时会发生什么? - Kelly Thomas
FYI,你可以使得getter可参数化。这需要将变量设置为自定义类,并覆盖__getitem__方法。但这会很奇怪,因为你将使用完全非标准的Python。 - will
2
@KellyThomas 只是试图保持示例简单。要做到正确,您需要完全创建和删除x __dict__条目,因为甚至None值可能已经被特别设置。但是,是的,您绝对正确,这是在生产用例中需要考虑的事情。 - Adam Donahue
类似Java的getter方法允许您进行完全相同的计算,不是吗? - qed
这很令人印象深刻...如果我理解你的思路,Python 中有一个类似的模式,称为描述符,在 3.6 版本中使用 __set_name__ 方法更加容易实现。 - Chris May

26
“使用属性对我来说更直观,而且更适合大多数代码。”
“比较”。
o.x = 5
ox = o.x

vs.

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

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

26
我认为两者都有各自的用处。使用@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.fgetC.p.fset 访问 getter 和 setter 函数,但你不能轻松地使用普通的方法继承(例如 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副本。


13

我认为属性是让你在实际需要时才编写getter和setter的开销。

Java编程文化强烈建议不要直接访问属性,而是通过getter和setter来访问,只使用真正需要的那些。 经常编写这些显然的代码有点冗长,并且注意到它们70%的时间从未被一些非平凡逻辑所替换。

在Python中,人们确实关心这种开销,因此您可以采用以下做法:

  • 一开始如果没有必要,不要使用getter和setter
  • 使用@property来实现它们,而不改变代码其余部分的语法。

2
“请注意,70%的时间它们从未被一些非平凡的逻辑所替换。”-- 这是一个相当具体的数字,它来自某个地方吗?还是你打算用它作为一个模糊的“绝大多数”的东西(我不是在开玩笑,如果有一项量化这个数字的研究,我会真诚地感兴趣阅读它)。 - Adam Parkin
2
哦,不好意思。听起来好像我有一些研究可以支持这个数字,但我只是想表达“大多数情况下”。 - fulmicoton
7
人们关心的不是开销,而是在Python中您可以从直接访问切换到访问器方法而不必更改客户端代码,因此一开始直接公开属性并没有什么损失。 - Neil G

12

在大多数情况下,我更愿意都不使用。属性的问题是使类不够透明。特别是,如果您要从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方法。

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


3
@user2239734,我认为你误解了“属性”的概念。虽然您可以在设置属性时验证值,但无需这样做。您可以在类中同时拥有属性和一个名为validate()的方法。当您在简单的obj.x = y赋值后面有复杂的逻辑时,就可以使用属性,具体取决于逻辑的实现方式。 - Zaur Nasibov

11

我很惊讶地发现,没有人提到属性是描述符类的绑定方法。 Adam DonohueNeilenMarais在他们的帖子中恰好涉及到了这个想法——获取器和设置器是函数,并且可以用于:

  • 验证
  • 更改数据
  • 鸭式类型转换(将类型强制转换为另一种类型)

这提供了一种“聪明”的方式来隐藏实现细节和代码杂乱无章,例如常规表达式、类型转换、try...except块、断言或计算值。

通常,对对象执行CRUD操作可能会相当平凡,但考虑要持久化到关系数据库中的数据的例子。ORM可以隐藏特定SQL术语的实现细节,而这些细节包含在定义为属性类中的fget、fset、fdel方法中,这些方法将管理在面向对象代码中非常丑陋的if...elif...else梯子中的代码 - 暴露了简单优雅的self.variable = something并规避了使用ORM的开发人员的细节。

如果将属性仅视为奴役和纪律语言(即Java)的枯燥遗迹,那么他们就错过了描述符的要点。


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