【问题标题】:Do Python nested functions copy-on-could-write?Python嵌套函数是否可以写入时复制?
【发布时间】:2017-12-23 23:03:51
【问题描述】:

请原谅 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

寻找其他可能会增加运行时间的东西我偶然发现了以下书呆子:

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 是否在幕后做了任何其他极其聪明的事情?与嵌套相比,这与嵌套的执行速度慢有关吗?

【问题讨论】:

    标签: python function nested copy-on-write


    【解决方案1】:

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

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

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

    【讨论】:

    • 感谢您的回答。我想我通过在删除引用和创建新对象之间创建大量新对象来防范那个特定的陷阱。那不行吗?
    • 我以为我什至想到了这一点。我确定在我的列表中,有一个大小合适的子列表!好吧,也许一个还不够。 - 好吧,你说服了我。也许,在你的答案中解释一下这个内存问题,也许代码和闭包对象是否被缓存,我很乐意接受。
    【解决方案2】:

    至于时差,看一下这两个函数的字节码就能得到一些提示。 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() 时都会创建一个新的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>
    

    【讨论】:

    • 感谢您的回答。我真的必须研究dis 似乎非常有用。 - 关于内存重用问题,正如我向其他回答者提到的那样,我知道这一点,但你的例子仍然具有指导意义。 inner2 和第二个 inner1 甚至有不同的代码对象,但仍然是相同的 id。此外,在该示例中,在两者之间创建列表列表 x 确实可以防止使用相同的内存。我希望我能更好地理解内存管理的东西!
    • 两个非常好的答案。但是您的内容比@Antti 的内容要多一些,所以我接受了。谢谢,
    猜你喜欢
    • 2012-08-15
    • 2021-11-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-23
    • 1970-01-01
    • 2020-11-30
    • 1970-01-01
    相关资源
    最近更新 更多