Skip to content

Latest commit

 

History

History
1148 lines (813 loc) · 61.2 KB

File metadata and controls

1148 lines (813 loc) · 61.2 KB

四、属性访问、属性和描述符

对象是特征的集合,包括方法和属性。object类的默认行为包括设置、获取和删除命名属性。我们通常需要修改此行为以更改对象中可用的属性。

本章将重点介绍以下五层属性访问:

  • 我们将研究内置的属性处理。
  • 我们将回顾@property装饰师。属性扩展了属性的概念,以包括在方法函数中定义的处理。
  • 我们将了解如何使用控制属性访问的较低级别的特殊方法:__getattr__()__setattr__()__delattr__()。这些特殊方法允许我们构建更复杂的属性处理。
  • 我们还将看一看__getattribute__()方法,它提供了对属性更细粒度的控制。这可以让我们编写非常不寻常的属性处理。
  • 我们来看看描述符。这些对象通过中介访问属性。因此,它们涉及一些更复杂的设计决策。描述符是用于实现属性、静态方法和类方法的基本结构。

在本章中,我们将详细了解默认处理是如何工作的。这将帮助我们决定何时何地覆盖默认行为。在某些情况下,我们希望属性不仅仅是实例变量。在其他情况下,我们可能希望防止添加属性。我们可能有更复杂行为的属性。

此外,在探索描述符时,我们将对 Python 的内部工作方式有更深入的了解。我们通常不需要显式地使用描述符。然而,我们经常隐式地使用它们,因为它们实现了许多 Python 特性。

由于类型提示现在在 Python 中可用,我们将研究如何注释属性和属性,以便像mypy这样的工具可以确认使用了适当类型的对象。

最后,我们将介绍新的dataclasses模块,以及如何使用它来简化类定义。

在本章中,我们将介绍以下主题:

  • 基本属性处理
  • 创建属性
  • 使用特殊方法进行属性访问
  • getattribute_()方法
  • 创建描述符
  • 对属性和属性使用类型提示
  • 使用dataclasses模块

技术要求

本章的代码文件可在中找到 https://git.io/fj2Uu

基本属性处理

默认情况下,我们创建的任何类都将允许以下四种与属性相关的行为:

  • 创建新属性并设置其值的步骤
  • 设置现有属性的值的步骤
  • 获取属性值的步骤
  • 删除属性的步骤

我们可以使用下面的代码这样简单的东西进行实验。我们可以创建一个简单的泛型类和该类的对象:

>>> 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' 

我们可以为这个实例创建属性,n。任何使用未定义属性的尝试都会引发异常。

SimpleNamespace类的实例与我们创建object类实例时看到的行为不同。object类的实例不允许创建新属性;它缺少 Python 用来存储属性和值的内部__dict__结构。

属性和 uuu init_uuuu()方法

大多数时候,我们使用类的__init__()方法创建属性的初始集合。理想情况下,我们为__init__()中的所有属性提供名称和默认值。

虽然在__init__()方法中不需要提供所有属性,但它是mypy检查的地方,用于收集对象的预期属性列表。可选属性可以用作对象状态的一部分,但是没有简单的方法将缺少属性描述为对象的有效状态。

一个可选属性还可以根据类定义推送封套的边缘。类由唯一的属性集合定义。最好通过创建子类或超类定义来添加(或删除)属性。属性的动态变化会让工具(如mypy和人)感到困惑。

通常,可选属性意味着隐藏的或非正式的子类关系。因此,当我们使用可选属性时,会遇到非常糟糕的多态性。多个多态子类通常是比可选属性更好的实现。

考虑一个 Twitter 的游戏,其中只有一个分裂是允许的。如果一只手被拆分,则无法重新拆分。我们可以通过几种方法对此约束进行建模:

  • 我们可以通过Hand.split()方法创建子类SplitHand的实例。我们不会详细介绍这一点。Hand的这个子类对split()有一个引发异常的实现。一旦将一个Hand拆分为创建两个SplitHand实例,则无法重新拆分这些实例。
  • 我们可以在名为Hand的对象上创建一个状态属性,该属性可以通过Hand.split()方法创建。理想情况下,这是一个布尔值,但我们也可以将其作为可选属性实现。

以下是一个版本的Hand.split(),它可以通过可选属性self.split_blocker检测可拆分的手和不可拆分的手:

def  split(self, deck): 
    assert self.cards[0].rank == self.cards[1].rank 
    try: 
        self.split_blocker 
        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_blocker = h1.split_blocker = True
        return h0, h1 

split()方法测试split_blocker属性的存在。如果该属性存在,则不应重新拆分该手牌;该方法引发一个定制的CannotSplit异常。如果split_blocker属性不存在,则允许拆分。每个结果对象都具有可选属性,以防止进一步拆分。

一个可选属性的优点是使__init__()方法与状态标志保持相对整洁。它的缺点是模糊了对象状态的一个方面。此外,mypy程序将因引用__init__()中未初始化的属性而受阻。管理对象状态的可选属性必须小心使用(如果有的话)。

创建属性

属性是一个方法函数,它(在语法上)看起来是一个简单的属性。我们可以使用与属性值语法相同的语法来获取、设置和删除属性值。不过,有一个重要的区别。属性实际上是一个方法,可以处理而不是简单地保留对另一个对象的引用。

除了复杂程度之外,属性和属性之间的另一个区别是,我们不能轻松地将新属性附加到现有对象,但我们可以非常轻松地向对象添加动态属性。属性在这一方面与简单属性不同。

有两种方法可以创建属性。我们可以使用@property装饰器,也可以使用property()函数。这些差异纯粹是句法上的。我们将关注装饰师。

现在,我们来看两种基本的属性设计模式:

  • 急切计算:在这个设计模式中,当我们通过属性设置一个值时,其他属性也会被计算出来。
  • 延迟计算:在这种设计模式中,计算被延迟,直到通过属性请求。

