我一直在阅读有关 python 中的魔术方法的信息,并且我发现了很多关于覆盖它们以及它们服务的目的的信息,但是我无法找到在语言中特定的运算符和操作映射到那些的位置方法(+
查找__add__
、+=
查找__iadd__
、从类中创建新对象可能会调用__new__
和__init__
等)是否有什么地方可以看到当 python 解释器(或任何较低级别的机制)遇到加号时会发生什么?
5 回答
你的问题有点笼统。有一个完整的“特殊方法”列表,即使它错过了一些 stdlib 特定的方法(例如__setstate__
和__getstate__
使用pickle
等。但它是模块的协议而pickle
不是语言协议)。
如果你想确切地知道解释器做了什么,你可以使用dis
模块来反汇编字节码:
>>> import dis
>>> def my_func(a):
... return a + 2
...
>>> dis.dis(my_func)
2 0 LOAD_FAST 0 (a)
3 LOAD_CONST 1 (2)
6 BINARY_ADD
7 RETURN_VALUE
您可以看到 intereperBINARY_ADD
在进行加法时执行了一个字节码。如果你想确切地看到BINARY_ADD
你可以下载 Python 的源代码并检查ceval.c
文件的操作:
case BINARY_ADD:
w = POP();
v = TOP();
if (PyInt_CheckExact(v) && PyInt_CheckExact(w)) {
/* INLINE: int + int */
register long a, b, i;
a = PyInt_AS_LONG(v);
b = PyInt_AS_LONG(w);
/* cast to avoid undefined behaviour
on overflow */
i = (long)((unsigned long)a + b);
if ((i^a) < 0 && (i^b) < 0)
goto slow_add;
x = PyInt_FromLong(i);
}
else if (PyString_CheckExact(v) &&
PyString_CheckExact(w)) {
x = string_concatenate(v, w, f, next_instr);
/* string_concatenate consumed the ref to v */
goto skip_decref_vx;
}
else {
slow_add:
x = PyNumber_Add(v, w);
}
Py_DECREF(v);
skip_decref_vx:
Py_DECREF(w);
SET_TOP(x);
if (x != NULL) continue;
break;
所以在这里我们可以看到 python 特殊情况 int 和 string 添加,并最终回退到PyNumber_Add
,它检查第一个操作数是否实现__add__
并调用它,最终它尝试__radd__
右侧,如果没有任何效果则引发 a TypeError
。
请注意,字节码是特定于版本的,因此dis
在不同版本上会显示不同的结果:
# python2.7
>>> def my_func():
... return map((lambda x: x+1), range(5))
...
>>> dis.dis(my_func)
2 0 LOAD_GLOBAL 0 (map)
3 LOAD_CONST 1 (<code object <lambda> at 0x16f8c30, file "<stdin>", line 2>)
6 MAKE_FUNCTION 0
9 LOAD_GLOBAL 1 (range)
12 LOAD_CONST 2 (5)
15 CALL_FUNCTION 1
18 CALL_FUNCTION 2
21 RETURN_VALUE
# python3
>>> dis.dis(my_func)
2 0 LOAD_GLOBAL 0 (map)
3 LOAD_CONST 1 (<code object <lambda> at 0x7f1161a76930, file "<stdin>", line 2>)
6 LOAD_CONST 2 ('my_func.<locals>.<lambda>')
9 MAKE_FUNCTION 0
12 LOAD_GLOBAL 1 (range)
15 LOAD_CONST 3 (5)
18 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
21 CALL_FUNCTION 2 (2 positional, 0 keyword pair)
24 RETURN_VALUE
同样的字节码可能会在未来的版本中进行优化,因此即使字节码相同,不同版本的python实际上也会执行不同的指令。
如果您有兴趣了解 python 如何在幕后工作,我建议您编写一些 C 扩展,遵循您可以在官方 python 网站上找到的教程和文档。
由于涉及的抽象级别,将 CPython 源映射运算符中的单个位置精确定位+
到特殊方法并非易事。__add__
正如其他人回应的那样,+
是用BINARY_ADD
操作码实现的,它调用PyNumber_Add
(在一些特别优化的情况下除外)。PyNumber_Add
另一方面,查看类型对象的tp_as_number
成员以获取其成员指向实现加法的 C 函数的结构。PyNumberMethods
nb_add
这对于直接定义 自己的 内置类型来说很简单nb_add
,但对于在 Python 中定义的类型来说有点复杂__add__
,需要将其转换为适当的nb_add
. 这部分由以下方式处理typeobject.c
:当您定义一个实现 的类时__add__
,其中的机器typeobject.c
安装到object->type->tp_as_number->nb_add
一个通用函数中,该函数查找__add__
对象并调用它来实现加法。对于 的情况__add__
,这个通用函数被调用slot_nb_add
并使用宏定义。SLOT1BIN
至于__new__
and __init__
,它们是从对象__call__
type
本身的运算符tp_call
中调用的(用 CPython 实现的术语)。这只是合乎逻辑的,因为在 Python 中,您正在调用类型来构造对象。
dis
模块可以在这方面为您提供一些帮助:
让我们举一个简单列表的例子:
In [12]: def func():
lis=[1,2,3]
for i in range(5):
lis+=[i]
....:
In [13]: def func1():
lis=[1,2,3]
for i in range(5):
lis =lis + [i]
....:
In [14]: dis.dis(func)
2 0 LOAD_CONST 1 (1)
3 LOAD_CONST 2 (2)
6 LOAD_CONST 3 (3)
#removed some lines of code
4 34 LOAD_FAST 0 (lis)
37 LOAD_FAST 1 (i)
40 BUILD_LIST 1
43 INPLACE_ADD # += means inplace add is used
# i.e `__iadd()__`
44 STORE_FAST 0 (lis)
47 JUMP_ABSOLUTE 28
>> 50 POP_BLOCK
>> 51 LOAD_CONST 0 (None)
54 RETURN_VALUE
In [15]: dis.dis(func1)
2 0 LOAD_CONST 1 (1)
3 LOAD_CONST 2 (2)
6 LOAD_CONST 3 (3)
9 BUILD_LIST 3
12 STORE_FAST 0 (lis)
#removed some lines of code
4 34 LOAD_FAST 0 (lis)
37 LOAD_FAST 1 (i)
40 BUILD_LIST 1
43 BINARY_ADD #normal binary add was used
#i.e __add__
44 STORE_FAST 0 (lis)
47 JUMP_ABSOLUTE 28
>> 50 POP_BLOCK
>> 51 LOAD_CONST 0 (None)
54 RETURN_VALUE
http://docs.python.org/2/library/dis.html
class x:
def __add__(self,other):
return "asd"
def test():
return x() + "aaaa"
import dis
dis.dis(test)
它返回类似的东西
2 0 LOAD_GLOBAL 0 (x)
3 CALL_FUNCTION 0
6 LOAD_CONST 1 ('aaaa')
9 BINARY_ADD
10 RETURN_VALUE
那是你最接近“低水平”的地方