Python 源码懂得 '+=' 跟 'xx = xx + xx' 的差别柒整头

日期:2017-12-03   

(点击上圆蓝字,疾速存眷我们)

起源:LinR

segmentfault.com/a/1190000009764209

若有好作品投稿,请面击 → 这里懂得细目

前菜

在我们使用Python的过程, 良多时辰会用到+运算, 例如:

a = 1 + 2

print a

 

# 输出

3

不但正在减法中应用, 在字符串的拼接也异样施展这主要的感化, 比方:

a = "abc" + "efg"

print a

 

# 输出

abcefg

一样的, 在列表中也能使用, 例如:

a = [1, 2, 3] + [4, 5, 6]

print a

 

# 输出

[1, 2, 3, 4, 5, 6]

为何上面分歧的对象履行同一个+会有不同的效果呢? 这就波及到+的重载, 但是这不是本文要谈论的重点, 上面的只是前菜而已~~~

注释

前看一个例子:

num = 123

num = num + 4

print num

 

# 输入

127

这段代码的用处很明白, 就是一个简单的数字相加, 但是这样仿佛很烦琐, 一点都Pythonic, 因而就有了下面的代码:

num = 123

num += 4

print num

 

# 输出

127

哈, 这样就很Pythonic了! 但是这类用法实的就是这么好么? 未必. 看例子:

# coding: utf8

l = [1, 2]

l = l + [3, 4]

print l

 

# 输出

[1, 2, 3, 4]

 

# ------------------------------------------

 

l = [1, 2]

l += [3, 4]  # 列表的+被重载了, 阁下操做数必需都是iterable对象, 不然会报错

print l

 

# 输出

[1, 2, 3, 4]

看起来结果都一样嘛~, 然而果然一样吗? 我们改下代码再看下:

# coding: utf8

l = [1, 2]

print "l之前的id: ", id(l)

l = l + [3, 4]

print "l以后的id: ", id(l)

 

# 输出

l之前的id:  40270024

l之后的id:  40389000

 

# ------------------------------------------

 

l = [1, 2]

print "l之前的id: ", id(l)

l += [3, 4]  # 列表的+被重载了, 左左操作数必须都是iterable对象, 否则会报错

print "l之后的id: ", id(l)

 

# 输出

l之前的id:  40270024

l之后的id:  40270024

看到结果了吗? 虽然结果一样, 但是经由过程id的值流露表示, 运算前后, 第一种方式对象是分歧的了, 而第二种还是统一个对象! 为何会如许?

结果剖析

先来看看字节码:

[root@test1 ~]# cat 2.py

# coding: utf8

l = [1, 2]

l = l + [3, 4]

print l

 

 

l = [1, 2]

l += [3, 4]  

print l

[root@test1 ~]# python -m dis 2.py

  2           0 LOADCONST               0 (1)

              3 LOADCONST               1 (2)

              6 BUILDLIST               2

              9 STORENAME               0 (l)

 

  3          12 LOADNAME                0 (l)

             15 LOADCONST               2 (3)

             18 LOADCONST               3 (4)

             21 BUILDLIST               2

             24 BINARYADD          

             25 STORENAME               0 (l)

 

  4          28 LOADNAME                0 (l)

             31 PRINTITEM          

             32 PRINTNEWLINE      

 

  7          33 LOADCONST               0 (1)

             36 LOADCONST               1 (2)

             39 BUILDLIST               2

             42 STORENAME               0 (l)

 

  8          45 LOADNAME                0 (l)

         ,皇冠游戏;    48 LOADCONST               2 (3)

             51 LOADCONST               3 (4)

             54 BUILDLIST               2

             57 INPLACEADD        

             58 STORENAME               0 (l)

 

  9          61 LOADNAME                0 (l)

             64 PRINTITEM          

             65 PRINTNEWLINE      

             66 LOADCONST               4 (None)

             69 RETURNVALUE

