[译] 属性访问、特性和描述符 1

注:原书做者 Steven F. Lott,原书名为 Mastering Object-oriented Pythonpython

对象就是一些特性的集合,包括方法和属性。object类的默认行为包括设置、获取和删除属性。咱们常常须要修改这些行为来改变一个对象的属性。程序员

本章将重点关注如下五个层次的字段访问:数据库

  • 内置字段的处理,这是最简单的,但最不精明的选择。设计模式

  • 回顾一下@property装饰器。特性扩展了属性的概念,把处理过程包含到了已定义的方法函数中。安全

  • 如何利用低级别的特殊方法去控制属性访问方法:__getattr__()__setattr__()__delattr__()。这些特殊的方法容许咱们构建更复杂的属性处理。网络

  • 了解__getattribute__()方法,它提供了更细粒度的属性控制。这可让咱们写不寻常的属性处理。app

  • 最后,咱们将看看描述符。这些都是用来访问一个属性的,但它们涉及到更复杂的设计决策。在Python中大量使用描述符来实现特性、静态方法和类方法。函数

在这一章,咱们将会看到默认处理如何工做的细节。咱们须要决定什么时候何地来覆写默认行为。在某些状况下,咱们但愿咱们的属性不只仅是实例化变量。在其余状况下,咱们可能想要防止属性的添加。咱们的属性可能有更复杂的行为。ui

一样,在咱们探索了解描述符时,咱们将更深刻的理解Python的内部是怎样工做的。咱们不须要常常显式的使用描述符。咱们常常隐式的使用它们,由于它们是实现Python一些特性的机制。this

基本属性处理

默认状况下,咱们建立的任何类对属性都将容许如下四个行为:

  • 经过设置值来建立一个新的属性

  • 给存在的属性设置值

  • 获取属性的值

  • 删除属性

咱们可使用像下面代码这样简单的表示。建立一个简单的、通用的类和该类的一个对象:

>>> class Generic:
...     pass
...
>>> g = Generic()

前面的代码容许咱们建立、获取、设置和删除属性。咱们能够轻松地建立和获取一个属性。如下是一些示例:

>>> g.attribute = "value"
>>> g.attribute
'value'
>>> g.unset
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Generic' object has no attribute 'unset'
>>> del g.attribute
>>> g.attribute
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Generic' object has no attribute 'attribute'

咱们能够添加、更改和删除属性。若是咱们试图获取一个未设置的属性或删除一个不存在的属性时会发生异常。

稍微更好的方法就是使用types.SimpleNamespace类的一个实例。设置特性是同样的,可是咱们不须要建立额外的类定义。咱们建立一个SimpleNamespace类对象来代替,以下:

>>> import types
>>> n = types.SimpleNamespace()

在如下代码中,咱们能够看到为SimpleNamespace类工做的相同用例:

>>> n.attribute = "value"
>>> n.attribute
'value'
>>> del n.attribute
>>> n.attribute
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'namespace' object has no attribute 'attribute'

咱们能够为这个对象建立属性。任何试图使用未定义的属性都会抛出异常。当咱们建立一个object类实例时SimpleNamespace会有不一样的行为。一个简单的object类实例不容许建立新的属性;它缺少内部__dict__结构,Python会保存属性和值到该结构里面。

一、属性和__init__()方法

大多数时候,咱们使用类的__init__()方法来建立一系列的初始属性。理想状况下,咱们为__init__()中全部属性提供默认值。

须要提供全部属性到__init__()方法。正由于如此,存在或不在的属性能够做为一个对象状态的一部分。

一个可选属性能够超越类定义的限制。对于一个类来讲,有一组好的属性定义意义甚大。经过建立一个子类或父类,属性一般能够更清晰地被添加(或删除)。

所以,可选属性意味着一种非正式的子类关系。所以,当咱们使用可选属性时会碰到可怜的多态性。

思考一下21点游戏,只有容许一次分牌。若是一手牌已经分牌,就不能再分牌。有几种方法,咱们能够模拟一下:

  • 咱们能够由Hand.split()方法建立一个SplitHand子类。在此咱们不详细展现。

  • 咱们能够在Hand对象中建立一个状态属性,由Hand.split()方法建立。理想状况下,这是一个布尔值,可是咱们能够实现它做为一个可选属性。

