我现在已经使用Python一段时间来解决实际问题了,但我仍然没有对幕后发生的事情有一个正确的理论上的理解。例如,我很难理解Python如何将函数视为对象。我知道函数是类'函数的对象,有一个' call'方法,我知道我可以通过编写一个'调用方法来使我的定制类表现得像函数一样。对他们来说但是我无法弄清楚在创建新函数时精确存储在内存中的内容,以及如何访问存储的信息。
为了实验,我写了一个小脚本,它创建了许多函数对象并将它们存储在列表中。我注意到这个程序占用了大量内存。
funct_list = []
for i in range(10000000):
def funct(n):
return n + i
funct_list.append(funct)
我的问题是:
定义新的函数对象时,精确存储在RAM中的是什么?我是否存储了如何实现该功能的细节?
如果是这样,我的函数对象是否具有允许我"检查"的属性或方法。 (或者甚至可能"回顾性地改变")函数的行为方式?
也许我以前的问题是循环的,因为函数对象的方法本身就是函数...
在我上面的代码中,一些RAM仅用于存储"指针"到列表中的我的函数对象。 RAM的其余部分可能用于存储有关我的函数对象实际工作方式的有趣内容。 RAM大致如何在这两个目的之间分配?
假设我通过使函数执行更复杂的操作来更改代码片段。结果会占用更多的RAM吗? (我希望如此。但是当我通过用1000行垃圾填充其身体来改变我的功能的定义时,似乎没有任何不同的RAM用量。)
< / LI>我很想找到关于此的全面参考。但无论我输入谷歌,我都无法找到我正在寻找的东西!
答案 0 :(得分:8)
功能对象的数据分为两个主要部分。对于由相同函数定义创建的所有函数而言相同的部分存储在函数的代码对象中,而即使在从相同函数定义创建的函数之间也可以更改的部分存储在函数对象中。
函数最有趣的部分可能是它的字节码。这是核心数据结构,它说明了执行函数的实际操作。它作为字符串存储在函数的代码对象中,您可以直接检查它:
>>> 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'
......但它的设计并不是人类可读的。
有了足够的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
这里的输出中有很多列,但我们最感兴趣的是ALL_CAPS和右边的列。
ALL_CAPS列显示函数的字节码指令。例如,LOAD_CONST
加载一个常量值,BINARY_ADD
是添加+
两个对象的指令。带有数字的下一列是字节码参数。例如,LOAD_CONST 3
表示在代码对象的常量中加载索引3处的常量。它们总是整数,并且它们与字节码指令一起被打包到字节码字符串中。最后一列主要提供字节码参数的人类可读解释,例如,说LOAD_CONST 3
中的3对应于常量(0, 1)
,或1
中的STORE_FAST 1
}对应于局部变量x
。此列中的信息实际上并非来自字节码字符串;通过检查代码对象的其他部分来解决它。
函数对象数据的其余部分主要是解析字节码参数所需的东西,比如函数的闭包或它的全局变量dict,以及刚存在的东西,因为它很方便内省,就像函数__name__
。
如果我们在C级看一下Python 3.6 function object struct definition:
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;
我们可以看到代码对象,然后是
__dict__
,__module__
,__qualname__
,完全限定名称在PyObject_HEAD
宏内部,还有类型指针和一些refcount / GC元数据。
我们没有必要直接去C查看大部分内容 - 我们可以查看dir
并过滤掉非实例属性,因为大部分数据都可以在Python级别获得 - 但结构定义提供了一个很好的,评论的,整洁的列表。
您也可以检查code object struct definition,但如果您还不熟悉代码对象,则内容不清楚,因此我不会将其嵌入到代码对象中。帖子。我只是解释代码对象。
代码对象的核心组件是Python字节码指令和参数的字节串。我们之前检查过其中一个。此外,代码对象包含诸如函数引用的常量元组之类的内容,以及确定如何实际执行每条指令所需的许多其他内部元数据。并非所有元数据 - 其中一些来自函数对象 - 但其中很多。其中一些,如常量元组,很容易理解,其中一些,如co_flags
(一堆内部标志)或co_stacksize
(用于临时值的堆栈大小)更深奥。
答案 1 :(得分:6)
函数就像任何其他函数一样:它们是类型(或类)的实例。您可以使用type(f)
获取函数的类型,其中f
是函数,或使用types
模块(types.FunctionType
)。
定义函数时,Python会构建一个函数对象并为其指定一个名称。此机制隐藏在def
语句后面,但它与任何其他类型的实例化相同。
这意味着在Python中,函数定义被执行,与其他语言不同。除此之外,这意味着在代码流到达之前函数不存在,因此在定义函数之前不能调用函数。
inspect
模块允许您窥探各种对象内部。其文档中的This table对于查看哪些类型的组件功能以及相关类型的对象(如方法)的制作以及如何获取它们非常有用。
函数内部的实际代码成为代码对象,其中包含Python解释器执行的字节代码。您可以使用dis
模块查看此内容。
查看函数和代码对象类型的help()
很有意思,因为它显示了为构建这些对象需要传递的参数。可以从原始字节代码创建新函数,将字节代码从一个函数复制到另一个函数但使用不同的闭包,依此类推。
help(type(lambda: 0))
help(type((lambda: 0).__code__))
您还可以使用compile()
函数构建代码对象,然后使用它们构建函数。
任何类型具有__call__()
方法的对象都是可调用的。函数是可调用的,其类型具有__call__()
方法。哪个是可以调用的。这意味着它也有一个__call__()
方法,它有一个__call__()
方法, ad adause,ad infinitum。
如何实际调用函数呢?对于在C中实现__call__
的对象,Python实际上绕过了__call__
,例如Python函数的__call__
方法。实际上,(lambda: 0).__call__
是method-wrapper
,用于包装C函数。