8. 错误和异常

至此,本教程还未深入介绍错误信息,但如果您尝试过本教程前文中的例子,应该已经看到过一些错误信息。错误可(至少)被分为两种:语法错误 异常

8.1. 语法错误

语法错误又称解析错误,是学习 Python 时最常见的错误: >>>
  1. >>> while True print('Hello world')
  2. File "<stdin>", line 1
  3. while True print('Hello world')
  4. ^^^^^
  5. SyntaxError: invalid syntax
解析器会重复存在错误的行并显示一个指向该行中检测到错误的词元的‘箭头’。 错误可能是由于所指向的词元 之前 缺少某个词元而导致的。 在这个例子中,错误是在函数 print() 上检测到的,原因是在它之前缺少一个冒号 (<font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">':'</font>)。 文件名和行号也会被打印出来以便你在输入是来自脚本时可以知道要去哪里查找问题。

8.2. 异常

即使语句或表达式使用了正确的语法,执行时仍可能触发错误。执行时检测到的错误称为 异常,异常不一定导致严重的后果:很快我们就能学会如何处理 Python 的异常。大多数异常不会被程序处理,而是显示下列错误信息: >>>
  1. >>> 10 * (1/0)
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in <module>
  4. ZeroDivisionError: division by zero
  5. >>> 4 + spam*3
  6. Traceback (most recent call last):
  7. File "<stdin>", line 1, in <module>
  8. NameError: name 'spam' is not defined
  9. >>> '2' + 2
  10. Traceback (most recent call last):
  11. File "<stdin>", line 1, in <module>
  12. TypeError: can only concatenate str (not "int") to str
错误信息的最后一行说明程序遇到了什么类型的错误。异常有不同的类型,而类型名称会作为错误信息的一部分中打印出来:上述示例中的异常类型依次是:ZeroDivisionError NameError TypeError。作为异常类型打印的字符串是发生的内置异常的名称。对于所有内置异常都是如此,但对于用户定义的异常则不一定如此(虽然这种规范很有用)。标准的异常类型是内置的标识符(不是保留关键字)。 此行其余部分根据异常类型,结合出错原因,说明错误细节。 错误信息开头用堆栈回溯形式展示发生异常的语境。一般会列出源代码行的堆栈回溯;但不会显示从标准输入读取的行。

内置异常 列出了内置异常及其含义。

8.3. 异常的处理

可以编写程序处理选定的异常。下例会要求用户一直输入内容,直到输入有效的整数,但允许用户中断程序(使用 Control-C 或操作系统支持的其他操作);注意,用户中断程序会触发 KeyboardInterrupt 异常。 >>>
  1. >>> while True:
  2. ... try:
  3. ... x = int(input("Please enter a number: "))
  4. ... break
  5. ... except ValueError:
  6. ... print("Oops! That was no valid number. Try again...")
  7. ...

try 语句的工作原理如下:

  • 首先,执行 try 子句 try except 关键字之间的(多行)语句)。
  • 如果没有触发异常,则跳过 except 子句try 语句执行完毕。
  • 如果在执行 try 子句时发生了异常,则跳过该子句中剩下的部分。 如果异常的类型与 except 关键字后指定的异常相匹配,则会执行 except 子句,然后跳到 try/except 代码块之后继续执行。
  • 如果发生的异常与 except 子句 中指定的异常不匹配,则它会被传递到外层的 try 语句中;如果没有找到处理器,则它是一个 未处理异常 且执行将停止并输出一条错误消息。

try 语句可以有多个 except 子句 来为不同的异常指定处理程序。 但最多只有一个处理程序会被执行。 处理程序只处理对应的 try 子句 中发生的异常,而不处理同一 <font style="color:rgb(0, 0, 0);">try</font> 语句内其他处理程序中的异常。 except 子句 可以用带圆括号的元组来指定多个异常,例如:

  1. ... except (RuntimeError, TypeError, NameError):
  2. ... pass
一个 except 子句中的类匹配的异常将是该类本身的实例或其所派生的类的实例(但反过来则不可以 —- 列出派生类的 except 子句 不会匹配其基类的实例)。 例如,下面的代码将依次打印 B, C, D:
  1. class B(Exception):
  2. pass
  3. class C(B):
  4. pass
  5. class D(C):
  6. pass
  7. for cls in [B, C, D]:
  8. try:
  9. raise cls()
  10. except D:
  11. print("D")
  12. except C:
  13. print("C")
  14. except B:
  15. print("B")
