返回介绍

13.6 增量赋值运算符

发布于 2024-02-05 21:59:47 字数 5814 浏览 0 评论 0 收藏 0

Vector 类已经支持增量赋值运算符 += 和 *= 了,如示例 13-15 所示。

示例 13-15 增量赋值不会修改不可变目标,而是新建实例,然后重新绑定

>>> v1 = Vector([1, 2, 3])
>>> v1_alias = v1  # ➊
>>> id(v1)  # ➋
4302860128
>>> v1 += Vector([4, 5, 6])  # ➌
>>> v1  # ➍
Vector([5.0, 7.0, 9.0])
>>> id(v1)  # ➎
4302859904
>>> v1_alias  # ➏
Vector([1.0, 2.0, 3.0])
>>> v1 *= 11  # ➐
>>> v1  # ➑
Vector([55.0, 77.0, 99.0])
>>> id(v1)
4302858336

❶ 复制一份,供后面审查 Vector([1, 2, 3]) 对象。

❷ 记住一开始绑定给 v1 的 Vector 实例的 ID。

❸ 增量加法运算。

❹ 结果与预期相符……

❺ ……但是创建了新的 Vector 实例。

❻ 审查 v1_alias,确认原来的 Vector 实例没被修改。

❼ 增量乘法运算。

❽ 同样,结果与预期相符,但是创建了新的 Vector 实例。

如果一个类没有实现表 13-1 列出的就地运算符,增量赋值运算符只是语法糖:a += b 的作用与 a = a + b 完全一样。对不可变类型来说,这是预期的行为,而且,如果定义了 __add__ 方法的话,不用编写额外的代码,+= 就能使用。

然而,如果实现了就地运算符方法,例如 __iadd__,计算 a += b 的结果时会调用就地运算符方法。这种运算符的名称表明,它们会就地修改左操作数,而不会创建新对象作为结果。

 不可变类型,如 Vector 类,一定不能实现就地特殊方法。这是明显的事实,不过还是值得提出来。

为了展示如何实现就地运算符,我们将扩展示例 11-12 中的 BingoCage 类,实现 __add__ 和 __iadd__ 方法。

我们把子类命名为 AddableBingoCage。示例 13-16 是我们想让 + 运算符具有的行为。

示例 13-16 使用 + 运算符新建 AddableBingoCage 实例

>>> vowels = 'AEIOU'
>>> globe = AddableBingoCage(vowels)  ➊
>>> globe.inspect()
('A', 'E', 'I', 'O', 'U')
>>> globe.pick() in vowels  ➋
True
>>> len(globe.inspect())  ➌
4
>>> globe2 = AddableBingoCage('XYZ')  ➍
>>> globe3 = globe + globe2
>>> len(globe3.inspect())  ➎
7
>>> void = globe + [10, 20]  ➏
Traceback (most recent call last):
  ...
TypeError: unsupported operand type(s) for +: 'AddableBingoCage' and 'list'

❶ 使用 5 个元素(vowels 中的各个字母)创建一个 globe 实例。

❷ 从中取出一个元素,确认它在 vowels 中。

❸ 确认 globe 的元素数量减少到 4 个了。

❹ 创建第二个实例,它有 3 个元素。

❺ 把前两个实例加在一起,创建第 3 个实例。这个实例有 7 个元素。

❻ AddableBingoCage 实例无法与列表相加,抛出 TypeError。那个错误消息是 __add__ 方法返回 NotImplemented 时 Python 解释器输出的。

AddableBingoCage 是可变的,实现 __iadd__ 方法后的行为如示例 13-17 所示。

示例 13-17 可以使用 += 运算符载入现有的 AddableBingoCage 实例(接续示例 13-16)

>>> globe_orig = globe  ➊
>>> len(globe.inspect())  ➋
4
>>> globe += globe2  ➌
>>> len(globe.inspect())
7
>>> globe += ['M', 'N']  ➍
>>> len(globe.inspect())
9
>>> globe is globe_orig  ➎
True
>>> globe += 1  ➏
Traceback (most recent call last):
  ...
