• 5. 解密Python中的整数在底层是如何实现的,以及为什么Python中大整数的运算不会溢出


    楔子

    这次我们来分析一下Python中的整数是如何实现的,我们知道Python中的整数是不会溢出的,换句话说,它可以计算无穷大的数。只要你的内存足够,它就能计算,但是对于C来说显然是不行的,可Python底层又是C实现的,那么它是怎么做到整数不会溢出的呢?

    既然想知道答案,那么看一下Python中的整型在底层是怎么定义的就行了。

    int实例对象的底层实现

    Python中的整数底层对应的结构体是PyLongObject,它位于longobject.h中。

    //longobject.h
    typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */
    
    //longintrepr.h
    struct _longobject {
        PyObject_VAR_HEAD
        digit ob_digit[1];
    };
    
    //合起来可以看成
    typedef struct {
        PyObject_VAR_HEAD
        digit ob_digit[1];
    } PyLongObject;
    
    //如果把这个PyLongObject更细致的展开一下就是
    typedef struct {
        Py_ssize_t ob_refcnt; //引用计数
        struct _typeobject *ob_type; //类型
        Py_ssize_t ob_size; //维护的元素个数
        digit ob_digit[1]; //digit类型的数组,长度为1
    } PyLongObject;
    

    别的先不说,就冲里面的ob_size我们就可以思考一番。首先Python中的整数有大小、但应该没有长度的概念吧,那为什么会有一个ob_size呢?从结构体成员来看,这个ob_size指的应该就是ob_digit数组的长度,而这个ob_digit数组显然只能是用来维护具体的值了。而数组的长度不同,那么对应的整数占用的内存也不同。所以答案出来了,整数虽然没有我们生活中的那种长度的概念,但它是个变长对象,因为不同的整数占用的内存可能是不一样的。

    因此这个ob_size它指的是底层数组的长度,因为Python中整数对应的值在底层是使用数组来存储的。尽管它没有字符串、列表那种长度的概念,或者说无法对整型使用len方法,但它是个变长对象。

    那么下面的重点就在这个ob_digit数组了,我们要从它的身上挖掘信息,看看Python中整数对应的值(比如123),是怎么放在这个数组里面的。不过首先我们要看看这个digit是个什么类型,它同样定义在longintrepr.h中

    //PYLONG_BITS_IN_DIGIT是一个宏,如果你的机器是64位的,那么它会被定义为30,32位机器则会被定义为15
    //至于这个宏是做什么的我们先不管
    #if PYLONG_BITS_IN_DIGIT == 30
    typedef uint32_t digit;
    // ...
    #elif PYLONG_BITS_IN_DIGIT == 15
    typedef unsigned short digit;
    // ...
    #endif
    

    而我们的机器现在基本上都是64位的,所以PYLONG_BITS_IN_DIGIT会等于30,因为digit等价于uint32_t(unsigned int),所以它是一个无符号32位整型。

    所以ob_digit这个数组是一个无符号32位整型数组,长度为1。当然这个数组具体多长则取决于你要存储的Python整数有多大,因为C中数组的长度不属于类型信息,你可以看成是长度n,而这个n是多少要取决于你的整数大小。显然整数越大,这个数组就越长,那么占用空间就越大。

    搞清楚了PyLongObject里面的所有成员,那么下面我们就来分析ob_digit是怎么存储Python中的整数,以及Python中的整数为什么不会溢出。

    不过说实话,关于Python的整数不会溢出这个问题,其实相信很多人已经有答案了,因为底层是使用数组存储的嘛,而数组的长度又没有限制,所以当然不会溢出啦。

    另外,还存在一个问题,那就是digit是一个无符号32位整型,那负数怎么存储?别着急,我们会举栗说明,将上面的疑问一一解答。

    首先如果你是Python的设计者,要保证整数不会溢出,你会怎么办?我们把问题简化一下,假设有一个8位的无符号整数类型,我们知道它能表示的最大数字是255,但这时候如果我想表示256,要怎么办?

    可能有人会想,那用两个数来存储不就好了。一个存储255,一个存储1,将这两个数放在数组里面。这个答案的话,虽然有些接近,但其实还有很大偏差:那就是我们并不能简单地按照大小拆分的,256拆分为255和1,要是265就拆分成255和10,而是要通过二进制的方式,我们来简单看一下。

    我们知道8位整数最大就是 2 ^ 8 - 1,也就是它的八位全部都是1,结果是255
    所以255对应的数组就是: [255], 因为此时一个8位整数就能存下
    
    但如果是256,那么8位显然存不下了,此时就还需要一个位
    所以这个时候会使用两个8位整数, 但并不是简单的相加, 而是使用一个新的8位整数来模拟更高的位
    

    而Python底层也是类似这种做法,但是考虑的会更加全面。下面就以Python中的整数为例,看看底层数组的存储方式。

    整数0:

    注意:当要表示的整数为0时,ob_digit这个数组为空,不存储任何值,ob_size为0,表示这个整数的值为0,这是一种特殊情况。

    整数1:

    当然存储的值为1时,ob_size的值就是1,此时ob_digit数组就是[1]。

    整数-1:

    我们看到ob_digit数组没有变化,但是ob_size变成了-1,没错,整数的正负号是通过这里的ob_size决定的。ob_digit存储的其实是绝对值,无论n取多少,-nn对应的ob_digit是完全一致的,但是ob_size则互为相反数。所以ob_size除了表示数组的长度之外,还可以表示对应整数的正负。

    所以我们之前说整数越大,底层的数组就越长。更准确的说是绝对值越大,底层数组就越长。所以Python在比较两个整型的大小时,会先比较ob_size,如果ob_size不一样则可以直接比较出大小来。显然ob_size越大,对应的整数越大,不管ob_size是正是负,都符合这个结论,可以想一下。

    整数2 ^ 30 -1:

    如果想表示2 ^30 - 1(^这里代指幂运算,当然对于Python程序猿来说两个星号也是幂运算,表达的意义是一样的),那么也可以使用一个digit表示。虽然如此,但为什么突然举2 ^ 30 - 1这个数字呢?答案是,虽然digit是4字节、32位,但是Python只用30个位。

    之所以这么做是和加法进位有关系,如果32个位全部用来存储其绝对值,那么相加产生进位的时候,可能会溢出,比如有一个将32个位全部占满的整数(2 ^ 32 - 1),即便它只加上1,也会溢出。这个时候为了解决这个问题,就需要先强制转换为64位再进行运算。

    但如果只用30个位的话,那么加法是不会溢出的,或者说相加之后依旧可以用32位整数保存。因为30个位最大就是2 ^ 30 - 1,即便两个这样的值相加,结果也是(2 ^ 30 - 1) * 2,即:2 ^ 31 - 2。而32个位的话最大是2 ^ 32 - 1,所以肯定不会溢出的;如果一开始30个位就存不下,那么数组中会有两个digit。

    所以虽然将32位全部用完,可以只用一个digit表示更多、更大的整数,但是可能面临相加之后一个digit存不下的情况,于是只用30个位,如果数值大到30个位存不下的话,那么就会多使用一个digit。可能有人发现了,如果是用31个位的话,那么相加产生的最大值就是2 ^ 32 - 2,结果依旧可以使用一个32位整型存储啊,那Python为啥要牺牲两个位呢?答案是没有为什么。

    //还记得这个宏吗?PYLONG_BITS_IN_DIGIT指的就是Python使用digit的位数
    //我们看到在32位机器上, digit相当于2字节、16位的整型,而它用了15位,确实只牺牲了一个位
    //64位机器上则牺牲两个位, 为什么这么做, 其实没有什么原因
    //或许64 32 和 30 15之间都正好是两倍的关系, 如果是31看起来不和谐吧(雾)
    #if PYLONG_BITS_IN_DIGIT == 30
    typedef uint32_t digit;
    // ...
    #elif PYLONG_BITS_IN_DIGIT == 15
    typedef unsigned short digit;
    // ...
    #endif
    

    整数2 ^ 30:

    问题来了,我们说digit只用30位,所以2 ^ 30 - 1是一个digit能存储的最大值,那么现在是2 ^ 30,所以数组中就要有两个digit了。

    我们看到此时就用两个digit来存储了,此时的数组里面的元素就是0和1,而且充当高位的放在后面,因为我们说了使用新的digit来模拟更高的位。由于一个digit只用30位,那么数组中第一个digit的最低位就是1,第二个digit的最低位就是31,第三个digit的最低位就是61,以此类推,所以如果ob_digit为[a, b, c],那么对应的整数就为: a * 2 ** 0 + b * 2 ** 30 + c * 2 ** 60,如果ob_digit不止3个,那么就按照30个位往上加,比如ob_digit还有第四个元素d,那么就再加上d * 2 ** 90即可。

    再比如我们反推一下,如果a = 88888888888,那么底层数组ob_digit中的值是多少?

    import numpy as np
    
    a = 88888888888
    # 我们说1个digit用30个位, 那么n个digit所能表示的最大整数就是2 ** (30 * n) - 1, 至于原因的话其实很好理解,但我们还是可以严格推导一下
    # 我们以n = 2为例, 显然两个digit最高能表示 (2 ** 30 - 1) + (2 ** 30 - 1) * 2 ** 30,
    # 它等于 (2 ** 30 - 1) + (2 ** 60 - 2 ** 30) = 2 ** 60 - 1, 因此两个digit最大可以表示 2 ** 60 - 1
    # 同理你可以n取3, 看看(2 ** 30 - 1) + (2 ** 30 - 1) * 2 ** 30 + (2 ** 30 - 1) * 2 ** 60是不是等于2 ** 90 - 1
    # 或者试试更大的数, 结论都是成立的
    print(np.log2(a))  # 36.37128404230425
    # 36超过了30个位、但小于90个位, 因此需要两个digit
    
    # 我们说 "整数 = ob_digit[0] + ob_digit[1] * 2 ** 30 + ob_digit[2] * 2 ** 60 + ..."
    # 但是对于ob_digit长度为2的情况下, 这里的a = ob_digit[0] + ob_digit[1] * 2 ** 30
    print(a // 2 ** 30)  # 82
    print(a - 82 * 2 ** 30)  # 842059320
    
    # 说明此时底层对应的ob_digit数组就是[842059320, 82]
    

    我们修改解释器源代码重新编译,通过在创建整数的时候打印ob_digit里面的元素的值,也印证了这个结论。

    这个时候,我们可以分析整数所占的字节了。相信所有人都知道可以使用sys.getsizeof计算大小,但是这大小到底是怎么来的,估计会一头雾水。因为Python中对象的大小,是根据底层的结构体计算出来的。

    我们说ob_refcnt、ob_type、ob_size这三个是整数所必备的,它们都是8字节,加起来24字节。所以任何一个整数所占内存都至少24字节,至于具体占多少,则取决于ob_digit里面的元素都多少个。

    因此Python中整数所占内存 = 24 + 4 * ob_digit数组长度

    import sys
    
    # 如果是0的话, ob_digit数组为空, 所以此时就是24字节
    print(sys.getsizeof(0))  # 24
    
    # 如果是1的话, ob_digit数组有一个元素, 所以此时是24 + 4 = 28字节
    print(sys.getsizeof(1))  # 28
    print(sys.getsizeof(2 ** 30 - 1))  # 28
    
    # 一个digit只用30位, 所以最大能表示2 ** 30 - 1
    # 如果是2 ** 30, 那么就需要两个元素, 所以是24 + 4 * 2 = 32字节
    # 如果是两个digit, 那么能表示的最大整数就是2 ** 60 - 1
    print(sys.getsizeof(2 ** 30))  # 32
    print(sys.getsizeof(2 ** 60 - 1))  # 32
    
    
    """
    相信下面的不需要解释了
    """
    print(sys.getsizeof(1 << 60))  # 36
    print(sys.getsizeof((1 << 90) - 1))  # 36
    
    print(sys.getsizeof(1 << 90))  # 40
    

    小整数对象池

    由于分析过了浮点数以及浮点类型对象,因此int类型对象的实现以及int实例对象的创建啥的就不说了,可以自己去源码中查看,我们后面会着重介绍它的一些操作。还是那句话,Python中的API设计的很优美,都非常的相似,比如创建浮点数可以使用PyFloat_FromDouble、PyFloat_FromString等等,那么创建整数也可以使用PyLong_FromLong、PyLong_FromDouble、PyLong_FromString等等,直接去Objects中对应的源文件中查看即可。

    这里说一下Python中的小整数对象池,我们知道Python中的整数属于不可变对象,运算之后会创建新的对象。

    >>> a = 666
    >>> id(a)
    2431274354736
    >>> a += 1
    >>> id(a)
    2431274355024
    >>>
    

    所以这种做法就势必会有性能缺陷,因为程序运行时会有大量对象的创建和销毁。根据浮点数的经验,我们猜测Python应该也对整数使用了缓存池吧。答案是差不多,只不过不是缓存池,而是小整数对象池。

    Python将那些使用频率高的整数预先创建好,而且都是单例模式,这些预先创建好的整数会放在一个静态数组里面,我们称为小整数对象池。如果需要使用的话会直接拿来用,而不用重新创建。注意:这些整数在Python解释器启动的时候,就已经创建了。

    所以这种做法就势必会有性能缺陷,因为程序运行时会有大量对象的创建和销毁。根据浮点数的经验,我们猜测Python应该也对整数使用了缓存池吧。答案是差不多,只不过不是缓存池,而是小整数对象池。小整数对象池的实现位于longobject.c中。

    #ifndef NSMALLPOSINTS
    #define NSMALLPOSINTS           257
    #endif
    #ifndef NSMALLNEGINTS
    #define NSMALLNEGINTS           5
    #endif
    
    static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
    
    • NSMALLPOSINTS宏规定了对象池中正数的个数 (从 0 开始,包括 0 ),默认 257 个;
    • NSMALLNEGINTS宏规定了对象池中负数的个数,默认5个;
    • small_ints是一个整数对象数组,保存预先创建好的小整数对象;

    以默认配置为例,Python解释器在启动的时候就会预先创建一个可以容纳232个整数的数组,并会依次初始化 -5 到 256(包括两端)之间的232个PyLongObject。所以小整数对象池的结构如下:

    但是为什么要实现缓存从-5到256之间的整数呢?因为Python认为这个范围内的整数使用频率最高,而缓存这些整数的内存相对可控。因此这只是某种权衡,很多程序的开发场景都没有固定的正确答案,需要根据实际情况来权衡利弊。

    >>> a = 256
    >>> b = 256
    >>> id(a), id(b)
    (140714000246400, 140714000246400)
    >>>
    >>> a = 257
    >>> b = 257
    >>> id(a), id(b)
    (2431274355184, 2431274354896)
    >>>
    

    256位于小整数对象池内,所以全局唯一,需要使用的话直接去取即可,因此它们的地址是一样的。但是257不再小整数对象池内,所以它们的地址不一样。

    我们上面是在交互式下演示的,但如果有小伙伴不是通过交互式的话,那么会得到出乎意料的结果。

    a = 257
    b = 257
    print(id(a) == id(b))  # True
    

    可能有人会好奇,为什么地址又是一样的了,257明明不在小整数对象池中啊。虽然涉及到了后面的内容,但是提前解释一下也是可以的。主要区别就在于一个是在交互式下执行的,另一个是通过 python3  xxx.py的方式执行的。

    首先Python的编译单元是函数,每个函数都有自己的作用域,在这个作用域中出现的所有常量都是唯一的,并且都位于常量池中,由co_consts指向。虽然我们上面的不是函数,而是在全局作用域中,但是全局你也可以看成是一个函数,它也是一个独立的编译单元。同一个编译单元中,常量只会出现一次。

    当a = 257的时候,会创建257这个整数、并放入常量池中;所以b = 257的时候就不会再创建了,因为常量池中已经有了,所以会直接从常量池中获取,因此它们的地址是一样的,因为是同一个PyLongObject。

    # Python3.6下执行, 该系列的所有代码都是基于Python3.8, 但是这里先使用Python3.6, 至于原因, 后面会说
    def f1():
        a = 256
        b = 257
        return id(a), id(b)
    
    
    def f2():
        a = 256
        b = 257
        return id(a), id(b)
    
    
    print(f1())  # (140042202371968, 140042204149712)
    print(f2())  # (140042202371968, 140042204255024)
    

    此时f1和f2显然是两个独立的编译单元,256属于小整数对象池中的整数、全局唯一,因此即便不在同一个编译单元的常量池中,它的地址也是唯一的,因为它是预先定义好的,所以直接拿来用。但是257显然不是小整数对象池中的整数,而且不在同一个编译单元的常量池中,所以地址是不一样的。

    而对于交互式环境来说,因为我们输入一行代码就会立即执行一行,所以任何一行可独立执行的代码都是一个独立的编译单元。注意:是可独立执行的代码,比如变量赋值、函数、方法调用等等;但如果是if、for、while、def等等需要多行表示的话,比如:if 2 > 1:,显然这就不是一行可独立执行的代码,它还依赖你输入的下面的内容。

    >>> if 2 > 1:  # 此时按下回车,我们看到不再是>>>, 而是..., 代表还没有结束, 还需要你下面的内容
    ...     print("2 > 1")
    ...  # 此时这个if语句整体才是一个独立的编译单元
    2 > 1
    >>>
    

    但是像a = 1、foo()、lst.appned(123)这些显然它们是一行可独立执行的代码,因此在交互式中它们是独立的编译单元。

    >>> a = 257  # 此时这行代码已经执行了,它是一个独立的编译单元
    >>> b = 257  # 这行代码也是独立的编译单元,所以它里面的常量池为空,因此要重新创建
    >>> id(a), id(b)  # 由于它们是不同常量池内的整数,所以id是不一样的。
    (2431274355184, 2431274354896)
    

    但是问题来了,看看下面的代码,a和b的地址为啥又一样了呢?666和777明显也不在常量池中啊。

    >>> a = 666;b=666
    >>> id(a), id(b)
    (2431274354896, 2431274354896)
    >>> a, b = 777, 777
    >>> id(a), id(b)
    (2431274354800, 2431274354800)
    >>>
    

    显然此时应该已经猜到原因了,因为上面两种方式无论哪一种,都是在同一行,因此整体会作为一个编译单元,所以地址是一样的。

    def f1():
        a = 256
        b = 2 ** 30
        return id(a), id(b)
    
    
    def f2():
        a = 256
        b = 2 ** 30
        return id(a), id(b)
    
    
    print(f1())  # (140714000246400, 2355781138896)
    print(f2())  # (140714000246400, 2355781138896)
    

    但是在Python3.8中,发现即便是大整数、并且不再同一个编译单元的常量池中,它们的地址也是一样的,说明Python在3.8版本的时候做了优化。

    另外,如果没有特殊说明,那么我们这个系列的所有代码都是在Python3.8下执行的。说实话,我就是因为发现在Python3.8中,打印的地址都是一样的,才在上面试了一下Python3.6。但是Python3.8中具体是怎么优化的,这里就暂时不讨论了(明明是你没有仔细研究)

    整数运算

    整数溢出是程序开发中一大难题,由此引发的 BUG 不计其数,而且相当隐蔽。之前使用golang刷LeetCode的时候,怎么也通不过,最后发现是因为LeetCode后台有一个测试用例比较特殊,导致整数太大,golang中的int64存不下。而Python 选择从语言层面彻底解决这个痛点,殚心竭虑地设计了整数对象。而我们也探索了整数对象,并初步掌握了整数对象的内部结构。

    Python中的整数是串联了多个C中的digit(uint32_t),通过一个C数组的形式来实现整数的表示。这么做的好处就是Python中的整数没有长度限制了,因此不会溢出(而浮点数使用C的double,所以它会溢出)。之所以不会溢出,是因为数组是没有长度限制的,所以只要你的内存足够,就可以算任意大的数。所以Python表示:存不下?会溢出?这都不是事儿,直接继续往数组里面塞digit就ok了。

    这里再重温一下PyLongObject的数据结构,我们说它是一个变长对象。ob_size指的是数组的长度,并且它除了表示长度还能体现出整数的正负,而ob_digit这个数组只用来存储其绝对值。

    但是说实话,用整数数组实现大整数的思路其实平白无奇,但难点在于大整数 数学运算 的实现,它们才是重点,也是也比较考验编程功底的地方。

    所以我们在分析浮点数的时候,一直说整数要比浮点数复杂,原因就在于此。浮点数相加的话直接两个double相加即可,但是整数相加可就没有那么简单了。

    整数支持的操作定义在什么地方相信不用我说了,直接去longobject.c中查看就可以了,根据浮点数的经验我们知道PyLong_Type中的tp_as_number成员也指向了PyNumberMethods结构体实例,里面的成员都是指向与整型运算相关的函数的指针。

    整数的大小比较

    先来看看Python中的整数在底层是如何比较的吧。

    static int
    long_compare(PyLongObject *a, PyLongObject *b)
    {	
        //sign是一个8字节的long, 用来表示a和b之间的比较结果
        //如果a == b, 那么sign = 0; 如果a > b, 那么sign > 0; 如果a < b, 那么sign < 0
        Py_ssize_t sign;
    	
        //Py_SIZE是一个宏:获取对象的ob_size,除此之外我们之前还见到了Py_REFCNT和Py_TYPE, 用来获取对象的引用计数和类型指针
        //如果两个整数的ob_size不一样, 我们说a和b一定不相等, 所以可以直接比较出大小
        if (Py_SIZE(a) != Py_SIZE(b)) {
            //如果一正一负, 那么肯定正的大, 因为ob_size还体现整数的正负, 所以正的ob_size对应的整数也会更大
            //如果都为正, 那么ob_size越大, 对应数组元素就越多, 显然整数就越大
            //如果都为负, 那么ob_size越大, 其绝对值就越小, 因为越接近0,所以对应的整数的绝对值也越小
              //但因为是负数,所以乘上-1之后,所以整数值反而会越大。比如: 1 < 100, 但是乘上-1之后, 小于号就要变成大于号
            //因此无论是哪种情况,如果两个整数的ob_size不一样,是可以直接比较出大小的。
            sign = Py_SIZE(a) - Py_SIZE(b);
            //所以sign > 0的话a > b, sign < 0的话a < b, 因为ob_size不一样, 所以sign不可能等于0
        }
        else {
            //如果相等, 那么说明a和b的符号相同, 数组中使用的digit也是一样的
            //那么接下来就只能挨个比较数组中的digit了
            //这里是获取数组的长度, 赋值给变量i
            Py_ssize_t i = Py_ABS(Py_SIZE(a));
            //我们之前说,一个digit存不下,那么会使用两个digit, 以此类推
            //并且代表整数高位的digit会放在后面, 而比较两个数的大小显然是从高位开始比
            //因此遍历数组是从后往前遍历的, 先比较a -> ob_digit[n]和 b -> ob_digit[n]
            //如果一样就比较a -> ob_digit[n-1]和a -> ob_digit[n-1],直到将数组的元素全部比完,显然只要有一个不一样,就可以直接决定绝对值的大小
            while (--i >= 0 && a->ob_digit[i] == b->ob_digit[i])
                //进行while循环, i是数组的长度, 因此数组的最大索引是i - 1, 所以这里的--i会先将i自减1,再判断自减1之后的i是否>=0
                //然后比较a->ob_digit[i]和b->ob_digit[i], 如果数组内元素全部一样, 那么循环结束之后i肯定是-1,只要有一个不一样, 那么i一定>=0
                ;
            if (i < 0)
                //所以如果i < 0,说明两个整数的数组全部一样, 因此两个整数是一样的
                //所以sign = 0
                sign = 0;
            else {
                //否则的话, 说明数组中索引为i的元素不一样, 那么直接相减就可以了
                //如果sign大于0, 显然a对应的绝对值比大, 否则a对应的绝对值比b小
                sign = (sdigit)a->ob_digit[i] - (sdigit)b->ob_digit[i];
                if (Py_SIZE(a) < 0)
                    //但是我们说计算的是绝对值,如果ob_size小于0,绝对值越大其值反而越小,那么sign还要乘上-1
                    sign = -sign;
            }
        }
        //因此最终: a > b则sign > 0, a < b则sign < 0, a == b则sign == 0
        //然后这里是一个嵌套的三元表达式, sign大于0则直接返回1表示a > b, 小于0返回-1表示a < b, 等于0则返回0表示a == b
        return sign < 0 ? -1 : sign > 0 ? 1 : 0;
    }
    

    所以我们看到Python中的整数就是按照上面这种方式比较的,总的来说就是先比较ob_size,ob_size不一样则可以直接比较。如果ob_size一样的话,那么会从后往前挨个比较数组中的元素,最终确定大小关系。

    整数的相加

    再来看看Python中的整数在底层是如何相加的,加法的实现显然是long_add,我们看一下。

    static PyObject *
    long_add(PyLongObject *a, PyLongObject *b)
    {	
        //a和b是两个PyLongObject *
        //z显然是指向a和b相加之后的PyLongObject
        PyLongObject *z;
    	
        //CHECK_BINOP是一个宏, 接收两个指针, 检测它们是不是都指向PyLongObject
        CHECK_BINOP(a, b);
    	
        //判断a和b的ob_size的绝对值是不是都小于等于1, 如果是的话, 那么说明数组中最多只有一个元素
        //数组没有元素,说明整数是0;有一个元素,那么直接取出来、再判断正负号即可,然后直接相加。
        //所以显然这里走的是快分支,因为绝对值超过2 ** 30 - 1的整数还是比较少的
        if (Py_ABS(Py_SIZE(a)) <= 1 && Py_ABS(Py_SIZE(b)) <= 1) {
            //MEDIUM_VALUE是一个宏, 接收一个abs(ob_size) <= 1的PyLongObject的指针
            //如果ob_size是0, 那么结果为0; 如果ob_size绝对值为1, 那么结果为 ob_digit[0] 或者 -ob_digit[0]
            //所以直接将MEDIUM_VALUE(a) + MEDIUM_VALUE(b)之后的结果转成PyLongObject,然后返回其指针即可
            //因此如果数组中元素不超过1个的话, 那么显然是可以直接相加的
            return PyLong_FromLong(MEDIUM_VALUE(a) + MEDIUM_VALUE(b));
        }
        //走到这里, 说明至少有一方的ob_size大于1
        //如果a < 0
        if (Py_SIZE(a) < 0) {
            //如果a < 0并且b < 0
            if (Py_SIZE(b) < 0) {
                //说明两者符号相同, 那么通过x_add直接将两个整数相加即可
                //这个x_add专门用于整数的绝对值相加,并且会返回PyLongObject *,它的实现我们后面会说
                //所以z指向的PyLongObject的内部成员是已经设置好了的
                //只不过x_add加的是两者的绝对值, z指向的PyLongObject内部ob_type的符号我们还需要再度判断一下
                z = x_add(a, b);
                if (z != NULL) {
                    assert(Py_REFCNT(z) == 1);
                    //因为a和b指向的整数都是负数, 所以将相加之后还要将ob_size乘上-1
                    Py_SIZE(z) = -(Py_SIZE(z));
                }
            }
            else
                //走到这里说明a < 0并且b > 0, 那么直接让b - a即可, 此时得到的结果一定是正
                //因此不需要考虑ob_size的符号问题
                z = x_sub(b, a);
        }
        else {
            //走到这里说明a > 0并且b < 0, 所以让a - b即可
            if (Py_SIZE(b) < 0)
                z = x_sub(a, b);
            else
                //此时两个整数均为正, 直接相加
                z = x_add(a, b);
        }
        //返回z的指针
        return (PyObject *)z;
    }
    

    所以long_add这个函数并不长,但是调用了辅助函数x_add和x_sub,显然核心逻辑是在这两个函数里面。至于long_add函数,它的逻辑如下:

    • 1. 定义一个变量z, 用于保存计算结果;
    • 2. 判断两个整数底层对应的数组是不是都不超过1, 如果是的话那么通过宏MEDIUM_VALUE直接将其转成C中的一个digit, 然后直接相加、返回即可。显然这里走的是快分支,或者快速通道;
    • 3. 但如果有一方ob_size绝对值不小于1, 那么判断两者的符号。如果都为负,那么通过x_add计算两者绝对值之和、再将ob_size乘上-1即可;
    • 4. 如果a的ob_size小于0, b的ob_size大于0, 那么通过x_sub计算b和a绝对值之差即可;
    • 5. 如果a的ob_size大于0, b的ob_size小于0, 那么通过x_sub计算a和b绝对值之差即可;
    • 6. 如果a的ob_size大于0, b的ob_size大于0, 那么通过x_add计算让b和a绝对值之和即可;

    所以Python中整数的设计非常的巧妙,ob_digit虽然是用来维护具体数值,但是它并没有考虑正负,而是通过ob_size来表示整数的正负号。这样运算的时候,计算的都是整数的绝对值,因此实现起来会方便很多。将绝对值计算出来之后,再通过ob_size来判断正负号。

    因此long_add将整数加法转成了 "绝对值加法(x_add)"和"绝对值减法(x_sub)":

    • x_add(a, b), 计算两者的绝对值之和, 即:|a| + |b|;
    • x_sub(a, b), 计算两者的绝对值之差, 即:|a| - |b|;

    由于绝对值加、减法不用考虑符号对计算结果的影响,实现更为简单,这是Python将整数运算转化成绝对值运算的缘由。虽然我们还没看到x_add和x_sub是如何对整数的绝对值进行相加和相减运算的,但也能从中体会到程序设计中逻辑的 划分 与 组合 的艺术,优秀的代码真的很美。

    那么下面我们的重心就在x_add和x_sub中了,看看它们是如何对大整数绝对值进行运算的。但是你可能会有疑问,大整数运算肯定很复杂,效率会差吧。显然这是必然的,整数数值越大,整数对象底层数组越长,运算开销也就越大。好在运算处理函数均以快速通道对小整数运算进行优化,将额外开销降到最低。

    比如上面的long_add,如果a和b对应的整数的绝对值都小于等于2 ^ 30 - 1,那么会直接转成C中的整型进行运算,性能损耗极小。并且走快速通道的整数的范围是:-(2 ^ 30 - 1) ~ 2 ^ 30 - 1,即:-1073741823 ~ 1073741823,显然它可以满足我们绝大部分的运算场景。

    绝对值加法x_add:

    在介绍绝对值加法之前,先来看看几个宏,先不管它们是干什么的,会在x_add中有体现:

    #define PyLong_SHIFT    30
    #define PyLong_BASE     ((digit)1 << PyLong_SHIFT)
    #define PyLong_MASK     ((digit)(PyLong_BASE - 1))
    //所以PyLong_MASK等于(1 << 30) - 1, 就等于2 ** 30 - 1, 说明32个位, 前两个位为0, 后面三十个位则都是1
    

    此外,再想象一下我们平时算加法的时候是怎么算的:

    而x_add在逻辑和上面是类似的,下面分析x_add的实现:

    static PyLongObject *
    x_add(PyLongObject *a, PyLongObject *b)
    {	
        //显然a和b指向了两个要想加的整数对象
        //这里获取a和b的ob_size的绝对值
        Py_ssize_t size_a = Py_ABS(Py_SIZE(a)), size_b = Py_ABS(Py_SIZE(b));
        //根据a和b的相加结果所创建的新的PyLongObject的指针
        PyLongObject *z;
        //循环变量
        Py_ssize_t i;
        //重点也是最难理解的地方: carry用于每个部分的运算结果(可不是大神带你carry哦)
        digit carry = 0;
    	
        //如果size_a小于size_b
        if (size_a < size_b) {
            //那么将a和b进行交换, 以及size_a和size_b也进行交换, 为什么这么做呢?因为方便
            //我们小时候计算两个整数相加时候, 如果一个位数多,一个位数少, 也会习惯将位数多的放在左边
            //最终从右往左, 也就是从低位往高位逐个相加, 大于10则进1
            { PyLongObject *temp = a; a = b; b = temp; }
            { Py_ssize_t size_temp = size_a;
                size_a = size_b;
                size_b = size_temp; }
            //如果size_a和size_b相等, 或者size_a大于size_b, 那么该if就无需执行了
        }
        //这里是创建一个ob_size为size_a + 1的PyLongObject, 然后返回其指针
        z = _PyLong_New(size_a+1);
        //但为什么是size_a + 1呢?
        //因为此时size_a 一定不小于 size_b, 那么a和b相加之后的z的ob_size一定不小于size_a
        //但是也可以也可能比size_a多1, 比如: a = 2 ** 60 - 1, b = 1
        //所以相加之后结果为2 ** 60次方, 所以ob_size就变成了3, 因此在创建z的时候,ob_digit的容量会等于size_a + 1
        
        //正常情况下, z是一个PyLongObject *, 但如果z == NULL, 表示分配失败(解释器也会异常退出)
        //但说实话, 除非你内存不够了, 否则这种情况不会发生
        if (z == NULL)
            return NULL;
        
        //重点来了, 如果a和b的ob_size不一样, 那么size_a会大于size_b
        //所以显然是先以size_b为准, 两者从低位向高位依次对应相加; 当b到头了, 再单独算a的剩余部分;
        //假设size_a == 4, size_b == 2, 对应到ob_digit的话
        //就是a -> ob_digit[0] + b -> ob_digit[0], 作为z -> ob_digit[0], 当然还需要考虑进位, 下面说
        //然后a -> ob_digit[1] + b -> ob_digit[1], 作为z -> ob_digit[1], 此时b到头了
        //继续a -> ob_digit[2]作为z -> ob_digit[2], a -> ob_digit[3]作为z -> ob_digit[3]
        //此时a和b相加就结束了, 如果不考虑相加进位的话, 那么整体流程就是这个样子。然后我们继续往下看
        
        //从索引为0开始遍历, 以i < size_b为条件
        for (i = 0; i < size_b; ++i) {
            //将a->ob_digit[i] + b->ob_digit[i]作为carry, 显然carry如果没有超过2 ** 30 - 1的话
            //显然它就是z -> ob_digit[i]
            carry += a->ob_digit[i] + b->ob_digit[i];
            //但是carry是可能溢出的, 所以z -> ob_digit[i] = carry & PyLong_MASK
            //这个PyLong_MASK就是我们在介绍x_add之前先介绍的几个宏之一, 它表示的是2 ** 30 - 1
            //我们说它的前两个位为0, 后面三十个位全是1, 因此对于后面三十个位来说, 在和carry进行"与运算"之后,对应的位还和carry保持一致
            //所以在carry小于等于2 ** 30 - 1的时候carry & PyLong_MASK就等于carry
            //但如果carry大于2 ** 30 - 1, 由于PyLong_MASK的前两位为0, 所以这一步可以确保carry不会超过2 ** 30 - 1
            z->ob_digit[i] = carry & PyLong_MASK;
            //但是carry的前两位显然不可以丢, 所以它们要作用在数组中下一个元素相加的结果上
            //比如a -> ob_digit[0] + b -> ob_digit[0]得到结果正好是2 ** 32 - 1, 那么carry的前两位也是1
            //而数组中下一元素相加之后, 其结果对应的位要比本次循环高出30
            //所以这里将carry右移30位, 然后作用到下一次循环中
            carry >>= PyLong_SHIFT;
        }
        for (; i < size_a; ++i) {
            //如果当b到头了, 那么继续从当前的i开始, 直到i == size_a, 逻辑还是和上面一样的
            //只不过将a->ob_digit[i] + b->ob_digit[i]换成了a->ob_digit[i], 因为b到头了
            carry += a->ob_digit[i];
            //这里也要"与上"PyLong_MASK, 因为也可能存在进位的情况, 拿生活中的99999 + 1为例
            //此时a = 99999, b = 1, 显然第一次循环b就到头了, 但后面单独循环a的时候, 依旧是要加进位的
            //所以这里也是同理
            z->ob_digit[i] = carry & PyLong_MASK;
            //carry右移30位
            carry >>= PyLong_SHIFT;
        }
        //两个循环结束之后, 其实还差一步, 还拿99999 + 1举例子, 按照顺序相加最后得到的是00000
        //因为最后还进了一个1, 所以这里的carry也是同理, 因此z的ob_size要比size_a多1, 目的就在于此
        z->ob_digit[i] = carry;
        //但如果最后的carry没有进位的话, 显然其结果就是0, 所以最后没有直接返回z, 而是返回了long_normalize(z)
        //这个long_normalize函数作用就是从后往前依次检查ob_digit的元素, 如果为0, 那么就将其ob_size减去1, 直到出现一个不为0的元素
        //当然对于我们当前来说, 显然最多只会检查一次
        return long_normalize(z);
    }
    

    Python中的整数在底层实现的很巧妙,不理解的话可以多看几遍,然后我们再使用再在Python的层面上反推一下,进一步感受底层运算的过程。

    # 假设有a和b两个整数, 当然这里是使用列表直接模拟的底层数组ob_digit
    a = [1073741744, 999, 765, 123341]
    b = [841, 1073741633, 2332]
    # 然后创建z, 表示a和b的相加结果
    z = []
    
    # 为了更直观, 我们一步步手动相加
    # 首先是将a[0] + b[0], 得到carry
    carry = a[0] + b[0]
    # 然后carry & (2 ** 30 - 1), 我们看到结果是761
    print(carry & (2 ** 30 - 1))  # 761
    # 因为如果carry小于等于 2 ** 30 - 1, 那么结果就是carry, 而这里是761, 显然carry肯定大于 2 ** 30 - 1
    print(carry > 2 ** 30 - 1)  # True
    # 因为carry & (2 ** 30 - 1) == 761, 所以z的第一个元素就是761
    z.append(761)
    
    
    # 然后计算a[1] + b[1]得到新的carry, 但是之前的carry大于 2 ** 30 - 1
    # 所以还要再加上之前的右移30位的carry
    carry = (carry >> 30) + a[1] + b[1]
    # 然后carry & (2 ** 30 - 1)得到809
    print(carry & (2 ** 30 - 1))  # 809
    # 说明carry依旧大于 2 ** 30 - 1
    print(carry > 2 ** 30 - 1)  # True
    # 然后z的第二个元素就是809
    z.append(809)
    
    
    # 计算a[2] + b[2]的时候也是同理
    carry = (carry >> 30) + a[2] + b[2]
    # 但是显然此时的carry已经不大于 2 ** 30 - 1了
    print(carry > 2 ** 30 - 1)  # False
    # 所以carry和carry & (2 ** 30 - 1)的结果都是carry本身
    print(carry, carry & (2 ** 30 - 1))  # 3098 3098
    # 说明z的第三个元素是3098
    z.append(3098)
    
    # 此时b到头了, 所以直接将a[3]作为carry, 当然我们不知道carry是否大于2 ** 30 - 1
    # 所以还是右移30位即可, 不过carry不大于2 ** 30 - 1的话, 那么 carry >> 30 就是0罢了
    carry = (carry >> 30) + a[3]
    print(carry)  # 123341
    # 说明z的最后一个元素是123341, 当然理论上我们还要在对carry和 2 ** 30 - 1进行一次判断
    # 当然由于我们知道carry肯定不会超过2 ** 30 - 1, 所以就不判断了
    z.append(123341)
    
    # 此时z为[761, 809, 3098, 123341]
    print(z)  # [761, 809, 3098, 123341]
    
    # 所以ob_digit为[1073741744, 999, 765, 123341]和[841, 1073741633, 2332]的两个PyLongObject相加
    # 得到的新的PyLongObject的ob_digit为[761, 809, 3098, 123341]
    
    # 我们根据ob_digit按照规则转成整数, 那么a + b的结果要和z是相等的
    a = 1073741744 + 999 * 2 ** 30 + 765 * 2 ** 60 + 123341 * 2 ** 90
    b = 841 + 1073741633 * 2 ** 30 + 2332 * 2 ** 60
    z = 761 + 809 * 2 ** 30 + 3098 * 2 ** 60 + 123341 * 2 ** 90
    print(a)  # 152688762386380073438430860672944
    print(b)  # 2689765870042689307465
    print(z)  # 152688762389069839308473549980409
    
    # 显然结果为True, 由此证明我们之前的结论是成立的。
    print(a + b == z)  # True
    

    看完绝对值加法x_add之后,再来看看绝对值减法x_sub,显然有了加法的经验之后再看减法会简单很多。

    static PyLongObject *
    x_sub(PyLongObject *a, PyLongObject *b)
    {	
        //依旧是获取两者的ob_size的绝对值
        Py_ssize_t size_a = Py_ABS(Py_SIZE(a)), size_b = Py_ABS(Py_SIZE(b));
        //z指向相加之后的PyLongObject
        PyLongObject *z;
        //循环变量
        Py_ssize_t i;
        //如果size_a 小于 size_b, 那么sign就是-1, 否则就是1
        int sign = 1;
        //之前carry保存的相加的结果, borrow保存相减的结果
        //名字很形象, 相加要进位叫carry、相减要结尾叫borrow
        digit borrow = 0;
    
        //如果size_a比size_b小, 说明a的绝对值比b小
        if (size_a < size_b) {
            //那么令sign = -1, 相减之后再乘上sign
            //因为计算的是绝对值之差, 符号是在绝对值之差计算完毕之后通过sign判断的
            sign = -1;
            //然后依旧交换两者的位置, 相减的时候也确保大的一方在左边
            //相加的时候其实大的一方在左边还是在右边没有太大影响, 但是相减的时候大的一方在左边显然会省事很多
            //但是交换之后再相减, 肯定要变符号, 因此将sign设置为-1
            { PyLongObject *temp = a; a = b; b = temp; }
            { Py_ssize_t size_temp = size_a;
                size_a = size_b;
                size_b = size_temp; }
            //可能有人会有疑问了,那如果a的ob_size是1, b的ob_size是-3,一正一负,此时起到的效果是相加才对啊
            //是的, 所以此时会将a和b传到x_add里面,而不是这里, 后面我们会总结
            //由于ob_digit里面的元素都为正, 所以x_add计算的是绝对值之和,x_sub计算的绝对值之差, 总之在理解逻辑的时候把a和b都想象成正数即可
        }
        else if (size_a == size_b) {
            //这一个条件语句可能有人会觉得费解,我们分析一下
            //如果两者相等, 那么两个ob_digit里面对应的元素也是有几率都相等的
            i = size_a;
            //所以从ob_digit的尾巴开始遍历
            while (--i >= 0 && a->ob_digit[i] == b->ob_digit[i])
                ;
            //如果都相等, 那么i会等于-1
            if (i < 0)
                //所以直接返回0即可
                return (PyLongObject *)PyLong_FromLong(0);
            //下面下面是为了计算相减之后的PyLongObject的ob_size
            //如果对应元素不相等, 假设a的ob_digit里面的元素是[2, 3, 4, 5], b的ob_digit是[1, 2, 3, 5]
            //因此上面的while循环结束之后, i会等于2, 显然只需要计算[2, 3, 4]和[1, 2, 3]之间的差即可, 因为最高位的5是一样的
            //然后判断索引为i时, 对应的值谁大谁小
            if (a->ob_digit[i] < b->ob_digit[i]) {
                //如果a->ob_digit[i] < b->ob_digit[i], 那么同样说明a小于b, 因此将sign设置为-1, 然后交换a和b的位置
                sign = -1;
                { PyLongObject *temp = a; a = b; b = temp; }
            }
            //因为做减法, 所以size_a和size_b直接设置成i + 1即可, 因为高位在减法的时候会被抵消掉, 所以它们完全可以忽略
            size_a = size_b = i+1;
        }
        //这里依旧是申请空间
        z = _PyLong_New(size_a);
        //申请失败返回NULL
        if (z == NULL)
            return NULL;
        
        //然后下面的逻辑和x_add是类似的
        for (i = 0; i < size_b; ++i) {
            //让a->ob_digit[i] - b->ob_digit[i], 但如果存在借位, 那么还要减掉
            //但是问题来了, 我们说digit貌似是无符号的吧, 但是对于低位来说a->ob_digit[i] 是完全可以小于 b->ob_digit[i]的
            //但是这样减出来不成负数了, 所以C语言中有这么个特点, 比如:这里相减得到的是-100
            //那么结果就是2 ** 32 - 100, 因为digit是无符号32位, 所以存储的负数会变成 2 ** 32 + 该负数, 或者2 ** 32 - 负数的绝对值
            //以我们平时做的减法为例:32 - 19, 我们知道结果是13, 但是低位的2减去低位的9结果是-7, 如果是负数
            //那么要像高位借个1, 从而得到10,因此最后一位是10 - 7 = 3
            //以此为例, a -> ob_digit[i] - b -> ob_digit[i], 如果小于0, 那么肯定要像数组中i + 1的元素进行借位, 但我们说它会比当前高30个位
            borrow = a->ob_digit[i] - b->ob_digit[i] - borrow;
            //因此这里借个1, 借的就不是10了, 而是2 ** 30次方
            //所以borrow为负, 那么结果显然加上2 ** 30才对, 但是当前borrow加的是2 ** 32次方
            //所以将borrow 还要 与上 PyLong_MASK,然后其结果才是z->ob_digit[i]的值
            z->ob_digit[i] = borrow & PyLong_MASK;
            //如果真的借了个1, 那么ob_digit中下一个元素肯定是要减去1的, 所以borrow右移30位
            borrow >>= PyLong_SHIFT;
            //和1进行与运算, 如果a -> ob_digit[i] - b -> ob_digit[i]为负, 那么就必须要借位
            //但由于digit只用30个位, 因此再加上2 ** 32次方之后,其结果的第31位一定是1
            //所以borrow右移30位之后, 再和1进行与运算之后结果肯定是1, 由此可以判断这次相减一定是借位了
            //如果为0代表结果为正、没有加上2 ** 32次方,那么结果borrow & 1的结果就是0
            borrow &= 1; 
            //所以Python底层的整数只用了30个位真的非常巧妙, 尤其是在减法的时候
            //因为借位一次借2 ** 30, 可由于C的特性会加上2 ** 32次方, 但是它们的结果只有前两个高位不一样, 后面30个位是一样的
            //所以再与上PyLong_MASK, 所以就等价于加上了2 ** 30次方,从而得到正确的结果
            //但如果一旦借位, 那么数组下一个元素要减去1。但问题是怎么判断它有没有借位呢?判断有没有借位就是判断两个元素相减之后是否为负
            //如果为负数,那么C会将这个负数加上2 ** 32次方, 而两个不超过2 ** 30 - 1的数相减得到的负数的绝对值显然也不会超过2 ** 30 - 1
            //换句话说其结果对应的第31位一定是0, 那么再和32个位全部是1的2 ** 32次方相加, 得到的结果的第31位一定是1
            //所以再让borrow右移30位、并和1进行与运算。如果结果为1, 证明相减为负数, 确实像下一个元素借了1, 因此下一次循环的会减去1
            //如果borrow为0, 那么就证明a->ob_digit[i] - b->ob_digit[i]得到的结果为正,根本不需要借位, 所以下一次循环等于减了一个0
        }
        
        //如果size_a和size_b一样, 那么这里的for循环是不会满足条件的, 但不一样的话, 肯定会走这里
        for (; i < size_a; ++i) {
            //我们看到这里的逻辑和之前分析x_add是类似的
            borrow = a->ob_digit[i] - borrow;
            z->ob_digit[i] = borrow & PyLong_MASK;
            borrow >>= PyLong_SHIFT;
            borrow &= 1; 
        }
        //只不过由于不会产生进位, 因此不需要对borrow再做额外判断, x_add中最后还要判断carry有没有进位
        assert(borrow == 0);
        if (sign < 0) {
            //如果sign < 0, 那么证明是负数
            Py_SIZE(z) = -Py_SIZE(z);
        }
        //最后同样从后往前将z -> ob_digit中为0的元素删掉, 直到遇见一个不为0的元素, 比如: 10000 - 9999, 虽然位数多, 但是结果是1
        //而z -> ob_digit在申请空间的时候只是根据长度申请的, 所以最后还需要这样的一次判断
        return long_normalize(z);
    }
    

    所以Python整数在底层的设计确实很精妙,尤其是在减法的时候,强烈建议多看几遍回味一下。

    整数的相减

    整数的相减调用的是long_sub函数,显然long_sub和long_add的思路都是一样的,核心还是在x_add和x_sub上面,所以long_sub就没有什么可细说的了。

    static PyObject *
    long_sub(PyLongObject *a, PyLongObject *b)
    {	
        //z指向a和b相加之后的PyLongObject
        PyLongObject *z;
        //判断a和b是否均指向PyLongObject
        CHECK_BINOP(a, b);
    	
        //这里依旧是快分支
        if (Py_ABS(Py_SIZE(a)) <= 1 && Py_ABS(Py_SIZE(b)) <= 1) {
            //直接相减,然后转成PyLongObject返回其指针
            return PyLong_FromLong(MEDIUM_VALUE(a) - MEDIUM_VALUE(b));
        }
        //a小于0
        if (Py_SIZE(a) < 0) {
            //a小于0,b小于0
            if (Py_SIZE(b) < 0)
                //调用绝对值减法, 因为两者符号一样
                z = x_sub(a, b);
            else
                //此时两者符号不一样,那么相加起到的是相加的效果
                z = x_add(a, b);
            if (z != NULL) {
                //但是x_add和x_sub运算的是绝对值, x_sub中考虑的sign是基于绝对值而言的
                //比如:x_sub接收的a和b的ob_size分别是-5和-3, 那么得到的结果肯定是正的, 因为会用绝对值大的减去绝对值小的
                //而显然这里的结果应该是负数, 所以还要乘上-1
                
                //如果x_sub接收的a和b的ob_size分别是-3和-5, 由于还是用绝对值大的减去绝对值小的,所以会交换、从而变号,得到的结果是负的
                //而显然这里的结果应该是正数, 所以也要乘上-1
                
                //至于x_add就更不用说了, 当a为负、b为正的时候, a - b,就等于a和b的绝对值相加乘上-1
                assert(Py_SIZE(z) == 0 || Py_REFCNT(z) == 1);
                Py_SIZE(z) = -(Py_SIZE(z));
            }
        }
        else {
            //a大于0, b小于0, 所以a - b等于a和b的绝对值相加
            if (Py_SIZE(b) < 0)
                z = x_add(a, b);
            else
                //a大于0, b大于0, 所以直接绝对值相减即可
                //而正数等于其绝对值, 所以x_sub里面考虑的符号就是真正的结果的符号
                //如果是上面的负数, 那么还要乘上-1
                z = x_sub(a, b);
        }
        //返回结果
        return (PyObject *)z;
    }
    
    

    所以关于什么时候调用x_add、什么时候调用x_sub,我们总结一下,总之核心就在于它们都是对绝对值进行运算的,掌握好这一点就不难了:

    a + b

    • 如果a是正、b是正,调用x_add(a, b),直接对绝对值相加返回结果
    • 如果a是负、b是负,调用x_add(a, b),但相加的是绝对值,所以long_add中在接收到结果之后还要对ob_size乘上-1
    • 如果a是正、b是负,调用x_sub(a, b),此时等价于a的绝对值减去b的绝对值。并且x_sub是使用绝对值大的减去绝对值小的,如果a的绝对值大,那么显然正常;如果a的绝对值小,x_sub中会交换,但同时也会自动变号,因此结果也是正常的。举个普通减法的例子:5 + -3, 那么在x_sub中就是5 - 3; 如果是3 + -5, 那么在x_sub中就是-(5 - 3), 因为发生了交换。但不管那种情况,符号都是一样的
    • 如果a是负、b是正,调用x_sub(b, a),此时等价于b的绝对值减去a的绝对值。所以这个和上面a是正、b是负是等价的。

    所以符号相同,会调用x_add、符号不同会调用x_sub。

    a - b

    • 如果a是正、b是负,调用x_add(a, b)直接对a和b的绝对值相加即可
    • 如果a是正、b是正,调用x_sub(a, b)直接对a和b的绝对值相减即可,会根据绝对值自动处理符号,而a、b为正,所以针对绝对值处理的符号,也是a - b的符号
    • 如果a是负、b是正,调用x_add(a, b)对绝对值进行相加, 但是结果显然为负,因此在long_sub中还要对结果的ob_size成员乘上-1
    • 如果a是负、b是负,调用x_sub(a, b)对绝对值进行相减, 会根据绝对值自动处理符号, 但是在为负的情况下绝对值越大,其值反而越小, 因此针对绝对值处理的符号,和a - b的符号是相反的。所以最终在long_sub中,也要对结果的ob_size成员乘上-1。举个普通减法的例子:-5 - -3, 那么在x_sub中就类似于5 - 3; 如果是-3 - -5, 那么在x_sub中就类似于-(5 - 3), 因为发生了交换。但不管那种情况得到的值的正负号都是相反的,所以要再乘上-1

    所以符号相同,会调用x_sub、符号不同会调用x_add。

    所以可以仔细回味一下Python中整数的设计思想,以及运算方式。为什么只使用digit的30个位, 以及在相加、相减的时候是怎么做的。

    当然还有乘法和除法,乘法Python内部采用的是效率更高的karatsuba算法,相当来说比较复杂,有兴趣可以自己查看一下。重点还是了解Python中的整数在底层是怎么存储的,以及为什么要这么存储。

    小结

    这一节我们介绍了整数的底层实现,并分析了Python中的整数为什么不会溢出,以及Python如何计算一个整数所占的字节。当然我们还说了小整数对象池,以及通过分析源码中的long_add和long_sub来了解底层是如何对整数进行运算的。

  • 相关阅读:
    信息安全系统设计基础实验五:通讯协议设计 20135211李行之 20135216刘蔚然
    信息安全系统设计基础实验四:外设驱动程序设计 20135211李行之&20135216刘蔚然
    信息安全系统设计基础第十二周学习总结
    实验三-实时系统的移植 20135211李行之 20135216刘蔚然
    实验二-固件设计 20135211李行之 20135216刘蔚然
    信息安全系统设计基础第十一周学习总结
    信息安全系统设计基础实验一 20135211&20135216
    信息安全设计基础第十周学习总结
    Oracle如何创建表空间、用户及授权
    设置nginx支持虚拟机,支持多域名多网站
  • 原文地址:https://www.cnblogs.com/traditional/p/13437243.html
Copyright © 2020-2023  润新知