请注意如果颠倒 except 子句 的顺序(把 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">except</font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);"> </font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">B</font> 放在最前),则会输出 B, B, B —- 即触发了第一个匹配的 except 子句 发生异常时,它可能具有关联值,即异常 参数 。是否需要参数,以及参数的类型取决于异常的类型。

except 子句 可能会在异常名称后面指定一个变量。 这个变量将被绑定到异常实例,该实例通常会有一个存储参数的 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">args</font> 属性。 为了方便起见,内置异常类型定义了 str() 来打印所有参数而不必显式地访问 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">.args</font>

>>>
  1. >>> try:
  2. ... raise Exception('spam', 'eggs')
  3. ... except Exception as inst:
  4. ... print(type(inst)) # the exception type
  5. ... print(inst.args) # arguments stored in .args
  6. ... print(inst) # __str__ allows args to be printed directly,
  7. ... # but may be overridden in exception subclasses
  8. ... x, y = inst.args # unpack args
  9. ... print('x =', x)
  10. ... print('y =', y)
  11. ...
  12. <class 'Exception'>
  13. ('spam', 'eggs')
  14. ('spam', 'eggs')
  15. x = spam
  16. y = eggs
未处理异常的 str() 输出会被打印为该异常消息的最后部分 (‘detail’)。

BaseException 是所有异常的共同基类。它的一个子类, Exception ,是所有非致命异常的基类。不是 Exception 的子类的异常通常不被处理,因为它们被用来指示程序应该终止。它们包括由 sys.exit() 引发的 SystemExit ,以及当用户希望中断程序时引发的 KeyboardInterrupt

Exception 可以被用作通配符,捕获(几乎)一切。然而,好的做法是,尽可能具体地说明我们打算处理的异常类型,并允许任何意外的异常传播下去。

处理 Exception 最常见的模式是打印或记录异常,然后重新提出(允许调用者也处理异常):
  1. import sys
  2. try:
  3. f = open('myfile.txt')
  4. s = f.readline()
  5. i = int(s.strip())
  6. except OSError as err:
  7. print("OS error:", err)
  8. except ValueError:
  9. print("Could not convert data to an integer.")
  10. except Exception as err:
  11. print(f"Unexpected {err=}, {type(err)=}")
  12. raise

try except 语句具有可选的 else 子句,该子句如果存在,它必须放在所有 except 子句 之后。 它适用于 try 子句 没有引发异常但又必须要执行的代码。 例如:

  1. for arg in sys.argv[1:]:
  2. try:
  3. f = open(arg, 'r')
  4. except OSError:
  5. print('cannot open', arg)
  6. else:
  7. print(arg, 'has', len(f.readlines()), 'lines')
  8. f.close()
使用 <font style="color:rgb(0, 0, 0);">else</font> 子句比向 try 子句添加额外的代码要好,可以避免意外捕获非 <font style="color:rgb(0, 0, 0);">try</font> <font style="color:rgb(0, 0, 0);">except</font> 语句保护的代码触发的异常。 异常处理程序不仅会处理在 try 子句 中立刻发生的异常,还会处理在 try 子句 中调用(包括间接调用)的函数。 例如: >>>
  1. >>> def this_fails():
  2. ... x = 1/0
  3. ...
  4. >>> try:
  5. ... this_fails()
  6. ... except ZeroDivisionError as err:
  7. ... print('Handling run-time error:', err)
  8. ...
  9. Handling run-time error: division by zero

8.4. 触发异常

raise 语句支持强制触发指定的异常。例如:

>>>
  1. >>> raise NameError('HiThere')
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in <module>
  4. NameError: HiThere

raise 唯一的参数就是要触发的异常。这个参数必须是异常实例或异常类(派生自 BaseException 类,例如 Exception 或其子类)。如果传递的是异常类,将通过调用没有参数的构造函数来隐式实例化:

  1. raise ValueError # shorthand for 'raise ValueError()'
如果只想判断是否触发了异常,但并不打算处理该异常,则可以使用更简单的 raise 语句重新触发异常: >>>
  1. >>> try:
  2. ... raise NameError('HiThere')
  3. ... except NameError:
  4. ... print('An exception flew by!')
  5. ... raise
  6. ...
  7. An exception flew by!
  8. Traceback (most recent call last):
  9. File "<stdin>", line 2, in <module>
  10. NameError: HiThere

8.5. 异常链

如果一个未处理的异常发生在 except 部分内,它将会有被处理的异常附加到它上面,并包括在错误信息中: >>>
  1. >>> try:
  2. ... open("database.sqlite")
  3. ... except OSError:
  4. ... raise RuntimeError("unable to handle error")
  5. ...
  6. Traceback (most recent call last):
  7. File "<stdin>", line 2, in <module>
  8. FileNotFoundError: [Errno 2] No such file or directory: 'database.sqlite'
  9. During handling of the above exception, another exception occurred:
  10. Traceback (most recent call last):
  11. File "<stdin>", line 4, in <module>
  12. RuntimeError: unable to handle error
为了表明一个异常是另一个异常的直接后果, raise 语句允许一个可选的 from 子句:
  1. # exc must be exception instance or None.
  2. raise RuntimeError from exc
转换异常时,这种方式很有用。例如: >>>
  1. >>> def func():
  2. ... raise ConnectionError
  3. ...
  4. >>> try:
  5. ... func()
  6. ... except ConnectionError as exc:
  7. ... raise RuntimeError('Failed to open database') from exc
  8. ...
  9. Traceback (most recent call last):
  10. File "<stdin>", line 2, in <module>
  11. File "<stdin>", line 2, in func
  12. ConnectionError
  13. The above exception was the direct cause of the following exception:
  14. Traceback (most recent call last):
  15. File "<stdin>", line 4, in <module>
  16. RuntimeError: Failed to open database
它还允许使用 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">from</font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);"> </font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">None</font> 表达禁用自动异常链: >>>
  1. >>> try:
  2. ... open('database.sqlite')
  3. ... except OSError:
  4. ... raise RuntimeError from None
  5. ...
  6. Traceback (most recent call last):
  7. File "<stdin>", line 4, in <module>
  8. RuntimeError
异常链机制详见 内置异常

8.6. 用户自定义异常

程序可以通过创建新的异常类命名自己的异常(Python 类的内容详见 )。不论是以直接还是间接的方式,异常都应从 Exception 类派生。 异常类可以被定义成能做其他类所能做的任何事,但通常应当保持简单,它往往只提供一些属性,允许相应的异常处理程序提取有关错误的信息。 大多数异常命名都以 “Error” 结尾,类似标准异常的命名。 许多标准模块定义了自己的异常,以报告他们定义的函数中可能出现的错误。

8.7. 定义清理操作

try 语句还有一个可选子句,用于定义在所有情况下都必须要执行的清理操作。例如:

>>>
  1. >>> try:
  2. ... raise KeyboardInterrupt
  3. ... finally:
  4. ... print('Goodbye, world!')
  5. ...
  6. Goodbye, world!
  7. Traceback (most recent call last):
  8. File "<stdin>", line 2, in <module>
  9. KeyboardInterrupt
如果存在 finally 子句,则 <font style="color:rgb(0, 0, 0);">finally</font> 子句是 try 语句结束前执行的最后一项任务。不论 <font style="color:rgb(0, 0, 0);">try</font> 语句是否触发异常,都会执行 <font style="color:rgb(0, 0, 0);">finally</font> 子句。以下内容介绍了几种比较复杂的触发异常情景:
  • 如果执行 <font style="color:rgb(0, 0, 0);">try</font> 子句期间触发了某个异常,则某个 except 子句应处理该异常。如果该异常没有 <font style="color:rgb(0, 0, 0);">except</font> 子句处理,在 <font style="color:rgb(0, 0, 0);">finally</font> 子句执行后会被重新触发。
  • <font style="color:rgb(0, 0, 0);">except</font> <font style="color:rgb(0, 0, 0);">else</font> 子句执行期间也会触发异常。 同样,该异常会在 <font style="color:rgb(0, 0, 0);">finally</font> 子句执行之后被重新触发。
  • 如果 <font style="color:rgb(0, 0, 0);">finally</font> 子句中包含 breakcontinue return 等语句,异常将不会被重新引发。
  • 如果执行 <font style="color:rgb(0, 0, 0);">try</font> 语句时遇到 break,、continue return 语句,则 <font style="color:rgb(0, 0, 0);">finally</font> 子句在执行 <font style="color:rgb(0, 0, 0);">break</font><font style="color:rgb(0, 0, 0);">continue</font> <font style="color:rgb(0, 0, 0);">return</font> 语句之前执行。
  • 如果 <font style="color:rgb(0, 0, 0);">finally</font> 子句中包含 <font style="color:rgb(0, 0, 0);">return</font> 语句,则返回值来自 <font style="color:rgb(0, 0, 0);">finally</font> 子句的某个 <font style="color:rgb(0, 0, 0);">return</font> 语句的返回值,而不是来自 <font style="color:rgb(0, 0, 0);">try</font> 子句的 <font style="color:rgb(0, 0, 0);">return</font> 语句的返回值。
例如: >>>
  1. >>> def bool_return():
  2. ... try:
  3. ... return True
  4. ... finally:
  5. ... return False
  6. ...
  7. >>> bool_return()
  8. False
这是一个比较复杂的例子: >>>
  1. >>> def divide(x, y):
  2. ... try:
  3. ... result = x / y
  4. ... except ZeroDivisionError:
  5. ... print("division by zero!")
  6. ... else:
  7. ... print("result is", result)
  8. ... finally:
  9. ... print("executing finally clause")
  10. ...
  11. >>> divide(2, 1)
  12. result is 2.0
  13. executing finally clause
  14. >>> divide(2, 0)
  15. division by zero!
  16. executing finally clause
  17. >>> divide("2", "1")
  18. executing finally clause
  19. Traceback (most recent call last):
  20. File "<stdin>", line 1, in <module>
  21. File "<stdin>", line 3, in divide
  22. TypeError: unsupported operand type(s) for /: 'str' and 'str'
如上所示,任何情况下都会执行 finally 子句。except 子句不处理两个字符串相除触发的 TypeError,因此会在 <font style="color:rgb(0, 0, 0);">finally</font> 子句执行后被重新触发。 在实际应用程序中,finally 子句对于释放外部资源(例如文件或者网络连接)非常有用,无论是否成功使用资源。

8.8. 预定义的清理操作

某些对象定义了不需要该对象时要执行的标准清理操作。无论使用该对象的操作是否成功,都会执行清理操作。比如,下例要打开一个文件,并输出文件内容:
  1. for line in open("myfile.txt"):
  2. print(line, end="")
这个代码的问题在于,执行完代码后,文件在一段不确定的时间内处于打开状态。在简单脚本中这没有问题,但对于较大的应用程序来说可能会出问题。with 语句支持以及时、正确的清理的方式使用文件对象:
  1. with open("myfile.txt") as f:
  2. for line in f:
  3. print(line, end="")
语句执行完毕后,即使在处理行时遇到问题,都会关闭文件 f。和文件一样,支持预定义清理操作的对象会在文档中指出这一点。

8.9. 引发和处理多个不相关的异常

在有些情况下,有必要报告几个已经发生的异常。这通常是在并发框架中当几个任务并行失败时的情况,但也有其他的用例,有时需要是继续执行并收集多个错误而不是引发第一个异常。 内置的 ExceptionGroup 打包了一个异常实例的列表,这样它们就可以一起被引发。它本身就是一个异常,所以它可以像其他异常一样被捕获。 >>>
  1. >>> def f():
  2. ... excs = [OSError('error 1'), SystemError('error 2')]
  3. ... raise ExceptionGroup('there were problems', excs)
  4. ...
  5. >>> f()
  6. + Exception Group Traceback (most recent call last):
  7. | File "<stdin>", line 1, in <module>
  8. | File "<stdin>", line 3, in f
  9. | ExceptionGroup: there were problems
  10. +-+---------------- 1 ----------------
  11. | OSError: error 1
  12. +---------------- 2 ----------------
  13. | SystemError: error 2
  14. +------------------------------------
  15. >>> try:
  16. ... f()
  17. ... except Exception as e:
  18. ... print(f'caught {type(e)}: e')
  19. ...
  20. caught <class 'ExceptionGroup'>: e
  21. >>>
通过使用 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">except*</font> 代替 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">except</font> ,我们可以有选择地只处理组中符合某种类型的异常。在下面的例子中,显示了一个嵌套的异常组,每个 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">except*</font> 子句都从组中提取了某种类型的异常,而让所有其他的异常传播到其他子句,并最终被重新引发。 >>>
  1. >>> def f():
  2. ... raise ExceptionGroup(
  3. ... "group1",
  4. ... [
  5. ... OSError(1),
  6. ... SystemError(2),
  7. ... ExceptionGroup(
  8. ... "group2",
  9. ... [
  10. ... OSError(3),
  11. ... RecursionError(4)
  12. ... ]
  13. ... )
  14. ... ]
  15. ... )
  16. ...
  17. >>> try:
  18. ... f()
  19. ... except* OSError as e:
  20. ... print("There were OSErrors")
  21. ... except* SystemError as e:
  22. ... print("There were SystemErrors")
  23. ...
  24. There were OSErrors
  25. There were SystemErrors
  26. + Exception Group Traceback (most recent call last):
  27. | File "<stdin>", line 2, in <module>
  28. | File "<stdin>", line 2, in f
  29. | ExceptionGroup: group1
  30. +-+---------------- 1 ----------------
  31. | ExceptionGroup: group2
  32. +-+---------------- 1 ----------------
  33. | RecursionError: 4
  34. +------------------------------------
  35. >>>
注意,嵌套在一个异常组中的异常必须是实例,而不是类型。这是因为在实践中,这些异常通常是那些已经被程序提出并捕获的异常,其模式如下: >>>
  1. >>> excs = []
  2. ... for test in tests:
  3. ... try:
  4. ... test.run()
  5. ... except Exception as e:
  6. ... excs.append(e)
  7. ...
  8. >>> if excs:
  9. ... raise ExceptionGroup("Test Failures", excs)
  10. ...

8.10. 用注释细化异常情况

当一个异常被创建以引发时,它通常被初始化为描述所发生错误的信息。在有些情况下,在异常被捕获后添加信息是很有用的。为了这个目的,异常有一个 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">add_note(note)</font> 方法接受一个字符串,并将其添加到异常的注释列表。标准的回溯在异常之后按照它们被添加的顺序呈现包括所有的注释。 >>>
  1. >>> try:
  2. ... raise TypeError('bad type')
  3. ... except Exception as e:
  4. ... e.add_note('Add some information')
  5. ... e.add_note('Add some more information')
  6. ... raise
  7. ...
  8. Traceback (most recent call last):
  9. File "<stdin>", line 2, in <module>
  10. TypeError: bad type
  11. Add some information
  12. Add some more information
  13. >>>
例如,当把异常收集到一个异常组时,我们可能想为各个错误添加上下文信息。在下文中,组中的每个异常都有一个说明,指出这个错误是什么时候发生的。 >>>
  1. >>> def f():
  2. ... raise OSError('operation failed')
  3. ...
  4. >>> excs = []
  5. >>> for i in range(3):
  6. ... try:
  7. ... f()
  8. ... except Exception as e:
  9. ... e.add_note(f'Happened in Iteration {i+1}')
  10. ... excs.append(e)
  11. ...
  12. >>> raise ExceptionGroup('We have some problems', excs)
  13. + Exception Group Traceback (most recent call last):
  14. | File "<stdin>", line 1, in <module>
  15. | ExceptionGroup: We have some problems (3 sub-exceptions)
  16. +-+---------------- 1 ----------------
  17. | Traceback (most recent call last):
  18. | File "<stdin>", line 3, in <module>
  19. | File "<stdin>", line 2, in f
  20. | OSError: operation failed
  21. | Happened in Iteration 1
  22. +---------------- 2 ----------------
  23. | Traceback (most recent call last):
  24. | File "<stdin>", line 3, in <module>
  25. | File "<stdin>", line 2, in f
  26. | OSError: operation failed
  27. | Happened in Iteration 2
  28. +---------------- 3 ----------------
  29. | Traceback (most recent call last):
  30. | File "<stdin>", line 3, in <module>
  31. | File "<stdin>", line 2, in f
  32. | OSError: operation failed
  33. | Happened in Iteration 3
  34. +------------------------------------
  35. >>>

15. Floating-Point Arithmetic: Issues and Limitations

浮点数在计算机硬件中是以基数为 2 (二进制) 的小数来表示的。 例如,十进制 小数 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">0.625</font> 的值为 6/10 + 2/100 + 5/1000,而同样的 二进制 小数 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">0.101</font> 的值为 1/2 + 0/4 + 1/8。 这两个小数具有相同的值,唯一的区别在于第一个写成了基数为 10 的小数形式,而第二个则写成的基数为 2 的小数形式。 不幸的是,大多数的十进制小数都不能精确地表示为二进制小数。这导致在大多数情况下,你输入的十进制浮点数都只能近似地以二进制浮点数形式储存在计算机中。 用十进制来理解这个问题显得更加容易一些。考虑分数 1/3 。我们可以得到它在十进制下的一个近似值
  1. 0.3
或者,更近似的,:
  1. 0.33
或者,更近似的,:
  1. 0.333
以此类推。结果是无论你写下多少的数字,它都永远不会等于 1/3 ,只是更加更加地接近 1/3 。 同样的道理,无论你使用多少位以 2 为基数的数码,十进制的 0.1 都无法精确地表示为一个以 2 为基数的小数。 在以 2 为基数的情况下, 1/10 是一个无限循环小数
  1. 0.0001100110011001100110011001100110011001100110011...
在任何一个位置停下,你都只能得到一个近似值。因此,在今天的大部分架构上,浮点数都只能近似地使用二进制小数表示,对应分数的分子使用每 8 字节的前 53 位表示,分母则表示为 2 的幂次。在 1/10 这个例子中,相应的二进制分数是 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">3602879701896397</font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);"> </font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">/</font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);"> </font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">2</font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);"> </font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">**</font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);"> </font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">55</font> ,它很接近 1/10 ,但并不是 1/10 。 由于值的显示方式大多数用户都不会意识到这个差异的存在。 Python 只会打印计算机中存储的二进制值的十进制近似值。 在大部分计算机中,如果 Python 要把 0.1 的二进制值对应的准确的十进制值打印出来,将会显示成这样: >>>
  1. >>> 0.1
  2. 0.1000000000000000055511151231257827021181583404541015625
这比大多数人认为有用的数位更多,因此 Python 通过改为显示舍入值来保留可管理的数位: >>>
  1. >>> 1 / 10
  2. 0.1
牢记,即使输出的结果看起来好像就是 1/10 的精确值,实际储存的值只是最接近 1/10 的计算机可表示的二进制分数。 有趣的是,有许多不同的十进制数共享相同的最接近的近似二进制小数。例如, <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">0.1</font> <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">0.10000000000000001</font> <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">0.1000000000000000055511151231257827021181583404541015625</font> 全都近似于 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">3602879701896397</font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);"> </font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">/</font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);"> </font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">2</font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);"> </font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">**</font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);"> </font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">55</font> 。由于所有这些十进制值都具有相同的近似值,因此可以显示其中任何一个,同时仍然保留不变的 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">eval(repr(x))</font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);"> </font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">==</font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);"> </font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">x</font> 在历史上,Python 提示符和内置的 repr() 函数会选择具有 17 位有效数字的来显示,即 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">0.10000000000000001</font>。 从 Python 3.1 开始,Python(在大多数系统上)现在能够选择这些表示中最短的并简单地显示 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">0.1</font> Note that this is in the very nature of binary floating point: this is not a bug in Python, and it is not a bug in your code either. You’ll see the same kind of thing in all languages that support your hardware’s floating-point arithmetic (although some languages may not display the difference by default, or in all output modes). 想要更美观的输出,你可能会希望使用字符串格式化来产生限定长度的有效位数: >>>
  1. >>> format(math.pi, '.12g') # give 12 significant digits
  2. '3.14159265359'
  3. >>> format(math.pi, '.2f') # give 2 digits after the point
  4. '3.14'
  5. >>> repr(math.pi)
  6. '3.141592653589793'
