Python中多个重复Lambda函数的内存成本是多少?

4
我想知道Python解释器实际上如何在内存中处理lambda函数。
如果我有以下内容:
def squares():
    return [(lambda x: x**2)(num) for num in range(1000)]

这会在内存中创建1000个lambda函数实例吗?还是Python足够聪明,知道这1000个lambda函数是相同的,因此将它们作为一个函数存储在内存中?


1
所有这些函数对象都会立即死亡。 - user2357112
你的示例函数返回了一个列表,它不是生成器。这看起来像是一个人工的例子,没有人会写出这样的代码。这是否是你想要询问的内容? - smci
1
有谁会从函数中返回一个列表呢? - Alex Wallish
@AlexWallish:没有人会故意在列表中返回完全相同的结果1000次(这会强制展开内容,而不像生成器那样)。 (为什么要这样做?)告诉我们更多上下文信息。我试图理解这是否是一个真正的问题。 - smci
这是一个明显的例子,正如@gilch所说,重点显然不是制作一个平方列表,而是询问Python在内存中处理lambda函数的方式。 - Alex Wallish
2个回答

1
TL;DR:您示例中lambda对象的内存成本是一个lambda的大小,但仅在squares()函数运行时有效,即使您保留对其返回值的引用,因为返回的列表不包含任何lambda对象。
但是,即使在您保留了从相同lambda表达式(或def语句)创建的多个函数实例的情况下,它们也共享相同的代码对象,因此每个附加实例的内存成本都小于第一个实例的成本。
在您的示例中,
[(lambda x: x**2)(num) for num in range(1000)]

你只是在列表中存储了lambda调用的结果,而不是lambda本身,因此lambda对象的内存将被释放。 lambda对象何时被垃圾回收取决于你的Python实现。CPython应该能够立即执行它,因为每个循环引用计数都会降至0:
>>> class PrintsOnDel:
...     def __del__(self):
...       print('del')  # We can see when this gets collected.
...
>>> [[PrintsOnDel(), print(x)][-1] for x in [1, 2, 3]]  # Freed each loop.
1
del
2
del
3
del
[None, None, None]

PyPy是另一回事。

>>>> from __future__ import print_function
>>>> class PrintsOnDel:
....   def __del__(self):
....     print('del')
....
>>>> [[PrintsOnDel(), print(x)][-1] for x in [1, 2, 3]]
1
2
3
[None, None, None]
>>>> import gc
>>>> gc.collect()  # Not freed until the gc actually runs!
del
del
del
0

它将随时间创建1000个不同的lambda实例,但它们不会全部一次性存在于内存中(在CPython中),并且它们都指向相同的代码对象,因此拥有多个函数实例并不像听起来那么糟糕:
>>> a, b = [lambda x: x**2 for x in [1, 2]]
>>> a is b  # Different lambda objects...
False
>>> a.__code__ is b.__code__  # ...point to the same code object.
True

拆解字节码可以帮助您准确了解解释器正在执行的操作:
>>> from dis import dis
>>> dis("[(lambda x: x**2)(num) for num in range(1000)]")
  1           0 LOAD_CONST               0 (<code object <listcomp> at 0x000001D11D066870, file "<dis>", line 1>)
              2 LOAD_CONST               1 ('<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_NAME                0 (range)
              8 LOAD_CONST               2 (1000)
             10 CALL_FUNCTION            1
             12 GET_ITER
             14 CALL_FUNCTION            1
             16 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x000001D11D066870, file "<dis>", line 1>:
  1           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                16 (to 22)
              6 STORE_FAST               1 (num)
              8 LOAD_CONST               0 (<code object <lambda> at 0x000001D11D0667C0, file "<dis>", line 1>)
             10 LOAD_CONST               1 ('<listcomp>.<lambda>')
             12 MAKE_FUNCTION            0
             14 LOAD_FAST                1 (num)
             16 CALL_FUNCTION            1
             18 LIST_APPEND              2
             20 JUMP_ABSOLUTE            4
        >>   22 RETURN_VALUE

Disassembly of <code object <lambda> at 0x000001D11D0667C0, file "<dis>", line 1>:
  1           0 LOAD_FAST                0 (x)
              2 LOAD_CONST               1 (2)
              4 BINARY_POWER
              6 RETURN_VALUE

请注意每个循环中的12 MAKE_FUNCTIONinstruction。它确实每次都会创建一个新的lambda实例。CPython的虚拟机是一个堆栈机器。参数由其他指令推入堆栈,然后由需要它们的后续指令消耗。请注意上面的MAKE_FUNCTION指令还推入了一个参数。
LOAD_CONST               0 (<code object <lambda>...

它重新使用了代码对象。

1
您的代码创建了一个 lambda 函数并调用它 1000 次,而不是在每次迭代中创建一个新对象。因为在每次迭代中,您存储了 lambda 函数的结果而不是函数本身。这相当于:
def square(x): return x*x # or def square x = lambda x: x*x
[square(x) for x in range(1000)]

相反,我们将为每个迭代创建一个lambda函数对象。看这个虚拟示例:

[lambda x: x*x for _ in range(3)]

给予:
[<function <listcomp>.<lambda> at 0x294358950>, 
 <function <listcomp>.<lambda> at 0x294358c80>, 
 <function <listcomp>.<lambda> at 0x294358378>]

这些lambda的内存地址都是不同的。因此,为每个lambda创建了一个不同的对象。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接