使用getter和setter的Pythonic方式是什么?

667

我这样做:

def set_property(property,value):  
def get_property(property):  
或者
object.property = value  
value = object.property

如何在Python中使用getter和setter的Pythonic方式?

9个回答

1084

试试这个:Python 属性

示例代码如下:

class C(object):
    def __init__(self):
        self._x = None

    @property
    def x(self):
        """I'm the 'x' property."""
        print("getter of x called")
        return self._x

    @x.setter
    def x(self, value):
        print("setter of x called")
        self._x = value

    @x.deleter
    def x(self):
        print("deleter of x called")
        del self._x


c = C()
c.x = 'foo'  # setter called
foo = c.x    # getter called
del c.x      # deleter called

5
在实例化 _x 时,是否会在初始化程序中调用 x 的设置器(setter)? - Casey
18
不行。对于 ._x 的引用(它只是一个普通的属性而不是属性),会绕过 property 封装。只有对 .x 的引用会经过 property - ShadowRanger
mypy 期望这两个函数在一起(x 及其 setter 中不包含其他函数)。 - Pierre.Sassoulas
11
如果你需要在getter或setter中执行某些操作或操作而不仅仅是检索私有属性,则这是典型的Pythonic方式。对于这种简单情况,即使在其他语言中使用getter/setter被认为是最佳实践,在Python中只需创建公共属性。简单、张扬而又直截了当。 - Mason3k

579

如何使用Pythonic的方式来使用getter和setter?

“Pythonic”的方式不是使用“getter”和“setter”,而是像问题所示使用普通属性,并使用del删除(但名称已更改以保护无辜者...内置函数):

value = 'something'

obj.attribute = value  
value = obj.attribute
del obj.attribute

如果以后你想修改设置和获取属性,你可以使用property装饰器而无需更改用户代码:
class Obj:
    """property demo"""
    #
    @property            # first decorate the getter method
    def attribute(self): # This getter method name is *the* name
        return self._attribute
    #
    @attribute.setter    # the property decorates with `.setter` now
    def attribute(self, value):   # name, e.g. "attribute", is the same
        self._attribute = value   # the "value" name isn't special
    #
    @attribute.deleter     # decorate with `.deleter`
    def attribute(self):   # again, the method name is the same
        del self._attribute

(每次使用装饰器都会复制并更新先前的属性对象,因此请注意对于每个set、get和delete函数/方法,应使用相同的名称。)
定义完上述内容后,原始的设置、获取和删除代码保持不变:
obj = Obj()
obj.attribute = value  
the_value = obj.attribute
del obj.attribute

您应该避免这样做:
def set_property(property,value):  
def get_property(property):  
首先,上述代码无法正常工作,因为你没有为属性设置实例提供参数(通常是self),正确的写法应该是:
class Obj:

    def set_property(self, property, value): # don't do this
        ...
    def get_property(self, property):        # don't do this either
        ...

其次,这重复了两个特殊方法__setattr____getattr__的目的。
第三,我们还有内置函数setattrgetattr
setattr(object, 'property_name', value)
getattr(object, 'property_name', default_value)  # default is optional

@property装饰器用于创建getter和setter。

例如,我们可以修改设置行为以限制所设置的值:

class Protective(object):

    @property
    def protected_value(self):
        return self._protected_value

    @protected_value.setter
    def protected_value(self, value):
        if acceptable(value): # e.g. type or range check
            self._protected_value = value

一般来说,我们应该避免使用property而直接使用属性。这是Python用户所期望的。按照最小惊讶原则,除非你有非常充分的理由,否则应该尽量满足用户的期望。
演示:
例如,假设我们需要保护对象的属性为0到100之间的整数,并防止其被删除,同时提供适当的消息来告知用户其正确的使用方法:
class Protective(object):
    """protected property demo"""
    #
    def __init__(self, start_protected_value=0):
        self.protected_value = start_protected_value
    # 
    @property
    def protected_value(self):
        return self._protected_value
    #
    @protected_value.setter
    def protected_value(self, value):
        if value != int(value):
            raise TypeError("protected_value must be an integer")
        if 0 <= value <= 100:
            self._protected_value = int(value)
        else:
            raise ValueError("protected_value must be " +
                             "between 0 and 100 inclusive")
    #
    @protected_value.deleter
    def protected_value(self):
        raise AttributeError("do not delete, protected_value can be set to 0")