必须重点了解的是,这在实际上只是一个假象:你只是将真正的机器码值进行了舍入操作再 显示 而已。 一个假象还可能导致另一个假象。 例如,由于这个 0.1 并非真正的 1/10,将三个 0.1 的值相加也无法恰好得到 0.3: >>>
  1. >>> 0.1 + 0.1 + 0.1 == 0.3
  2. False
而且,由于这个 0.1 无法精确表示 1/10 而这个 0.3 也无法精确表示 3/10 的值,使用 round() 函数进行预先舍入也是没用的: >>>
  1. >>> round(0.1, 1) + round(0.1, 1) + round(0.1, 1) == round(0.3, 1)
  2. False
虽然这些数字无法精确表示其所要代表的实际值,但是可以使用 math.isclose() 函数来进行不精确的值比较: >>>
  1. >>> math.isclose(0.1 + 0.1 + 0.1, 0.3)
  2. True
或者,也可以使用 round() 函数来大致地比较近似程度: >>>
  1. >>> round(math.pi, ndigits=2) == round(22 / 7, ndigits=2)
  2. True
Binary floating-point arithmetic holds many surprises like this. The problem with “0.1” is explained in precise detail below, in the “Representation Error” section. See Examples of Floating Point Problems for a pleasant summary of how binary floating point works and the kinds of problems commonly encountered in practice. Also see The Perils of Floating Point for a more complete account of other common surprises. As that says near the end, “there are no easy answers.” Still, don’t be unduly wary of floating point! The errors in Python float operations are inherited from the floating-point hardware, and on most machines are on the order of no more than 1 part in 2**53 per operation. That’s more than adequate for most tasks, but you do need to keep in mind that it’s not decimal arithmetic and that every float operation can suffer a new rounding error. 虽然病态的情况确实存在,但对于大多数正常的浮点运算使用来说,你只需简单地将最终显示的结果舍入为你期望的十进制数值即可得到你期望的结果。 str() 通常已足够,对于更精度的控制可参看 格式字符串语法 str.format() 方法的格式描述符。 对于需要精确十进制表示的使用场景,请尝试使用 decimal 模块,该模块实现了适合会计应用和高精度应用的十进制运算。 另一种形式的精确运算由 fractions 模块提供支持,该模块实现了基于有理数的算术运算(因此可以精确表示像 1/3 这样的数值)。 如果你是浮点运算的重度用户那么你应当了解一下 NumPy 包以及由 SciPy 项目所提供的许多其他数学和统计运算包。 参见 <https://scipy.org>。 Python 还提供了一些工具可能在你 确实 想要知道一个浮点数的精确值的少数情况下提供帮助。 例如 float.as_integer_ratio() 方法会将浮点数值表示为一个分数: >>>
  1. >>> x = 3.14159
  2. >>> x.as_integer_ratio()
  3. (3537115888337719, 1125899906842624)