为了比较前面两种属性方法,我们将Hand对象的一些常见特征拆分为一个抽象超类,如下所示:

    class     Hand:

    def __init__(
            self,
            dealer_card: BlackJackCard,
            *cards: BlackJackCard
        ) -> None:
        self.dealer_card = dealer_card
        self._cards = list(cards)

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

        def         __repr__    (    self    ) ->     str    :
            return     (
                f"{self.__class__.        __name__        }"
                    f"({self.dealer_card!r}, " 
                    f"{', '.join(        map        (        repr        , self.card))})"
                    )

在前面的代码中,我们定义了对象初始化方法,它实际上什么都不做。提供了两种字符串表示方法。此类是内部卡片列表的包装器,保存在实例变量_cards中。我们在实例变量上使用了一个前导的_作为提醒,这是一个可能会更改的实现细节。

__init__()用于为mypy提供实例变量名和类型提示。在这种抽象类定义中,尝试使用None作为默认值将违反类型提示。dealer_card属性必须是BlackJackCard的实例。若要允许此变量具有None的初始值,类型提示必须是Optional[BlackJackCard],并且对此变量的所有引用还需要一个保护if语句来确保该值不是None

以下是Hand的一个子类,其中total是仅在需要时计算的惰性属性:

    class     Hand_Lazy(Hand):

        @property
                    def     total(    self    ) ->     int    :
        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    ) -> List[BlackJackCard]:
            return         self    ._cards

        @card.setter
                    def     card(    self    , aCard: BlackJackCard) ->     None    :
            self    ._cards.append(aCard)

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

Hand_Lazy类设置dealer_card_cards实例变量。total属性基于一种仅在请求时计算总数的方法。此外,我们还定义了一些其他属性来更新手中的卡片集合。card属性可以获取、设置或删除手中的卡。我们将在setterdeleter属性部分了解这些属性。

我们可以创建一个 Hand_Lazy 对象。total似乎是一个简单的属性:

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

每次请求总计时,通过重新扫描手中的卡来延迟计算总计。对于简单的BlackJackCard实例,这是一个相对便宜的计算。对于其他类型的项目,这可能会涉及相当大的开销。

急切计算的财产

下面是Hand的一个子类,total是一个简单的属性,在添加每张卡时,它会被急切地计算出来:

    class     Hand_Eager(Hand):

        def         __init__    (
                self    ,
            dealer_card: BlackJackCard,
            *cards: BlackJackCard
    ) ->     None    :
            self    .dealer_card = dealer_card
            self    .total =     0
                        self    ._delta_soft =     0
                        self    ._hard_total =     0
                        self    ._cards: List[BlackJackCard] =     list    ()
            for     c     in     cards:
                # Mypy cannot discern the type of the setter.
                    # https://github.com/python/mypy/issues/4167
                            self    .card = c      # type: ignore

                    @property
                    def     card(    self    ) -> List[BlackJackCard]:
            return         self    ._cards

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

        @card.deleter
                    def     card(    self    ) ->     None    :
        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    ) ->     None    :
            if         self    ._hard_total +     self    ._delta_soft <=     21    :
                self    .total =     self    ._hard_total +     self    ._delta_soft
            else    :
                self    .total =     self    ._hard_total

Hand_Eager类的__init__()方法将急切计算的total初始化为零。它还使用另外两个实例变量_delta_soft_hard_total来跟踪手中 ace 卡的状态。当每张卡片放在手上时,这些总数将被更新。

self.card的每次使用看起来都像一个属性。它实际上是对用@card.setter修饰的属性方法的引用。此方法的参数aCard将是赋值语句中=右侧的值

在这种情况下,每次通过卡片setter属性添加卡片时,total属性都会更新。

另一个饰有@card.deletercard属性在移除卡片时急切地更新total属性。我们将在下一节详细介绍deleter

