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

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

【问题讨论】:

    标签: python function nested


    【解决方案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(它们会有所不同)。相关解释见stackoverflow.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
            """
            

            【讨论】:

              猜你喜欢
              • 2021-09-09
              • 2012-12-12
              • 2021-03-28
              • 1970-01-01
              • 1970-01-01
              • 2018-05-10
              • 2022-01-10
              • 2010-12-08
              相关资源
              最近更新 更多