Python嵌套函数是否可以写入拷贝?

时间:2017-12-23 23:03:51

标签: python function nested copy-on-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检测到对缓存的嵌套函数有外部引用,那么它会创建一个新的函数对象。

现在 - 假设我的推理到目前为止还没有完全消失 - 我的问题是:这有什么好处?

我的第一个想法是"好的,如果用户有对缓存功能的引用,他们可能会搞乱它,所以最好做一个干净的新功能。"但是在第二个想法似乎没有洗,因为副本不是一个深层副本,如果用户混淆了该功能然后抛出参考怎么办?

补充问题:Python在幕后做了其他任何非常聪明的事情吗?这与嵌套的执行速度较慢有关吗?

2 个答案:

答案 0 :(得分:5)

你的推理完全没了。 Python总是在正常的程序流中创建一个新的函数对象每个时间def - 没有例外。

只是在 CPython 中,新创建的函数的id可能与旧版本的"CORP"相同。请参阅"Why does id({}) == id({}) and id([]) == id([]) in CPython?"

现在,如果保存了对内部函数的引用,则在创建下一个函数之前不会删除它,当然新函数不能在同一个内存地址中共存。

答案 1 :(得分:1)

对于时差,查看两个函数的字节码可提供一些提示。 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        

对于内部函数缓存部分,另一个答案已经阐明每次运行嵌套()时都会创建一个新的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>