在 Python 中嵌套函数时是不是有开销?

Posted

技术标签:

【中文标题】在 Python 中嵌套函数时是不是有开销?【英文标题】:Is there an overhead when nesting functions in Python?在 Python 中嵌套函数时是否有开销? 【发布时间】:2011-12-11 23:35:31 【问题描述】:

在 Python 中,如果我在父函数中有一个子函数,每次调用父函数时子函数是否“初始化”(创建)?将一个函数嵌套在另一个函数中是否存在任何性能开销?

【问题讨论】:

【参考方案1】:

是的,每次都会创建一个新对象。除非您将其紧密循环,否则这可能不是问题。分析会告诉您是否存在问题。

In [80]: def foo():
   ....:     def bar():
   ....:         pass
   ....:     return bar
   ....: 

In [81]: id(foo())
Out[81]: 29654024

In [82]: id(foo())
Out[82]: 29651384

【讨论】:

明确一点,每次都会创建一个新的函数对象。底层代码对​​象被重用。因此,无论内部函数的长度如何,开销都是恒定的。 FWIW,如果函数被装饰,那么只要函数对象被重新创建,装饰器就会被调用。 ... 虽然在很多情况下,这只是意味着您获得了两个或三个 O(1) 函数对象创建。对创建进行繁重工作的装饰器很少见,大多数只是创建一个小对象或闭包。 这两个 id 是偶然的。 Python 恰好为第二个bar() 使用了相同的内存,因为第一个立即被垃圾回收。尝试a = foo(); b = foo() 并比较 ID(它们会有所不同)。相关解释见***.com/questions/2906177/…。 @SvenMarnach:我知道你想说什么,但我的回答中的 id 不一样。 (此外,ipython 会自动将调用结果保存在变量中,因此它们都不会被 gc'd)【参考方案2】:

代码对象是预编译的,因此该部分没有开销。函数对象在每次调用时构建——它将函数名称绑定到代码对象、记录默认变量等。

执行摘要:它不是免费的。

>>> from dis import dis
>>> def foo():
        def bar():
                pass
        return bar

>>> dis(foo)
  2           0 LOAD_CONST               1 (<code object bar at 0x1017e2b30, file "<pyshell#5>", line 2>)
              3 MAKE_FUNCTION            0
              6 STORE_FAST               0 (bar)

  4           9 LOAD_FAST                0 (bar)
             12 RETURN_VALUE 

【讨论】:

【参考方案3】:

有影响,但在大多数情况下,它非常小,您不必担心 - 大多数重要的应用程序可能已经存在性能瓶颈,其影响比这个大几个数量级。而是担心代码的可读性和可重用性。

这里有一些代码比较了每次通过循环重新定义函数与重用预定义函数的性能。

import gc
from datetime import datetime

class StopWatch:
     def __init__(self, name):
         self.name = name

     def __enter__(self):
         gc.collect()
         self.start = datetime.now()

     def __exit__(self, type, value, traceback):
         elapsed = datetime.now()-self.start
         print '** Test "%s" took %s **' % (self.name, elapsed)

def foo():
     def bar():
          pass
     return bar

def bar2():
    pass

def foo2():
    return bar2

num_iterations = 1000000

with StopWatch('FunctionDefinedEachTime') as sw:
    result_foo = [foo() for i in range(num_iterations)]

with StopWatch('FunctionDefinedOnce') as sw:
    result_foo2 = [foo2() for i in range(num_iterations)]

当我在运行 OS X Lion 的 Macbook Air 上运行 Python 2.7 时,我得到:

** Test "FunctionDefinedEachTime" took 0:00:01.138531 **
** Test "FunctionDefinedOnce" took 0:00:00.270347 **

【讨论】:

【参考方案4】:

其他答案都很好,确实很好地回答了这个问题。我想补充一点,在 python 中使用 for 循环、生成函数等可以避免大多数内部函数。

考虑以下示例:

def foo():
    # I need to execute a function on two sets of arguments:
    argSet1 = (1, 3, 5, 7)
    argSet2 = (2, 4, 6, 8)

    # A Function could be executed on each set of args
    def bar(arg1, arg2, arg3, arg4):
        return (arg1 + arg2 + arg3 + arg4)

    total = 0
    for argSet in [argSet1, argSet2]:
      total += bar(*argSet)
    print( total )

    # Or a loop could be used on the argument sets
    total = 0
    for arg1, arg2, arg3, arg4 in [argSet1, argSet2]:
        total += arg1 + arg2 + arg3 + arg4
    print( total )

这个例子有点傻,但我希望你能明白我的意思。通常不需要内部函数。

【讨论】:

【参考方案5】:

我也对此感到好奇,所以我决定弄清楚这会产生多少开销。 TL;DR,答案不多。

Python 3.5.2 (default, Nov 23 2017, 16:37:01) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from timeit import timeit
>>> def subfunc():
...     pass
... 
>>> def no_inner():
...     return subfunc()
... 
>>> def with_inner():
...     def s():
...         pass
...     return s()
... 
>>> timeit('[no_inner() for _ in range(1000000)]', setup='from __main__     import no_inner', number=1)
0.22971350199986773
>>> timeit('[with_inner() for _ in range(1000000)]', setup='from __main__ import with_inner', number=1)
0.2847519510000893

我的直觉是查看百分比(with_inner 慢 24%),但在这种情况下,这个数字具有误导性,因为我们实际上永远不会只从外部函数返回内部函数的值,尤其是对于那些实际上什么都不做。 在犯了那个错误之后,我决定将它与其他常见的事情进行比较,看看这在什么时候重要,什么时候不重要:

    >>> def no_inner():
    ...     a = 
    ...     return subfunc()
    ... 
    >>> timeit('[no_inner() for _ in range(1000000)]', setup='from __main__ import no_inner', number=1)
    0.3099582109998664

看看这个,我们可以看到它比创建一个空 dict (the fast way) 花费的时间更少,所以如果你正在做任何不重要的事情,这可能根本不重要。

【讨论】:

【参考方案6】:

是的。这启用了闭包以及函数工厂。

闭包使内部函数在调用时记住其环境的状态。

def generate_power(number):

    # Define the inner function ...
    def nth_power(power):
        return number ** power

    return nth_power

例子

>>> raise_two = generate_power(2)
>>> raise_three = generate_power(3)

>>> print(raise_two(3))
8
>>> print(raise_three(5))
243
"""

【讨论】:

以上是关于在 Python 中嵌套函数时是不是有开销?的主要内容,如果未能解决你的问题,请参考以下文章

函数——基本语法,嵌套匿名高阶递归函数

python测试开发(02-闭包函数+装饰器)

Angular 8 getRole 不是嵌套对象中的函数

Python:嵌套的while循环将从第一个while开始“继续”,而不是嵌套的while

在 python 中删除某些嵌套的 JSON 对象时遇到问题

python笔记--3--函数生成器装饰器函数嵌套定义函数柯里化