PyFunctionObject对象

在Python中,任何一个东西都是对象,函数也不例外。函数这种抽象机制,是通过一个Python对象——PyFunctionObject来实现的

typedef struct {PyObject_HEADPyObject *func_code;  //编译后的PyCodeObject对象PyObject *func_globals; //函数运行时的global名字空间PyObject *func_defaults;  //默认参数(tupple或NULL)PyObject *func_closure;    //NULL or a tuple of cell objects,用于实现closure(闭包)PyObject *func_doc;     //函数的文档(PyStringObject)PyObject *func_name;   //函数名称,函数的__name__属性,(PyStringObject)PyObject *func_dict;   //函数的__dict__属性(PyDictObject或NULL)PyObject *func_weakreflist; PyObject *func_module;  //函数的__module__,可以是任何对象
} PyFunctionObject;

  

在Python中,有两个对象和函数有关,PyCodeObject和PyFunctionObject。PyCodeObject对象是对一段Python源代码的静态表示,Python源代码经过编译后,对一个Code Block会产生一个且只有一个PyCodeObject对象,这个PyCodeObject对象包含了在这个Code Block的一些静态信息,所谓静态信息即可以在源代码中看到的信息,如Code Block有a = 1这样的表达式,那么符号a和值1,以及它们之间的联系就是一种静态信息,这些信息会分别存储在PyCodeObject的常量表co_consts、符号表co_names以及字节码co_code中,这些信息是编译时就可以得到,因为PyCodeObject是编译时的结果

而PyFunctionObject则不同,PyFunctionObject对象是Python代码在运行时动态产生的,更准确的说,是在执行一个def语句时创建的。在PyFunctionObject中,也会包含这个函数的静态信息,这些信息存储在func_code中。实际上,func_code一定会指向函数代码所对应的PyCodeObject对象。除此之外,PyFunctionObject对象还包含一些函数在执行时必须的动态信息,即上下文信息,比如func_globals,就是函数在执行时关联的global作用域。global作用于的符号和值对应的关系必须在运行时才能确定,所以这部分必须在运行时动态创建,无法存储在PyCodeObject中

对于一段Python代码,其对应的PyCodeObject对象只有一个,而代码所对应的PyFunctionObject对象却可能有多个,比如一个函数多次调用,Python会在运行时创建多个PyFunctionObject对象,每一个PyFunctionObject对象的func_code域都会关联到这个PyCodeObject对象,如图1-1展示了PyFunctionObject与PyCodeObject对象之间的关系:

图1-1

无参数调用

我们对Python函数的剖析从无参数的调用开始,因为无参数调用是最简单的形式

# cat demo.py
def f():print("Function")f()# python2.5
……
>>> source = open("demo.py").read()
>>> co = compile(source, "demo.py", "exec")
>>> import dis
>>> dis.dis(co)1           0 LOAD_CONST               0 (<code object f at 0x7fd9831c3648, file "demo.py", line 1>)3 MAKE_FUNCTION            06 STORE_NAME               0 (f)5           9 LOAD_NAME                0 (f)12 CALL_FUNCTION            015 POP_TOP             16 LOAD_CONST               1 (None)19 RETURN_VALUE
>>> from demo import f
Function
>>> dis.dis(f)2           0 LOAD_CONST               1 ('Function')3 PRINT_ITEM          4 PRINT_NEWLINE       5 LOAD_CONST               0 (None)8 RETURN_VALUE

  

demo.py会产生两个PyCodeObject对象,一个是源文件编译后所生成的PyCodeObject对象,还有一个是函数f编译后所生成的PyCodeObject对象,包含在源文件对应的PyCodeObject对象中。

从上面的例子可以看到,在dis编译demo.py时,并没有将f函数的字节码编译出来,而当我们把函数f传给dis模块时,我们才得到函数f对应的字节码。这表明,函数f的字节码根本不在demo.py所对应的PyCodeObject对象的co_code域中,而是在函数f所对应的PyCodeObject对象中,函数f所对应的PyCodeObject对象,包含在demo.py所对应的PyCodeObject对象中,这是我们必须注意的字节码指令的层次结构。因此,函数的声明与函数的实现实际上是分离的,它们分离在不同的PyCodeObject对象中。尽管从源代码上来看,我们的第一直觉认为声明和实现应该是在一起的,但实际上不是。

