假设我们有以下功能:
def functionA(b, c):
def _innerFunction(b, c):
return b + c
return _innerFunction(b, c)
def _outerFunction(b, c):
return b + c
def functionB(b, c):
return _outerFunction(b, c)
Run Code Online (Sandbox Code Playgroud)
functionA并将functionB做同样的事情。_outerFunction全局可用,而_innerFunction仅适用于functionA. 嵌套函数对于数据隐藏和隐私很有用,但它们的内存效率如何?根据我的理解,_outerFunction只能加载一次,而其工作方式类似于“本地”变量,因此每次调用_innerFunction时都必须加载。functionA那是对的吗?
关于内存,两者的内存占用几乎相同。
函数由包含实际编译代码的代码对象和包含闭包、名称和其他动态变量的函数对象组成。
在运行代码之前,将针对所有函数(内部函数和外部函数)编译代码对象。它是驻留在.pyc文件中的内容。
内部函数和外部函数之间的区别在于函数对象的创建。外部函数只会创建该函数一次,而内部函数将加载相同的常量代码对象并在每次运行时创建该函数。
由于代码对象是等效的,_inner并且_outer的内存占用是等效的:
functionA名称将用于在每次运行时构造内部函数对象,而functionB名称中的名称将用于引用全局模块并搜索外部函数。functionA.然而,运行时并不等效:functionB需要调用比内部函数稍慢的全局函数,但functionA需要在每次运行时创建一个新的函数对象,这要慢得多。
为了证明它们是多么等价,让我们检查一下代码本身:
>>> functionA.__code__.co_consts
(None, <code object _innerFunction at 0x00000296F0B6A600, file "<stdin>", line 2>, 'functionA.<locals>._innerFunction')
Run Code Online (Sandbox Code Playgroud)
我们可以将代码对象视为存储在内部的 const functionA。让我们提取实际编译的字节码:
>>> functionA.__code__.co_consts[1].co_code
b'|\x00|\x01\x17\x00S\x00'
Run Code Online (Sandbox Code Playgroud)
现在让我们提取外部函数的字节码:
>>> _outerFunction.__code__.co_code
b'|\x00|\x01\x17\x00S\x00'
Run Code Online (Sandbox Code Playgroud)
这是完全相同的代码!
局部变量位置相同,代码编写相同,因此实际编译的代码完全相同。
>>> functionA.__code__.co_names
()
>>> functionB.__code__.co_names
('_outerFunction',)
Run Code Online (Sandbox Code Playgroud)
在 中functionB,名称不是保存在 const 中,而是保存在co_names稍后用于调用全局函数的 a 中。
functionA因此,内存占用的唯一区别是和的代码functionB:
>>> functionA.__code__.co_code
b'd\x01d\x02\x84\x00}\x02|\x02|\x00|\x01\x83\x02S\x00'
>>> functionB.__code__.co_code
b't\x00|\x00|\x01\x83\x02S\x00'
Run Code Online (Sandbox Code Playgroud)
functionA需要在每次运行时创建一个函数对象,并且内部函数的名称包括functionA.<locals>.,这需要一些额外的字节(可以忽略不计)并且运行速度较慢。
就运行时而言,如果多次调用内部函数,functionA速度会稍快一些:
def functionA(b, c):
def _innerFunction():
return b + c
for i in range(10_000_000):
_innerFunction() # Faster
def _outerFunction(b, c):
return b + c
def functionB(b, c):
for i in range(10_000_000):
_outerFunction(b, c) # Slower
def functionC(b, c):
outerFunction = _outerFunction
for i in range(10_000_000):
outerFunction(b, c) # Almost same as A but still slower.
py -m timeit -s "import temp;" "temp.functionA(1,2)"
1 loop, best of 5: 2.45 sec per loop
py -m timeit -s "import temp;" "temp.functionB(1,2)"
1 loop, best of 5: 3.21 sec per loop
py -m timeit -s "import temp;" "temp.functionC(1,2)"
1 loop, best of 5: 2.66 sec per loop
Run Code Online (Sandbox Code Playgroud)
如果多次调用外部函数,functionB由于避免创建函数对象,速度会明显加快:
def functionA(b, c): # Significantly slower
def _innerFunction():
return b + c
return _innerFunction()
def _outerFunction(b, c):
return b + c
def functionB(b, c): # Significantly faster
return _outerFunction(b, c)
py -m timeit -s "import temp;" "for i in range(10_000_000): temp.functionA(1,2)"
1 loop, best of 5: 9.46 sec per loop
py -m timeit -s "import temp;" "for i in range(10_000_000): temp.functionB(1,2)"
1 loop, best of 5: 5.48 sec per loop
Run Code Online (Sandbox Code Playgroud)
@KellyBundy:递归怎么样?
我的答案仅适用于顺序运行。
如果内部函数在 A 和 B 内部递归,则运行时间或内存消耗没有真正的区别,只是 A稍快一些。如果函数 A 和 B 自行递归,则 B 将不允许更深层次的递归,但速度会明显更快,并且需要更少的内存。
在顺序运行中,内存中没有差异,因为有一个函数对象要么存储在全局模块中,要么存储为不断重新创建的局部变量。A在外部 ( | ) 递归的情况下,B存在内存差异:_innerFunction存储对象的局部变量不会被清除,这意味着为每个向内递归创建了一个附加函数对象。在这个具体的例子中,我们可以看到Python和其他语言之间的一个重要区别——Python没有尾部调用优化,这意味着当我们向内递归时,帧不会被重用,变量也不会被删除,即使没有人将不再引用它们。
欢迎您尝试以下可视化效果。
我猜堆栈空间完全相同,所有差异都在堆上?
当您使用 Python 时,很难将事物分为堆栈和堆。C 堆栈是无关紧要的,因为 Python 使用自己的虚拟堆栈。Python 的堆栈实际上是由函数当前运行的框架链接的,并在调用函数时加载或创建。这就是它们也被称为堆栈帧的原因 - 有一个链接列表或帧“堆栈”,每个帧都有自己的迷你堆栈,称为值堆栈。栈和帧都存储在堆上。
使用这种方法有很多好处,其中一个不错的轶事是生成器函数。我实际上已经写了一篇关于该主题的文章,但简而言之,能够随意加载和卸载堆栈,允许我们在函数执行过程中暂停它,并且是生成器和 asyncio 的基础。
| 归档时间: |
|
| 查看次数: |
680 次 |
| 最近记录: |