PyList_Reverse 是 C-API 的一部分,如果您在 C 中操作 Python 列表,您会调用它,它不会用于任何两种情况。
这两个都通过list_reverse_impl(实际上是list_reverse,它包装了list_reverse_impl),这是实现list.reverse和list_instance.reverse的C函数。
这两个调用都由ceval 中的call_function 处理,在为它们生成的CALL_METHOD 操作码执行后到达那里(dis.dis 看到它的语句)。 call_function 在 Python 3.8 中经历了很多变化(引入了 PEP 590),所以从那里发生的事情可能是一个太大的主题,无法在一个问题中讨论。
其他问题:
两个python表达式的两条路径如何收敛?如果我理解正确,反汇编和讨论字节码以及堆栈发生了什么,特别是LOAD_METHOD,会澄清这一点。
让我们在两个表达式编译成各自的字节码表示之后开始:
l = [1, 2, 3, 4]
案例 A,对于l.reverse(),我们有:
1 0 LOAD_NAME 0 (l)
2 LOAD_METHOD 1 (reverse)
4 CALL_METHOD 0
6 RETURN_VALUE
案例 B,对于list.reverse(l),我们有:
1 0 LOAD_NAME 0 (list)
2 LOAD_METHOD 1 (reverse)
4 LOAD_NAME 2 (l)
6 CALL_METHOD 1
8 RETURN_VALUE
我们可以放心地忽略RETURN_VALUE 操作码,在这里并不重要。
让我们关注每个操作码的单独实现,即LOAD_NAME、LOAD_METHOD 和CALL_METHOD。我们可以通过查看operations 被调用的内容来查看推送到value stack 上的内容。 (注意,它被初始化为指向每个表达式的框架对象内的值堆栈。)
LOAD_NAME:
在这种情况下执行的操作非常简单。给定我们的名称,l 或 list 在每种情况下,(每个名称都可以在 `co->co_names 中找到,这是一个存储我们在代码对象中使用的名称的元组),步骤如下:
- 在
locals 中查找名称。如果找到,请转到 4。
- 在
globals 中查找名称。如果找到,请转到 4。
- 在
builtins 中查找名称。如果找到,请转到 4。
- 如果找到,将由名称表示的值压入堆栈。否则,NameError。
在案例 A 中,名称 l 在全局变量中找到。在案例 B 中,它可以在内置函数中找到。所以,在LOAD_NAME 之后,堆栈看起来像:
案例A:stack_pointer -> [1, 2, 3, 4]
案例B:stack_pointer -> <type list>
LOAD_METHOD:
首先,我不应该只在执行属性访问时生成此操作码(即obj.attr)。您也可以获取一个方法并通过a = obj.attr 然后a() 调用它,但这会导致生成CALL_FUNCTION 操作码(更多信息请参见下文)。
在加载了可调用的名称(两种情况下都是reverse)后,我们在object on the top of the stack([1, 2, 3, 4] 或list)中搜索名为reverse 的方法。这是通过_PyObject_GetMethod 完成的,其文档指出:
如果找到方法返回 1,如果是常规属性则返回 0
来自__dict__ 或使用描述符返回的内容
协议。
当我们通过列表对象的实例访问属性 (reverse) 时,只能在案例 A 中找到方法。在情况 B 中,调用描述符协议后返回可调用对象,因此返回值为 0(但我们当然要取回对象!)。
这里我们在返回值上存在分歧:
情况 A:
SET_TOP(meth);
PUSH(obj); // self
我们有一个SET_TOP,后跟一个PUSH。我们将方法移到堆栈顶部,然后再次推送该值。在这种情况下,stack_pointer 现在看起来:
stack_pointer -> [1, 2, 3, 4]
<reverse method of lists>
在案例 B 中,我们有:
SET_TOP(NULL);
Py_DECREF(obj);
PUSH(meth);
再次是SET_TOP,然后是PUSH。 obj(即list)的引用计数减少了,因为据我所知,它不再需要了。在这种情况下,堆栈现在看起来像这样:
stack_pointer -> <reverse method of lists>
NULL
对于案例 B,我们有一个额外的 LOAD_NAME。按照前面的步骤,案例 B 的堆栈现在变为:
stack_pointer -> [1, 2, 3, 4]
<reverse method of lists>
NULL
非常相似。
CALL_METHOD:
这不会对堆栈进行任何修改。这两种情况都会导致对call_function 的调用传递线程状态、堆栈指针和位置参数的数量(oparg)。
唯一的区别在于用于传递位置参数的表达式。
对于案例 A,我们需要考虑应作为第一个位置参数插入的隐式 self。由于为其生成的操作码并不表示已传递位置参数(因为没有显式传递):
4 CALL_METHOD 0
我们用oparg + 1 = 0 + 1 = 1 调用call_function 来表示堆栈中存在一个位置参数([1, 2, 3, 4])。
在情况 B 中,我们将实例作为第一个参数显式传递,这需要考虑:
6 CALL_METHOD 1
因此对call_function 的调用可以立即将oparg 作为位置参数的值传递。
压入堆栈的“未绑定方法”是什么?它是“C 函数”(哪个?)还是“Python 对象”?
它是一个封装了 C 函数的 Python 对象。 Python 对象是一个方法描述符,它包装的 C 函数是list_reverse。
所有内置方法和函数都在 C 中实现。在初始化期间,CPython initializes 所有内置函数(请参阅 list here)并在所有 methods 周围添加包装器。这些包装器(对象)是用于实现Methods and Functions 的描述符。
当通过其中一个实例从类中检索方法时,我们称该方法绑定到该实例。这可以通过查看分配给它的 __self__ 属性来看到:
m = [1, 2, 3, 4].reverse
m() # use __self__
print(m.__self__) # [4, 3, 2, 1]
即使没有限定它的实例,仍然可以调用此方法。它绑定到那个实例。 (注意:这是由CALL_FUNCTION 操作码处理的,而不是LOAD/CALL_METHOD 操作码)。
未绑定的方法是尚未绑定到实例的方法。 list.reverse 未绑定,它正在等待通过实例调用以绑定到它。
未绑定的东西并不意味着它不能被调用,如果你明确地将self 参数作为参数传递,list.reverse 被调用就好了。请记住,方法只是特殊函数(除其他外)在绑定到实例后隐式传递 self 作为第一个参数。
如何判断它是 listobject.c.h 文件中的 list_reverse 函数?
这很简单,您可以在listobject.c 中看到列表的方法正在初始化。 LIST_REVERSE_METHODDEF 只是一个宏,当被替换时,会将 list_reverse 函数添加到该列表中。然后将列表的tp_methods 包装在函数对象中,如前所述。
这里的事情可能看起来很复杂,因为 CPython 使用内部工具 argument clinic 来自动处理参数。这有点移动定义,稍微混淆。