第 十六 章 运算符重载

有一些事情让我感到有些不安,比如运算符重载。我决定不支持运算符重载,这完全是个人的选择,因为我看到太多 C++ 程序员在中滥用它James Gosling,Creator of Java在 Python 中,您可以使用如下公式计算复利:interest = principal * ((1 + rate) ** peri...
摘要由CSDN通过智能技术生成

有一些事情让我感到有些不安,比如运算符重载。我决定不支持运算符重载,这完全是个人的选择,因为我看到太多 C++ 程序员在中滥用它

                                                                                        James Gosling, Creator of Java

在 Python 中,您可以使用如下公式计算复利:

interest = principal * ((1 + rate) ** periods - 1)

出现在操作数之间的运算符,如 1 + rate中的+,是中缀运算符。在 Python 中,中缀运算符可以处理任意类型。因此,如果您处理的是真实货币,您可以确保principal、rate和periods是准确的数字——Python decimal.Decimal 类的实例——并且该公式将按照字面意思工作,产生准确的结果。

但是在 Java 中,如果从 float 切换到 BigDecimal 以获得精确结果,则不能再使用中缀运算符,因为它们仅适用于原始类型。这是在 Java 中使用 BigDecimal数字类型的编码的相同公式:

BigDecimal interest = principal.multiply(BigDecimal.ONE.add(rate)
                        .pow(periods).subtract(BigDecimal.ONE));

很明显,中缀运算符使公式更具可读性。运算符重载对于支持用户定义或扩展类型(例如 Numpy 数组)的中缀运算符表示法是必要的。在高级、易于使用的语言中实现运算符重载可能是 Python 在数据科学(包括金融和科学应用)中取得巨大成功的关键原因。

在“模拟数字类型”(第 1 章)中,我们看到了一些简单的 Vector 类中运算符的简单实现。示例 1-2 中的 __add__ 和 __mul__ 方法用于展示特殊方法如何支持运算符重载,但它们的实现中存在一些我们忽略的细微问题。此外,在示例 11-2 中,我们注意到 Vector2d.__eq__ 方法认为结果为 True:Vector(3, 4) == [3, 4]——这可能有意义,也可能没有意义。我们将在本章中讨论这些问题,以及:

  • 中缀运算符方法应如何表达无法处理的操作数
  • 使用鸭子类型或天鹅类型来处理不同类型的操作数
  • 众多比较运算符(例如 ==、>、<= 等)的特殊行为
  • 增强赋值运算符(例如 +=)的默认处理以及如何重载它们

本章的新内容

天鹅类型是 Python 的关键部分,但静态类型不支持numbers ABC,因此我将示例 16-11 更改为使用鸭子类型而不是针对 numbers.Real 的显式 isinstance 检查.

我在 Fluent Python 第一版中介绍了 @矩阵乘法运算符,作为 3.5 仍处于 alpha 阶段时即将进行的更改。因此,该运算符不再位于旁注中,而是集成在“使用 @ 作为中缀运算符”章节中。我利用 天鹅类型使 __matmul__ 的实现比第一版中的更安全,同时不影响灵活性。

“进一步阅读”现在有一些新的参考资料——包括 Guido van Rossum 的一篇博客文章。我还提到了两个库,它们展示了数学领域之外运算符重载的有效使用:pathlib 和 Scapy。

运算符重载 101

运算符重载允许用户定义的对象使用中缀运算符(例如 + 和 |)进行互操作,或者是一元运算符,如 - 和 ~。一般来说,函数调用 (())、属性访问 (.) 和元素访问/切片 ([]) 也是 Python 中的运算符,但本章涵盖的是一元和中缀运算符。

运算符重载在某些圈子里名声不好。它是一种可能(并且已经)被滥用的语言特性,导致程序员的混淆、错误以及意外的性能瓶颈。但如果使用得当,它会带来令人愉悦的 API 和可读的代码。 Python 通过施加一些限制在灵活性、可用性和安全性之间取得了良好的平衡:

  • 我们不能改变内置类型的运算符的含义。
  • 我们不能创建新的运算符,只能重载现有的运算符。
  • 一些运算符不能重载:is、and、or、not(但按位运算符 &、|、~、可以)。

在第 12 章中,我们已经在 Vector 中实现了一个中缀运算符:==,由 __eq__ 方法支持。在本章中,我们将改进 __eq__ 的实现,以更好地处理 Vector 以外的类型的操作数。但是,众多比较运算符(==、!=、>、<、>=、<=)是运算符重载中的特殊情况,因此我们将从 Vector 中的四个算术运算符重载开始:一元运算符 - 和 +,然后是中缀运算符 + 和 *         。

让我们从最简单的话题开始:一元运算符。

一元运算符

在 Python 语言参考中,“6.5.一元算术和按位运算”列出了三个一元运算符,此处显示了它们相关的特殊方法:

-,由 __neg__ 实现:一元取负算术运算符。如果 x 是 -2,则 -x == 2。

+,由 __pos__ 实现:一元取正算术运算符。通常 x == +x,但也有少数情况并非如此。如果您好奇,请参阅“当 x 和 +x 不相等时”。

~,由 __invert__ 实现:对整数的按位取反,定义为 ~x == -(x+1)。如果 x 是 2 那么 ~x == -3

The Python Language Reference 的 Data Model 章节还列出了 abs() 内置函数作为一元运算符。相关的特殊方法是 __abs__,正如我们之前所见。

支持一元运算符很容易。只需实现适当的特殊方法,这些方法只有一个参数:self。使用任何在你的类中有意义的逻辑,但要遵守操作符的一般规则:始终返回一个新对象。换句话说,不要修改接收者(self),而是创建并返回一个合适类型的新实例。

对 - 和 + 来说,结果可能是与 self 相同的类的实例。对于一元 +,如果接收者是不可变的,你应该返回 self;否则,返回 self 的副本。对于 abs(),结果应该是一个标量数。至于~,如果你不处理整数中的位,很难说什么是合理的结果。在pandas数据分析包中,波浪号否定布尔过滤条件;有关示例,请参阅 pandas 文档中的布尔索引。如前所述,我们将在第 12 章的 Vector 类上实现几个新的运算符。例 16-1 显示了我们在例 12-16 中已有的 __abs__ 方法,以及新添加的 __neg__ 和 __pos__ 一元运算符方法。

例 16-1。 vector_v6.py:将一元运算符 - 和 + 添加到示例 12-16中

    def __abs__(self):
        return math.hypot(*self)

    def __neg__(self):
        return Vector(-x for x in self)  1

    def __pos__(self):
        return Vector(self)  2
  1. 要计算 -v,请构建一个新的 Vector,其中 self 的每个分量都取相反。
  2. 要计算 +v,请使用 self 的每个分量构建一个新的 Vector。

回想一下 Vector 实例是可迭代的,并且 Vector.__init__ 接受一个可迭代的参数,所以 __neg__ 和 __pos__ 的实现是短小精悍的。我们打算实现 __invert__,因此如果用户在 Vector 实例上尝试 ~v,Python 将抛出TypeError 并带有明确的消息:“bad operand type for unary ~: 'Vector'.”

下面的侧边栏涵盖了一个奇怪的问题, 某天它可能会帮助您赢得关于一元运算符+的赌注。


当 X 和 +X 不相等时

每个人都期望 x == +x,这在 Python 中几乎一直都是正确的,但我在标准库中发现了两种x != +x的情况。第一种情况涉及decimal.Decimal 类。如果 x 是在算术上下文中创建的 Decimal 实例,然后在具有不同设置的上下文中计算 +x,那么 x != +x 。例如,x 在具有确定精度的上下文中计算,但随后上下文的精度发生变化,然后计算 +x。请参见示例 16-2。

例 16-2。算术运算上下文精度的变化可能导致 x 与 +x 不同

>>> import decimal
>>> ctx = decimal.getcontext()  1
>>> ctx.prec = 40  2
>>> one_third = decimal.Decimal('1') / decimal.Decimal('3')  3
>>> one_third  4
Decimal('0.3333333333333333333333333333333333333333')
>>> one_third == +one_third  5
True
>>> ctx.prec = 28  6
>>> one_third == +one_third  7
False
>>> +one_third  8
Decimal('0.3333333333333333333333333333')
  1. 获取对当前全局算术运算上下文的引用。
  2. 将算术运算上下文的精度设置为 40
  3. 使用当前精度计算 1/3。
  4. 检查结果;小数点后有40位。
  5. 此时one_third == +one_third 为真。
  6. 将精度降低到 28——十进制算术运算的默认值。
  7. 现在 one_third == +one_third 是 False。
  8. 检查+one_third; 小数点后是28位

事实上,表达式 +one_third 的每次都会根据 one_third 的值生成一个新的 Decimal 实例,使用的是当前算术运算上下文的精度。

您可以在 collections.Counter 文档中找到 x != +x 的第二种情况。Counter 类实现了几个算术运算符,包括中缀运算符 + 将来自两个 Counter 实例的计数加在一起。但是,由于实用角度,Counter加法会从结果中丢弃任何具有负数或零计数的项目。前缀一元运算符 + 是添加空的Counter的快捷方式,因此它会生成一个新的Counter,仅保留大于零的计数。请参见示例 16-3。

例 16-3。一元运算符 + 产生一个没有零或负值的新的Counter

>>> ct = Counter('abracadabra')
>>> ct
Counter({'a': 5, 'r': 2, 'b': 2, 'd': 1, 'c': 1})
>>> ct['r'] = -3
>>> ct['d'] = 0
>>> ct
Counter({'a': 5, 'b': 2, 'c': 1, 'd': 0, 'r': -3})
>>> +ct
Counter({'a': 5, 'b': 2, 'c': 1})

如您所见,+ct 返回一个Counter,其中所有计数都大于零。下面回归正题。

重载向量加法运算符 + 

Vector 类是一个序列类型,在官方 Python 文档的数据模型章节中3.3.6. Emulating container types说明序列应该支持 + 运算符(用于连接)和 * (用于重复复制)。但是,这里我们将 + 和 * 实现为数学向量运算符,这有点难,但对 Vector 类型更有意义。

TI

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值