欢迎来到 2019 🎈,新年第一天我们来聊下,python 中那些看似平常却容易掉进去的陷阱

陷阱很多,所以本文会长期更新的。

1 float 转 str 类型

最近在写开源项目 cn2an(它是一个快速转化「中文数字」和「阿拉伯数字」的工具包)时, 碰到了这样一个问题,单元测试中的self.assertEqual(str(0.00005), "0.00005")居然出错了,难道是 str(0.00005) 和 “0.00005” 不相等? 我怀着诡异的心情分别打印了这两个参数。

>>> str(0.00005)
'5e-05'
>>> "0.00005"
'0.00005'
>>> str(0.00005) == "0.00005"
False

原来是浮点数的小数点后,零的数量大于等于 4 时,python 会自动把它转化成科学计数表示,这样导致了我使用 str() 函数进行转换时,不能得到想要的结果。

我又测试了一些其他情况,发现当小数点后的零的数量小于 4 时,不会有这个问题。

>>> str(0.0005)
'0.0005'
>>> "0.0005"
'0.0005'
>>> str(0.0005) == "0.0005"
True

那如何解决它呢?我先是网上检索了一波,发现给出的方案并不能 work。

>>> import numpy as np
>>> np.set_printoptions(suppress=True)
>>> print(0.00005)
5e-05

而且这个方案还要引入一个叫numpy的第三方包,这对完全不依赖 numpy 中任何方法的代码来说,代价太大了! 所以不管能不能 work,我都不会采用这个方案的。

既然找不到办法就只能自己动手解决了。我尝试直接把科学计数法的还原回去,发现可以成功转化。下面是代码👇:

>>> def convert_number_to_string(number_data):
...     string_data = str(number_data)
...     if "e" in string_data:
...         string_data_list = string_data.split("e")
...         string_key = string_data_list[0]
...         string_value = string_data_list[1]
...         if string_value[0] == "-":
...             string_data = "0." + "0"*(int(string_value[1:])-1) + string_key
...         else:
...             string_data = string_key + "0"*int(string_value)
...     return string_data
...
>>> convert_number_to_string(0.00005) == "0.00005"
True

搞定!如果你有更好的方法,欢迎发邮件和我交流。

2 float 类型有效精度

不知道你有没有碰到过,计算结果与期望不一致的情况,比如下面这两种:

>>> "{:.30f}".format(1/3)
'0.333333333333333314829616256247'

>>> 1.1 + 2.2
3.3000000000000003

是不是看起来有点奇怪,似乎前面的数字还对,后面的就出错了。这是因为 python 的 float 类型的绝对有效精度只有 15 位。


NOTE: 下面是选读内容,可跳过。

为什么只有 15 位绝对有效精度呢?

这个就要说浮点数在机器中的表示方式了。python 的 float 类型相当于其他语言的 double 类型,在 64 位的机器中占 8 个字节,可以用 64 位二进制描述。 它的组成如下:

float 类型(64位) = 符号(1位) + 指数(11位)+ 尾数(52位)

  • 符号:表示浮点数的正负;
  • 指数:表示指数的有效数字,可以界定出浮点数的取值范围;
  • 尾数:表示数字的有效数字,可以界定出浮点数的有效精度。

你可以很容易的看出,二进制尾数的所能表示的十进制数字的上限,就是浮点数的有效精度上限。

>>> 2**52
4503599627370496
>>> len(str(2**52))
16

通过计算得出,尾数能够表示的最大十进制数字的长度为 16 位,这意味着最多能有 16 位有效精度,但绝对能保证的有效精度只有 15 位。


在正式动手解决这个问题之前,我先仔细想了一下。

首先这种问题应该不会只有我碰到,另外也隐隐感觉到,这种问题官方应该会给出方案的。于是便找到了decimal这个库,能够完美解决这个问题。

Decimal numbers can be represented exactly. Unlike hardware based binary floating point, the decimal module has a user alterable precision (defaulting to 28 places) which can be as large as needed for a given problem.

decimal 可以准确表示十进制数字,并且用户可以随意更改的精度大小。

>>> from decimal import Decimal

>>> Decimal("1")/Decimal("3")
Decimal('0.3333333333333333333333333333')

>>> Decimal("1.1")+Decimal("2.2")
Decimal('3.3')

# decimal 的默认精度为 28,可以通过 getcontext 控制
>>> from decimal import getcontext
>>> getcontext().prec
28

# 这里精度可以根据需要进行增加或者减少,比如设置成 2
>>> getcontext().prec = 2

>>> Decimal("1")/Decimal("3")
Decimal('0.33')

>>> Decimal("1.1")+Decimal("2.2")
Decimal('3.3')

3 隐藏在 bool 函数中的细节

在 python 的 bool 函数有一个特性,任何字符串都会被转化成 True

>>> bool("False")
True

这会带来什么问题呢?在使用命令行解析参数模块argparse时就会出现意想不到错误。

看下面这样一个例子:

# test.py
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--is_debug", type=bool)
args = parser.parse_args()
print(args)

接着我传递一个False参数做下测试。

python test.py --is_debug False
# 输出:
# Namespace(is_debug=True)

而程序解析到的参数居然是True

如何解决呢?只需要自定义一个 bool 函数就可以了。 (如果你其他地方有用到默认的 bool 函数,这里最好换个名字,比如叫 new_bool )

# test.py
import argparse

def bool(arg):
    arg = str(arg).lower()
    if arg == "true":
        return True
    elif arg == "false":
        return False
    else:
        raise ValueError

parser = argparse.ArgumentParser()
parser.add_argument("--is_debug", type=bool)
args = parser.parse_args()
print(args)

好了,我们再来做一下测试。

python test.py --is_debug False
# 输出:
# Namespace(is_debug=False)

希望上文能够帮你直接跳过这些陷阱,新年快乐鸭🎉

参考