Python: 链式赋值的坑

2

在我们使用Python的过程中, 经常遇到赋值语句, 就像下面的那样:

a = 3
b = 3

可能你会觉得我又要说什么变量赋值就是引用, 这么简单的知识就不讨论啦, 相信聪明的大家肯定都知道的, 我想讲的是链式赋值

先科普下什么是链式赋值:

链式赋值: 同时对几个变量进行赋值

例如:

a = b = c = 3

好了, 现在正式进入正题:

>>> s = [1, 2, 3, 4, 5, 6]
>>> i = 0
>>> i = s[i] = 3 

i 和 s 的值分别是什么? 可能大家一眼看下去, 就能得出答案:

i 的值: 3
s 的值: [3, 2, 3, 4, 5, 6]

然而, 这个答案只是对了一半, 因为s的值错了! 有兴趣的朋友可以自行上机试下, 正确答案是:

i 的值: 3
s 的值: [1, 2, 3, 3, 5, 6]

s的列表, 并没有像我们想象中的那样, 就i=0位置上的元素, 变成3, 而是将i=3位置的元素改成3了, 为什么会这样? 一起来解析下吧, 上dis大杀器!

[root@iZ23pynfq19Z ~]# cat 2.py 
s = [1, 2, 3, 4, 5]
i = 0
g = i = s[i] = 3
[root@iZ23pynfq19Z ~]# python -m dis 2.py 
  1           0 LOAD_CONST               0 (1)
              3 LOAD_CONST               1 (2)
              6 LOAD_CONST               2 (3)
              9 LOAD_CONST               3 (4)
             12 LOAD_CONST               4 (5)
             15 BUILD_LIST               5
             18 STORE_NAME               0 (s)

  2          21 LOAD_CONST               5 (0)
             24 STORE_NAME               1 (i)

  3          27 LOAD_CONST               2 (3)
             30 DUP_TOP             
             31 STORE_NAME               2 (g)
             34 DUP_TOP             
             35 STORE_NAME               1 (i)
             38 LOAD_NAME                0 (s)
             41 LOAD_NAME                1 (i)
             44 STORE_SUBSCR        
             45 LOAD_CONST               6 (None)
             48 RETURN_VALUE 

第一列的数字, 代表中间的字节码是属于哪一行代码的.
第1~2行简单解释下:
分别LOAD_CONST5个数字, 组成一个列表, 赋值给s,再取一个0, 赋值给i.接下来的就是我们关心的, 也是带给我们意外的代码.

第3行:
LOAD_CONST取出常量3, 它并不是像上面执行STORE_NAME, 而是采用DUP_TOP, 这是什么鬼, 我们这要去看下这指令具体是干嘛的:

//取自 python/ceval.c

PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
        ... (省略)
        TARGET_NOARG(DUP_TOP)
        {
            v = TOP();    // 复制运行栈帧的顶部值
            Py_INCREF(v); // 增加引用计数
            PUSH(v);      // 再压入运行栈帧
            FAST_DISPATCH();
        }
        ... (省略)
}

DUP_TOP指令说白了, 就是将刚才LOAD_CONST指令取出的常量3, 复制一份给v,然后再压回去运行栈帧, 这样就有两个3了, 为什么要这么做, 肯呢个大家已经猜到了, 不过我们还是得继续看具体是不是像我们想的那样, 继续看会字节码:

             35 STORE_NAME               1 (i)
             38 LOAD_NAME                0 (s)
             41 LOAD_NAME                1 (i)
             44 STORE_SUBSCR        

果然不出我们所料, 开始将这些3通过STORE_NAME赋值给i, 而对于s, 它反而是, 再一次LOAD_NAME取出i的值, 此时i的值是3, 不是一开始的0了, 在通过STORE_SUBSCR指令, 将刚才压入运行时时栈的3赋值给位置是3的元素, 具体的源码就不再看, 到这就够了.
所以看到这, 相信大家都能清楚, 为什么结果是 [1, 2, 3, 3, 5, 6]

这跟我们想象中的链式赋值很不同, 我们以前总是觉得, 赋值要从右到左依次执行, 先执行 s[i] = 3, 再执行 i=3, 然而这些是类似c语言这类支持表达式赋值才允许的. 在c语言中, s = 3表达式是有返回值的. 它会返回赋值的结果3, 所以在它们的链式赋值中, 是将右边表达式的返回值, 再赋值给左边的, 例如:

a = s = 3
等价于:
a = (s = 3)
也就是 s=3 返回3, 再赋值给a

而在python是不支持这种表达式赋值的, 也就是表达式是没有返回值的, 如果硬要a = (s = 3)只会触发SyntaxError: invalid syntax
希望大家以后在用到这种链式赋值时, 尽量避免这些问题哦


感谢@Daetalus童鞋指出问题:

支持表达式赋值是Python语言的核心,比如a = b + 3。这里的 b + 3
就是表达式。如果不支持表达式赋值就没法继续下去了。

a = (s = 3)出错的原因是因为s = 3是赋值语句,而不是表达式。

Python的表达式是由操作符连接而成的,但“=”在Python中并不是操作符(Operator),只是语法分隔符(Delimiters)。参见https://docs.python.org/relea...

欢迎各位大神指点交流,转载请注明来源: https://segmentfault.com/a/11...


如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

billiards · 2017年05月08日

请问说Python不支持链式赋值,那么a=b=c=3算吗;

回复

0

它并不像c语言那样支持链式赋值,从最右边的返回值,再赋值给左边,相反,它用了另外一种形式去实现,就是大家都同时用最右边的值

Lin_R 作者 · 2017年05月08日
Daetalus · 2017年05月10日

感谢作者提出这个知识点。挑个小毛病。

而在python是不支持这种表达式赋值的, 也就是表达式是没有返回值的, 如果硬要a = (s = 3)只会触发SyntaxError: invalid syntax

支持表达式赋值是Python语言的核心,比如a = b + 3。这里的 b + 3 就是表达式。如果不支持表达式赋值就没法继续下去了。

a = (s = 3)出错的原因是因为s = 3是赋值语句,而不是表达式。

Python的表达式是由操作符连接而成的,但“=”在Python中并不是操作符(Operator),只是语法分隔符(Delimiters)。参见https://docs.python.org/relea...

文中关于链式赋值本身是没有错误的。

回复

0

是的, 多谢指出! 感觉这个表达式赋值在不同语言不同场景, 所代表的意义和局限性也是不同的, 正如在c语言或者在其他语言, 赋值语句也算一个表达式, printf 也算表达式, 这些表达式都是有返回值的, 举个栗子就是printf("3"), 除了因为打印输出的3, 它本身的也会返回一个3.感谢童鞋指出

Lin_R 作者 · 2017年05月10日
DZL1943 · 2017年05月21日

mark

回复

dayinfinte · 2017年06月09日

mark,终于理解了

回复

zwen45 · 2017年11月30日
  1. = (yield 5)

为何m是none,不是很懂,和你这关系大吗 谢谢

回复

载入中...