[译]Python内部:可调用对象如何工作

栏目: Python · 发布时间: 5年前

内容简介:作者:

作者: Eli Bendersky

原文链接: https://eli.thegreenplace.net/2012/03/23/python-internals-how-callables-work/

本文中描述的 Python 版本是 3.x ,更确切地—— Python 3.3 alpha 发布。

Python 里可调用( callable )概念是基本的。在考虑什么可以被“调用”时,最直接的答案是函数。不管是用户定义函数(你写的),还是内置函数(最有可能是在 CPython 解释器里用 C 实现),函数意味着被调用,不是吗?

嗯,还有方法,但它们不是特别有趣,因为它们只是绑定到对象的特殊函数。还有别的什么可以调用?你可能或者可能不熟悉调用对象的能力,只要它们属于定义了 __call__ 魔法方法的类。因此,对象可以充当函数。进一步考虑一下,类也是可调用的。毕竟,我们是这样创建新对象的:

class Joe :

... [contents of class]

joe = Joe()

这里,我们“调用” Joe 来创建一个新实例。因此类也可以作为函数!

事实证明,所有这些概念在 CPython 实现中很好地结合了在一起。 Python 里一切都是对象,包括前面章节里描述的每个实体(用户及内置函数,方法,对象,类)。所有这些调用由一个机制服务。这个机制是优雅且不难理解,因此值得了解它。让我们从头开始。

编译调用

CPython 在两个主要步骤里执行我们的程序:

  1. Python 源代码被编译为字节码。
  2. 一个 VM 执行这个字节码,使用内置对象和模块的 工具 箱来帮助它完成工作。

在本节,我将快速概述第一步如何应用于调用。我不会太深入,因为这些细节不是我想在本文里关注的真正有趣的部分。如果你想更多地了解 Python 源代码在编译器中所经历的流程,阅读 这个

简单地说, Python 编译器将一个表达式内,任何后跟 (arguments…) 的东西识别为调用【 1 】。对此的 AST 节点是 Call 。编译器在 Python/compile.c 里的函数 compiler_call 中为 Call 发布代码。在大多数情形里,将发布 CALL_FUNCTION 字节码。在本文中,我将忽略一些变形。例如,如果调用有“ star args ”—— func(a, b, *args) ,有处理这—— CALL_FUNCTION_VAR 的特殊函数。它和其他特殊指令只是同一个主题的不同变形。

CALL_FUNCTION

因此 CALL_FUNCTION 是我们准备在这里关注的指令。这就是 它的作用

CALL_FUNCTION(argc)

调用一个函数。 Argc 的低位字节表示位置参数的个数,高位字节表示关键字参数的个数。在栈上,操作码首先找到关键字参数。对每个关键字实参,值在这个键之上。关键字参数之下,位置参数在栈上,最右边的参数在顶部。这些参数之下,栈上是要调用的函数对象。弹出所有的函数,以及函数本身,并压入返回值。

CPython 字节码由 Python/ceval.c 里的巨函数 PyEval_EvalFrameEx 求值。这个函数有点吓人,但它只不过是操作码的一个花哨的调度程序。它从给定函数框的代码对象读取指令并执行它们。例如,下面是 CALL_FUNCTION 的处理句柄(清理了一点,删除了记录与计时的宏):

TARGET(CALL_FUNCTION)

{

PyObject **sp;

sp = stack_pointer;

x = call_function(&sp, oparg);

stack_pointer = sp;

PUSH(x);

if (x != NULL )

DISPATCH();

break ;

}

不太坏——它实际上可读性非常高。 Call_function 进行实际的调用(我们将稍微调查一下), oparg 是指令的数值实参,而 stack_pointer 指向栈顶【 2 】。 Call_function 返回的值压回栈上, DISPATCH 只是调用下一条指令的某个宏魔法。

Call_function 也在 Python/ceval.c 里。它实现了指令的实际功能。 80 行它不是很长,但足以让我不能完整地把它贴在这里。相反,我将解释一般性的流程并将小片段贴到相关的地方;欢迎你在你喜欢的编辑器里跟踪代码。

任何调用只是一个对象调用

