使用@property与getter和setter

自闭症网瘾萝莉.ら 提交于 2020-01-06 17:14:29

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

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

class MyClass(object):
    ...
    def get_my_attr(self):
        ...

    def set_my_attr(self, value):
        ...

class MyClass(object):
    ...        
    @property
    def my_attr(self):
        ...

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

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


#1楼

在大多数情况下,我都不想使用两者。 属性的问题在于它们使类不那么透明。 特别是,如果您要向设置员提出例外情况,这将是一个问题。 例如,如果您具有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:

  • 因为预先为所有属性定义它们非常耗时,
  • 不必要地增加了代码量,使理解和维护代码更加困难,
  • 如果仅根据需要为属性定义它们,则类的界面将发生变化,从而损害该类的所有用户

我更喜欢在定义明确的位置(例如在验证方法中)执行复杂的逻辑,而不是使用属性和获取/设置方法:

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

或类似的Account.save方法。

请注意,我并不是想说在任何情况下属性都是有用的,只是如果您可以使类足够简单和透明以至于不需要它们,则可能会更好。


#2楼

[ 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()

您可能想知道为什么我没有在对象的__init__方法中调用defaultXdefaultY 。 原因是,对于我们的情况,我想假设someDefaultComputation方法返回随时间变化的值,例如时间戳记,并且每当未设置x (或y )时(对于本示例而言,其中“未设置”意思是“设置为None”),我想要x (或y )的默认值。

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

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示例中做到了这一点。 基本上,我们正在运行一个函数主体以在未设置任何值时选择一个默认值。 这是非常常见的模式。

但是,我们正在失去什么,我们不能做什么?

在我看来,主要的烦恼是,如果您定义吸气剂(如我们在此处所做的那样),则还必须定义一个setter。[1] 那是使代码混乱的额外噪音。

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

第三,与类似Java的示例不同,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是一个类方法, valueUberProperty的值,并且我添加了isSet因为None可能是一个真实值,这使我们可以采用一种干净的方式来声明确实没有“值”。 另一种方式是某种形式的哨兵。

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

class Example(object):

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

当然,这实际上还行不通。 我们必须实现uberProperty并确保它同时处理uberProperty和set。

让我们从获取开始。

我的第一次尝试是简单地创建一个新的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创建后,由装饰器创建的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类型的任何属性。

现在,uber-properties很酷,我们可能会想大量使用它们,因此仅创建一个对所有子类都执行此操作的基类是有意义的。 我认为您知道将要调用的基类。

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
  • 默认情况下,我们需要ex返回exgetValue()

    • 我们实际上将看到的是模型失败的领域。
    • 事实证明,我们始终需要使用函数调用来获取值。
    • 但是我们可以使其看起来像常规函数调用,而不必使用exgetValue() 。 (如果您还没有解决问题,那么这样做很明显。)
  • 我们需要ex directly支持设置ex directly ,例如ex = <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]对于是否仍然如此,我可能会落后。


#3楼

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

  • 验证
  • 修改数据
  • 鸭子类型(强制类型为其他类型)

这提供了一种隐藏实现细节和代码残废(例如正则表达式,类型强制转换,尝试..除了块,断言或计算值之外)的聪明方法。

通常,对对象执行CRUD可能通常很平凡,但请考虑将数据保存到关系数据库的示例。 ORM可以在绑定到属性类中定义的fget,fset,fdel的方法中隐藏特定SQL语言的实现细节,该类将管理糟糕的OO代码中的.. elif .. else梯形图-暴露出简单而又优雅的self.variable = something并且使用 ORM为开发人员self.variable = something了细节。

如果人们仅将属性视为束缚和纪律语言(即Java)的沉闷痕迹,那么他们就错过了描述符的意义。


#4楼

在复杂的项目中,我更喜欢使用带有显式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()

#5楼

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

@property优点

  • 您无需在更改数据访问的实现时更改接口。 当您的项目较小时,您可能希望使用直接属性访问来访问类成员。 例如,假设您有一个类型为Foo的对象foo ,其成员num 。 然后,您可以简单地使用num = foo.num获得此成员。 随着项目的发展,您可能会觉得需要对简单的属性访问进行一些检查或调试。 然后,您可以类中使用@property 进行操作 。 数据访问接口保持不变,因此无需修改客户端代码。

    引用自PEP-8

    对于简单的公共数据属性,最好仅公开属性名称,而不使用复杂的访问器/更改器方法。 请记住,如果您发现简单的数据属性需要增强功能行为,则Python为将来的增强提供了简便的方法。 在这种情况下,使用属性将功能实现隐藏在简单的数据属性访问语法之后。

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

    • 它可以增强您作为Python(不是Java)程序员的自我认同。

    • 如果您的面试官认为Java风格的getter和setter是反模式的,那么它可以帮助您进行工作面试。

传统吸气剂和吸气剂的优点

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

    def Foo: def set_num(self, num, force=False): ...
  • 传统的getter和setter 明确表明,类成员访问是通过方法进行的。 这意味着:

    • 结果所得到的结果可能与该类中确切存储的结果不同。

    • 即使访问看起来像简单的属性访问,其性能也可能相差很大。

    除非您的班级用户期望每个属性访问语句后面都隐藏一个@property否则将其明确显示可以最大程度地减少您的班级用户的惊讶。

  • @NeilenMarais本文所提到的,在子类中扩展传统的getter和setters比扩展属性更容易。

  • 长期以来,传统的吸气剂和吸气剂已以多种语言广泛使用。 如果您的团队中有不同背景的人,那么他们看起来比@property更为熟悉。 另外,随着项目的发展,如果您可能需要从Python迁移到另一种不具有@property语言,则使用传统的getter和setter可以使迁移过程更加顺畅。

注意事项

  • @property或传统的getter和setter都不@property类成员@property私有,即使您在其名称前使用双下划线也是如此:

    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
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!