由于这个比值是精确的,它可以被用来无损地重建原始值: >>>
  1. >>> x == 3537115888337719 / 1125899906842624
  2. True

float.hex() 方法会以十六进制(以 16 为基数)来表示浮点数,同样能给出保存在你的计算机中的精确值:

>>>
  1. >>> x.hex()
  2. '0x1.921f9f01b866ep+1'
这种精确的十六进制表示形式可被用来精确地重建浮点数值: >>>
  1. >>> x == float.fromhex('0x1.921f9f01b866ep+1')
  2. True
由于这种表示法是精确的,它适用于跨越不同版本(平台无关)的 Python 移植数值,以及与支持相同格式的其他语言(例如 Java 和 C99)交换数据. 另一个有用的工具是 sum() 函数,它能够帮助减少求和过程中的精度损失。 它会在数值被添加到总计值的时候为中间舍入步骤使用扩展的精度。 这可以更好地保持总体精确度,使得错误不会积累到能够影响最终总计值的程度: >>>
  1. >>> 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 == 1.0
  2. False
  3. >>> sum([0.1] * 10) == 1.0
  4. True

math.fsum() 更进一步地会在数值被添加到总计值的时候跟踪“丢失的数位”以使得结果只执行一次舍入。 此函数要比 sum() 慢但在大量输入几乎相互抵销导致最终总计值接近零的少见场景中将会更为精确:

