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:我指的是原始版本,在现在扩展之前)及其下面的评论解释了部分内容,但我'我仍然缺少一些东西,希望看到一个全面的答案。
- 两个Python表达式的两条路径如何收敛?如果我理解正确,反汇编和讨论字节码以及堆栈发生了什么,特别是
LOAD_METHOD
,将会澄清这一点。 (正如 Dimitris 回答下的评论所做的那样。)
- 压入堆栈的"unbound method"是什么?是 "C function"(哪个?)还是 "Python object"?
- 如何判断是
listobject.c.h
文件中的 list_reverse
函数?我认为 Python 解释器不像 "let's look for a file that sounds similar and for a function that sounds similar"。我宁愿怀疑 list
类型在某处定义并且以某种方式 "registered" 在名称“list
” 下,并且 reverse
函数是 "registered" 在名称“reverse
”(也许这就是 LIST_REVERSE_METHODDEF
宏的作用?)。
- 我(对于这个问题)对栈帧、参数处理和类似的事情不感兴趣(所以可能 里面发生的事情不多
call_function
).真正让我感兴趣的是我最初所说的,path 从 Python 表达式到那个 C 文件中的 C 代码。最好是我一般如何找到这样的路径。
解释一下我的动机:对于 ,我想知道当我调用 list.reverse(mylist)
时是什么 C 代码完成了工作。我相当有信心通过四处浏览和搜索名称找到它。但我想更确定一些,总体上更好地理解这些联系。
PyList_Reverse
是 C-API 的一部分,如果你在 C 中操作 Python 列表,你会调用它,它不用于两者中的任何一个案例。
这些都经过 list_reverse_impl
(实际上是 list_reverse
包装 list_reverse_impl
),这是实现 list.reverse
和 list_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_NAME
、LOAD_METHOD
和 CALL_METHOD
。我们可以看到被推到 value stack by viewing what operations 上的东西被调用了。 (注意,它被初始化为指向每个表达式的框架对象内的值堆栈。)
在这种情况下执行的操作非常简单。给定我们的名字,在每种情况下 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>
首先,我不应该认为只有在执行属性访问时才会生成此操作码(即 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
,然后是 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_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 来自动处理参数。这有点移动定义,稍微混淆。
大概 mylist.reverse()
和 list.reverse(mylist)
最终都执行了 reverse_slice
in listobject.c
via list_reverse_impl
or PyList_Reverse
。但他们实际上是如何到达那里的呢?从 Python 表达式到该 C 文件中的 C 代码的路径是什么?他们有什么联系?他们经历了这两个反向函数中的哪一个(如果有的话)?
赏金更新:Dimitris 的回答(更新 2:我指的是原始版本,在现在扩展之前)及其下面的评论解释了部分内容,但我'我仍然缺少一些东西,希望看到一个全面的答案。
- 两个Python表达式的两条路径如何收敛?如果我理解正确,反汇编和讨论字节码以及堆栈发生了什么,特别是
LOAD_METHOD
,将会澄清这一点。 (正如 Dimitris 回答下的评论所做的那样。) - 压入堆栈的"unbound method"是什么?是 "C function"(哪个?)还是 "Python object"?
- 如何判断是
listobject.c.h
文件中的list_reverse
函数?我认为 Python 解释器不像 "let's look for a file that sounds similar and for a function that sounds similar"。我宁愿怀疑list
类型在某处定义并且以某种方式 "registered" 在名称“list
” 下,并且reverse
函数是 "registered" 在名称“reverse
”(也许这就是LIST_REVERSE_METHODDEF
宏的作用?)。 - 我(对于这个问题)对栈帧、参数处理和类似的事情不感兴趣(所以可能 里面发生的事情不多
call_function
).真正让我感兴趣的是我最初所说的,path 从 Python 表达式到那个 C 文件中的 C 代码。最好是我一般如何找到这样的路径。
解释一下我的动机:对于 list.reverse(mylist)
时是什么 C 代码完成了工作。我相当有信心通过四处浏览和搜索名称找到它。但我想更确定一些,总体上更好地理解这些联系。
PyList_Reverse
是 C-API 的一部分,如果你在 C 中操作 Python 列表,你会调用它,它不用于两者中的任何一个案例。
这些都经过 list_reverse_impl
(实际上是 list_reverse
包装 list_reverse_impl
),这是实现 list.reverse
和 list_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_NAME
、LOAD_METHOD
和 CALL_METHOD
。我们可以看到被推到 value stack by viewing what operations 上的东西被调用了。 (注意,它被初始化为指向每个表达式的框架对象内的值堆栈。)
在这种情况下执行的操作非常简单。给定我们的名字,在每种情况下 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>
首先,我不应该认为只有在执行属性访问时才会生成此操作码(即 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
,然后是 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_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 来自动处理参数。这有点移动定义,稍微混淆。