Python虚拟机在执行def语句时,会动态创建一个函数,即PyFunctionObject,在这个过程中,MAKE_FUNCTION指令时一个关键:

ceval.c

case MAKE_FUNCTION:v = POP(); //获得与函数f对应的PyCodeObjectx = PyFunction_New(v, f->f_globals);Py_DECREF(v);//处理函数参数的默认值if (x != NULL && oparg > 0){v = PyTuple_New(oparg);if (v == NULL){Py_DECREF(x);x = NULL;break;}while (--oparg >= 0){w = POP();PyTuple_SET_ITEM(v, oparg, w);}err = PyFunction_SetDefaults(x, v);Py_DECREF(v);}PUSH(x);break;

  

在MAKE_FUNCTION之前,Python虚拟机会执行"0   LOAD_CONST   0",将与函数f对应的PyCodeObject对象压入到运行时栈。在执行MAKE_FUNCTION时,再将其取出,然后以该对象和当前PyFrameObject对象中维护的global名字空间f_globals对象为参数,通过PyFunction_New创建一个新的PyFunctionObject对象,而这个f_globals,将成为函数f运行时栈时的global名字空间

funcobject.c

PyObject * PyFunction_New(PyObject *code, PyObject *globals)
{//[1]:申请PyFunctionObject对象所需的内存空间PyFunctionObject *op = PyObject_GC_New(PyFunctionObject, &PyFunction_Type);static PyObject *__name__ = 0;if (op != NULL) {……//设置PyCodeObject对象op->func_code = code;//设置global名字空间op->func_globals = globals;//设置函数名op->func_name = ((PyCodeObject *)code)->co_name;Py_INCREF(op->func_name);op->func_defaults = NULL; /* No default arguments */op->func_closure = NULL;//函数中的常量对象表consts = ((PyCodeObject *)code)->co_consts;//函数文档if (PyTuple_Size(consts) >= 1) {doc = PyTuple_GetItem(consts, 0);if (!PyString_Check(doc) && !PyUnicode_Check(doc))doc = Py_None;}elsedoc = Py_None;……}elsereturn NULL;_PyObject_GC_TRACK(op);return (PyObject *)op;
}

  

创建PyFunctionObject对象之后,MAKE_FUNCTION还会进行一些处理函数参数的动作,由于我们的f是最简单的无参函数,没有任何参数,所以参数的传递机制会在后面的章节剖析

在MAKE_FUNCTION结束之后,新建的PyFunctionObject对象通过PUSH操作压入运行时栈中,随后的"6   STORE_NAME   0"和"9   LOAD_NAME   0"不再详述。下面,图1-1展示了def f()语句执行完成后,运行时栈和local名字空间的情况:

图1-1创建PyFunctionObject对象后的虚拟机状态

函数调用

从"12   CALL_FUNCTION   0"开始,Python虚拟机就开始进行调用函数的动作了

ceval.c

case CALL_FUNCTION:{PyObject **sp;PCALL(PCALL_ALL);sp = stack_pointer;x = call_function(&sp, oparg);stack_pointer = sp;PUSH(x);if (x != NULL)continue;break;}

  

CALL_FUNCTION的实现非常简单,仅仅是取得栈顶指针,然后就调用call_function函数了。那么我们就跟随着Python虚拟机的步伐,进入call_function函数一探究竟

ceval.c

static PyObject * call_function(PyObject ***pp_stack, int oparg)
{//[1]:处理函数参数信息int na = oparg & 0xff;int nk = (oparg >> 8) & 0xff;int n = na + 2 * nk;//[2]:获得PyFunctionObject对象PyObject **pfunc = (*pp_stack) - n - 1;PyObject *func = *pfunc;PyObject *x, *w;if (PyCFunction_Check(func) && nk == 0){……}else{if (PyMethod_Check(func) && PyMethod_GET_SELF(func) != NULL){……}elsePy_INCREF(func);READ_TIMESTAMP(*pintr0);//[3]:对PyFunctionObject对象进行调用if (PyFunction_Check(func))x = fast_function(func, pp_stack, n, na, nk);elsex = do_call(func, pp_stack, na, nk);……}……return x;
}

  