下面是经过一个可选属性检测可分离和不可分离的Hand.split()

def split(self, deck):
    assert self.cards[0].rank == self.cards[1].rank
    try:
        self.split_count
        raise CannotResplit
    except AttributeError:
        h0 = Hand(self.dealer_card, self.cards[0], deck.pop())
        h1 = Hand(self.dealer_card, self.cards[1], deck.pop())
        h0.split_count = h1.split_count = 1
    return h0, h1

实际上,split()方法是检测是否有split_count属性。若是有这个属性,则是已经分牌的手牌且该方法抛出异常。若是split_count属性不存在,容许分牌。

一个可选属性的优点是使__init__()方法有相对整洁的状态标识。劣势是模糊了对象的状态。使用try:块来肯定对象状态可能会变得很是混乱,咱们应该避免。

建立特性

特性是一个方法函数,它(语法上)看上去像一个简单的属性。咱们能够获取、设置和删除特性值就像咱们如何获取、设置和和删除属性值同样。这里有一个重要的区别,特性其实是一个函数且能够处理,而不是简单地保存一个引用到一个对象。

除了更加尖端以外,特性和属性之间的另外一个差异就是咱们不能轻易将新特性附加到现有对象上;然而,默认状况下咱们能够地轻易给对象添加属性。在这方面特性和简单的属性是不同的。

有两种方法建立特性。咱们可使用@property装饰器或者咱们可使用property()函数。纯粹是语法的差别。咱们将更多的关注装饰器。

咱们看看特性的两个基本设计模式:

  • 及早计算:在这个设计模式中,当咱们经过特性设置一个值时,其余属性也一样计算。

  • 延迟计算:在这个设计模式中,计算将被推迟直到须要的时候,经过特性。

为了比较前两种特性处理,咱们将分割Hand对象的常见的特性到一个抽象父类,以下所示:

class Hand:

    def __str__(self):
        return ", ".join(map(str, self.card))

    def __repr__(self):
        return "{__class__.__name__}({dealer_card!r}, {_cards_str})"
        .format(__class__=self.__class__, _cards_str=", "
        .join(map(repr, self.card)), **self.__dict__)

在前面的代码中,咱们只是定义了一些字符串表示方法。

下面是Hand的一个子类,total是一个延迟属性,只有在须要的时候进行计算:

class Hand_Lazy(Hand):

    def __init__(self, dealer_card, *cards):
        self.dealer_card = dealer_card
        self._cards = list(cards)

    @property
    def total(self):
        delta_soft = max(c.soft-c.hard for c in self._cards)
        hard_total = sum(c.hard for c in self._cards)
        if hard_total + delta_soft <= 21:
            return hard_total + delta_soft
        return hard_total

    @property
    def card(self):
        return self._cards

    @card.setter
    def card(self, aCard):
        self._cards.append(aCard)

    @card.deleter
    def card(self):
        self._cards.pop(-1)

Hand_Lazy类初始化一个带有一组Cards对象的Hand对象。total特性是一个只有在须要的时候计算总和的方法。此外,咱们定义了一些其余特性更新手中的牌。Card属性能够获取、设置或删除手中的牌。咱们将在setterdeleter属性章节看到这些。

咱们能够建立一个Hand对象,total做为一个简单的属性出现:

>>> d = Deck()
>>> h = Hand_Lazy(d.pop(), d.pop(), d.pop())
>>> h.total
19
>>> h.card = d.pop()
>>> h.total
29

在每次须要总和的时候,经过从新扫描手中的牌延迟计算。这但是很是昂贵的开销。

一、及早计算属性

如下是Hand的一个子类,total是一个简单的属性,它会在每张牌被添加后当即计算:

class Hand_Eager(Hand):

    def __init__(self, dealer_card, *cards):
        self.dealer_card = dealer_card
        self.total = 0
        self._delta_soft = 0
        self._hard_total = 0
        self._cards = list()
        for c in cards:
            self.card = c

    @property
    def card(self):
        return self._cards

    @card.setter
    def card(self, aCard):
        self._cards.append(aCard)
        self._delta_soft = max(aCard.soft - aCard.hard, self._delta_soft)
        self._hard_total += aCard.hard
        self._set_total()

    @card.deleter
    def card(self):
        removed = self._cards.pop(-1)
        self._hard_total -= removed.hard
        # Issue: was this the only ace?
        self._delta_soft = max(c.soft - c.hard for c in self._cards)
        self._set_total()

    def _set_total(self):
        if self._hard_total+self._delta_soft <= 21:
            self.total = self._hard_total + self._delta_soft
        else:
            self.total = self._hard_total

