Python 源码懂得 '+=' 跟 'xx = xx + xx' 的差别柒整头
更新时间:2017-09-30

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

起源: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技巧


友情链接:
Copyright 2017-2018 新世纪娱乐 版权所有,未经协议授权禁止转载。