可以看到,不光是Function,还有CFunction和Method也会进入call_function,这里的CFunction为内建函数,Method为实例方法或类方法。在这里,我们的重心还是在Function上,先不管CFunction和Method

在call_function的[1]处,有一些处理函数参数信息的动作,n指明了在运行时栈中,栈顶有多少个元素是与参数相关的。当然,对于我们的f,这里的na、nk和n都是0,所以[2]处的pfunc为(*pp_stack) - 1,因为pp_stack就是我们在CALL_FUNCTION的指令代码中传入的当前运行时栈的栈顶指针

很显然,*pFunc,也就是func,指向了Python虚拟机在CALL_FUNCTION之前通过"9   LOAD_NAME   0"指令压入到运行时栈中的那个在MAKE_FUNCTION中创建的PyFunctionObject对象

Python虚拟机代码在通过[3]处的检查,就会进入fast_function。这里,我们也进入fast_function函数

ceval.c

static PyObject * fast_function(PyObject *func, PyObject ***pp_stack, int n, int na, int nk)
{PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func);PyObject *globals = PyFunction_GET_GLOBALS(func);PyObject *argdefs = PyFunction_GET_DEFAULTS(func);PyObject **d = NULL;int nd = 0;……//[1]:一般函数的快速通道if (argdefs == NULL && co->co_argcount == n && nk == 0 &&co->co_flags == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE)){PyFrameObject *f;PyObject *retval = NULL;PyThreadState *tstate = PyThreadState_GET();PyObject **fastlocals, **stack;int i;……f = PyFrame_New(tstate, co, globals, NULL);……//再次进入Python虚拟机retval = PyEval_EvalFrameEx(f, 0);……return retval;}if (argdefs != NULL){d = &PyTuple_GET_ITEM(argdefs, 0);nd = ((PyTupleObject *)argdefs)->ob_size;}//PyEval_EvalCodeEx的实现中,依然会调用PyEval_EvalFrameExreturn PyEval_EvalCodeEx(co, globals,(PyObject *)NULL, (*pp_stack) - n, na,(*pp_stack) - 2 * nk, nk, d, nd,PyFunction_GET_CLOSURE(func));
}

  

进入fast_function之后,首先会从PyFunctionObject对象中抽取出之前保存的PyCodeObject对象及函数运行时的global名字空间等信息。然后我们可以看到fast_function的执行分两条路径,无参函数会在fast_function的[1]处作出判断,进入函数的快速通道,Python虚拟机会为其创建一个新的PyFrameObject对象,进而调用PyEval_EvalFrameEx

而在另一条路上,则是PyEval_EvalCodeEx,函数PyEval_EvalCodeEx包含一系列复杂的动作,但如果追踪下去,它最后依旧要调用PyEval_EvalFrameEx函数完成字节码指令的执行。从PyEval_EvalFrameEx开始,Python虚拟机才真正开始进入函数调用的状态。这个过程实际上就是对x86平台上函数调用的模拟,在调用函数时创建新的栈帧,在新栈帧中执行函数代码。

有一点需要注意,在最终通过PyEval_EvalFrameEx时,PyFunctionObject对象的影响已经消失了,真正对栈帧产生影响的是PyFunctionObject中存储的PyCodeObject对象和global名字空间。说白了,PyFunctionObject只是为调用PyEval_EvalFrameEx时运输PyCodeObject对象和global名字空间

之前我们有提到函数的快速通道,那什么样的函数才可以进入快速通道呢?像C、C++和Java这样的函数就可以,也就是形如:func(x, y)这样的函数,如果像Python独有的函数func(a, *args, **kwargs)这样的函数则不行,Python正是靠函数参数的形式决定是否可以进入快速通道的

转载于:https://www.cnblogs.com/beiluowuzheng/p/9515473.html