(请注意,__init__ 引用 self.protected_value,但属性方法引用 self._protected_value。这样做是为了让 __init__ 通过公共 API 使用属性,确保它是“受保护的”。)
使用方法:
>>> p1 = Protective(3)
>>> p1.protected_value
3
>>> p1 = Protective(5.0)
>>> p1.protected_value
5
>>> p2 = Protective(-5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __init__
  File "<stdin>", line 15, in protected_value
ValueError: protectected_value must be between 0 and 100 inclusive
>>> p1.protected_value = 7.3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 17, in protected_value
TypeError: protected_value must be an integer
>>> p1.protected_value = 101
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 15, in protected_value
ValueError: protectected_value must be between 0 and 100 inclusive
>>> del p1.protected_value
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 18, in protected_value
AttributeError: do not delete, protected_value can be set to 0

名称重要吗?

是的,它们确实很重要。 .setter.deleter会复制原始属性。这使得子类能够适当地修改行为而不更改父级的行为。

class Obj:
    """property demo"""
    #
    @property
    def get_only(self):
        return self._attribute
    #
    @get_only.setter
    def get_or_set(self, value):
        self._attribute = value
    #
    @get_or_set.deleter
    def get_set_or_delete(self):
        del self._attribute

为了让这个工作正常运行,你必须使用相应的名称:

obj = Obj()
# obj.get_only = 'value' # would error
obj.get_or_set = 'value'  
obj.get_set_or_delete = 'new value'
the_value = obj.get_only
del obj.get_set_or_delete
# del obj.get_or_set # would error

我不确定这个用法有什么用处,但如果你想要一个只能进行获取、设置、删除操作的属性,可以使用该方法。最好保持语义相同的属性名称相同。

结论

从简单的属性开始。

如果以后需要设置、获取和删除功能,则可以使用 property 装饰器添加它。

避免使用名称为 set_...get_... 的函数,因为这就是属性的作用。


11
在你的演示中,__init__ 方法引用了 self.protected_value ,但是 getter 和 setter 引用了 self._protected_value。您能否解释一下这是如何工作的?我测试了您的代码,发现它可以正常运行 - 所以这不是一个错别字。 在您的示例代码中,self.protected_value 实际上是对 _protected_value 的访问,它们两者是等效的。Python 中的命名约定是使用前导下划线来表示一个变量或方法是受保护的,但并不会阻止对其进行访问。因此,在 __init__ 方法中,您可以使用 self.protected_value 来初始化 _protected_value。而在 getter 和 setter 方法中,使用 _protected_value 是更常见的写法,因为这些方法通常也是受保护的,并且使用前导下划线更符合 Python 的约定。 - codeforester
3
@codeforester 我本来希望在我的答案中回复,但在我能够这样做之前,这个评论应该足够了。我希望你能看到它是通过公共API使用属性,确保它是“受保护的”。如果在__init__中使用非公共API而不是使用属性来“保护”它,那么这是没有意义的。 - Russia Must Remove Putin
5
是的,@AaronHall现在明白了。 我之前没有意识到self.protected_value = start_protected_value实际上是在调用setter函数;我以为这只是一个赋值语句。 - codeforester
4
依我之见,如果我理解正确的话,这应该是被采纳的答案。Python与Java相比正好相反,Python默认将所有东西都设为公开的,需要时再添加隐私,而不是Java默认将所有东西都设为私有的,需要时再编写额外的代码来使其公开。 - 463035818_is_not_a_number
3
根据PEP 8,对于简单的公共数据属性,最好只公开属性名称,而不是复杂的访问器/修改器方法。请记住,Python提供了一个简单的路径以实现未来增强,如果您发现一个简单的数据属性需要增加功能行为,请使用属性将功能实现隐藏在简单的数据属性访问语法后面。 - wjandrea
显示剩余21条评论

34
In [1]: class test(object):
    def __init__(self):
        self.pants = 'pants'
    @property
    def p(self):
        return self.pants
    @p.setter
    def p(self, value):
        self.pants = value * 2
   ....: 
In [2]: t = test()
In [3]: t.p
Out[3]: 'pants'
In [4]: t.p = 10
In [5]: t.p
Out[5]: 20

27

使用@property@attribute.setter不仅可以帮助您使用“Python式”的方式,还可以在创建对象和修改对象时检查属性的有效性。

class Person(object):
    def __init__(self, p_name=None):
        self.name = p_name

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, new_name):
        if type(new_name) == str: #type checking for name property
            self._name = new_name
        else:
            raise Exception("Invalid value for name")

通过这种方式,您实际上可以将_name属性从客户端开发人员中“隐藏”,并对名称属性类型进行检查。请注意,即使在初始化期间也会调用setter方法。因此:

p = Person(12)

将导致:

Exception: Invalid value for name

但是:

>>>p = person('Mike')
>>>print(p.name)
Mike
>>>p.name = 'George'
>>>print(p.name)
George
>>>p.name = 2.3 # Causes an exception

我认为 self.name 应该改为 self._name,而且 p = person('Mike') 应该改为 p = Person('Mike')。编辑队列已满... 干杯! - lcnittl
如果我们在__init__方法中将self.name更改为self._name,则在对象实例化时我们将失去验证。这样,Person(12)就可以被创建(尽管之后无法更新为另一个数字作为名称)。 - Farzad Vertigo

16

这是一个旧问题,但主题非常重要且一直很受关注。如果有人想超越简单的getter/setter,我已经撰写了一篇关于在Python中实现超强属性的文章,支持插槽、可观察性和减少样板代码。

这是一个旧问题,但主题非常重要且一直很受关注。如果有人想超越简单的getter/setter,我已经撰写了一篇关于在Python中实现超强属性的文章,支持插槽、可观察性和减少样板代码。
from objects import properties, self_properties


class Car:
    with properties(locals(), 'meta') as meta:

        @meta.prop(read_only=True)
        def brand(self) -> str:
            """Brand"""

        @meta.prop(read_only=True)
        def max_speed(self) -> float:
            """Maximum car speed"""

        @meta.prop(listener='_on_acceleration')
        def speed(self) -> float:
            """Speed of the car"""
            return 0  # Default stopped

        @meta.prop(listener='_on_off_listener')
        def on(self) -> bool:
            """Engine state"""
            return False

    def __init__(self, brand: str, max_speed: float = 200):
        self_properties(self, locals())

    def _on_off_listener(self, prop, old, on):
        if on:
            print(f"{self.brand} Turned on, Runnnnnn")
        else:
            self._speed = 0
            print(f"{self.brand} Turned off.")

    def _on_acceleration(self, prop, old, speed):
        if self.on:
            if speed > self.max_speed:
                print(f"{self.brand} {speed}km/h Bang! Engine exploded!")
                self.on = False
            else:
                print(f"{self.brand} New speed: {speed}km/h")
        else:
            print(f"{self.brand} Car is off, no speed change")

这个类可以像这样使用:

mycar = Car('Ford')

# Car is turned off
for speed in range(0, 300, 50):
    mycar.speed = speed

# Car is turned on
mycar.on = True
for speed in range(0, 350, 50):
    mycar.speed = speed

这段代码将产生以下输出:

Ford Car is off, no speed change
Ford Car is off, no speed change
Ford Car is off, no speed change
Ford Car is off, no speed change
Ford Car is off, no speed change
Ford Car is off, no speed change
Ford Turned on, Runnnnnn
Ford New speed: 0km/h
Ford New speed: 50km/h
Ford New speed: 100km/h
Ford New speed: 150km/h
Ford New speed: 200km/h
Ford 250km/h Bang! Engine exploded!
Ford Turned off.
Ford Car is off, no speed change

更多信息关于如何以及为什么在这里:https://mnesarco.github.io/blog/2020/07/23/python-metaprogramming-properties-on-steroids


获取“对象”的推荐方式是什么? - user66081
@user66081,代码在文章中。您可以复制/粘贴它。这是一个相当小的脚本。 - mnesarco

11

属性非常有用,因为您可以将其与赋值一起使用,但也可以包括验证。您可以看到此代码,其中使用装饰器@property和@property_name.setter创建方法:

# Python program displaying the use of @property 
class AgeSet:
    def __init__(self):
        self._age = 0

    # using property decorator a getter function
    @property
    def age(self):
        print("getter method called")
        return self._age

    # a setter function
    @age.setter
    def age(self, a):
        if(a < 18):
            raise ValueError("Sorry your age is below eligibility criteria")
        print("setter method called")
        self._age = a

pkj = AgeSet()

pkj.age = int(input("set the age using setter: "))

print(pkj.age)

我在这篇文章中写了更多细节:https://pythonhowtoprogram.com/how-to-create-getter-setter-class-properties-in-python-3/


6

你可以使用访问器/修改器(即@attr.setter@property),也可以不使用,但最重要的是保持一致性!

如果你仅仅是使用@property来访问属性,例如:

class myClass:
    def __init__(a):
        self._a = a

    @property
    def a(self):
        return self._a

使用它来访问每个*属性!使用@property访问一些属性而留下其他一些属性公共(即没有下划线的名称)没有访问器是不好的实践,例如不要这样做

