嵌套函数的性能开销是多少?

Eri*_*rik 4 python performance function

在Python中,可以嵌套这样的函数:

def do_something():
    def helper():
        ....
    ....
Run Code Online (Sandbox Code Playgroud)

除非Python更巧妙地处理这种情况,否则helper每次都必须重新创建do_something.事实上,这样做会影响性能,而不是在主要功能之外创建辅助功能,如果是的话,它有多棒?

Ell*_*ioh 7

性能损失肯定存在。如果一个函数是在另一个函数的调用中创建的,则每次调用外部函数时都会真正创建函数对象。但这种惩罚很小,通常可以被忽略。特别是考虑到一个明显的事实:在大多数情况下,只有当嵌套函数不能放置在外部时,才应该创建它。

您可能需要嵌套函数的原因是需要访问嵌套函数内外部函数的作用域变量。通常,这将导致直接或间接从外部函数返回内部函数对象(如在装饰器中),或者可能将内部函数作为回调传递到某处。嵌套函数访问的变量将一直存在,直到嵌套函数对象被销毁为止,并且对于嵌套函数的不同实例,它们将是不同的,因为每个实例都看到来自不同作用域实例的变量。

在我看来,仅仅比较创建空内部函数所需的时间与使用放置在外部的相同函数所需的时间几乎毫无意义。性能差异纯粹是由代码行为的差异引起的。所需的代码行为应该让您选择放置函数的位置。

只是一个小例子:

def outer(n):
    v1 = "abc%d" % n
    v2 = "def"
    def inner():
        print locals().keys()
        return v1
    v1 = "_" + v1
    return inner
f1 = outer(1)
f2 = outer(2)
print f1()
print f2()
Run Code Online (Sandbox Code Playgroud)

输出是:

['v1']
_abc1
['v1']
_abc2
Run Code Online (Sandbox Code Playgroud)

关键时刻:

  1. 内部函数的 locals() 仅包括它使用的外部函数局部变量(v1,但不包括 v2)。

  2. v1 在函数对象创建后发生变化。然而,即使 v1 的类型是不可变的 (str),这些更改仍然对内部函数可见。因此,内部函数看到的是外部函数局部变量的真实子集,而不仅仅是在函数对象创建时存储的引用。幸运的是,内部函数对象的存在并不能阻止 v1 以外的作用域变量被破坏。如果我将 v2 值替换为在被销毁时打印某些内容的对象,则当外部函数退出时,它会立即打印消息。

  3. inner() 的不同实例不共享单个外部作用域实例:v1 值不同。

如果不使用嵌套函数,就无法实现所有这些效果。这就是为什么应该使用嵌套函数,而且实际上不会有性能损失:额外的行为需要额外的时间。如果您需要额外的行为,您应该使用嵌套函数。如果你不需要它,你就不应该。

  • 我不同意你的说法,即这是应该使用嵌套函数的唯一情况。通常,我将一个辅助函数放置在(唯一)使用它的函数中,因为:a)不需要用它来扰乱模块范围;b)因为这样可以更明显地看出辅助函数所属的位置。 (6认同)

Eri*_*rik 6

是的,在main函数中声明一个帮助器的速度比单独声明它们要慢:

### test_nested.py ###
import timeit
def foo():
    def bar():
        pass
    pass
print(timeit.timeit("foo()", setup="from __main__ import foo"))

### test_flat.py ###
import timeit
def foo():
    pass
def bar():
    pass
print(timeit.timeit("foo()", setup="from __main__ import foo, bar"))


### Shell ###
? python3 ./test_flat.py
0.42562198638916016
? python3 ./test_nested.py
0.5836758613586426
Run Code Online (Sandbox Code Playgroud)

这是一个约30%的放缓.请记住,在这个简单的案例中,创建和调用函数都是解释器所做的.在任何实际使用中,差异将小得多.

  • 确实,因为每次执行外部函数时都会重新创建内部的嵌套函数对象;创建对象涉及调用函数构造函数并传入(已编译的)代码对象。 (3认同)
  • 根据[Raymond Hettinger的回答"在Python中嵌套函数时是否存在开销?"](http://stackoverflow.com/a/7839697/4958),代码对象被重用,所以不管内部的长度如何函数(例如),唯一的开销是从O(1)创建的函数对象.因此嵌套函数不是免费的(就像我猜的那样添加一个赋值),但是当你的嵌套函数"太大"时你也不必担心:无论你的嵌套函数是微不足道的还是非常重要的,开销都是一样的. (2认同)