在这种状况下,每添加一张牌,total属性就会更新。

其余Card——deleter特性——及早地更新total属性不管牌在什么时候被删除。咱们将在下一节详细查看deleter

客户端认为这两个子类之间的语法相同(Hand_Lazy()Hand_Eager()

d = Deck()
h1 = Hand_Lazy(d.pop(), d.pop(), d.pop())
print(h1.total)
h2 = Hand_Eager(d.pop(), d.pop(), d.pop())
print(h2.total)

在这两种状况下,客户端软件简单的使用total字段。

使用特性的优点是,当实现改变时语法没有改变。咱们能够作一个相似getter/setter简单要求的方法函数。然而,getter/setter方法函数涉及到并无什么用处的额外语法。如下是两个例子,其中一个是使用setter方法,另外一个是使用赋值运算符:

obj.set_something(value)
obj.something = value

赋值运算符(=)的存在乎图很简单。许多程序员发现赋值语句比setter方法函数看起来更清晰。

二、setterdeleter特性

在前面的例子中,咱们定义了Card特性来处理额外的牌到Hand类对象。

自从setter(和deleter)属性的建立来自getter,咱们必须常常定义getter特性使用以下代码:

@property
def card(self):
    return self._cards

@card.setter
def card(self, aCard):
    self._cards.append(aCard)

@card.deleter
def card(self):
    self._cards.pop(-1)

这容许咱们用一条简单的语句添加一张牌到手中像下面这样:

h.card = d.pop()

前面的赋值语句有一个缺点,由于它看起来像一张牌替代了全部的牌。另外一方面,它也有一个优点,由于它使用简单赋值来更新一个可变对象的状态。咱们可使用__iadd__()特殊方法,这样作更简洁。但咱们会等到第七章《建立数字》引入其余特殊方法。

咱们当前的例子,没有使人信服的理由来使用deleter特性。即便没有一个使人信服的理由,仍是有一些deleter用法。不管如何,咱们还能够利用它来删除最后一张处理过的牌。这能够用做分牌过程的一部分。

咱们能够思考一下如下版本的split(),以下代码显示:

def split(self, deck):
    """Updates this hand and also returns the new hand."""
    assert self._cards[0].rank == self._cards[1].rank
    c1 = self._cards[-1]
    del self.card
    self.card = deck.pop()
    h_new = self.__class__(self.dealer_card, c1, deck.pop())
    return h_new

前面的方法更新给定的手牌并返回新的Hand对象。下面是一个分牌的例子:

>>> d = Deck()
>>> c = d.pop()
>>> h = Hand_Lazy(d.pop(), c, c) # Force splittable hand
>>> h2 = h.split(d)
>>> print(h)
2♠, 10♠
>>> print(h2)
2♠, A♠

一旦咱们有两张牌,咱们可使用split()产生第二个手牌。一张牌从最初的手牌中被移除。

这个版本的split()固然是可行的。然而,彷佛有所好转的使用split()方法返回两个新的Hand对象。这样,旧的、预分牌的Hand实例能够用做收集统计数据。

对属性访问使用特殊方法

咱们来看看这三个规范的访问属性的特殊方法:getattr()setattr()delattr()。此外,咱们会知道__dir__()方法会显示属性名称。咱们推迟到下一节来介绍__getattribute__()

第一节默认行为的展现以下:

  • __setattr__()方法将建立并设置属性。

  • __getattr__()方法将作两件事。首先,若是一个属性已经有值,__getattr__()不使用,只是返回属性值。其次,若是属性没有值,那么__getattr__()会有机会返回有意义的值。若是没有属性,它必定会抛出一个AttributeError异常。

  • __delattr__()方法删除一个属性。

  • __dir__()方法返回属性名称列表。

__getattr__()方法函数在更大的处理过程当中只有一个步骤;只有当属性是未知的才会去使用。若是属性是已知的,不使用这种方法。__setattr__()__delattr__()方法没有内置的处理。这些方法不与额外的处理过程进行交互。

对于控制属性访问咱们有许多设计可选。这根据咱们的三个基本设计来选择是扩展、包装或发明。选择以下:

  • 咱们能够扩展一个类,经过重写__setattr__()__delattr__()使它几乎不可变。咱们也能够经过__slots__替换内部的__dict__

  • 咱们能够包装类和委托属性访问到即将包装的对象(或复合对象)。这可能涉及到覆写全部三种方法。

  • 咱们能够在一个类中实现类特性行为。使用这些方法,咱们能够确保全部属性集中处理。

  • 咱们能够建立延迟属性值尽管它的值在须要的时候没有(或不能)计算。可能会有一个属性没有值,直到从文件、数据库或网络中读取到。这对于__getattr__()是经常使用用法。

  • 咱们能够有及早属性,在其余属性中自动设置时建立一个属性值。这是经过覆写__setattr__()作到的。

咱们不会看全部这些选择。相反,咱们将关注两个最经常使用的技术:扩展和包装。咱们将建立不可变对象,看看其余方法来及早计算特性值。

一、经过__slots__建立不可变对象

若是咱们不可以设置一个属性或建立一个新的,且对象是不可变的。则如下是咱们但愿在交互式Python中所可以看到的:

>>> c = card21(1,'♠')
>>> c.rank = 12
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 30, in __setattr__
TypeError: Cannot set rank
>>> c.hack = 13
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 31, in __setattr__
AttributeError: 'Ace21Card' has no attribute 'hack'

前面的代码显示,咱们是不容许改变这个对象的属性或添加一个到这个对象种。

为了让此操做能够顺利工做咱们须要变化这个类定义中的两个地方。咱们将忽略不少类,只关注三个特性,使一个对象不可变,以下所示:

class BlackJackCard:
    """Abstract Superclass"""
    __slots__ = ('rank', 'suit', 'hard', 'soft')
    def __init__(self, rank, suit, hard, soft):
        super().__setattr__('rank', rank)
        super().__setattr__('suit', suit)
        super().__setattr__('hard', hard)
        super().__setattr__('soft', soft)

    def __str__(self):
        return "{0.rank}{0.suit}".format(self)

    def __setattr__(self, name, value):
        raise AttributeError("'{__class__.__name__}' has no attribute '{name}'"
        .format(__class__ = self.__class__, name = name))

咱们作了三个重要的变更:

  • 咱们设置__slots__到只被容许的属性。这个将关闭对象内部__dict__的特性且容许限制属性。

  • 咱们定义的__setattr__()会引起一个异常比不作任何事有用的多。

  • 咱们定义__init__()使用的超类版本的__setattr__()这样值就能够正确设置,尽管这个类中缺乏了正常工做的__setattr__()方法。

当心一些,若是这样作咱们能够绕过不变性特性。

object.__setattr__(c, 'bad', 5)

这给咱们带来了一个问题。咱们如何防止“邪恶的”程序员绕过不变性特性?这个问题是愚蠢的。咱们并不能阻止邪恶的程序员。另外一个一样愚蠢的问题是,为何一些邪恶的程序员写代码来规避不变性?咱们并不能阻止邪恶的程序员作邪恶的事情。

若是这个虚构的程序员不喜欢类中的不变性,他们能够修改类的定义来删除从新定义的__setattr__()。不可变对象的重点是保证__hash__()返回一个一致的值,而不是阻止人们写烂的代码。

不要滥用__slots__

__slots__特性的主要目的是经过限制字段的数量来节省内存。

二、建立不可变对象做为元组的子类

咱们也能够经过给Card属性一个元组子类并覆写__getattr__()来建立一个不可变对象。在这种状况下,咱们将翻译__getattr__(name)请求为self[index]请求。在第六章《建立容器和集合》中咱们将看到,self[index]是由__getitem__(index)来实现的。

下面是内置tuple类的一个小扩展:

class BlackJackCard2(tuple):

    def __new__(cls, rank, suit, hard, soft):
        return super().__new__(cls, (rank, suit, hard, soft))

    def __getattr__(self, name):
        return self[{'rank':0, 'suit':1, 'hard':2 , 'soft':3}[name]]

    def __setattr__(self, name, value):
        raise AttributeError

在本例中,咱们只是简单的抛出了AttributeError异常而不是提供详细的错误消息。

当咱们使用前面的代码中,咱们看到如下交互:

>>> d = BlackJackCard2('A', '♠', 1, 11)
>>> d.rank
'A'
>>> d.suit
'♠'
>>> d.bad = 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in __setattr__AttributeError

咱们不能轻易的改变牌值。然而,咱们仍然能够调整d.__dict__来引入额外的属性。

有这必要吗

也许,简单的工做能够确保对象不是不当心误用。实际上,咱们对从异常获得的诊断信息和跟踪,比咱们在极其安全的不可变类中更感兴趣。

三、及早计算属性

咱们能够定义一个对象,它的属性在设置值后尽量快的及早计算。对象最优访问就是进行一次计算结果屡次使用。

咱们可以定义不少的setter特性来作这些。然而,过多的setter特性,每一个属性都计算,会使得计算变得冗长复杂。

咱们能够集中式的进行属性处理。在接下来的例子中,咱们将对其调整来扩展Python的内部dict类型。扩展dict的优势是,它可以很好地处理字符串的format()方法。同时,咱们没必要过多担忧设置额外的被忽略的属性值。

咱们但愿相似下面的代码:

>>> RateTimeDistance(rate=5.2, time=9.5)
{'distance': 49.4, 'time': 9.5, 'rate': 5.2}
>>> RateTimeDistance(distance=48.5, rate=6.1)
{'distance': 48.5, 'time': 7.950819672131148, 'rate': 6.1}

咱们能够在RateTimeDistance对象中设置值。额外的属性能够很轻松的被计算。咱们能够一次性作到这些,以下代码所示:

>>> rtd = RateTimeDistance()
>>> rtd.time = 9.5
>>> rtd
{'time': 9.5}
>>> rtd.rate = 6.24
>>> rtd
{'distance': 59.28, 'time': 9.5, 'rate': 6.24}

下面是内置dict类型的扩展。咱们扩展了基本dict映射用来实现计算缺失的属性:

class RateTimeDistance(dict):

    def __init__(self, *args, **kw):
        super().__init__(*args, **kw)
        self._solve()

    def __getattr__(self, name):
        return self.get(name,None)

    def __setattr__(self, name, value):
        self[name] = value
        self._solve()

    def __dir__(self):
        return list(self.keys())

    def _solve(self):
        if self.rate is not None and self.time is not None:
            self['distance'] = self.rate * self.time
        elif self.rate is not None and self.distance is not None:
            self['time'] = self.distance / self.rate
        elif self.time is not None and self.distance is not None:
            self['rate'] = self.distance / self.time

dict类型使用__init__()来填充内部字典,而后试图解决当前数据太多的问题。它使用__setattr__()来添加新项目到字典。它也试图在每次设置值的时候解答等式。

__getattr__()中,在等式中咱们使用None代表值的缺失。这容许咱们设置一个字段为None代表它是一个缺失的值,这将迫使为此寻找解决方案。例如,咱们能够基于用户输入或者一个网络请求,全部参数被赋予一个值,但一个变量设置为None

咱们能够以下使用:

>>> rtd = RateTimeDistance(rate=6.3, time=8.25, distance=None)
>>> print("Rate={rate}, Time={time}, Distance={distance}".format(**rtd))
Rate=6.3, Time=8.25, Distance=51.975

请注意,咱们不能轻易地在这个类里面设置属性值。

让咱们考虑下面这行代码:

self.distance = self.rate * self.time

若是咱们要编写以前的代码片断,咱们会在__setattr__()_solve()之间进行无限的递归调用。当咱们使用self['distance']到这个例子中,咱们避免了递归调用__setattr__()

一样重要的是要注意,一旦设置了全部三个值,该对象不能轻易被改变来提供新的解决方案。

咱们不能简单地给rate设置一个新值且计算time新值必须让distance不变。为了调整这个模型,咱们须要清除一个变量以及为另外一个变量设置一个新值:

>>> rtd.time = None
>>> rtd.rate = 6.1
>>> print("Rate={rate}, Time={time}, Distance={distance}".format(**rtd))
Rate=6.1, Time=8.25, Distance=50.324999999999996

这里,咱们清除time且改变rate获得一个新的解决方案来使用既定的distance值。

咱们能够设计一个模型,跟踪设置变量的顺序;这一模型能够节省咱们在设置另外一个变量从新计算相关结果以前清除一个变量。