导引第十五章,十六章——python导引编译之十四终结篇

导引第十五章,十六章——python导引编译之十四终结篇

标题15浮点算术:问题和限度. Floating Point Arithmetic: Issues and Limitations

浮点数字在计算机硬件中是用两种分数来表达,例如浮点数
0.125
有十进制的分数表示法1/10+2/100+5/1000,也有二进制的分数表示法,例如
0.001
就有值0/2+0/4+1/8。这两类表示方式生成的分数有同等的值,唯一的区别是:第一种是十进制的符号,第二种是二进制的符号。不幸的是,大多数的十进制分数无法精确地表达为二进制的数。结果,一般而言,你敲进去的十进制的浮点数仅近似地储存在用二进制浮点数表达的机器之中。
这个问题在十进制的分数中更容易理解。考虑十进制分数1/3,你可以近似地用浮点数来表示这个十进制的分数。
0.3
或者,你要表达得更准确一些
0.33
或者,你还要表达得更准确一些
0.333
等等。无论你愿意写下多少个小数点,结果都不会是那个1/3,只是它的近似值,不断地逼近那个1/3.
完全同样,无论你使用多少位二进制的小数,十进制的小数0.1,都不能够精确地用二进制的小数来表达1/10,在二进制中,1/10是一串无穷重复的分数。
0.0001100110011001100110011001100110011001100110011…
你总会停在某个有限字节的数上,得到一个近似值。今天的绝大多数机器浮点被近似地使用一个二进制分数,通常是开头数起的第53个字节,然后带上2的幂次方。如果分数是1/10,则二进制的分数就是
3602879701896397 / 2 ** 55
它靠近1/10,但不等于1/10的值。
许多用户不知道这个近似值,因为值显示的这种方式。python仅打印出一个十进制的近似值,那个二进制的值储存在机器之中。大多数计算机,如果python将打印出这个真实的储存在机器中的那个表示0.1的值,,这个值会显示出来。

>>> 0.1
0.1000000000000000055511151231257827021181583404541015625

这么多的小数点当然不是都有用,所以,python保留下部分小数位,仅显示约数。

>>> 1 / 10
0.1

请记住,即使打印出的结果显得很像1/10的精确值,实际上储存的值是最接近的可表达的二进制分数。
有趣的是,有许多不同的十进制数,这些数共享同样的近似二进制分数,例如数字0.1,数字0.10000000000001和以下长长的数字:
0.1000000000000000055511151231257827021181583404541015625 。
这些数字全都与以下数字近似:
3602879701896397 / 2 ** 55.
因为,这些十进制值共享同样的近似值,它们当中的任何一个,在保留不变值eval(repr(x))==x的同时,共享同样的近似值。
历史上,python提示符和内置函数repr()会选择那个带有17位有意义的小数: 0.10000000000000001.。从python3.1开始,大多数系统中的python现在都能选择这些值中最短的一个,简单地就表示为0.1.。
注意,这是在二进制浮点数非常自然的情况:这不是python的一个错误,也不是你的编码中的错误。在所有语言中你会看到同样的事物类别,这里的语言,指的是支持你的硬件浮点算术的语言(虽然,有些语言也许不展示这个区别,而是表示为缺省,或者在所有的输出模式之中。)
对于更多令人愉悦的输出,你也许希望使用字符串格式化去产生一个有限的有意义小数的数:

>>> format(math.pi, '.12g')  # give 12 significant digits
'3.14159265359'

>>> format(math.pi, '.2f')   # give 2 digits after the point
'3.14'

>>> repr(math.pi)
'3.141592653589793'

以下认识十分重要,这就是,在一个实在的意义上,一个假象:你只是在大约地展示一个真实的值。
一个假象也许会产生另外的假象。例如,因为0.1并不是1/10的精确值,那么把三个0.1相加的值就可能不是0.3,或者出现这样的情景:

>>> .1 + .1 + .1 == .3
False

还有,因为这个0.1不能够得到1/10更精确的值,并且,0.3也得不到3/10的更精确的值,由此,用round()来四舍五入就得不到帮助:

>>> round(.1, 1) + round(.1, 1) + round(.1, 1) == round(.3, 1)
False

虽然这些数字得不到它们想要的精确值,那个round()对于舍掉尾部数以便用这个不精确数字值和另外一个值进行比较确是可用的。

>>> round(.1 + .1 + .1, 10) == round(.3, 10)
True

二进制浮点算术含有许多类似的让人惊奇的功能。使用0.1产生的问题可在以下细节描述中得到解释,即在“表达错误”那一节中。若想对这些共同的惊奇有更多的了解,请看浮点风险 The Perils of Floating Point

