我一直在使用Python一段时间来解决实际问题,但我仍然没有对引擎背后发生的事情有一个正确的理论上的理解.例如,我很难理解Python如何将函数视为对象.我知道函数是类'function'的对象,带有'call'方法,我知道我可以通过为它们编写'call方法'来使我的定制类表现得像函数一样.但我无法弄清楚在创建新函数时精确存储在内存中的内容,以及如何访问存储的信息.
为了实验,我写了一个小脚本,它创建了许多函数对象并将它们存储在一个列表中.我注意到这个程序占用了大量内存.
funct_list = []
for i in range(10000000):
def funct(n):
return n + i
funct_list.append(funct)
Run Code Online (Sandbox Code Playgroud)
我的问题是:
在定义新的函数对象时,精确地存储在RAM中的是什么?我是否存储了如何实现该功能的细节?
如果是这样,我的函数对象是否具有允许我"检查"(或甚至可能"追溯"改变"函数行为方式)的属性或方法?
也许我之前的问题是循环的,因为函数对象的方法本身就是函数...
在我上面的代码中,一些RAM仅用于存储列表中我的函数对象的"指针".RAM的其余部分可能用于存储有关我的函数对象实际工作方式的有趣内容.RAM大致如何在这两个目的之间分配?
假设我通过使函数做更复杂的事情来改变代码片段.结果会占用更多的RAM吗?(我希望如此.但是当我通过用1000行垃圾填充其身体来改变我的函数的定义时,所用的RAM量似乎没有任何差别.)
我很想找到关于此的全面参考.但无论我输入谷歌,我似乎无法找到我正在寻找的东西!
功能对象的数据分为两个主要部分.对于由相同函数定义创建的所有函数而言相同的部分存储在函数的代码对象中,而即使在从相同函数定义创建的函数之间可以更改的部分也存储在函数对象中.
函数中最有趣的部分可能是它的字节码.这是核心数据结构,它说明了执行函数的实际操作.它作为字节串存储在函数的代码对象中,您可以直接检查它:
>>> def fib(i):
... x, y = 0, 1
... for _ in range(i):
... x, y = y, x+y
... return x
...
>>> fib.__code__.co_code
b'd\x03\\\x02}\x01}\x02x\x1et\x00|\x00\x83\x01D\x00]\x12}\x03|\x02|\x01|\x02\x17\x00\x02\x00}\x01}\x02q\x1
2W\x00|\x01S\x00'
Run Code Online (Sandbox Code Playgroud)
......但它的设计并不是人类可读的.
有了Python字节码的实现细节的足够知识,你可以自己解析,但描述所有这些将花费太长时间.相反,我们将使用该dis模块为我们反汇编字节码:
>>> import dis
>>> dis.dis(fib)
2 0 LOAD_CONST 3 ((0, 1))
2 UNPACK_SEQUENCE 2
4 STORE_FAST 1 (x)
6 STORE_FAST 2 (y)
3 8 SETUP_LOOP 30 (to 40)
10 LOAD_GLOBAL 0 (range)
12 LOAD_FAST 0 (i)
14 CALL_FUNCTION 1
16 GET_ITER
>> 18 FOR_ITER 18 (to 38)
20 STORE_FAST 3 (_)
4 22 LOAD_FAST 2 (y)
24 LOAD_FAST 1 (x)
26 LOAD_FAST 2 (y)
28 BINARY_ADD
30 ROT_TWO
32 STORE_FAST 1 (x)
34 STORE_FAST 2 (y)
36 JUMP_ABSOLUTE 18
>> 38 POP_BLOCK
5 >> 40 LOAD_FAST 1 (x)
42 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)
输出中有很多列,但我们最感兴趣的是ALL_CAPS和右边的列.
ALL_CAPS列显示函数的字节码指令.例如,LOAD_CONST加载一个常量值,并且BINARY_ADD是添加两个对象的指令+.带有数字的下一列是字节码参数.例如,LOAD_CONST 3说要在代码对象的常量中加载索引3处的常量.它们总是整数,并且它们与字节码指令一起打包到字节码字符串中.最后一列主要提供字节码参数的人类可读解释,例如,说3 in LOAD_CONST 3对应于常量(0, 1),或者1in STORE_FAST 1对应于局部变量x.此列中的信息实际上不是来自字节码字符串; 通过检查代码对象的其他部分来解决它.
函数对象数据的其余部分主要是解析字节码参数所需的东西,比如函数的闭包或它的全局变量dict,以及刚存在的东西,因为它对内省很方便,就像函数一样__name__.
如果我们看一下C级的Python 3.6 函数对象结构定义:
typedef struct {
PyObject_HEAD
PyObject *func_code; /* A code object, the __code__ attribute */
PyObject *func_globals; /* A dictionary (other mappings won't do) */
PyObject *func_defaults; /* NULL or a tuple */
PyObject *func_kwdefaults; /* NULL or a dict */
PyObject *func_closure; /* NULL or a tuple of cell objects */
PyObject *func_doc; /* The __doc__ attribute, can be anything */
PyObject *func_name; /* The __name__ attribute, a string object */
PyObject *func_dict; /* The __dict__ attribute, a dict or NULL */
PyObject *func_weakreflist; /* List of weak references */
PyObject *func_module; /* The __module__ attribute, can be anything */
PyObject *func_annotations; /* Annotations, a dict or NULL */
PyObject *func_qualname; /* The qualified name */
/* Invariant:
* func_closure contains the bindings for func_code->co_freevars, so
* PyTuple_Size(func_closure) == PyCode_GetNumFree(func_code)
* (func_closure may be NULL if PyCode_GetNumFree(func_code) == 0).
*/
} PyFunctionObject;
Run Code Online (Sandbox Code Playgroud)
我们可以看到有代码对象,然后
__dict__,__module__,__qualname__,完全合格的名称在PyObject_HEAD宏内部,还有类型指针和一些refcount/GC元数据.
我们没有必要直接去C来检查大部分内容 - 我们可以查看dir并过滤掉非实例属性,因为大多数数据都可以在Python级别获得 - 但结构定义提供了一个很好的评论整洁的清单.
您也可以检查代码对象结构定义,但如果您还不熟悉代码对象,则内容不是很清楚,因此我不打算将其嵌入到帖子中.我只是解释代码对象.
代码对象的核心组件是Python字节码指令和参数的字节串.我们之前检查过其中一个.此外,代码对象包含诸如函数引用的常量元组之类的内容,以及确定如何实际执行每条指令所需的许多其他内部元数据.并非所有元数据 - 其中一些来自函数对象 - 但其中很多.其中一些,如常量元组,很容易理解,其中一些,如co_flags(一堆内部标志)或co_stacksize(用于临时值的堆栈大小)更为深奥.
函数就像任何其他函数一样:它们是类型(或类)的实例.您可以使用函数的类型,函数type(f)where f或使用typesmodule(types.FunctionType).
定义函数时,Python会构建一个函数对象并为其指定一个名称.这个机器隐藏在def语句后面,但它的工作原理与任何其他类型的实例化相同.
这意味着在Python中,函数定义的执行与其他语言不同.除此之外,这意味着在代码流到达之前函数不存在,因此在定义函数之前无法调用函数.
该inspect模块可让您窥探各种物体.该文档中的该表对于查看哪些类型的组件功能以及相关类型的对象(例如方法)是如何制作以及如何获取它们非常有用.
函数内部的实际代码成为代码对象,其中包含Python解释器执行的字节代码.您可以使用该dis模块查看此信息.
纵观help()该类型的功能和代码的对象很有趣,因为它表明你需要传递在建立这些对象哪些参数.可以从原始字节代码创建新函数,将字节代码从一个函数复制到另一个函数但使用不同的闭包,依此类推.
help(type(lambda: 0))
help(type((lambda: 0).__code__))
Run Code Online (Sandbox Code Playgroud)
您还可以使用该compile()函数构建代码对象,然后使用它们构建函数.
类型具有__call__()方法的任何对象都是可调用的.函数是可调用的,它们的类型有一个__call__()方法.哪个是可以调用的.这意味着它也有一种__call__()方法,有一种__call__()方法,令人作呕,无穷无尽.
那么函数实际上是如何被调用的呢?Python实际上绕过__call__了用__call__C实现的对象,例如Python函数的__call__方法.实际上,(lambda: 0).__call__是一个method-wrapper,用于包装C函数.