【问题标题】:How are mylist.reverse() and list.reverse(mylist) executed?mylist.reverse() 和 list.reverse(mylist) 是如何执行的?
【发布时间】:2020-03-14 12:33:49
【问题描述】:

大概mylist.reverse()list.reverse(mylist) 最终都会通过list_reverse_implPyList_Reverselistobject.c 中执行reverse_slice。但他们实际上是如何到达那里的呢?从 Python 表达式到该 C 文件中的 C 代码的路径是什么?是什么将它们联系在一起?他们通过了这两个反向函数中的哪一个(如果有的话)?

更新赏金:Dimitris 的回答(更新 2:我的意思是原始版本,在它现在被扩展之前)和它下面的 cmets 解释了部分,但我仍然缺少一些事情,并希望看到一个全面的答案。

  • 两个 Python 表达式的两条路径如何收敛?如果我理解正确,反汇编和讨论字节码以及堆栈发生了什么,特别是LOAD_METHOD,会澄清这一点。 (正如 Dimitris 回答下的 cmets 所做的那样。)
  • 压入堆栈的“未绑定方法”是什么?它是“C 函数”(哪个?)还是“Python 对象”?
  • 如何判断它是listobject.c.h 文件中的list_reverse 函数?我认为 Python 解释器不像 “让我们寻找一个听起来相似的文件和一个听起来相似的函数”。我宁愿怀疑list 类型是在某处定义的,并且以某种方式“注册”在名称“list”下,并且reverse 函数以名称“reverse”“注册”(也许就是这样LIST_REVERSE_METHODDEF 宏呢?)。
  • 我(对于这个问题)对堆栈帧、参数处理和类似的事情不感兴趣(所以可能没有太多内部 call_function)。真正让我感兴趣的是我最初所说的,从 Python 表达式到那个 C 文件中的那个 C 代码的 path。最好是我一般如何找到这样的路径。

解释我的动机:对于another question,当我调用list.reverse(mylist) 时,我想知道什么C 代码在工作。我相当有信心通过浏览和搜索名称找到了它。但我想更确定并更好地了解这些联系。

【问题讨论】:

  • 我认为他们应该很快见面,因为mylist.reverselist.reverse 的绑定方法实例。
  • @kaya3 我也这么认为。我还有另一个问题,我实际使用list.reverse(mylist),重要的是它是从“内部视图”执行的,比如要求列表自行反转。这让我意识到我实际上并不知道这是怎么发生的,所以这就是我询问这两种表格的原因。
  • 有一个很好的documentation,如果你真的想自己检查一下内部调用了什么以及函数的流程是什么。
  • 我从Exploring CPython’s Internals 找到了上面的链接。还有其他链接也很好读..

标签: python cpython python-internals


【解决方案1】:

PyList_Reverse 是 C-API 的一部分,如果您在 C 中操作 Python 列表,您会调用它,它不会用于任何两种情况。

这两个都通过list_reverse_impl(实际上是list_reverse,它包装了list_reverse_impl),这是实现list.reverselist_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_NAMELOAD_METHODCALL_METHOD。我们可以通过查看operations 被调用的内容来查看推送到value stack 上的内容。 (注意,它被初始化为指向每个表达式的框架对象内的值堆栈。)

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[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,然后是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。由于为其生成的操作码并不表示已传递位置参数(因为没有显式传递):

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 来自动处理参数。这有点移动定义,稍微混淆。

【讨论】:

  • CALL_METHOD之前,有LOAD_METHOD,其documentation“这个字节码区分两种情况:如果TOS有一个正确名字的方法,字节码推送未绑定的方法和TOS。TOS 将作为 CALL_METHOD 调用未绑定方法时的第一个参数(self)。否则,NULL 和属性查找返回的对象被推送"
  • mylist.reverse() 是第一种情况,list.reverse(mylist) 是第二种情况吗?那么第一种情况将未绑定的方法和mylist 压入堆栈,而第二种情况将未绑定的方法压入堆栈,然后LOAD_NAME (mylist)mylist 压入堆栈?那么在CALL_METHOD这一点上,这两种情况的堆栈是相同的吗?
  • 在重新阅读该文档并阅读ceval.cLOAD_METHODCALL_METHOD 的处理后进行轻微更正:我现在认为在CALL_METHOD 处,两者的堆栈确实相同情况,除了在第一种情况下,堆栈中有一个额外的NULL。并且这两种情况都会导致第 3343 行和第 3360 行中的 call_function 调用相同。对吗?
  • @HeapOverflow 在案例 1 中,即 mylist.reverse()self 被压入堆栈(参见第 3304 行),从而导致我们知道 self.method(args) 导致 method(self, args) 的常见转换.这意味着在第一种情况下,我们在堆栈上有一个附加值 (self),位于函数对象的正上方。
  • 现在阅读,到目前为止看起来很棒!已经有一个问题:您说list.reverse 是一种常规属性访问,但对我来说它看起来像描述符协议。因为list.reverse.__get__ 存在并告诉我它是&lt;method-wrapper '__get__' of method_descriptor object at 0x0158E438&gt;,而hex(id(list.reverse)) 告诉我'0x158e438'doc“定义这些方法中的任何一个,并且一个对象被认为是一个描述符”
猜你喜欢
  • 2011-12-11
  • 1970-01-01
  • 2023-02-24
  • 2013-05-08
  • 1970-01-01
  • 2012-08-18
  • 2019-05-03
  • 2011-01-08
  • 2015-12-09
相关资源
最近更新 更多