在尾端附近说,没有容易的答案。,这不是浮点过度敏感。在python中,浮点运算的错误浮点硬件遗传而来,而且,在大多数机器中,每一次操作2**53中不大于1的那个部分都处在规定的秩序之中。对大多数任务而言,这样的操作都可以胜任,但是你必须记在心中,那不是十进制的算术,每一个浮点运算可以遭受一个新的舍入的错误。
当反常案例的确存在,对于大多数随意的浮点算术运用而言,你都将看到,你在尾端希望看到的结果,如果你只是对你期待的十进制小数的最后结果的显示进行舍入,函数str()通常就足够,若需要更为精良的控制,请看方法str.format()的格式化,该条目在格式化的字符串语法中。
对于使用案例,要求精确的十进制表达的案例,设法去运用十进制的模块,这类模块对于计数应用和高精确的应用,配有合适有效的十进制算术。
另一种精准算术的形式被分数模块所支持,该模块使得基于有理数的算术模块有效(所以,像1/3这样的数字可以精准地表达)
如果你是浮点运算的经常使用者,你应该看看数字python打包Numerical Python package,并且还有许多别的为数学和统计学运用的打包,这些打包由SciPy项目提供。请看网页https://scipy.org
python提供这样一类工具,可以对那些稀有的场合给予帮助,当你真的需要知道那个浮点的精确值的时候。The float.as_integer_ratio() 这种方法表示了一个浮点作为一个分数的精确值:

>>> x = 3.14159
>>> x.as_integer_ratio()
(3537115888337719, 1125899906842624)

因为这个比例是精确的,它可以用来无损地再创这个原创值:

>>> x == 3537115888337719 / 1125899906842624
True

这个float.hex()方法表达了在16进制中的一个浮点,给出储存在你计算机中的那个精确的值:

>>> x.hex()
'0x1.921f9f01b866ep+1'

这个精确的16进制表达式可以用来建构精确的浮点值:

>>> x == float.fromhex('0x1.921f9f01b866ep+1')
True

因为这个表达式是精确的,它用来可靠的传递这个值给不同的python版本(各个独立平台)并且用另外的语言交换这些数据,就是十分有用的,当然那些不同的语言需要支持同样的格式(例如JAVA和C99)。
还有一个有用的工具是math.fsum()函数,该函数在整个加总期间可以帮助减轻精确度的损失。它跟踪作为值的“失去的小数”,把这些加到一个运行的总体上。这可能构成精确性上的差异,以便缺陷可以不会积累起来,达到影响最终总体的程度。

sum([0.1] * 10) == 1.0
False
math.fsum([0.1] * 10) == 1.0
True

标题15.1. 表述错误Representation Error

这一节在细节上解释0.1的问题,显示你如何能够像这样依赖自己来进行一些案例的精确分析。对于二进制的浮点表述有一个基本的熟悉,这是预先假定的。
表达缺陷涉及到这样的事实,某一些十进制的分数不能精确地表述为二进制的分数。为什么python(或者perl,C,C++,Java, Fortran,还有很多其它的语言)不能显示出你期待的精确的十进制数,这是主要的原因。
为什么是这样?1/10不被精确地表达为一个二进制分数。今天几乎所有的机器(2000年11月)使用IEEE-754浮点算术。复制的754含有个字节,所以,输入到计算机去把0.1转换为最靠近的分数,它就能够成为J/2**N的形式,其中,J是一个整数,恰含有53个字节。重写为:

1 / 10 ~= J / (2N)

J ~= 2
N / 10的时候。
回想一下J有53个字节 (也就是 >= 252 但是< 253), 则对于N最好的值就是 56:

>>> 2**52 <=  2**56 // 10  < 2**53
True

那就是,56是对于N 仅有的值,而这个N让J带有53个字节。J最可能的值由此就是那个圈起来的商数:

>>> q, r = divmod(2**56, 10)
>>> r
6

因为余数超出了10的半数,最接近的值就被归整而获得的近似值:

>>> q+1
7205759403792794

因此,1/10在754双重精准中最可能的近似值就是:

7205759403792794 / 2 ** 56

由这两个数相除获得的分数就是:

3602879701896397 / 2 ** 55

注意既然我们四舍五入了,这就是比1/10稍大一点的一个数,如果我们没有舍入,这个商就比1/10小那么一点点。但不可能完全等同于1/10.
所以,计算机绝对看不到1/10:它看到的东西以上给定的一个精确的分数,754双重近似可以给出的最好的近似值是:

>>> 0.1 * 2 ** 55
3602879701896397.0

如果我们用10**55乘以那个分数,我们可以看到55位数的十进制小数的一个值:

>>> 3602879701896397 * 10 ** 55 // 2 ** 55
1000000000000000055511151231257827021181583404541015625

