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

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

内容简介:作者:

作者: 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仅接受位置实参。


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

查看所有标签

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

High Performance Python

High Performance Python

Andrew Lewis / O'Reilly Media, Inc. / 2010-09-15 / USD 34.99

Chapter 1. Introduction Section 1.1. The High Performance Buzz-word Chapter 2. The Theory of Computation Section 2.1. Introduction Section 2.2. Problems Section 2.3. Models of Computati......一起来看看 《High Performance Python》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

MD5 加密
MD5 加密

MD5 加密工具