楔子
通过考察整数的底层实现,我们明白了它能够保证不溢出的缘由。整数的值在底层是由 C 数组来维护的,通过组合多个 digit(uint32_t)来实现大整数的存储。这么做的好处就是 Python 整数没有大小限制了,因此不会溢出,而不会溢出是因为数组没有长度限制,所以只要你的内存足够,就可以存任意大的数。
Python 表示:存不下?会溢出?这都不是事儿,直接继续往数组里面塞 digit 就 ok 了。另外数组存的是绝对值,符号是通过 lv_tag 体现的。
不过说实话,用数组实现大整数的做法非常普遍,并没有什么神秘的,就是将多个整数组合起来,模拟具有更高位的大整数。但这种实现方式的难点在于大整数的数学运算,它们才是重点,实现的时候比较考验编程技巧以及思维逻辑。
因此 Python 的整数比浮点数要复杂的多,浮点数在底层直接用 C 的 double 来维护具体的值,因此运算的话,比如相加,直接转成 C 的 double 做加法即可。但整数就不行了,在底层不能简单地将数组相加。
下面来看看具体实现。
整数的大小比较
先来看看整数的大小比较,由于整数具体的值是通过数组维护的,显然数组越长,整数的绝对值就越大,这是毫无疑问的。至于整数的正负,则由 lv_tag 来体现。
如果整数大于 0,那么 lv_tag 等于 ob_digit 长度 << 3;
如果整数小于 0,那么 lv_tag 等于 (ob_digit 长度 << 3) | 2;
如果整数等于 0,那么 lv_tag 等于 ob_digit 长度,显然此时两者都是 1;
所以,如果整数的 lv_tag 和 3 按位与之后结果为 0,那么是正数;如果和 3 按位与之后结果为 2,那么是负数;如果和 3 按位与之后结果为 1,那么等于 0。
因此可以得出一个结论:当两个整数在比大小时,可以先比较各自的 lv_tag。如果 lv_tag 不一样,那么可以直接比较出整数的大小。
但如果两个整数的 lv_tag 一样,那么就从数组 ob_digit 的尾部元素开始,不断向前进行比较。只要两个整数的 ob_digit 中有一个对应元素不相同,那么就可以比较出大小。
之所以从数组的尾部开始,是因为数组元素的索引越大,那么充当的位数就越高,而在比较的时候显然要从高位开始比。
# ob_digit = [892311, 32, 3]
a1 = 3458764548181171607
# ob_digit = [2296, 31, 3]
a2 = 3458764547106539768
我们以 a1 和 a2 为例,显然 a1 大于 a2,那么在底层,它们是如何比较的呢?
如果 lv_tag 不相等,那么可以直接比较出大小。但这里两个整数的 lv_tag 是相等的,所以需要比较 ob_digit,并且是从后往前比。具体做法就是让索引从 ob_digit 长度减 1 开始,依次往前遍历。
因为 a->long_value.ob_digit[2] 等于 b->long_value.ob_digit[2],此时无法比较出大小,因此索引减一,比较上一个元素。
因为 a->long_value.ob_digit[1] 大于 b->long_value.ob_digit[1],所以成功比较出大小,可以得出 |a1| 大于 |a2|。
当然啦,由于数组反映的是绝对值的大小,所以还需要判断符号。如果是正数,那么和绝对值相同;但如果是负数,那么绝对值越大,对应的整数反而越小,因此比较之后的结果还要再乘上 -1。
from ctypes import *
class PyLongObject(Structure):
_fields_ = [
("ob_refcnt", c_ssize_t),
("ob_type", c_void_p),
("lv_tag", c_ssize_t),
("ob_digit", c_uint32 * 3)
]
a1 = 3458764548181171607
a2 = 3458764547106539768
long_obj1 = PyLongObject.from_address(id(a1))
long_obj2 = PyLongObject.from_address(id(a2))
print(list(long_obj1.ob_digit)) # [892311, 32, 3]
print(list(long_obj2.ob_digit)) # [2296, 31, 3]
以上就是整数的大小比较逻辑,下面再看一下具体的源码实现。