7.1 numbers的抽象基类
numbers包提供了大量的数值类型,它们都实现了numbers.Number。另外,fractions和decimal模块提供了可扩展的数值类型:fractions.Fraction和decimal.Decimal。
这些类定义基本和数学中数的分类是一致的。如果要了解数论中不同数的基本概念。可以参见这篇文章http://en.wikipedia.org/wiki/Number_theory。
重点是计算机把数学中的抽象实现到了什么程度。更确切地说,我们希望在数学领域任何可以计算的事物都可通过使用一台计算机来完成。所谓的“完整的图灵”编程语言是说,它可以计算由抽象图灵机完成的任何任务,可参见这篇文章http://en.wikipedia.org/wiki/ Computability_theory。
Python 定义了以下的抽象类以及它们相关的实现。这些抽象类的关系是基于继承的层次结构。可以先看看这些类的功能。因为包含的类很少,因此它们的关系像是塔而不是树。
- complex实现了numbers.Complex 。
- float实现了`numbers.Real。
- fractions.Fraction实现了`numbers.Rational 。
- int实现了numbers.Integral。
此外,还有decimal.Decimal,虽看起来像float,但它不是numbers.Real的子类。
| float的值仅仅是一个近似值,而非精确值。虽然这很显然,但仍需再次强调一下。 |
不要对此感到吃惊。以下是应用式子
求近似值的例子。
>>> (35711)/(1113172329)
0.0007123135264946712
>>> _131723*29
105.00000000000001
从原则上来说,在数塔中,越接近塔底部的数,无穷大阶数越小。这可能会带来一些疑惑。不同的数字都定义了各自的无穷大阶数,可以证明,各自的无穷大阶数大小是不同的。可以得出结论,原则上,浮点数的表示比整数表示包含了更多的数字。实际上,一个64位的浮点数和64位的整数包含了相同数量的不同的数字。
在数值类型的定义中,包含了一系列不同类型之间的转换。实现所有类型间的互转是不可能的,因此需要有明确的定义,哪些类型之间可以转换,哪些类型之间不能转换,如下是一个总结。
- complex:它无法转换到任何其他类型。一个complex值可被分解为real和imag部分,它们都是float。
- float:它可以被显式转换到任何类型,包括decimal.Decimal。算术运算符无法隐式地将float值转换为Decimal。
- Fractions.Fraction:它可以被转换到除了decimal.Decimal之外的其他任何类型。转为decimal包括了两部分操作:(1)转为float;(2)转为decimal.Decimal。所求得的是近似值。
- int:可以被转换为其他任何类型。
- Decimal:可被转换为其他任何类型,但算术运算符不会隐式完成转换过程。
以上的转换正是之前所提到的数塔中每种数字抽象类的转换。
7.1.1 决定使用哪种类型
由于数字处理的过程中存在转换,因此在实际中主要会遇到如下4类问题。
- 复杂类型:一旦涉及复杂的数学操作,将会使用complex、float和cmath模块。可能根本不会使用Fraction或者Decimal。可是,没有任何理由要给数值类型强加限制,大多数数字都可被转换为复杂类型。
- 货币类型:对于有关货币的操作,必须使用Decimal。一般地,进行货币计算时,进行十进制和非十进制的混合计算是不明智的。有些场景会用到int类型,可是也不建议与float或complex以及Decimal混合计算。应当记得,浮点数只是近似值,用来进行货币计算是不妥的。
- 位运算:当涉及位和字节的计算,总会使用int类型。
- 常规情况:除上述之外的其他情况。对于大多数常规的数学运算来说,int、float和Fraction都是可以互转的。是的,对于一个完美的函数来说,总是可以很好地支持多态,它将很好地支持任何数值类型。Python中的类型,尤其是float和int,将经常涉及隐式转换。从这点来看,为这类问题选择一个特殊的数值类型是没有意义的。
一般一个问题包含了几个方面。通常情况下,要把一些涉及科学或工程计算以及复杂数字的应用,和一些包含了金融、货币以及小数计算的应用区分开来并不算难。在应用程序中大可放宽对数值类型的使用限制,对数值类型使用isinstance()进行检测并加以类型限制是浪费时间的做法。
7.1.2 方法解析和运算符映射
算术运算符(+、−、、/、//、%和*等)都会映射为方法。例如,当进行355+133这样的运算时,+运算符会被映射为一个数字类中的add()方法。以上的计算可以写作355.add(133)。其中有一个简单的规则,那就是最左边的运算符决定了要使用哪个类。
不仅如此。当表达式包含了复杂类型,Python会分别调用两个操作数类中各自特殊方法的实现。考虑7-0.14这样的表达式。左边操作数为int类型,表达式被相应转换为7.sub(0.14)。这带来了一点复杂性,因为int运算符的参数为0.14,是float类型,而从float转int`会损失精度。因为从高精度往低精度转总会损失精度。
对于float的转换情景,表达式可以为:0.14.rsub(7)。这样的话,float运算符的参数是一个int类型,值为7;从int往float类型转并不会(通常)损失精度。(对于超大int类型的值来说会损失精度;然而,超大int只是针对特殊情况在技术上的一种实现,一般情况下不会用到。)
rsub()操作是反向减法。例如,X.sub(Y)对应的操作就是X-Y,而A.rsub(B)对应的操作就是B-A,方法的实现来自右边的操作数类。可以看到以下两点规则。
- 先尝试调用左边操作数类中的运算符特殊方法,如果返回值为NotImplemented则执行下面这条规则。
- 尝试调用右边操作数中的运算符特殊方法。如果返回值为NotImplemented,则抛出异常。
当两个操作数构成继承关系时,这种情况需要注意。以下规则会作为特殊情况被优先执行。
- 如果右边操作数是左边操作数的子类并且子类中定义了运算符特殊方法的实现,那么该方法会被调用。子类中重写的运算符特殊方法并会被调用,尽管它处于运算符的右边。
- 否则,执行之前提到的规则,从左边操作数开始判断。
假设实现了float的子类MyFloat。对于这样的表达式2.0 - MyFloat(1),右边的操作数是左边操作数的子类。由于它们构成了继承关系,Python 会先尝试调用MyFloat(1).rsub(2.0)。这条规则的关键点是,子类的优先级高。
这意味着,如果一个类需要转换就必须实现之前所提到的,包括反向操作符。当实现或扩展一个数值类型,也必须相应提供从该类型到其他可转换类型的行为。