TypeError: right operand in += must be 'AddableBingoCage' or an iterable

❶ 复制一份,供后面检查对象的标识。

❷ 现在 globe 有 4 个元素。

❸ AddableBingoCage 实例可以从同属一类的其他实例那里接受元素。

❹ += 的右操作数也可以是任何可迭代对象。

❺ 在这个示例中,globe 始终指代 globe_orig 对象。

❻ AddableBingoCage 实例不能与非可迭代对象相加,错误消息会指明原因。

注意,与 + 相比,+= 运算符对第二个操作数更宽容。+ 运算符的两个操作数必须是相同类型(这里是 AddableBingoCage),如若不然,结果的类型可能让人摸不着头脑。而 += 的情况更明确,因为就地修改左操作数,所以结果的类型是确定的。

 通过观察内置 list 类型的工作方式,我确定了要对 + 和 += 的行为做什么限制。 my_list + x 只能用于把两个列表加到一起,而 my_list += x 可以使用右边可迭代对象 x 中的元素扩展左边的列表。list.extend() 的行为也是这样的,它的参数可以是任何可迭代对象。

我们明确了 AddableBingoCage 的行为,下面来看实现方式,如示例 13-18 所示。

示例 13-18 bingoaddable.py:AddableBingoCage 扩展 BingoCage,支持 + 和 +=

import itertools  ➊

from tombola import Tombola
from bingo import BingoCage


class AddableBingoCage(BingoCage):  ➋

  def __add__(self, other):
    if isinstance(other, Tombola):  ➌
      return AddableBingoCage(self.inspect() + other.inspect())  ➍
    else:
      return NotImplemented

  def __iadd__(self, other):
    if isinstance(other, Tombola):
      other_iterable = other.inspect()  ➎
    else:
      try:
        other_iterable = iter(other)
      except TypeError:  ➏
        self_cls = type(self).__name__
        msg = "right operand in += must be {!r} or an iterable"
        raise TypeError(msg.format(self_cls))
    self.load(other_iterable)  ➐
    return self  ➑

❶ “PEP 8—Style Guide for Python Code”建议,把导入标准库的语句放在导入自己编写的模块之前。

❷ AddableBingoCage 扩展 BingoCage。

❸ __add__ 方法的第二个操作数只能是 Tombola 实例。

❹ 如果 other 是 Tombola 实例,从中获取元素。

❺ 否则,尝试使用 other 创建迭代器。11

11内置的 iter 函数在下一章讨论。这里,本可以使用 tuple(other),这样做是可以的,但是 .load(...) 方法迭代参数时要构建大量元组,资源消耗大。

❻ 如果尝试失败,抛出异常,并且告知用户该怎么做。如果可能,错误消息应该明确指导用户怎么解决问题。

❼ 如果能执行到这里,把 other_iterable 载入 self。

❽ 重要提醒:增量赋值特殊方法必须返回 self。

通过示例 13-18 中 __add__ 和 __iadd__ 返回结果的方式可以总结出就地运算符的原理。

__add__

调用 AddableBingoCage 构造方法构建一个新实例,作为结果返回。

__iadd__

把修改后的 self 作为结果返回。

最后,示例 13-18 中还有一点要注意:从设计上看,AddableBingoCage 不用定义 __radd__ 方法,因为不需要。如果右操作数是相同类型,那么正向方法 __add__ 会处理,因此,Python 计算 a + b 时,如果 a 是 AddableBingoCage 实例,而 b 不是,那么会返回 NotImplemented,此时或许可以让 b 所属的类接手处理。可是,如果表达式是 b + a,而 b 不是 AddableBingoCage 实例,返回了 NotImplemented,那么 Python 最好放弃,抛出 TypeError,因为无法处理 b。

 一般来说,如果中缀运算符的正向方法(如 __mul__)只处理与 self 属于同一类型的操作数,那就无需实现对应的反向方法(如 __rmul__),因为按照定义,反向方法是为了处理类型不同的操作数。

我们对 Python 运算符重载的讨论到此结束。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文