>>>
  1. >>> arr = [-0.10430216751806065, -266310978.67179024, 143401161448607.16,
  2. ... -143401161400469.7, 266262841.31058735, -0.003244936839808227]
  3. >>> float(sum(map(Fraction, arr))) # Exact summation with single rounding
  4. 8.042173697819788e-13
  5. >>> math.fsum(arr) # Single rounding
  6. 8.042173697819788e-13
  7. >>> sum(arr) # Multiple roundings in extended precision
  8. 8.042178034628478e-13
  9. >>> total = 0.0
  10. >>> for x in arr:
  11. ... total += x # Multiple roundings in standard precision
  12. ...
  13. >>> total # Straight addition has no correct digits!
  14. -0.0051575902860057365

15.1. 表示性错误

本小节将详细解释 “0.1” 的例子,并说明你可以怎样亲自对此类情况进行精确分析。 假定前提是已基本熟悉二进制浮点表示法。

表示性错误 是指某些(其实是大多数)十进制小数无法以二进制(以 2 为基数的计数制)精确表示这一事实造成的错误。 这就是为什么 Python(或者 Perl、C、C++、Java、Fortran 以及许多其他语言)经常不会显示你所期待的精确十进制数值的主要原因。

为什么会这样? 1/10 是无法用二进制小数精确表示的。 至少从 2000 年起,几乎所有机器都使用 IEEE 754 二进制浮点运算标准,而几乎所有系统平台都将 Python 浮点数映射为 IEEE 754 binary64 “双精度” 值。 IEEE 754 binary64 值包含 53 位精度,因此在输入时计算机会尽量将 0.1 转换为以 J/2**N 形式所能表示的最接近的小数,其中 J 为恰好包含 53 比特位的整数。 重新将
  1. 1 / 10 ~= J / (2**N)