客户机在Hand的这两个子类(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属性。延迟实现将总计的计算推迟到需要时,但每次都会重新计算。急切的实现立即计算总数,并且只在手部发生变化时重新计算总数。权衡是一个重要的软件工程问题,最终的选择取决于整个应用程序如何使用total属性。

使用属性的优点是,当实现更改时,语法不会更改。我们可以对getter/setter方法函数提出类似的要求。然而,getter/setter方法函数涉及额外的语法,这不是很有帮助,也不是很有用。以下是两个示例,其中一个涉及使用setter方法,而另一个使用赋值运算符:

obj.set_something(value) 
obj.something = value 

赋值运算符(=的出现使得意图非常明确。许多程序员发现寻找赋值语句比寻找setter方法函数更清晰。

setter 和 deleter 属性

在前面的示例中,我们定义了card属性,将额外的卡处理到Hand类的对象中。

由于setter(和deleter属性是从getter属性创建的,因此我们必须首先使用如下示例所示的代码定义getter属性:

    @property
        def     card(    self    ) -> List[BlackJackCard]:
        return         self    ._cards

    @card.setter
        def     card(    self    , aCard: BlackJackCard) ->     None    :
        self    ._cards.append(aCard)

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

这样,我们就可以用一个简单的语句将卡片添加到手上,如下所示:

h.card = d.pop() 

前面的赋值语句有一个缺点,因为它看起来像是用一张卡替换了所有的卡。另一方面,它的优势在于它使用简单的赋值来更新可变对象的状态。我们可以使用__iadd__()特殊的方法来做得更干净一点。但是,我们要等到第 8 章创建数字之后,才能介绍其他特殊方法。

我们将考虑一个类似于以下代码的 AutoT0p 版本:

    def     split(    self    , deck: Deck) ->     "Hand"    :
        """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实例并返回一个新的Hand对象。由于此方法位于Hand类定义内,因此必须将类名显示为字符串,因为该类尚未完全定义。

del语句将从当前手牌中移除最后一张牌。这将使用@card.deleter属性执行删除卡的工作。对于一个懒惰的人来说,没有什么需要做的了。对于急切的手,总数必须更新。del语句前面的赋值语句用于将最后一张卡保存到局部变量c1

以下是拆分手的示例:

>>> d = Deck() 
>>> c = d.pop() 
>>> h = Hand_Lazy(d.pop(), c, c)  # Create a 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__()方法。这个特殊方法的默认行为是引发一个AttributeError异常。我们可以重写它以返回有意义的结果,而不是引发异常。
  • __delattr__()方法删除一个属性。
  • __dir__()方法返回属性名列表。这通常与__getattr__( )相结合,为动态计算的属性提供无缝接口。

__getattr__()方法功能只是更大流程中的一个步骤;当属性名称未知时使用。如果名称是已知属性,则不使用此方法

我们有许多用于控制属性访问的设计选项。其中一些设计选择如下:

  • 我们可以将内部的__dict__替换为__slots__。这使得添加新属性变得困难。但是,命名属性仍然是可变的。
  • 我们可以使用这两种特殊方法通过重写__setattr__()__delattr__()向类添加属性。动态属性使得mypy很难评估类型提示。
  • 我们可以在类中实现类似属性的行为。使用__getattr__()__setattr__()方法,我们可以确保在这两种方法中集中处理各种属性。
  • 我们可以创建惰性属性,其中的值在需要时才计算(或无法计算)。例如,我们可以创建一个在从文件、数据库或网络读取之前没有值的属性。这是__getattr__()的常用用法。
  • 我们可以使用渴望属性,设置属性会立即在其他属性中创建值。这是通过对__setattr__()的覆盖来完成的。

我们不会考虑所有这些替代方案。相反,我们将关注最常用的技术。我们将创建具有有限数量属性的对象,并研究计算动态属性值的其他方法。

当我们无法设置属性或创建新属性时,对象是不可变的。下面是我们希望在交互式 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' 

前面的代码显示了一个Card对象,其中我们不允许更改属性或向该对象添加属性。

实现完全不可变行为的最简单方法是扩展typing.NamedTuple。我们将在下面的章节中介绍这一点。这是首选方法。在此之前,我们将研究一些更复杂的选择不变性特征的替代方案。

使用 _ 插槽限制属性名称 __

我们可以使用__slots__创建一个类,在这个类中我们不能添加新属性,但可以修改属性的值。此示例显示如何限制属性名称:

    class     BlackJackCard:
                __slots__     = (    "rank"    ,     "suit"    ,     "hard"    ,     "soft"    )

        def         __init__    (    self    , rank:     str    , suit:     "Suit"    , hard:     int    , soft:     int    ) ->     None    :
            self    .rank = rank
            self    .suit = suit
            self    .hard = hard
            self    .soft = soft

我们对该类以前的定义做了一个重大更改:将__slots__属性设置为允许的属性名称。这将关闭对象的内部__dict__功能,并将我们限制为仅使用这些属性名称。即使无法添加新属性,定义的属性值也是可变的。

此特性的主要用例是限制默认创建的内部__dict__结构占用的内存。__slots__结构使用更少的内存,通常在创建大量实例时使用。

带有 _ugetattr__u()的动态属性

我们可以创建对象,其中属性是通过单个集中的__getattr__()方法计算的。当属性值由单独的属性计算时,许多方法的存在可以方便地封装各种算法。然而,在某些情况下,将所有计算合并到一个方法中可能是明智的。在本例中,mypy基本上看不到属性的名称,因为它们不是 Python 源文本的明显部分。

以下示例中显示了一种计算方法:

    class     RTD_Solver:

        def         __init__    (
            self    , *,
        rate:     float     =     None    ,
        time:     float     =     None    ,
        distance:     float     =     None
                ) ->     None    :
            if     rate:
                self    .rate = rate
            if     time:
                self    .time = time
            if     distance:
                self    .distance = distance

        def         __getattr__    (    self    , name:     str    ) ->     float    :
            if     name ==     "rate"    :
                return         self    .distance /     self    .time
            elif     name ==     "time"    :
                return         self    .distance /     self    .rate
            elif     name ==     "distance"    :
                return         self    .rate *     self    .time
            else    :
                raise         AttributeError    (    f"Can't compute {name}"    )

RTD_Solver类的一个实例是用三个值中的两个值构建的。其思想是从另外两个值中计算缺失的第三个值。在本例中,我们选择将缺少的值作为可选属性,并在需要时计算该属性的值。这个类的属性是动态的:三个可能的属性中有两个将被使用。

该类的使用如以下代码段所示:

 >>> r1 = RTD_Solver(rate=6.25, distance=10.25)
        >>> r1.time
        1.64
        >>> r1.rate
        6.25 

RTD_Solver类的一个实例是用三个可能属性中的两个构建的。在这个例子中,它是ratedistance。请求time属性值会导致从速率和距离计算时间。

但是,速率属性值的请求不涉及__getattr__()方法。因为实例有ratedistance属性,所以直接提供。为确认__getattr__()未被使用,在速率计算中插入print()函数,如下代码段所示:

    if     name ==     "rate"    :
        print    (    "Computing Rate"    )
        return         self    .distance /     self    .time

当使用__init__()方法设置的属性值创建RTD_Solver实例时,__getattr__()方法不用于获取属性。__getattr__()方法仅用于未知属性。

将不可变对象创建为 NamedTuple 子类

创建不可变对象的最佳方法是将Card属性设置为typing.NamedTuple的子类。

以下是对内置typing.NamedTuple类的扩展:

    class     AceCard2(NamedTuple):
    rank:     str
                suit: Suit
    hard:     int     =     1
                soft:     int     =     11

                    def         __str__    (    self    ) ->     str    :
            return         f"{self.rank}{self.suit}"

当我们使用前面的代码时,我们会看到以下类型的交互:

 >>> c = AceCard2("A", Suit.Spade)
        >>> c.rank
        'A'
        >>> c.suit
        <Suit.Spade: '♠'>
        >>> c.hard
        1 

我们可以创建一个实例,它具有所需的属性值。但是,我们不能添加或更改任何属性。所有属性名称的处理都由NamedTuple类定义处理:

 >>> c.not_allowed = 2
        Traceback (most recent call last):
          File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run
            compileflags, 1), test.globs)
          File "<doctest __main__.__test__.test_comparisons_2[3]>", line 1, in <module>
            c.not_allowed = 2
        AttributeError: 'AceCard2' object has no attribute 'not_allowed'
        >>> c.rank = 3
        Traceback (most recent call last):
          File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run
            compileflags, 1), test.globs)
          File "<doctest __main__.__test__.test_comparisons_2[4]>", line 1, in <module>
            c.rank = 3
        AttributeError: can't set attribute 

热切计算的属性、数据类和 post_init__;()

我们可以定义一个对象,在该对象中,属性是在设置值后立即计算的。这样的对象可以通过执行一次计算并将结果多次使用来优化访问。

这可以通过属性设置器完成。然而,一个包含大量属性设置器的类,每个属性设置器都计算许多属性,看起来可能相当复杂。在某些情况下,所有派生值计算都可以集中进行。

dataclasses模块为我们提供了一个具有一系列内置特性的类。其中一个特点是一种__post_init__()方法,我们可以使用它来急切地推导值。

我们想要的代码如下所示:

 >>> RateTimeDistance(rate=5.2, time=9.5)
        RateTimeDistance(rate=5.2, time=9.5, distance=49.4)
        >>> RateTimeDistance(distance=48.5, rate=6.1)    RateTimeDistance(rate=6.1, time=7.950819672131148, distance=48.5)

我们可以在此RateTimeDistance对象中设置三个必需值中的两个。将立即计算附加属性,如以下代码块所示:

    from dataclasses import dataclass

@dataclass
        class     RateTimeDistance:

    rate: Optional[    float    ] =     None
                time: Optional[    float    ] =     None
                distance: Optional[    float    ] =     None

            def     __post_init__(    self    ) -> None:
            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

@dataclass装饰器定义的类将接受各种初始化值。设置值后,调用__post_init__()方法。这可用于计算附加值

这里的属性是可变的,创建一个速率、时间和距离值不一致的对象相对简单。我们可以执行以下操作来创建属性值之间存在不正确内部关系的对象:

 >>> r1 = RateTimeDistance(time=1, rate=0)
        >>> r1.distance = -99 

为了防止这种情况,可以使用@dataclass(frozen=True)装饰器。这种变体的行为有点像NamedTuple

使用 uuu setattr_uuuu()的增量计算

我们可以创建使用__setattr__()检测属性值变化的类。这可能导致增量计算。其思想是在设置初始属性值之后构建派生值。

注意属性设置有两种含义的复杂性。

  • 客户端视图:可以设置属性并计算其他派生值。在这种情况下,使用了复杂的__setattr__()
  • 内部视图:设置属性不得导致任何额外的计算。如果进行额外的计算,这将导致设置属性和从这些属性计算派生值的无限递归。在这种情况下,必须使用超类的基本__setattr__()方法。

这一区别很重要,很容易被忽视。下面是一个在__setattr__()方法中设置属性和计算派生属性的类:

    class     RTD_Dynamic:
        def         __init__    (    self    ) ->     None    :
            self    .rate :     float
                        self    .time :     float
                        self    .distance :     float

                super    ().    __setattr__    (    'rate'    ,     None    )
            super    ().    __setattr__    (    'time'    ,     None    )
            super    ().    __setattr__    (    'distance'    ,     None    )

        def         __repr__    (    self    ) ->     str    :
        clauses = []
            if         self    .rate:
            clauses.append(    f"rate={self.rate}"    )
            if         self    .time:
            clauses.append(    f"time={self.time}"    )
            if         self    .distance:
            clauses.append(    f"distance={self.distance}"    )
            return (
                    f"{self.__class__.        __name__        }"
            f"({', '.join(clauses)})"
        )

                    def         __setattr__    (    self    , name:     str    , value:     float    ) ->     None    :
            if     name ==     'rate'    :
                super    ().    __setattr__    (    'rate'    , value)
            elif     name ==     'time'    :
                super    ().    __setattr__    (    'time'    , value)
            elif     name ==     'distance'    :
                super    ().    __setattr__    (    'distance'    , value)

            if         self    .rate     and         self    .time:
                super    ().    __setattr__    (    'distance'    ,     self    .rate *     self    .time)
            elif         self    .rate     and         self    .distance:
                super    ().    __setattr__    (    'time'    ,     self    .distance /     self    .rate)
            elif         self    .time     and         self    .distance:
                super    ().    __setattr__    (    'rate'    ,     self    .distance /     self    .time)

__init__()方法使用__setattr__()超类设置默认属性值,而无需启动递归计算。实例变量以类型提示命名,但不执行赋值。

RTD_Dynamic类提供了一个__setattr__()方法来设置属性。如果存在足够的值,它还将计算派生值。super().__setattr__()的内部使用特别避免了使用object超类属性设置方法进行任何额外的计算。

下面是使用此类的示例:

 >>> rtd = RTD_Dynamic()
        >>> rtd.time = 9.5
        >>> rtd
        RTD_Dynamic(time=9.5)
        >>> rtd.rate = 6.25
        >>> rtd
        RTD_Dynamic(rate=6.25, time=9.5, distance=59.375)
        >>> rtd.distance
        59.375 

Note that we can't set attribute values inside some methods of this class using simple self.name = syntax.

假设我们试图在这个类定义的__setattr__()方法中编写以下代码行:

self.distance = self.rate*self.time 

如果我们要编写前面的代码片段,__setattr__()方法中会有无限递归。在self.distance=x行中,这被实现为self.__setattr__('distance', x)。如果在__setattr__()的主体中出现self.distance=x这样的行,则表示在尝试实现属性设置时必须使用__setattr__()__setattr__()超类不做任何额外的工作,并且不会与自身发生递归纠缠。

还需要注意的是,一旦设置了所有三个值,更改属性将不会简单地重新计算其他两个属性。计算规则基于一个明确的假设,即一个属性缺失,另两个属性可用。

要正确地重新计算值,我们需要做两个更改:1)将所需属性设置为None,2)提供一个值以强制重新计算。

我们不能简单地为rate设置一个新值,并在保持distance不变的情况下为time计算一个新值。要调整此模型,我们需要清除一个变量并为另一个变量设置新值:

 >>> rtd.time = None
        >>> rtd.rate = 6.125
        >>> rtd    RTD_Dynamic(rate=6.125, time=9.5, distance=58.1875)

在这里,我们清除了time并更改了rate,以使用之前为distance建立的值来获得time的新解决方案。

\uuuu getattribute\uuuuu()方法

更低级的属性处理是__getattribute__()方法。此方法的默认实现尝试将该值定位为内部__dict__(或__slots__中的现有属性。如果未找到该属性,此方法将调用__getattr__()作为回退。如果所定位的值是描述符(请参阅下面的创建描述符部分),则它将处理描述符。否则,只返回值。

通过重写此方法,我们可以执行以下任何类型的任务:

  • 我们可以有效地防止访问属性。这种方法通过引发异常而不是返回值,可以使属性比仅使用前导下划线(_)将名称标记为实现的私有名称更为机密。
  • 我们可以发明新的属性,就像__getattr__()可以发明新属性一样。然而,在这种情况下,我们可以绕过由__getattribute__()的默认版本完成的默认查找。
  • 我们可以使属性执行独特和不同的任务。这可能会使程序很难理解或维护,这也可能是一个糟糕的想法。
  • 我们可以改变描述符的行为方式。虽然从技术上讲是可能的,但更改描述符的行为听起来像是一个糟糕的想法。

当我们实现__getattribute__()方法时,需要注意的是,方法体中不能有任何内部属性引用。如果我们试图获取self.name的值,它将导致__getattribute__()方法的无限递归。

The __getattribute__() method cannot use any simple self.name attribute access; it will lead to infinite recursions.

为了在__getattribute__()方法中获取属性值,我们必须显式引用在超类中定义的基类方法,或者基类object,如以下代码段所示:

object.__getattribute__(self, name) 

我们可以使用这种处理将调试、审计或安全控制注入到类定义中。例如,当在一个特别重要的类中访问某个属性时,我们可能会在日志中写入一行。合理的安全测试可能会限制具有已定义访问控制的人员的访问。

下面的示例将展示一个简单的使用__getattribute__()来阻止对类中单个前导_实例变量和方法的访问。为此,我们将为这些名称中的任何一种提出一个AttributeError例外。

下面是类定义:

    class     SuperSecret:

        def         __init__    (    self    , hidden: Any, exposed: Any) ->     None    :
            self    ._hidden = hidden
            self    .exposed = exposed

        def         __getattribute__    (    self    , item:     str    ):
            if     (    len    (item) >=     2         and     item[    0    ] ==     "_"
                                and     item[    1    ] !=     "_"    ):
                raise         AttributeError    (item)
            return         super    ().    __getattribute__    (item)

我们已重写__getattribute__()以仅在私有名称上引发属性错误。这将使 Python 的内部__名称可见,但任何带有单个_前缀的名称都将被隐藏。_hidden属性将几乎不可见。以下是正在使用的此类对象的示例:

 >>> x = SuperSecret('onething', 'another')
        >>> x.exposed
        'another'
        >>> x._hidden  # doctest: +IGNORE_EXCEPTION_DETAIL
        Traceback (most recent call last):
          File "/Users/slott/miniconda3/envs/py37/lib/python3.7/doctest.py", line 1329, in __run
            compileflags, 1), test.globs)
          File "<doctest __main__.__test__.test_secret[3]>", line 1, in <module>
            x._hidden  #
          File "/Users/slott/Documents/Writing/Python/Mastering OO Python 2e/mastering-oo-python-2e/Chapter_4/ch04_ex4.py", line 132, in __getattribute__
            raise AttributeError(item)    AttributeError: _hidden

对象x将响应对公开属性的请求,但对以_开头的属性的任何引用都会引发异常

然而,这并不能完全隐藏所有的_名称。dir()函数将显示_hidden属性的存在。要纠正此问题,必须重写__dir__()特殊方法,以隐藏以一个_开头的名称。

作为一般建议,很少需要更改__getattribute__()的实现。默认实现允许我们通过属性定义或对__getattr__()进行更改来访问灵活的功能。

创建描述符

描述符是调解属性访问的类。描述符类可用于获取、设置或删除属性值。描述符对象在类定义时构建在类内部。描述符是 Python 实现方法、属性和属性的本质。

描述符设计模式有两部分:一个所有者类属性描述符本身。owner 类使用一个或多个描述符作为其属性。描述符类定义了__get____set____delete__方法的一些组合。描述符类的实例将是所有者类的属性。

描述符是与所属类分离的类的实例。因此,描述符允许我们创建可重用的、通用的属性。所属类可以有每个描述符类的多个实例来管理具有类似行为的属性。

与其他属性不同,描述符是在类级别创建的。它们不是在__init__()初始化中创建的。虽然描述符实例可以在初始化期间设置值,但描述符实例通常是作为类的一部分构建的,在任何方法函数之外。每个描述符对象都是一个描述符类的实例。描述符实例必须绑定到所有者类中的属性名称。

要被识别为描述符,类必须实现以下三种方法的任意组合:

  • Descriptor.__get__(self, instance, owner):在该方法中,instance参数是被访问对象的self变量。owner参数是拥有的类对象。如果在类上下文中调用此描述符,instance参数将获得一个None值。这必须返回描述符的值。
  • Descriptor.__set__(self, instance, value):在该方法中,instance参数是被访问对象的self变量。value参数是描述符需要设置的新值。
  • Descriptor.__delete__(self, instance):在该方法中,instance参数是被访问对象的self变量。描述符的此方法必须删除此属性的值。

有时,描述符类还需要一个__init__()方法函数来初始化描述符的内部状态。基于定义的方法,描述符有两种设计模式,如下所示:

  • 非数据描述符:这种描述符只定义__get__()方法。非数据描述符的思想是通过其自身的方法或属性提供对另一个对象的间接引用。非数据描述符在被引用时也可以执行某些操作。
  • 数据描述符:该描述符定义__get__()__set__()来创建可变对象。它还可以定义__delete__()。对具有数据描述符值的属性的引用被委托给描述符对象的__get__()__set__()__delete__()方法。

描述符有各种各样的用例。在内部,Python 使用描述符有几个原因:

  • 类的方法被实现为描述符。这些是将方法函数应用于对象和各种参数值的非数据描述符。
  • property()函数通过为命名属性创建数据描述符来实现。
  • 类方法或静态方法作为描述符实现。在这两种情况下,该方法都将应用于该类,而不是该类的实例。

当我们在第 12 章通过 SQLite存储和检索对象时,我们会看到许多 ORM 类定义使用描述符将 Python 类定义映射到 SQL 表和列。

在考虑描述符的用途时,我们还必须检查描述符使用的数据的三种常见用例,如下所示:

  • 描述符对象具有或获取数据值。在这种情况下,描述符对象的self变量是相关的,描述符对象是有状态的。使用数据描述符,__get__()方法可以返回此内部数据。对于非数据描述符,描述符可以包括用于获取或处理数据的其他方法或属性。任何描述符状态都应用于整个类。
  • 所有者实例包含数据。在这种情况下,描述符对象必须使用instance参数来引用所属对象中的值。使用数据描述符,__get__()方法从实例获取数据。对于非数据描述符,描述符的其他方法访问实例数据。
  • 所有者类包含相关数据。在这种情况下,描述符对象必须使用owner参数。当描述符实现应用于整个类的静态方法或类方法时,通常使用这种方法。

我们将详细了解第一个案例。这意味着使用__get__()__set__()方法创建一个数据描述符。我们还将研究如何在不使用__get__()方法的情况下创建非数据描述符。

第二种情况(拥有实例中的数据)本质上是@property装饰器所做的。编写描述符类而不是创建常规属性可能有一个小的优势——描述符可以用来将计算重构为描述符类。虽然这可能会导致类设计的碎片化,但当计算真正具有史诗般的复杂性时,它会有所帮助。这本质上就是策略设计模式,一个包含特定算法的独立类。

第三个案例展示了@staticmethod@classmethod装饰器是如何实现的。我们不需要重新发明那些轮子。

使用非数据描述符

在内部,Python 使用非数据描述符作为类方法和静态方法实现的一部分。这是可能的,因为描述符提供对所属类以及实例的访问。

我们将看一个描述符的例子,该描述符更新实例,并与文件系统一起工作,以提供使用描述符的额外副作用。

对于本例,我们将向类添加一个描述符,该描述符将创建一个对类的每个实例都是唯一的工作目录。这可用于缓存状态、调试历史记录,甚至审核复杂应用程序中的信息。

下面是一个可能在内部使用StateManager的抽象类示例:

    class     PersistentState:
        """Abstract superclass to use a StateManager object"""
                _saved: Path

PersistentState类定义包含对属性_saved的引用,该属性具有Path类型提示。这以mypy可以检测到的方式形式化了对象之间的关系。

下面是一个描述符示例,该描述符提供对文件的访问以保存对象状态:

    class     StateManager:
        """May create a directory. Sets _saved in the instance."""

                    def         __init__    (    self    , base: Path) ->     None    :
            self    .base = base

        def         __get__    (    self    , instance: PersistentState, owner: Type) -> Path:
            if not         hasattr    (instance,     "_saved"    ):
            class_path =     self    .base / owner.    __name__
                        class_path.mkdir(    exist_ok    =    True    ,     parents    =    True    )
            instance._saved = class_path /     str    (    id    (instance))
            return     instance._saved

在类中创建此描述符时,将提供一个基Path。引用此实例时,它将确保存在一个工作目录。它还将保存一个正在工作的Path对象,设置_saved实例属性。

以下是使用此描述符访问工作目录的类:

    class     PersistentClass(PersistentState):
    state_path = StateManager(Path.cwd() /     "data"     /     "state"    )

        def         __init__    (    self    , a:     int    , b:     float    ) ->     None    :
            self    .a = a
            self    .b = b
            self    .c: Optional[    float    ] =     None
                        self    .state_path.write_text(    repr    (    vars    (    self    )))

        def     calculate(    self    , c:     float    ) ->     float    :
            self    .c = c
            self    .state_path.write_text(    repr    (    vars    (    self    )))
            return         self    .a *     self    .b +     self    .c

        def         __str__    (    self    ) ->     str    :
            return         self    .state_path.read_text()

在类级别,将创建此描述符的单个实例。它被分配给state_path属性。有三个地方提到了self.state_path。因为对象是描述符,所以每次引用变量时都会隐式调用__get__()方法。这意味着任何这些引用都将用于创建必要的目录和工作文件路径。

这种对StateManager类的__get__()方法的隐式使用将保证每次引用时的一致性处理。其思想是将操作系统级别的工作集中到一个方法中,该方法是可重用描述符类的一部分。

作为调试的辅助,__str__()方法转储状态已写入的文件的内容。当我们与该类交互时,我们会看到如下示例所示的输出:

 >>> x = PersistentClass(1, 2)
        >>> str(x)
        "{'a': 1, 'b': 2, 'c': None, '_saved': ...)}"
        >>> x.calculate(3)
        5
        >>> str(x)    "{'a': 1, 'b': 2, 'c': 3, '_saved': ...)}"

我们创建了一个PersistentClass类的实例,为两个属性ab提供初始值。第三个属性c的默认值为None。使用str()显示保存状态文件的内容。

self.saved_state的引用调用了描述符的__get__()方法,确保目录存在并且可以写入。

此示例演示了非数据描述符的基本特性。__get__()方法的隐含用法可以方便地执行一些需要隐藏实现细节的有限类型的自动化处理。对于静态方法和类方法,这非常有用。

使用数据描述符

数据描述符用于使用外部类定义构建类似属性的处理。__get__()__set__()__delete__()的描述符方法对应于@property可用于构建gettersetterdeleter方法的方式。描述符的重要区别是一个单独的、可重用的类定义,允许重用属性定义。

我们将使用描述符设计一个过于简单的单元转换模式,这些描述符可以在其__get__()__set__()方法中执行适当的转换。

以下是单位描述符的超类,用于与标准单位进行转换:

    class     Conversion:
        """Depends on a standard value."""
                conversion:     float
                standard:     str

                    def         __get__    (    self    , instance: Any, owner:     type    ) ->     float    :
            return         getattr    (instance,     self    .standard) *     self    .conversion

        def         __set__    (    self    , instance: Any, value:     float    ) ->     None    :
            setattr    (instance,     self    .standard, value /     self    .conversion)

    class     Standard(Conversion):
        """Defines a standard value."""
                conversion =     1.0

Conversion类执行简单的乘法和除法,将标准单位转换为其他非标准单位,反之亦然。这不适用于温度转换,需要一个子类来处理这种情况。

Standard类是Conversion类的扩展,该类为给定测量设置标准值,而不应用任何转换系数。这主要是为了为任何特定类型的测量提供一个非常明显的标准名称。

通过这两个超类,我们可以从标准单位定义一些转换。我们来看看速度的度量。一些具体的描述符类定义如下:

    class     Speed(Conversion):
    standard =     "standard_speed"          # KPH

        class     KPH(Standard, Speed):
        pass

        class     Knots(Speed):
    conversion =     0.5399568

        class     MPH(Speed):
    conversion =     0.62137119

摘要Speed类为各种转换子类KPHKnotsMPH提供标准源数据。任何基于Speed类的子类的属性都将使用标准值。

KPH类被定义为Standard类和Speed类的子类。从Standard可以得到 1.0 的换算系数。从Speed中获取用于保持速度测量标准值的属性名称。

其他类是Speed的子类,它执行从标准值到所需值的转换。

以下Trip类将这些转换用于给定测量:

    class     Trip:
    kph = KPH()
    knots = Knots()
    mph = MPH()

        def         __init__    (
            self    ,
        distance:     float    ,
        kph: Optional[    float    ] =     None    ,
        mph: Optional[    float    ] =     None    ,
        knots: Optional[    float    ] =     None    ,
    ) ->     None    :
            self    .distance = distance      # Nautical Miles
                        if     kph:
                self    .kph = kph
            elif     mph:
                self    .mph = mph
            elif     knots:
                self    .knots = knots
            else    :
                raise         TypeError    (    "Impossible arguments"    )
            self    .time =     self    .distance /     self    .knots

        def         __str__    (    self    ) -> str:
            return     (
                f"distance: {self.distance} nm, "
                    f"rate: {self.kph} "
                    f"kph = {self.mph} "
                    f"mph = {self.knots} knots, "
                    f"time = {self.time} hrs"
                    )

类级属性kphknotsmph中的每一个都是不同单元的描述符。当引用这些属性时,各种描述符的__get__()__set__()方法将与标准值进行适当的转换。

以下是与Trip类交互的示例:

 >>> m2 = Trip(distance=13.2, knots=5.9)
        >>> print(m2)
        distance: 13.2 nm, rate: 10.92680006993152 kph = 6.789598762345432 mph = 5.9 knots, time = 2.23728813559322 hrs
        >>> print(f"Speed: {m2.mph:.3f} mph")
        Speed: 6.790 mph
        >>> m2.standard_speed    10.92680006993152

我们通过设置属性distance,设置一个可用描述符,然后计算派生值time,创建了Trip类的对象。在本例中,我们设置了knots描述符。这是Speed类的子类,是Conversion类的子类,因此,该值将转换为标准值。

当我们将值显示为一个大字符串时,使用了每个描述符的__get__()方法。这些方法从拥有对象获取内部kph属性值,应用转换因子,并返回结果值。

创建描述符的过程允许重用基本单元定义。这些计算可以精确地陈述一次,并且它们与任何特定的应用程序类定义是分开的。将此与紧密绑定到包含它的类的@property方法进行比较。类似地,各种转换因子只声明一次,可被许多相关应用程序广泛重用

核心描述,转换,体现了一个相对简单的计算。当计算更复杂时,它可以导致整个应用程序的全面简化。在处理数据库和数据序列化问题时,描述符非常流行,因为描述符的代码可能涉及到对不同表示形式的复杂转换。

对属性和属性使用类型提示

当使用mypy时,我们需要为类的属性提供类型提示。这通常通过__init__()方法处理。大多数情况下,参数类型提示都是必需的。

在前面的示例中,我们定义了如下类:

    class     RTD_Solver:

        def         __init__    (
            self    , *,
        rate: Optional[    float]     =     None    ,
        time: Optional[float]         =     None    ,
        distance: Optional[float]         =     None
                ) ->     None    : 
        if rate:
            self.rate = rate
        if time:
            self.time = time
        if distance:
            self.distance = distance

参数上的类型提示用于识别实例变量self.rateself.timeself.distance的类型。

当我们在__init__()方法中指定默认值时,我们有两种常见的设计模式。

  • 当我们可以急切地计算一个值时,可以通过赋值语句中的mypy来识别类型。
  • 当提供默认None值时,必须明确说明类型。

我们可能会看到如下转让声明:

self.computed_value: Optional[float] = None

此赋值语句告诉mypy变量将是float的实例或None对象。这种初始化方式使类属性类型显式

对于属性定义,类型提示是属性方法定义的一部分。我们经常会看到如下代码:

@property
    def some_computed_value(self) -> float: ...    

该定义为object.some_computed_value类型提供了明确的说明。mypy使用此选项来确保对该属性名称的引用中的所有类型都匹配。

使用数据类模块

从 Python 3.7 开始,dataclasses模块可用。这个模块提供了一个超类,我们可以使用它创建具有明确属性定义的类。dataclass 的核心用例是类属性的简单定义。

这些属性用于自动创建常用的属性访问方法,包括__init__()__repr__()__eq__()。下面是一个例子:

    from     dataclasses     import     dataclass
    from     typing     import     Optional, cast

    @dataclass
        class     RTD:
    rate: Optional[    float    ]
    time: Optional[    float    ]
    distance: Optional[    float    ]

        def compute(self) -> "RTD":
        if (
            self.distance is None and self.rate is not None 
            and self.time is not None
        ):
            self.distance = self.rate * self.time
        elif (
            self.rate is None and self.distance is not None 
            and self.time is not None
        ):
            self.rate = self.distance / self.time
        elif (
            self.time is None and self.distance is not None 
            and self.rate is not None
        ):
            self.time = self.distance / self.rate
        return self

该类的每个实例都有三个属性,ratetimedistance。装饰者将创建一个__init__()方法来设置这些属性。它还将创建一个__repr__()方法来显示属性值的详细信息。编写了一个__eq__()方法来对所有属性值执行简单的相等性检查。

仔细检查None和非None值有助于mypy。这种显式检查保证了Optional[float]类型将具有非None值。

请注意,这三个名称是作为类定义的一部分编写的。它们用于构建__init__()方法,该方法是结果类的一部分。这些将成为结果对象中的实例变量。

compute()方法更改对象的内部状态。我们提供了一个类型提示,将返回值描述为类的实例。下面是如何使用此类的实例:

 >>> r = RTD(distance=13.5, rate=6.1, time=None)
        >>> r.compute()
        RTD(rate=6.1, time=2.2131147540983607, distance=13.5) 

在这个代码片段中,我们创建了一个实例,为distancerate提供了非-None值。compute()方法为time属性计算了一个值。

默认的@dataclass装饰器将没有比较方法。它将创建一个可以更改属性值的可变类

我们可以要求一些额外的可选功能。我们可以向装饰器提供可选参数来控制可选特性。我们可以使用比较运算符为不可变对象创建一个类,代码如下:

    @dataclass    (    frozen    =    True    ,     order    =    True    )
    class     Card:
    rank:     int
                suit:     str

                    @property
                    def     points(    self    ) -> int:
            return         self    .rank

本例中的frozen参数引导装饰器将类变成一个不可变的冻结对象。@dataclass修饰符的order参数创建类定义中的比较方法。这对于创建简单的、不可变的对象非常有帮助。因为这两个属性都包含类型提示,mypy可以确认Card数据类使用正确

继承与数据类一起工作。我们可以按照以下示例声明类:

    class     Ace(Card):

        @property
                    def     points(    self    ) ->     int    :
            return         1

        class     Face(Card):

        @property
                    def     points(    self    ) ->     int    :
            return         10

这两个类继承了__init__()__repr__()__eq__()__hash__()以及Card超类的比较方法。这两个类在points()方法的实现上有所不同。

@dataclass装饰器简化了类定义。倾向于与属性有直接关系的方法由装饰器生成。

属性设计模式

来自其他语言(特别是 Java 和 C++)的程序员可以尝试将所有属性私有化,并编写扩展的gettersetter函数。对于类型定义静态编译到运行时的语言,这种设计模式可能是必需的。这在 Python 中是不必要的。Python 依赖于一组不同的通用模式。

在 Python 中,通常将所有属性都视为公共属性。这意味着:

  • 所有属性都应记录在案。
  • 属性应正确反映对象的状态;它们不应该是临时值或瞬态值。
  • 在极少数情况下,属性的值可能会令人困惑(或脆弱),单个前导下划线字符(_将名称标记为而不是定义接口的一部分。它在技术上不是私有的,但在下一版本的框架或包中不能依赖它。

重要的是要把私人属性看作是一种讨厌的东西。封装不会因为语言中缺乏复杂的隐私机制而被破坏;只有糟糕的设计才能破坏正确的电子封装。

此外,我们必须在属性或与属性具有相同语法但可以具有更复杂语义的属性之间进行选择

属性与属性

在大多数情况下,可以在类之外设置属性,而不会产生不良后果。我们的Hand类示例显示了这一点。对于该类的许多版本,我们可以简单地附加到hand.cards,而total通过属性的延迟计算将非常有效。

如果某个属性的更改会导致其他属性的相应更改,则需要更复杂的类设计:

  • 一种方法可以澄清状态变化。当需要多个参数值且必须同步更改时,这是必需的。
  • setter属性可能比方法函数更清晰。当需要单个值时,这将是一个明智的选择。
  • 我们还可以使用 Python 的就地运算符,例如+=。我们将推迟到第 8 章创建数字之后。

没有严格的规定。方法函数和属性之间的区别完全在于语法以及语法传达意图的程度。对于计算值,属性允许延迟计算,而属性则需要快速计算。这就涉及到性能问题。惰性计算与急切计算的好处基于预期的用例。

最后,对于一些非常复杂的情况,我们可能需要使用底层 Python 描述符。

用描述符设计

描述符的许多用法已经成为 Python 的一部分。我们不需要重新设计属性、类方法或静态方法。

创建新描述符的最引人注目的例子是 Python 对象与 Python 之外的其他软件之间的映射。例如,对象关系数据库映射需要非常小心,以确保 Python 类以正确的顺序具有正确的属性,以匹配 SQL 表和列。此外,当映射到 Python 之外的对象时,描述符类可以处理数据的编码和解码,或者从外部源获取数据。

在构建 Web 服务客户端时,我们可以考虑使用描述符来生成 Web 服务请求。例如,__get__()方法可能会变成 HTTPGET请求,__set__()方法可能会变成 HTTPPUT请求。在某些情况下,单个请求可能会填充多个描述符的数据。在这种情况下,__get__()方法将在发出 HTTP 请求之前检查实例缓存并返回该值。

许多数据描述符操作更简单地由属性处理。这为我们提供了一个开始编写属性的地方。如果属性处理变得过于扩展或复杂,那么我们可以切换到描述符来重构类。

总结

在本章中,我们介绍了几种处理对象属性的方法。我们可以使用object类的内置特性简单有效地获取和设置属性值。我们可以使用@property创建类似属性的方法。

如果我们想要更复杂,我们可以调整__getattr__()__setattr__()__delattr__()__getattribute__()的底层特殊方法实现。这些允许我们对属性行为进行非常细粒度的控制。当我们接触这些方法时,我们走的是一条很好的路线,因为我们可以对 Python 的行为进行基本的(并且令人困惑的)更改。

在内部,Python 使用描述符来实现诸如类方法、静态方法和属性等特性。许多真正优秀的描述符用例已经是该语言的一流特性。

类型提示的使用有助于确认对象的使用是否正确。强烈鼓励将它们作为单元测试的补充,以确保参数和值一致

新的dataclasses模块有助于简化类定义。在许多情况下,使用@dataclass 装饰器创建的类可能是设计良好的软件的精髓。

在下一章中,我们将仔细研究我们将在第 6 章使用可调用对象和上下文第 7 章创建容器和集合第 8 章中利用的ABC类(抽象基类),创建数字。这些 ABC 将帮助我们定义与现有 Python 功能完美集成的类。它们还允许我们创建强制一致设计和扩展的类层次结构。