近期我们将 rb 库做了升级以支持 Python3.7,考虑到原本的 rb 库并不支持 Python3,所以我们这一次没有像上一次更改包库的调用名字,只需要简单的一步安装,就能得到在 Python3.7 下正常工作的 rb 库了。

pip install rb3

目前已知的存在的问题可以在 issue 列表里看到:issues

在升级过程中,遇到了一些问题,这些问题中最大的是 Python3 与 Python2 的 crc32 计算结果存在不一致,本篇文章对此做个总结。

crc32 与 Python3 一致

如果想让 Python2 的 crc32 计算结果同 Python3 的保持一致,很简单,在 Python 的文档中直接提供了方法,只需对计算结果增加 & 0xffffffff 运算即可:

>>> from binascii import crc32
>>> data = 'foo'   # Py2
>>> data = b'foo'  # Py3
>>> crc32(data) & 0xffffffff
2356372769

crc32 与 Python2 一致

在官方的文档里并没有直接提供让 Python3 的计算结果同 Python2 保持一致的方法,但是在 Python 的 Issue 里,发现 2007 年有人提了这个问题:Issue 1202: zlib.crc32() and adler32() return value - Python tracker,Guido van Rossum 提供了一个解决方案,稍微麻烦了一些:

>>> from binascii import crc32
>>> # under Python3
>>> x = crc32(data)
>>> x - ((x & 0x80000000) <<1)
-1938594527

原理

本节预备知识如下:

  • 原码、补码与反码
  • 位运算
  • 16 进制

我们以从 Python3 得到 Python2 的结果为例子说明,下面是本节在 Python3 下使用的代码:

>>> from binascii import crc32
>>> x = crc32(b'foo')
>>> x
2356372769
>>> 0x80000000
2147483648
>>> format(0x80000000, 'b')
'10000000000000000000000000000000'
>>> format(x, 'b')
10001100011100110110010100100001'
>>> format((x & 0x80000000), 'b')
'10000000000000000000000000000000'  # 2147483648
>>> format((x & 0x80000000) << 1, 'b')
'100000000000000000000000000000000'  # 4294967296

先来看 (x & 0x80000000) 在做些什么。& 符号告诉我们这是一个在二进制层面做 AND 运算的表达式。所以数字应当从二进制的角度看。0x80000000 是一个 16 进制的数字,转换成二进制是 10000000000000000000000000000000,x 的值是 10001100011100110110010100100001,根据 AND 运算符的工作方式(只在输入都为真的情况输出真,否则输出假),得到结果为 10000000000000000000000000000000

然后通过 << 运算符,将二进制数字左移两位,即添加两个 0,得到 100000000000000000000000000000000,换成 10 进制则为 4294967296,等价于乘以了 2 的一次方。其中我们不难验证计算结果减去的值 (x & 0x80000000) << 1 等价于 ( 2^{31} \cdot 2 ):

>>> 2**31*2 == (x & 0x80000000) << 1
True

那么究竟发生了什么?首先明确一个变化:

  • Python2.6, Python2.7 的 crc32 的值域是 $ [-2^{31}, 2^{31}-1] $
  • Python3.7 的 crc32 的值域是 $ [0, 2^{32}-1] $

实际上代码的目的是将 Python3 计算结果中的 $ [2^{31}, 2^{32}-1] $ 之间的值映射到 Python2.7 的值域的左半部分上,其余保持不变。这里最巧妙的判断就是没有依赖于 $ 2^{31} $ 的十进制数字,而是在二进制层面做运算实现的转换。

结语

在 Python 文档中,一直有这么一段话:

Compute CRC-32, the 32-bit checksum of data, starting with an initial CRC of value. The default initial CRC is zero. The algorithm is consistent with the ZIP file checksum. Since the algorithm is designed for use as a checksum algorithm, it is not suitable for use as a general hash algorithm.

不知道为什么 rb 的开发者 getsentry 团队在 Python 文档一直说 crc32 不适合做通用哈希值算法的情况下依旧使用 crc32 算法作为选择站点策略的算法。

无论如何,短期之内我们仍将 crc32 算法作为默认的站点选择算法,但是我们考虑在未来用一个更稳定的算法来取代它。

参考资料