Python虚拟机函数机制之无参调用(一)相关推荐

  1. Python虚拟机类机制之descriptor(三)

    从slot到descriptor 在Python虚拟机类机制之填充tp_dict(二)这一章的末尾,我们介绍了slot,slot包含了很多关于一个操作的信息,但是很可惜,在tp_dict中,与__ge ...

  2. Python的函数名作为参数传入调用以及map、reduce、filter

    零.python的lambda函数: 1 #lambda function 2 func = lambda x : x+1 3 #这里是一个匿名函数,x是参数,x+1是对参数的操作 4 func(1) ...

  3. 【python教程入门学习】Python函数定义及传参方式详解(4种)

    这篇文章主要介绍了Python函数定义及传参方式详解(4种),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧 一.函数初识 1.定 ...

  4. python中函数的定义包括_python中函数的定义及调用

    python中函数的定义及使用方法 1.函数的概念:函数是将具有独立功能的代码块组织为一个整体,使其具有特殊功能的代码集; 2.函数的作用:使用函数可以加强代码的复用性,提高程序编写的效率; 3.函数 ...

  5. Eel——js 如何调用python的函数

    它拥有一个本地网络服务器,然后让你用Python注释函数,以便从Javascript调用它们,反之亦然. 无可避免的,若要从后端语言获取数据,必须得开个服务器,Eel可以免去写服务器 pip inst ...

  6. java怎么无参构造方法_Java中如何在无参构造方法中调用有参构造?

    展开全部 一般正常的都是参数多的调用参数少的.有参数的调用无参数的居e68a843231313335323631343130323136353331333365643537多. 当然你要无参调用的参的 ...

  7. Python 匿名函数 lambda - Python零基础入门教程

    目录 一.Python 匿名函数 lambda 语法 二.Python 匿名函数 lambda 调用 1.lambda 匿名函数常规使用 2. lambda 匿名函数使用不定长参数 3. lambda ...

  8. html绑定带有形参的函数,Python中函数参数类型和参数绑定

    参数类型 Python函数的参数类型一共有五种,分别是: POSITIONAL_OR_KEYWORD(位置参数或关键字参数) VAR_POSITIONAL(可变参数) KEYWORD_ONLY(关键字 ...

  9. 四、python的函数基础--附代码案例

    四.python的函数基础 4.1 函数概述 4.2 函数的定义 4.3 函数的要素 4.3.1 函数名 4.3.2 库 4.3.3 形式参数 4.3.4 返回值 4.4 函数的调用 4.5 函数执行 ...

最新文章

  1. mariadb编译安装流程
  2. mysql 数据目录迁移_MySQL数据库数据文件路径迁移步骤
  3. 485通信自动收发电路,历史上最详细的解释
  4. 巴铁 无人驾驶_巴铁骗局再现 深圳无人巴士谎言又来忽悠人
  5. SAP License:SAP顾问是如何炼成的——SAP顾问的真实生活
  6. 报表默认执行查询及汉字无法查询原因处理
  7. C++输入输出操作符重载
  8. large计算机应用,cies - 计算机应用.pdf
  9. Json 转换 1 转成 true 0 转成false
  10. 【bug】记一个有趣的“bug”
  11. 算法上均匀分布的随机抽奖,如何避免现实的现场抽奖的中奖号码有时出现集中扎堆的现象?
  12. 电子邮件服务器传输到电子邮件客户端,6.3.4 电子邮件客户端和服务器
  13. 对立与统一(期货反向跟单-交易员培训法则)
  14. 怎么在ppt上设置文字滑动的效果html,PPT 中如何设置图片滚动切换效果
  15. Docker容器技术 笔记
  16. MAC最详细配置rz/sz命令
  17. TensorFlow 特征列介绍
  18. 基于CNN和LSTM的气象图降水预测示例
  19. matlab中区分fplot和plot,以及作图时sin(1./x)需要点除而不是除
  20. 千锋网络安全笔记部分

热门文章

  1. jQuery Portamento 滑动定位
  2. 单线驱动74hc595(转)
  3. iOS博客 视频课程网站
  4. 没有找到**.dll的解决方案
  5. 开发sharepoint工作流过程中的Ptifall(容易犯的错误)
  6. 腾讯开源国内首个H.266/VVC视频播放器
  7. 项目合作| 视频监控解决隧道洗车线的安全问题
  8. linux核显驱动与内核冲突,英特尔第十二代核显现身Linux驱动库
  9. 自注意力机制不一定是灵丹妙药???基于MLP的sMLPNet!MSRA出品
  10. 30篇「CVPR2020」最新论文抢先看!看计算机视觉2020在研究什么?