Python 嵌套函数可以写时复制吗?
Do Python nested functions copy-on-could-write?
请原谅Python爱好者一个主要是学术性的问题。
我感兴趣的是嵌套函数的成本(如果有的话)- 不是那些在功能上合理的使用闭包等的函数,而是保持外部命名空间整洁的多样性。
所以我做了一个简单的测量:
def inner(x):
return x*x
def flat(x):
return inner(x)
def nested(x):
def inner(x):
return x*x
return inner(x)
# just to get a feel of the cost of having two more lines
def fake_nested(x):
y = x
z = x
return inner(x)
from timeit import timeit
print(timeit('f(3)', globals=dict(f=flat)))
print(timeit('f(3)', globals=dict(f=nested)))
print(timeit('f(3)', globals=dict(f=fake_nested)))
# 0.17055258399341255
# 0.23098028398817405
# 0.19381927204085514
所以似乎有一些开销,而且似乎比多两行所解释的要多。
然而,似乎内部 def
语句在每次调用外部函数时都没有被评估,实际上内部函数对象似乎被缓存了:
def nested(x):
def inner(x):
return x*x
print(id(inner), id(inner.__code__), id(inner.__closure__))
return inner(x)
nested(3)
x = [list(range(i)) for i in range(5000)] # create some memory pressure
nested(3)
# 139876371445960 139876372477824 8845216
# 139876371445960 139876372477824 8845216
在寻找可能会增加运行时间的其他东西时,我偶然发现了以下 nerdgasm:
def nested(x):
def inner(x):
return x*x
print(id(inner), id(inner.__code__), id(inner.__closure__))
return inner
nested(3)
x = [list(range(i)) for i in range(5000)] # create some memory pressure
a = nested(3)
x = [list(range(i)) for i in range(5000)] # create some memory pressure
nested(3)
# 139906265032768 139906264446704 8845216
# 139906265032768 139906264446704 8845216
# 139906264258624 139906264446704 8845216
似乎如果Python检测到缓存嵌套函数有外部引用,那么它会创建一个新的函数对象。
现在 - 假设到目前为止我的推理还没有完全结束 - 我的问题:这有什么用?
我的第一个想法是 "Ok, if the user has a reference to the cached function, they may have messsed with it, so better make a clean new one." 但转念一想,这似乎并没有被洗掉,因为副本不是深层副本,而且如果用户弄乱了函数然后丢弃了引用怎么办?
补充问题:Python在幕后是否还做了其他高明的事情?这是否与 nested 与 flat 相比执行速度较慢有关?
你的推理完全不对。 Python 每次在正常程序流程中遇到 def
时 总是创建一个新的函数对象 - 没有例外。
只是在CPython中,新创建的函数id
很可能与旧函数相同。参见 "Why does id({}) == id({}) and id([]) == id([]) in CPython?"。
现在,如果你保存了对内部函数的引用,在创建下一个函数之前它不会被删除,自然新函数不能共存于同一个内存地址。
至于时间差,看看这两个函数的字节码就可以提供一些提示。 nested()
和 fake_nested()
之间的比较表明,虽然 fake_nested
只是加载已经定义的全局函数 inner()
,但嵌套必须创建此函数。这里会有一些开销,而其他操作会相对较快。
>>> import dis
>>> dis.dis(flat)
2 0 LOAD_GLOBAL 0 (inner)
3 LOAD_FAST 0 (x)
6 CALL_FUNCTION 1
9 RETURN_VALUE
>>> dis.dis(nested)
2 0 LOAD_CONST 1 (<code object inner at 0x7f2958a33830, file "<stdin>", line 2>)
3 MAKE_FUNCTION 0
6 STORE_FAST 1 (inner)
4 9 LOAD_FAST 1 (inner)
12 LOAD_FAST 0 (x)
15 CALL_FUNCTION 1
18 RETURN_VALUE
>>> dis.dis(fake_nested)
2 0 LOAD_FAST 0 (x)
3 STORE_FAST 1 (y)
3 6 LOAD_FAST 0 (x)
9 STORE_FAST 2 (z)
4 12 LOAD_GLOBAL 0 (inner)
15 LOAD_FAST 0 (x)
18 CALL_FUNCTION 1
21 RETURN_VALUE
至于内部函数缓存部分,另一个答案已经阐明了每次nested() [=33=时都会创建一个新的inner()
函数].要更清楚地看到这一点,请查看 nested()
、cond_nested()
的以下变体,它基于标志创建具有两个不同名称的相同函数。第一次创建这个带有 False
标志的 运行 第二个函数 inner2()
。接下来,当我将标志更改为 True
时,将创建第一个函数 inner1()
并释放第二个函数 inner2()
占用的内存。因此,如果我再次 运行 并使用 True
标志,第一个函数将再次创建并分配给第二个函数占用的内存,该内存现在是空闲的。
>>> def cond_nested(x, flag=False):
... if flag:
... def inner1(x):
... return x*x
... cond_nested.func = inner1
... print id(inner1)
... return inner1(x)
... else:
... def inner2(x):
... return x*x
... cond_nested.func = inner2
... print id(inner2)
... return inner2(x)
...
>>> cond_nested(2)
139815557561112
4
>>> cond_nested.func
<function inner2 at 0x7f2958a47b18>
>>> cond_nested(2, flag=True)
139815557561352
4
>>> cond_nested.func
<function inner1 at 0x7f2958a47c08>
>>> cond_nested(3, flag=True)
139815557561112
9
>>> cond_nested.func
<function inner1 at 0x7f2958a47b18>
请原谅Python爱好者一个主要是学术性的问题。
我感兴趣的是嵌套函数的成本(如果有的话)- 不是那些在功能上合理的使用闭包等的函数,而是保持外部命名空间整洁的多样性。
所以我做了一个简单的测量:
def inner(x):
return x*x
def flat(x):
return inner(x)
def nested(x):
def inner(x):
return x*x
return inner(x)
# just to get a feel of the cost of having two more lines
def fake_nested(x):
y = x
z = x
return inner(x)
from timeit import timeit
print(timeit('f(3)', globals=dict(f=flat)))
print(timeit('f(3)', globals=dict(f=nested)))
print(timeit('f(3)', globals=dict(f=fake_nested)))
# 0.17055258399341255
# 0.23098028398817405
# 0.19381927204085514
所以似乎有一些开销,而且似乎比多两行所解释的要多。
然而,似乎内部 def
语句在每次调用外部函数时都没有被评估,实际上内部函数对象似乎被缓存了:
def nested(x):
def inner(x):
return x*x
print(id(inner), id(inner.__code__), id(inner.__closure__))
return inner(x)
nested(3)
x = [list(range(i)) for i in range(5000)] # create some memory pressure
nested(3)
# 139876371445960 139876372477824 8845216
# 139876371445960 139876372477824 8845216
在寻找可能会增加运行时间的其他东西时,我偶然发现了以下 nerdgasm:
def nested(x):
def inner(x):
return x*x
print(id(inner), id(inner.__code__), id(inner.__closure__))
return inner
nested(3)
x = [list(range(i)) for i in range(5000)] # create some memory pressure
a = nested(3)
x = [list(range(i)) for i in range(5000)] # create some memory pressure
nested(3)
# 139906265032768 139906264446704 8845216
# 139906265032768 139906264446704 8845216
# 139906264258624 139906264446704 8845216
似乎如果Python检测到缓存嵌套函数有外部引用,那么它会创建一个新的函数对象。
现在 - 假设到目前为止我的推理还没有完全结束 - 我的问题:这有什么用?
我的第一个想法是 "Ok, if the user has a reference to the cached function, they may have messsed with it, so better make a clean new one." 但转念一想,这似乎并没有被洗掉,因为副本不是深层副本,而且如果用户弄乱了函数然后丢弃了引用怎么办?
补充问题:Python在幕后是否还做了其他高明的事情?这是否与 nested 与 flat 相比执行速度较慢有关?
你的推理完全不对。 Python 每次在正常程序流程中遇到 def
时 总是创建一个新的函数对象 - 没有例外。
只是在CPython中,新创建的函数id
很可能与旧函数相同。参见 "Why does id({}) == id({}) and id([]) == id([]) in CPython?"。
现在,如果你保存了对内部函数的引用,在创建下一个函数之前它不会被删除,自然新函数不能共存于同一个内存地址。
至于时间差,看看这两个函数的字节码就可以提供一些提示。 nested()
和 fake_nested()
之间的比较表明,虽然 fake_nested
只是加载已经定义的全局函数 inner()
,但嵌套必须创建此函数。这里会有一些开销,而其他操作会相对较快。
>>> import dis
>>> dis.dis(flat)
2 0 LOAD_GLOBAL 0 (inner)
3 LOAD_FAST 0 (x)
6 CALL_FUNCTION 1
9 RETURN_VALUE
>>> dis.dis(nested)
2 0 LOAD_CONST 1 (<code object inner at 0x7f2958a33830, file "<stdin>", line 2>)
3 MAKE_FUNCTION 0
6 STORE_FAST 1 (inner)
4 9 LOAD_FAST 1 (inner)
12 LOAD_FAST 0 (x)
15 CALL_FUNCTION 1
18 RETURN_VALUE
>>> dis.dis(fake_nested)
2 0 LOAD_FAST 0 (x)
3 STORE_FAST 1 (y)
3 6 LOAD_FAST 0 (x)
9 STORE_FAST 2 (z)
4 12 LOAD_GLOBAL 0 (inner)
15 LOAD_FAST 0 (x)
18 CALL_FUNCTION 1
21 RETURN_VALUE
至于内部函数缓存部分,另一个答案已经阐明了每次nested() [=33=时都会创建一个新的inner()
函数].要更清楚地看到这一点,请查看 nested()
、cond_nested()
的以下变体,它基于标志创建具有两个不同名称的相同函数。第一次创建这个带有 False
标志的 运行 第二个函数 inner2()
。接下来,当我将标志更改为 True
时,将创建第一个函数 inner1()
并释放第二个函数 inner2()
占用的内存。因此,如果我再次 运行 并使用 True
标志,第一个函数将再次创建并分配给第二个函数占用的内存,该内存现在是空闲的。
>>> def cond_nested(x, flag=False):
... if flag:
... def inner1(x):
... return x*x
... cond_nested.func = inner1
... print id(inner1)
... return inner1(x)
... else:
... def inner2(x):
... return x*x
... cond_nested.func = inner2
... print id(inner2)
... return inner2(x)
...
>>> cond_nested(2)
139815557561112
4
>>> cond_nested.func
<function inner2 at 0x7f2958a47b18>
>>> cond_nested(2, flag=True)
139815557561352
4
>>> cond_nested.func
<function inner1 at 0x7f2958a47c08>
>>> cond_nested(3, flag=True)
139815557561112
9
>>> cond_nested.func
<function inner1 at 0x7f2958a47b18>