理解 Python 里调用如何工作最重要的第一步是忽略 call_function 的大部分工作。是的,我是认真的。这个函数里绝大部分代码处理各种常见情形的优化。可以删除它,而不影响解析器的正确性,仅是其性能。如果我们暂时忽略所有的优化, call_function 所做的就是从 CALL_FUNCTION 的单个实参里解码出实参个数与关键字实参个数,转发给 do_call 。后面我们将回到优化,因为它们是有趣的,但目前,让我们看一下核心流程是什么。

Do_call 从栈将所有实参载入 PyObject 对象(一个用于位置实参的元组、一个用于关键字实参的字典),进行自己的一点追踪和优化,但最终调用 PyObject_Call

PyObject_Call 是一个超级重要的函数。它还可以用于 Python C API 中的扩展。下面就是,带有它所有的荣耀:

PyObject *

PyObject_Call (PyObject *func, PyObject *arg, PyObject *kw)

{

ternaryfunc call;

if ((call = func->ob_type->tp_call) != NULL ) {

PyObject *result;

if (Py_EnterRecursiveCall( " while calling a Python object" ))

return NULL ;

result = (*call)(func, arg, kw);

Py_LeaveRecursiveCall();

if (result == NULL && !PyErr_Occurred())

PyErr_SetString(

PyExc_SystemError,

"NULL result without error in PyObject_Call" );

return result;

}

PyErr_Format(PyExc_TypeError, "'%.200s' object is not callable" ,

func->ob_type->tp_name);

return NULL ;

}

深度递归保护以及错误处理放一边【 3 】, PyObject_Call 提取对象序列的 tp_call 属性【 4 】并调用它。这是可能的,因为 tp_call 保存了一个函数指针。

让它一边凉快去一会儿。就是这样。忽略所有美妙的优化,在 Python 里所有调用归结为:

  • Python 里一切是对象【 5 】:
  • 每个对象有一个类型;对象的类型决定了这个对象可以处理的东西。
  • 在调用这个对象时,调用其类型的 tp_call 属性。

作为 Python 的一个使用者,在你希望你的对象可调用时,你仅需要直接与 tp_call 交互。如果在 Python 里你定义了类,出于这个目的,你必须实现 __call__ 方法。这个方法被 CPython 直接映射到 tp_call 。如果你把你的类定义为 C 扩展,你必须手动在你的类的类型对象里对 tp_call 赋值。

但记住,类本身被“调用”来创建新对象,因此 tp_call 也在这里扮演一个角色。甚至更为重要的,当你定义了一个类时,也涉及一个调用——类的元类。这是一个有趣的话题,我将在将来的文章里讨论。

额外学分:CALL_FUNCTION中的优化

这部分是可选的,因为本文的主要观点在前面章节已经给出。也就是说,我认为这个材料是有趣的,因为它提供了一些你通常不认为是对象的东西,实际上在 Python 里是对象的例子。

正如我之前提到的,对每个 CALL_FUNCTION 我们可以只使用 PyObject_Call ,然后完成。事实上,在这可能是过度的常见情形里,进行一些优化是合理的。 PyObject_Call 是一个非常通用的函数,需要其所有实参在特殊的元组及字典对象中(分别用于位置及关键字实参)。这些实参需要从栈获取,放入 PyObject_Call 期望的容器里。在某些常见的情形里,我们可以避免这个开销,这正是 call_function 中的优化。

Call_function 处理的第一个特殊情形是:

/* Always dispatch PyCFunction first, because these are

presumed to be the most frequent callable object.

*/