class myClass:
    def __init__(a, b):
        self.a = a
        self.b = b

    @property
    def a(self):
        return self.a


请注意,即使它是公共的,此处的self.b没有明确的访问器。
同样地,在使用setter(或mutator)时,请随意使用@attribute.setter,但请保持一致!当您执行例如:
class myClass:
    def __init__(a, b):
        self.a = a
        self.b = b 

    @a.setter
    def a(self, value):
        return self.a = value

我很难猜测您的意图。一方面,您说都是公共的(名称中没有下划线),因此我理论上应该被允许访问/更改(获取/设置)它们两个。但是,您指定了一个显式mutator只适用于a,这告诉我也许我不应该能够设置b。由于您提供了一个显式的mutator,我不确定缺少显式的访问器(@property)是否意味着我不能访问这两个变量之一,还是您只是在使用@property时节约了开销。

*例外情况是当您显式想要使某些变量可访问或可变但不是这两者或者希望在访问或修改属性时执行一些其他逻辑。这是我个人使用@property和@attribute.setter(否则不需要公共属性的显式访问器/ mutators)。

最后,PEP8和Google Style Guide建议:

PEP8,设计继承表示:

对于简单的公共数据属性,最好仅暴露属性名称,而不使用复杂的访问器/ mutator方法。请记住,Python提供了一条易于实现的路径,以便在发现简单数据属性需要增加功能行为时进行未来增强。在这种情况下,使用属性将函数实现隐藏在简单数据属性访问语法背后。

另一方面,根据Google Style GuidePython语言规则/属性的建议是:

在新代码中使用属性来访问或设置您通常会使用简单轻量级的访问者或setter方法的数据。属性应该用@property装饰器创建。

此方法的优点:

通过消除简单属性访问的显式获取和设置方法调用来提高可读性。允许计算变得懒惰。被认为是维护类接口的Pythonic方式。在性能方面,当直接访问变量是合理的时,允许属性绕过需要微不足道的访问器方法。这还允许在不破坏接口的情况下添加访问器方法。

缺点:

必须从object继承Python 2。可以隐藏副作用,就像操作符重载一样。对子类可能会产生困惑。


2
我强烈不同意。如果我的对象有15个属性,而我想用@property计算其中一个属性,那么让其他属性也使用@property似乎是一个不明智的决定。 - Quelklef
1
嗯,也许吧。我个人认为这不好——完全是多余的。但我能理解你的想法。我猜我只是把你的帖子看作是在说“使用@property最重要的事情是保持一致性”。 - Quelklef
@TomaszBartkowiak 不, property 装饰器的整个要点就是不需要编写样板getter和setter。 Python不是Java。此外,从Google建议提供的示例中可以看出,他们谈论的是您的getter实际上正在做某事的情况,而不仅仅是 return self._some_var。因此,这与PEP8并不矛盾。 - juanpa.arrivillaga
绝对不行!Python访问器的优点在于您不需要保持一致。我从来没有一个类是全部使用@property,但我几乎总是需要在其中添加一些。 - stenci
getter是一个可以作为参数传递给其他函数的方法,例如valid_if_not=self.is_finalized)。对于糟糕的属性,您每次都必须进行lambda转换。此外,可以将显式设置组合在序列中,例如:.set_name('asd').set_value(3). 对于糟糕的属性,您无法这样做。因此不要使用属性。 - iperov
显示剩余4条评论

3
你可以使用魔术方法__getattribute____setattr__
class MyClass:
    def __init__(self, attrvalue):
        self.myattr = attrvalue
    def __getattribute__(self, attr):
        if attr == "myattr":
            #Getter for myattr
    def __setattr__(self, attr):
        if attr == "myattr":
            #Setter for myattr

请注意,__getattr____getattribute__不是同一个东西。当属性未被找到时,只会调用__getattr__


3
class ChangingPassword(object):
    def __init__(self, username, password):
        """use _ for change to read only type(protected)."""
        self.username = username
        self._password = password

    def username(self):
        return self.username

    @property
    def password(self):
        return self._password

    @password.setter
    def password(self, new_password: int):
        if isinstance(new_password, int):
            if self._password != new_password:
                self._password = new_password
            else:
                raise ValueError('Enter different value!')


user01 = ChangingPassword('Herment', 1321)
print(user01.password)
user01.password = 6301
print(user01.password)

1
你的回答可以通过提供额外的支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是否正确。你可以在帮助中心找到关于如何撰写良好答案的更多信息。 - Diego Borba

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