前菜
在我们使用Python的过程, 很多时候会用到 + 运算, 例如:
1. a = 1 + 2
2.
3. print a
4.
5. # 输出
6.
7. 3
不光在加法中使用, 在字符串的拼接也同样发挥这重要的作用, 例如:
1. a = 'abc' + 'efg'
2.
3. print a
4.
5. # 输出
6.
7. abcefg
同样的, 在列表中也能使用, 例如:
1. a = [1, 2, 3] + [4, 5, 6]
2.
3. print a
4.
5. # 输出
6.
7. [1, 2, 3, 4, 5, 6]
为什么上面不同的对象执行同一个 + 会有不同的效果呢? 这就涉及到 + 的重载, 然而这不是本文要讨论的重点, 上面的只是前菜而已~~~
正文
先看一个例子:
1. num = 123
2.
3. num = num + 4
4.
5. print num
6.
7. # 输出
8.
9. 127
这段代码的用途很明确, 就是一个简单的数字相加, 但是这样似乎很繁琐, 一点都Pythonic, 于是就有了下面的代码:
1. num = 123
2.
3. num += 4
4.
5. print num
6.
7. # 输出
8.
9. 127
哈, 这样就很Pythonic了! 但是这种用法真的就是这么好么? 不一定. 看例子:
1. # coding: utf8
2.
3. l = [1, 2]
4.
5. l = l + [3, 4]
6.
7. print l
8.
9. # 输出
10.
11. [1, 2, 3, 4]
12.
13. # ------------------------------------------
14.
15. l = [1, 2]
16.
17. l += [3, 4] # 列表的+被重载了, 左右操作数必须都是iterable对象, 否则会报错
18.
19. print l
20.
21. # 输出
22.
23. [1, 2, 3, 4]
看起来结果都一样嘛~, 但是真的一样吗? 我们改下代码再看下:
1. # coding: utf8
2.
3. l = [1, 2]
4.
5. print 'l之前的id: ', id(l)
6.
7. l = l + [3, 4]
8.
9. print 'l之后的id: ', id(l)
10.
11. # 输出
12.
13. l之前的id: 40270024
14.
15. l之后的id: 40389000
16.
17. # ------------------------------------------
18.
19. l = [1, 2]
20.
21. print 'l之前的id: ', id(l)
22.
23. l += [3, 4] # 列表的+被重载了, 左右操作数必须都是iterable对象, 否则会报错
24.
25. print 'l之后的id: ', id(l)
26.
27. # 输出
28.
29. l之前的id: 40270024
30.
31. l之后的id: 40270024
看到结果了吗? 虽然结果一样, 但是通过 id 的值表示, 运算前后, 第一种方法对象是不同的了, 而第二种还是同一个对象! 为什么会这样?
结果分析
先来看看字节码:
1. [root@test1 ~]# cat 2.py
2. # coding: utf8
3. l = [1, 2]
4. l = l + [3, 4]
5. print l
6.
7.
8. l = [1, 2]
9. l += [3, 4]
10. print l
11. [root@test1 ~]# python -m dis 2.py
12. 2 0 LOAD_CONST 0 (1)
13. 3 LOAD_CONST 1 (2)
14. 6 BUILD_LIST 2
15. 9 STORE_NAME 0 (l)
16.
17. 3 12 LOAD_NAME 0 (l)
18. 15 LOAD_CONST 2 (3)
19. 18 LOAD_CONST 3 (4)
20. 21 BUILD_LIST 2
21. 24 BINARY_ADD
22. 25 STORE_NAME 0 (l)
23.
24. 4 28 LOAD_NAME 0 (l)
25. 31 PRINT_ITEM
26. 32 PRINT_NEWLINE
27.
28. 7 33 LOAD_CONST 0 (1)
29. 36 LOAD_CONST 1 (2)
30. 39 BUILD_LIST 2
31. 42 STORE_NAME 0 (l)
32.
33. 8 45 LOAD_NAME 0 (l)
34. 48 LOAD_CONST 2 (3)
35. 51 LOAD_CONST 3 (4)
36. 54 BUILD_LIST 2
37. 57 INPLACE_ADD
38. 58 STORE_NAME 0 (l)
39.
40. 9 61 LOAD_NAME 0 (l)
41. 64 PRINT_ITEM
42. 65 PRINT_NEWLINE
43. 66 LOAD_CONST 4 (None)
44. 69 RETURN_VALUE
在上诉的字节码, 我们着重需要看的是两个: BINARY_ADD 和 INPLACE_ADD ! 很明显:
l = l + [3, 4, 5] 这种背后就是 BINARY_ADD
l += [3, 4, 5] 这种背后就是 INPLACE_ADD
深入理解
虽然两个单词差很远, 但其实两个的作用是很类似的, 最起码前面一部分是, 为什么这样说, 请看源码:
1. # 取自ceva.c
2. # BINARY_ADD
3. TARGET_NOARG(BINARY_ADD)
4. {
5. w = POP();
6. v = TOP();
7. if (PyInt_CheckExact(v) && PyInt_CheckExact(w)) { // 检查左右操作数是否 int 类型
8. /* INLINE: int + int */
9. register long a, b, i;
10. a = PyInt_AS_LONG(v);
11. b = PyInt_AS_LONG(w);
12. /* cast to avoid undefined behaviour
13. on overflow */
14. i = (long)((unsigned long)a + b);
15. if ((i^a) < 0 && (i^b) < 0)
16. goto slow_add;
17. x = PyInt_FromLong(i);
18. }
19. else if (PyString_CheckExact(v) &&
20. PyString_CheckExact(w)) { // 检查左右操作数是否 string 类型
21. x = string_concatenate(v, w, f, next_instr);
22. /* string_concatenate consumed the ref to v */
23. goto skip_decref_vx;
24. }
25. else {
26. slow_add: // 两者都不是, 请走这里~
27. x = PyNumber_Add(v, w);
28. }
29. ...(省略)
30.
31.
32. # INPLACE_ADD
33. TARGET_NOARG(INPLACE_ADD)
34. {
35. w = POP();
36. v = TOP();
37. if (PyInt_CheckExact(v) && PyInt_CheckExact(w)) { // 检查左右操作数是否 int 类型
38. /* INLINE: int + int */
39. register long a, b, i;
40. a = PyInt_AS_LONG(v);
41. b = PyInt_AS_LONG(w);
42. i = a + b;
43. if ((i^a) < 0 && (i^b) < 0)
44. goto slow_iadd;
45. x = PyInt_FromLong(i);
46. }
47. else if (PyString_CheckExact(v) &&
48. PyString_CheckExact(w)) { // 检查左右操作数是否 string 类型
49. x = string_concatenate(v, w, f, next_instr);
50. /* string_concatenate consumed the ref to v */
51. goto skip_decref_v;
52. }
53. else {
54. slow_iadd:
55. x = PyNumber_InPlaceAdd(v, w); // 两者都不是, 请走这里~
56. }
57. ... (省略)
从上面可以看出, 不管是 BINARY_ADD 还是 INPLACE_ADD , 他们都会有如下相同的操作:
检查是不是都是`int`类型, 如果是, 直接返回两个数值相加的结果
检查是不是都是`string`类型, 如果是, 直接返回字符串拼接的结果
因为两者的行为真的很类似, 所以在这着重讲 INPLACE_ADD , 对 BINARY_ADD 感兴趣的童鞋可以在源码文件: abstract.c , 搜索: PyNumber_Add .实际上也就少了对列表之类对象的操作而已.
那我们接着继续, 先贴个源码:
1. PyObject *
2. PyNumber_InPlaceAdd(PyObject *v, PyObject *w)
3. {
4. PyObject *result = binary_iop1(v, w, NB_SLOT(nb_inplace_add),
5. NB_SLOT(nb_add));
6. if (result == Py_NotImplemented) {
7. PySequenceMethods *m = v->ob_type->tp_as_sequence;
8. Py_DECREF(result);
9. if (m != NULL) {
10. binaryfunc f = NULL;
11. if (HASINPLACE(v))
12. f = m->sq_inplace_concat;
13. if (f == NULL)
14. f = m->sq_concat;
15. if (f != NULL)
16. return (*f)(v, w);
17. }
18. result = binop_type_error(v, w, "+=");
19. }
20. return result;
INPLACE_ADD 本质上是对应着 abstract.c 文件里面的 PyNumber_InPlaceAdd 函数, 在这个函数中, 首先调用 binary_iop1 函数, 然后进而又调用了里面的 binary_op1 函数, 这两个函数很大一个篇幅, 都是针对 ob_type->tp_as_number , 而我们目前是 list , 所以他们的大部分操作, 都和我们的无关. 正因为无关, 所以这两函数调用最后, 直接返回 Py_NotImplemented , 而这个是用来干嘛, 这个有大作用, 是列表相加的核心所在!
因为 binary_iop1 的调用结果是 Py_NotImplemented , 所以下面的判断成立, 开始寻找对象( 也就是演示代码中l对象 )的 ob_type->tp_as_sequence 属性.
因为我们的对象是l(列表), 所以我们需要去 PyList_type 需找真相:
1. # 取自: listobject.c
2. PyTypeObject PyList_Type = {
3. ... (省略)
4. &list_as_sequence, /* tp_as_sequence */
5. ... (省略)
6. }
可以看出, 其实也就是直接取 list_as_sequence , 而这个是什么呢? 其实是一个结构体, 里面存放了列表的部分功能函数.
1. static PySequenceMethods list_as_sequence = {
2. (lenfunc)list_length, /* sq_length */
3. (binaryfunc)list_concat, /* sq_concat */
4. (ssizeargfunc)list_repeat, /* sq_repeat */
5. (ssizeargfunc)list_item, /* sq_item */
6. (ssizessizeargfunc)list_slice, /* sq_slice */
7. (ssizeobjargproc)list_ass_item, /* sq_ass_item */
8. (ssizessizeobjargproc)list_ass_slice, /* sq_ass_slice */
9. (objobjproc)list_contains, /* sq_contains */
10. (binaryfunc)list_inplace_concat, /* sq_inplace_concat */
11. (ssizeargfunc)list_inplace_repeat, /* sq_inplace_repeat */
12. };
接下来就是一个判断, 判断咱们这个 l 对象是否有 Py_TPFLAGS_HAVE_INPLACEOPS 这个特性, 很明显是有的, 所以就调用上步取到的结构体中的 sq_inplace_concat 函数, 那接下来呢? 肯定就是看看这个函数是干嘛的:
1. list_inplace_concat(PyListObject *self, PyObject *other)
2. {
3. PyObject *result;
4.
5. result = listextend(self, other); # 关键所在
6. if (result == NULL)
7. return result;
8. Py_DECREF(result);
9. Py_INCREF(self);
10. return (PyObject *)self;
11. }
终于找到关键了, 原来最后就是调用这个 listextend 函数, 这个和我们 python 层面的列表的extend方法 很类似, 在这不细讲了!
把 PyNumber_InPlaceAdd 的执行调用过程, 简单整理下来就是:
1. INPLACE_ADD(字节码)
2. -> PyNumber_InPlaceAdd
3. -> 判断是否数字: 如果是, 直接返回两数相加
4. -> 判断是否字符串: 如果是, 直接返回`string_concatenate`的结果
5. -> 都不是:
6. -> binary_iop1 (判断是否数字, 如果是则按照数字处理, 否则返回Py_NotImplemented)
7. -> binary_iop (判断是否数字, 如果是则按照数字处理, 否则返回Py_NotImplemented)
8. -> 返回的结果是否 Py_NotImplemented:
9. -> 是:
10. -> 对象是否有Py_TPFLAGS_HAVE_INPLACEOPS:
11. -> 是: 调用对象的: sq_inplace_concat
12. -> 否: 调用对象的: sq_concat
13. -> 否: 报错
所以在上面的结果, 第二种代码: l += [3,4,5] , 我们看到的 id 值并没有改变, 就是因为 +=通过 sq_inplace_concat 调用了列表的 listextend 函数, 然后导致新列表以追加的方式去处理.
结论
现在我们大概明白了 += 实际上是干嘛了: 它应该能算是一个加强版的 + , 因为它比 + 多了一个写回本身的功能.不过是否能够写回本身, 还是得看对象自身是否支持, 也就是说是否具备 Py_NotImplemented 标识, 是否支持 sq_inplace_concat , 如果具备, 才能实现, 否则, 也就是和 + 效果一样而已.
登录 | 立即注册