写为
  1. J ~= 2**N / 10
并且由于 J 恰好有 53 位 (即 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">>=</font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);"> </font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">2**52</font> <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);"><</font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);"> </font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">2**53</font>),N 的最佳值为 56: >>>
  1. >>> 2**52 <= 2**56 // 10 < 2**53
  2. True
也就是说,56 是唯一能使 J 恰好有 53 位的 N 值。 这样 J 可能的最佳就是舍入之后的商: >>>
  1. >>> q, r = divmod(2**56, 10)
  2. >>> r
  3. 6
由于余数超于 10 的一半,所以最佳近似值可通过向上舍入获得: >>>
  1. >>> q+1
  2. 7205759403792794
因此在 IEEE 754 双精度下可能达到的 1/10 的最佳近似值为:
  1. 7205759403792794 / 2 ** 56
分子和分母都除以二则结果小数为:
  1. 3602879701896397 / 2 ** 55
请注意由于我们做了向上舍入,这个结果实际上略大于 1/10;如果我们没有向上舍入,则商将会略小于 1/10。 但无论如何它都不会是 精确的 1/10! 因此计算机永远不会 “看到” 1/10: 它实际看到的就是上面所给出的小数,即它能达到的最佳 IEEE 754 双精度近似值: >>>
  1. >>> 0.1 * 2 ** 55
  2. 3602879701896397.0