在上诉的字节码, 我们着重需要看的是两个: BINARYADD 和 INPLACEADD!

很显著:

l = l + [3, 4, 5]    这种当面就是BINARYADD

l += [3, 4, 5]     这种背地就是INPLACEADD

深刻理解

固然两个单伺候好最远, 当心其真两个的感化是很相似的, 最最少后面一部门是, 为什么如许说, 请看源码:

# 取自ceva.c

# BINARYADD

TARGETNOARG(BINARYADD)

        {

            w = POP();

            v = TOP();

            if (PyIntCheckExact(v) && PyIntCheckExact(w)) {    // 检讨摆布操作数是不是 int 类别

                /* INLINE: int + int */

                register long a, b, i;

                a = PyIntASLONG(v);

                b = PyIntASLONG(w);

                /* cast to avoid undefined behaviour

                   on overflow */

                i = (long)((unsigned long)a + b);

                if ((i^a) < 0 && (i^b) < 0)

                    goto slowadd;

                x = PyIntFromLong(i);

            }

            else if (PyStringCheckExact(v) &&

                     PyStringCheckExact(w)) {                   // 检查左右操作数是不是 string 类型

                x = stringconcatenate(v, w, f, nextinstr);

                /* stringconcatenate consumed the ref to v */

                goto skipdecrefvx;

            }

            else {

              slowadd:                                          // 二者皆不是, 请行这里~

                x = PyNumberAdd(v, w);

            }

           ...(省略)

 

 

# INPLACEADD

TARGETNOARG(INPLACEADD)

        {

            w = POP();

            v = TOP();

            if (PyIntCheckExact(v) && PyIntCheckExact(w)) {   // 检查左右操作数是不是 int 类型

                /* INLINE: int + int */

                register long a, b, i;

                a = PyIntASLONG(v);

                b = PyIntASLONG(w);

                i = a + b;

                if ((i^a) < 0 && (i^b) < 0)

                    goto slowiadd;

                x = PyIntFromLong(i);

            }

            else if (PyStringCheckExact(v) &&

                     PyStringCheckExact(w)) {                 // 检查阁下操作数是不是 string 类型

                x = stringconcatenate(v, w, f, nextinstr);

                /* stringconcatenate consumed the ref to v */

                goto skipdecrefv;

            }

            else {

              slowiadd:                          

                x = PyNumberInPlaceAdd(v, w);                 // 两者都不是, 请走这里~

            }

           ... (省略)

从下面可以看出, 不管是BINARYADD 借是INPLACEADD, 他们都邑有以下雷同的草拟:

检查能否是都是`int`类型, 如果是, 直接返回两个数值相加的结果

检查是不是是都是`string`类型, 如果是, 直接返回字符串拼接的结果

因为两者的行动真的很类似, 所以在这侧重讲INPLACEADD, 对BINARYADD感兴致的童鞋可以在源码文件: abstract.c, 搜寻: PyNumberAdd.现实上也就少了对列表之类对象的操作而已.

那我们接着持续, 先揭个源码:

PyObject *

PyNumberInPlaceAdd(PyObject *v, PyObject *w)

{

    PyObject *result = binaryiop1(v, w, NBSLOT(nbinplaceadd),    

                                   NBSLOT(nbadd));

    if (result == PyNotImplemented) {

        PySequenceMethods *m = v->obtype->tpassequence;

        PyDECREF(result);

        if (m != NULL) {

            binaryfunc f = NULL;

            if (HASINPLACE(v))

                f = m->sqinplaceconcat;

            if (f == NULL)

                f = m->sqconcat;

            if (f != NULL)

                return (*f)(v, w);

        }

        result = binoptypeerror(v, w, "+=");

    }

    return result;

INPLACEADD实质上是对答着abstract.c文明里面的PyNumberInPlaceAdd函数, 在这个函数中, 起首调用binaryiop1函数, 然落后而又调用了里面的binaryop1函数, 这两个函数很年夜一个篇幅, 都是针对obtype->tpasnumber, 而我们今朝是list, 所以他们的年夜部分操作, 都和我们的无闭. 正因为有关, 所以这两函数调用最后, 直接返回PyNotImplemented, 而这个是用来干嘛, 这个有鸿文用, 是列表相加的中心地点!

因为binaryiop1的调用结果是PyNotImplemented, 所以上面的判断建立, 进部属脚寻觅对象(也就是演示代码中l对象)的obtype->tpassequence属性.

因为我们的对象是l(列表), 所以我们须要来PyListtype需找本相:

# 取自: listobject.c

PyTypeObject PyListType = {

    ... (省略)

    &listassequence,                          /* tpassequence */

    ... (省略)

}

能够看出, 实在也便是间接与listassequence, 而那个是甚么呢? 现实上是一个构造体, 外面寄存了列表的局部功效函数.

static PySequenceMethods listassequence = {

    (lenfunc)listlength,                       /* sqlength */

    (binaryfunc)listconcat,                    /* sqconcat */

    (ssizeargfunc)listrepeat,                  /* sqrepeat */

    (ssizeargfunc)listitem,                    /* sqitem */

    (ssizessizeargfunc)listslice,              /* sqslice */

    (ssizeobjargproc)listassitem,             /* sqassitem */

    (ssizessizeobjargproc)listassslice,       /* sqassslice */

    (objobjproc)listcontains,                  /* sqcontains */

    (binaryfunc)listinplaceconcat,            /* sqinplaceconcat */

    (ssizeargfunc)listinplacerepeat,          /* sqinplacerepeat */

};

接下去就是一个判断, 判定我们这个l对象是不是有PyTPFLAGSHAVEINPLACEOPS这个特征, 很显明是有的, 以是就挪用上步取到的结构体中的sqinplaceconcat函数, 那接上去呢? 确定就是看看这个函数是干嘛的:

listinplaceconcat(PyListObject *self, PyObject *other)

{

    PyObject *result;

 

    result = listextend(self, other);    # 症结地点

    if (result == NULL)

        return result;

    PyDECREF(result);

    PyINCREF(self);

    return (PyObject *)self;

}

终究找到要害了, 本来最后就是调用这个listextend函数, 这个和我们python层面的列表的extend办法很类似, 在这不细讲了!

把PyNumberInPlaceAdd的执止调用过程, 简略整理下来就是:

INPLACEADD(字节码)

    -> PyNumberInPlaceAdd

        -> 判断是不是数字: 如果是, 直接返回两数相加

        -> 断定是不是字符串: 如果是, 曲接前往`stringconcatenate`的成果

        -> 都不是:

            -> binaryiop1 (判断是不是数字, 如果是则按照数字处理处分, 否则返回PyNotImplemented)

                -> binaryiop (判断是不是数字, 如果是则依照数字处理, 否则返回PyNotImplemented)

            -> 返回的结果是不是 PyNotImplemented:

                -> 是:

                    -> 对象是不是有PyTPFLAGSHAVEINPLACEOPS:

                        -> 是: 调用对象的: sqinplaceconcat

                        -> 可: 挪用工具的: sqconcat

                -> 否: 报错

所以在上里的结果, 第发布种代码: l += [3,4,5], 我们看到的id值并不转变, 就是果为+=经过进程sqinplaceconcat调用了列表的listextend函数, 而后招致新列表以逃加的款式格式往处理.

论断

现在咱们大略懂得�搭理了+=实践上是干吗了: 它应当能算是一个增强版的+, 由于它比+多了一个写回自身的功能.不外是否是能够写回本身, 仍是得看对付象本身是没有是支撑, 也就是道是不是具有PyNotImplemented标识, 是不是收持sqinplaceconcat, 假如具有, 才干完成, 不然, 也就是跟 + 后果一样罢了.

看完本文有播种?请转收分享给更多人

存眷「Python开辟者」,晋升Python技巧