这意味着储存在计算机中精确数字等于以下十进制的值:
0.1000000000000000055511151231257827021181583404541015625. 但没有展示出全部十进制值,约相当于17位小数点的结果:

>>> format(0.1, '.17f')
'0.10000000000000001'

这个分数和十进制模块使得这类计算变得容易了:

>>> from decimal import Decimal
>>> from fractions import Fraction

>>> Fraction.from_float(0.1)
Fraction(3602879701896397, 36028797018963968)

>>> (0.1).as_integer_ratio()
(3602879701896397, 36028797018963968)

>>> Decimal.from_float(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')

>>> format(Decimal.from_float(0.1), '.17')
'0.10000000000000001'

标题16. 附录Appendix

标题16.1.交互式模式 Interactive Mode

标题16.1.1. 错误处理Error Handling

当一个错误产生时,解释器打印出错误信息和一个堆的路线。在交互模式中,接着就返回到主提示符;当输入来自于一个文件时,文件在打印出堆路线之后用一种非零的存在状态而存在。(在测试陈述中的一个例外从句因处理造成的异常在这种语境中不是错误)。有些错误属于无条件地致命错误,这会造成用非零的退出而形成的退出;这运用到内部的不一致性和某些内存用尽的情形之中。所有错误信息被写到那个标准的错误流中;来自处理指令的标准输出,则被写到标准输出中。
打印中断字符(通常使用control C键或者Delete键)给主提示符或者第二提示符,那就取消了输入,返回到主提示符prompt.1,而打印一个中断是在一个指令被处理时打的,则产生键盘中断异常,这可能应该由一个try陈述来负责。

标题16.1.2.处理python脚本 Executable Python Scripts

在BSDish Unix系统中,python脚本可以类似shel脚本那样,通过放置以下行列而直接地被处理

#!/usr/bin/env python3.5

(假定这个解释器在用户的PATH上)脚本的开头,在给定的一个可处理的模式文件。那个#!必须是该文件的头两个字符。在某些平台上,首行必须用一个Unix风格的行结束符号 (’\n’),而非Windows行结束符号。注意:那个散列符号hash或者英镑符号,字符,#等,都被用来开始一个在python中的评论。
这个脚本可以给定一个处理模式,或者允许,使用那个chmod 指令。

$ chmod +x myscript.py

在Windows系统中,没有处理模式概念。python安装者自动连上.py 文件,用指令python.exe,在python文件上双击将让这个文件作为脚本运行。扩展可以是pyw,在该情形下,那个自动出现的便捷窗口将被关闭。

标题16.1.3. 交互式启动文件The Interactive Startup File

当你交互式使用python时,很容易随之就有解释器启动的每一次处理过的标准指令。通过设置一个名为PYTHONSTARTUP的环境变元给一个含有你启动指令的文件名称,你可以做到这一点。这类似于那个Unix shells的函数.profile的文件特性。
这个文件在交互期间仅可读,不是当python从脚本读到指令时,也不是当/dev/ttv/作为清晰的指令源泉被给定的时候(否则就和在交互期间的行为是一样的了)。
文件在同样的名称空间被处理,名称空间所在,交互指令被处理,所以它定义的或者导出的对象在交互期间没有认定也可以使用。你也可以在文件中改变这个提示符sys.ps1和sys.ps2。
如果你想读取一个从当前目录附加的启动文件,你可以用全域的启动文件,使用像if os.path.isfile(‘pythonrc.py’):exec(open(’.pythonrc.py’).read())。如果你想使用在脚本中的启动文件,你必须在脚本中明确地做到这一点。

import os
filename = os.environ.get('PYTHONSTARTUP')
if filename and os.path.isfile(filename):
    with open(filename) as fobj:
        startup_file = fobj.read()
    exec(startup_file)

标题16.1.4. 定制化模块The Customization Modules

python提供两种挂钩,让你定制,一个是位置定制sitecustomize,另一个是用户定制usercustomize。为了看到它们如何发挥作用,你需要首先找到用户的位置打包目录的地方。启动python并且运行这个编码。

>>> import site
>>> site.getusersitepackages()
'/home/user/.local/lib/python3.5/site-packages'

现在你就可以创建一个在该目录中名为usercustomize.py的文件,把你想放在其中的任何东西都放进去。它将影响python的每一个请求,除非它用-s选择启动,去使得自动导入不可行。
那个位置定制sitecustomize以同样的方式发挥作用,但是典型地被那个计算机的管理员所创建,在全域的位置打包目录中创建,并在usercustomize定制之前被导入。若想有更多的细节介绍,请看位置模块文件。

脚注Footnotes

1一个带有GNU在线阅读打包的问题也许可以防止这一点

猜你喜欢

转载自blog.csdn.net/weixin_41670255/article/details/110487592