如果我们将该小数乘以 10**55,我们可以看到该值输出 55 个十进制数位: >>>
  1. >>> 3602879701896397 * 10 ** 55 // 2 ** 55
  2. 1000000000000000055511151231257827021181583404541015625
这意味着存储在计算机中的确切数字等于十进制数值 0.1000000000000000055511151231257827021181583404541015625。 许多语言(包括较旧版本的 Python)都不会显示这个完整的十进制数值,而是将结果舍入到 17 位有效数字: >>>
  1. >>> format(0.1, '.17f')
  2. '0.10000000000000001'

fractions decimal 模块使得这样的计算更为容易:

>>>
  1. >>> from decimal import Decimal
  2. >>> from fractions import Fraction
  3. >>> Fraction.from_float(0.1)
  4. Fraction(3602879701896397, 36028797018963968)
  5. >>> (0.1).as_integer_ratio()
  6. (3602879701896397, 36028797018963968)
  7. >>> Decimal.from_float(0.1)
  8. Decimal('0.1000000000000000055511151231257827021181583404541015625')
  9. >>> format(Decimal.from_float(0.1), '.17')
  10. '0.10000000000000001'

16. 附录

16.1. 交互模式

16.1.1. 错误处理

当发生错误时,解释器会打印错误消息和栈回溯。 在交互模式下,将返回到主提示符;当输入是来自文件的时候,它将在打印栈回溯之后退出并附带一个非零的退出状态码。 (由 try 语句中 except 子句所处理的异常在此上下文中不属于错误。) 有些错误属于无条件致命错误,会导致程序附带非零状态码退出;这适用于内部一致性丧失以及某些内存耗尽的情况等。 所有错误消息都将被写入到标准错误流;来自被执行命令的正常输出测会被写入到标准输出。 将中断字符(通常为 Control-C Delete )键入主要或辅助提示符会取消输入并返回主提示符。 [1] 在执行命令时键入中断引发的 KeyboardInterrupt 异常,可以由 try 语句处理。