if (PyCFunction_Check(func) && nk == 0 ) {

这处理 builtin_function_or_method 类型的对象(由 C 实现里的 PyCFunction 类型表示)。在 Python 里有很多这样的东西,就像上面注释指出的。所有以 C 实现的函数与方法,不管在 CPython 解释器,还是在 C 扩展里,都归在这个类别。例如:

>>> type(chr)

>>> type("".split)

>>> from pickle import dump

>>> type(dump)

还有一个附加条件,如果传递给函数的关键字实参数量为零。这允许一些重要的优化。如果所讨论的函数不接受实参(在该函数创建时由 METH_NOARGS 标记),或者只接受一个实参( METH_0 标记), call_function 不经过通常的实参打包,可以直接调用底下的函数指针。为了理解这如何可能,强烈建议文档关于 PyCFunction METH_ 标记的 这个部分

接下来,是一些 Python 写的对类方法的特殊处理:

else {

if (PyMethod_Check(func) && PyMethod_GET_SELF(func) != NULL ) {

PyMethod 是用于表示 被绑定方法 bound method )的内部对象。关于方法的特殊之处是,它们携带它们绑定对象的一个引用。 Call_function 提取这个对象,并把它放在栈是,为接下来做准备。

下面是调用代码的余下部分(在它之后在 call_object 里只是一些栈清理):

if (PyFunction_Check(func))

x = fast_function(func, pp_stack, n, na, nk);

else

x = do_call(func, pp_stack, na, nk);

我们已经遇到过 do_call ——它实现了最一般形式的调用。不过,还有一个优化——如果 func 是一个 PyFunction (用于内部表示在 Python 代码里定义函数的对象),采取另一个路径—— fast_function

为了理解 fast_function 做什么,首先考虑在执行一个 Python 函数时发生了什么是重要的。简单地说,其代码对象被求值(使用 PyEval_EvalCodeEx 本身)。这个代码期望其实参在栈上。因此,在大多数情形里,没有必要将实参打包到容器里,然后再拆包。只要稍加注意,它们可以留在栈上,可以节省许多宝贵的 CPU 周期。

其他都落到 do_call 。通过这个方法,这包括了有关键字实参的 FyCFunction 对象。这个事实的一个奇怪的方面是,不将关键字参数传递给 C 函数会更有效,因为这些函数要么接受关键字参数,要么只接受位置参数。例如【 6 】:

$ ~/test/python_src/33/python -m timeit -s's="a;b;c;d;e"' 's.split(";")'

1000000 loops, best of 3: 0.3 usec per loop

$ ~/test/python_src/33/python -m timeit -s's="a;b;c;d;e"' 's.split(sep=";")'

1000000 loops, best of 3: 0.469 usec per loop

这是一个大的差别,但输入非常小。对更大的字符串,差别几乎看不见:

$ ~/test/python_src/33/python -m timeit -s's="a;b;c;d;e"*1000' 's.split(";")'

10000 loops, best of 3: 98.4 usec per loop

$ ~/test/python_src/33/python -m timeit -s's="a;b;c;d;e"*1000' 's.split(sep=";")'

10000 loops, best of 3: 98.7 usec per loop

总结

本文的目的是讨论在 Python 中可调用的含义,从尽可能低的层次( CPython 虚拟机的实现细节)着手处理这个概念。个人的,我发现这个实现非常优雅,因为它将几个概念整合为一个。就如额外学分一节所示,我们通常不视为对象的 Python 实体——函数与方法 + 实际上是对象,可以统一的方式处理。正如我承诺的,将来的文章将深入 tp_call 对创建新 Python 对象及类的意义。

[1]

这是一个内部的简化——()扮演其他角色,像类定义(用于列出基类),函数定义(用于列出实参),修饰符等——这些都不在表达式里。我还故意忽略了表达式生成器。

[2]

CPython VM是一台 栈机器

[3]

在C代码可能以调用 Python 代码结束时,需要Py_EnterRecursiveCall,以允许CPython追踪其递归程度,并在太深时摆脱出来。注意用C编写的函数无需遵守这个递归限制。这是为什么在调用PyObject Call之前,do_call处理PyCFunction特殊情形的原因。

[4]

这里的“属性”指的是结构字段(有时在文档里也称为“槽”)。如果你完全不熟悉Python C扩展定义的方式,参考 这个页面

[5]

在我说一切是对象时,我是认真的。你可能认为对象是你定义类的实例。不过,深入到C层面,CPython为你创建并应付许多对象。类型(类),内置对象,函数,模块——这些都由对象表示。

[6]

这个例子仅运行在Python 3.3上,因为对split,sep关键字实参是新引入这个版本的。在之前的Python版本中split仅接受位置实参。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

深度探索C++对象模型

深度探索C++对象模型

[美] Stanley B. Lippman / 侯捷 / 华中科技大学出版社 / 2001-5 / 54.00元

这本书探索“对象导向程序所支持的C++对象模型”下的程序行为。对于“对象导向性质之基础实现技术”以及“各种性质背后的隐含利益交换”提供一个清楚的认识。检验由程序变形所带来的效率冲击。提供丰富的程序范例、图片,以及对象导向观念和底层对象模型之间的效率测量。一起来看看 《深度探索C++对象模型》 这本书的介绍吧!

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具