mylist.reverse() 和 list.reverse(mylist) 是如何执行的?

How are mylist.reverse() and list.reverse(mylist) executed?

大概 mylist.reverse()list.reverse(mylist) 最终都执行了 reverse_slice in listobject.c via list_reverse_impl or PyList_Reverse。但他们实际上是如何到达那里的呢?从 Python 表达式到该 C 文件中的 C 代码的路径是什么?他们有什么联系?他们经历了这两个反向函数中的哪一个(如果有的话)?

赏金更新:Dimitris 的回答(更新 2:我指的是原始版本,在现在扩展之前)及其下面的评论解释了部分内容,但我'我仍然缺少一些东西,希望看到一个全面的答案。

解释一下我的动机:对于 ,我想知道当我调用 list.reverse(mylist) 时是什么 C 代码完成了工作。我相当有信心通过四处浏览和搜索名称找到它。但我想更确定一些,总体上更好地理解这些联系。

PyList_Reverse 是 C-API 的一部分,如果你在 C 中操作 Python 列表,你会调用它,它不用于两者中的任何一个案例。

这些都经过 list_reverse_impl(实际上是 list_reverse 包装 list_reverse_impl),这是实现 list.reverselist_instance.reverse 的 C 函数。

这两个电话都由 call_function in ceval, getting there after the CALL_METHOD opcode generated for them is executed (dis.dis the statements to see it). call_function has gone under a good deal of changes in Python 3.8 (with the introduction of PEP 590) 处理,所以从那以后发生的事情可能太大了,无法在一个问题中讨论。

其他问题:

How do the two paths from the two python expressions converge? If I understand things correctly, disassembling and discussing the byte code and what happens to the stack, particularly LOAD_METHOD, would clarify this.

让我们在两个表达式都编译为各自的字节码表示后开始:

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_NAMELOAD_METHODCALL_METHOD。我们可以看到被推到 value stack by viewing what operations 上的东西被调用了。 (注意,它被初始化为指向每个表达式的框架对象内的值堆栈。)

LOAD_NAME:

在这种情况下执行的操作非常简单。给定我们的名字,在每种情况下 llist,(每个名字都在 `co->co_names 中找到,这是一个存储我们在代码对象中使用的名字的元组)步骤是:

  1. locals里面找名字。如果找到,转到4。
  2. globals里面找名字。如果找到,转到4。
  3. builtins里面找名字。如果找到,转到4。
  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 (either [1, 2, 3, 4] or list) for a method named reverse. This is done with _PyObject_GetMethod,其文档说明:

Return 1 if a method is found, 0 if it's a regular attribute from __dict__ or something returned by using a descriptor protocol.

当我们通过列表对象的实例访问属性(reverse)时,只有在情况 A 中才能找到方法。在情况 B 中,可调用对象在描述符协议被调用后 returned,因此 return 值为 0(但我们当然会取回对象!)。

这里我们对值有分歧returned:

案例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,然后是 PUSHobj(即 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。由于为其生成的操作码并不表示已传递位置参数(因为 none 已明确传递):

4 CALL_METHOD              0

我们用 oparg + 1 = 0 + 1 = 1 调用 call_function 来表示堆栈中存在一个位置参数 ([1, 2, 3, 4])。

在情况 B 中,我们明确地将实例作为第一个参数传递,这被解释为:

6 CALL_METHOD              1

因此对 call_function 的调用可以立即传递 oparg 作为位置参数的值。

What is the "unbound method" pushed onto the stack? Is it a "C function" (which one?) or a "Python object"?

它是一个环绕 C 函数的 Python 对象。 Python 对象是一个方法描述符,它包装的 C 函数是 list_reverse

所有的内置方法和函数都是用C实现的。初始化时,CPythoninitializes all builtins (see list here) and adds wrappers around all the methods. These wrappers (objects) are descriptors that are used to implement Methods and Functions

当一个方法通过它的一个实例从 class 中检索时,它被称为绑定到该实例。通过查看分配给它的 __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 作为第一个参数。

How can I tell that it's the list_reverse function in the listobject.c.h file?

这很简单,您可以在 listobject.c 中看到列表的方法正在初始化。 LIST_REVERSE_METHODDEF 只是一个宏,当被替换时,会将 list_reverse 函数添加到该列表中。然后将列表的 tp_methods 包装在函数对象中,如前所述。

这里的事情可能看起来很复杂,因为 CPython 使用内部工具 argument clinic 来自动处理参数。这有点移动定义,稍微混淆。