16.1.2. 可执行的Python脚本

在 BSD 等类Unix系统上,Python 脚本可以像 shell 脚本一样直接执行,通过在第一行添加:
  1. #!/usr/bin/env python3
(假设解释器位于用户的 <font style="color:rgb(0, 0, 0);">PATH</font> )脚本的开头,并将文件设置为可执行。 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">#!</font> 必须是文件的前两个字符。在某些平台上,第一行必须以Unix样式的行结尾(<font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">'\n'</font>)结束,而不是以Windows(<font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">'\r\n'</font>)行结尾。注意,“散列字符”,或者说“磅字符”, <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">'#'</font> ,在Python中代表注释开始。 可以使用 chmod 命令为脚本提供可执行模式或权限。
  1. $ chmod +x myscript.py
在Windows系统上,没有“可执行模式”的概念。 Python安装程序自动将 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">.py</font> 文件与 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">python.exe</font> 相关联,这样双击Python文件就会将其作为脚本运行。扩展也可以是 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">.pyw</font> ,在这种情况下,会隐藏通常出现的控制台窗口。

16.1.3. 交互式启动文件

当您以交互模式使用 Python 时,您可能会希望在每次启动解释器时,解释器先执行几条您预先编写的命令,然后您再以交互模式继续使用。您可以通过将名为 PYTHONSTARTUP 的环境变量设置为包含启动命令的文件的文件名来实现。这类似于 Unix shell 的 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">.profile</font> 功能。 Python 只有在交互模式时,才会读取此文件,而非在从脚本读指令或是将 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">/dev/tty</font> 显式作为被运行的 Python 脚本的文件名时(后者反而表现得像一个交互式会话)。这个文件与交互式指令共享相同的命名空间,所以它定义或导入的对象可以在交互式会话中直接使用。您也可以在该文件中更改提示符 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">sys.ps1</font> <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">sys.ps2</font> 如果您想 从当前目录中 读取一个额外的启动文件,您可以在上文所说的全局启动文件中编写像 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">if</font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);"> </font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">os.path.isfile('.pythonrc.py'):</font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);"> </font><font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">exec(open('.pythonrc.py').read())</font> 这样的代码。如果要在脚本中使用启动文件,则必须在脚本中显式执行此操作:
  1. import os
  2. filename = os.environ.get('PYTHONSTARTUP')
  3. if filename and os.path.isfile(filename):
  4. with open(filename) as fobj:
  5. startup_file = fobj.read()
  6. exec(startup_file)

16.1.4. 定制模块

Python 提供了两个钩子供你进行自定义: sitecustomize 和 usercustomize。 要了解它是如何工作的,首先需要找到用户 site-packages 目录的位置。 启动 Python 并运行以下代码: >>>
  1. >>> import site
  2. >>> site.getusersitepackages()
  3. '/home/user/.local/lib/python3.x/site-packages'
现在,您可以在该目录中创建一个名为 <font style="color:rgb(0, 0, 0);background-color:rgb(236, 240, 243);">usercustomize.py</font> 的文件,并将所需内容放入其中。它会影响Python的每次启动,除非它以 -s 选项启动,以禁用自动导入。 sitecustomize 的工作方式相同,但通常由计算机管理员在全局 site-packages 目录中创建,并在 usercustomize 之前导入。 更多细节请参阅 site 模块的文档。

备注

[1] GNU Readline 包的问题可能会阻止这种情况。