内联 Python 函数定义的性能

Performance of inline Python function definitions

一个比我更了解函数定义内部的人的一般问题。

一般来说,做这样的事情是否有性能折衷:

def my_function():
    def other_function():
        pass

    # do some stuff
    other_function()

对战:

def other_function():
    pass

def my_function():
    # do some stuff
    other_function()

我以前见过开发人员内联函数以将一个小的、单一用途的函数保持在接近实际使用它的代码附近,但我一直想知道做这样的事情是否会导致内存(或计算)性能下降.

想法?

在我的 mac 上使用 timeit 似乎有利于在模块级别定义函数(稍微),显然结果可能因一台计算机而异...:[=​​11=]

>>> import timeit
>>> def fun1():
...   def foo():
...     pass
...   foo()
... 
>>> def bar():
...   pass
... 
>>> def fun2():
...   bar()
... 
>>> timeit.timeit('fun1()', 'from __main__ import fun1')
0.2706329822540283
>>> timeit.timeit('fun2()', 'from __main__ import fun2')
0.23086285591125488

请注意,这种差异很小 (~10%),因此它不会对程序的运行时间产生重大影响,除非这是一个非常紧密的循环。

在另一个函数中定义一个函数的最常见原因是在闭包中获取 out 函数的局部变量。如果您不需要闭包,那么您应该选择最容易阅读 的变体。 (我的偏好几乎总是将功能放在模块级别)。

将较大的函数拆分为更易读、更小的函数是编写 Pythonic 代码的一部分——您要完成的任务应该很明显,较小的函数更易于阅读、检查错误、维护和重用。

一如既往,"which has better performance" 问题应始终由 profiling the code 解决,也就是说,它通常取决于方法的签名和您的代码在做什么。

例如如果你将一个大字典传递给一个单独的函数而不是引用一个本地框架,你最终会得到与从另一个调用 void 函数不同的性能特征。

例如,这里有一些微不足道的行为:

import profile
import dis

def callee():
    for x in range(10000):
        x += x
    print("let's have some tea now")

def caller():
    callee()


profile.run('caller()')

let's have some tea now
         26 function calls in 0.002 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        2    0.000    0.000    0.000    0.000 :0(decode)
        2    0.000    0.000    0.000    0.000 :0(getpid)
        2    0.000    0.000    0.000    0.000 :0(isinstance)
        1    0.000    0.000    0.000    0.000 :0(range)
        1    0.000    0.000    0.000    0.000 :0(setprofile)
        2    0.000    0.000    0.000    0.000 :0(time)
        2    0.000    0.000    0.000    0.000 :0(utf_8_decode)
        2    0.000    0.000    0.000    0.000 :0(write)
        1    0.002    0.002    0.002    0.002 <ipython-input-3-98c87a49b247>:4(callee)
        1    0.000    0.000    0.002    0.002 <ipython-input-3-98c87a49b247>:9(caller)
        1    0.000    0.000    0.002    0.002 <string>:1(<module>)
        2    0.000    0.000    0.000    0.000 iostream.py:196(write)
        2    0.000    0.000    0.000    0.000 iostream.py:86(_is_master_process)
        2    0.000    0.000    0.000    0.000 iostream.py:95(_check_mp_mode)
        1    0.000    0.000    0.002    0.002 profile:0(caller())
        0    0.000             0.000          profile:0(profiler)
        2    0.000    0.000    0.000    0.000 utf_8.py:15(decode)

对比

import profile
import dis

def all_in_one():
    def passer():
        pass
    passer()
    for x in range(10000):
        x += x
    print("let's have some tea now")    

let's have some tea now
         26 function calls in 0.002 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        2    0.000    0.000    0.000    0.000 :0(decode)
        2    0.000    0.000    0.000    0.000 :0(getpid)
        2    0.000    0.000    0.000    0.000 :0(isinstance)
        1    0.000    0.000    0.000    0.000 :0(range)
        1    0.000    0.000    0.000    0.000 :0(setprofile)
        2    0.000    0.000    0.000    0.000 :0(time)
        2    0.000    0.000    0.000    0.000 :0(utf_8_decode)
        2    0.000    0.000    0.000    0.000 :0(write)
        1    0.002    0.002    0.002    0.002 <ipython-input-3-98c87a49b247>:4(callee)
        1    0.000    0.000    0.002    0.002 <ipython-input-3-98c87a49b247>:9(caller)
        1    0.000    0.000    0.002    0.002 <string>:1(<module>)
        2    0.000    0.000    0.000    0.000 iostream.py:196(write)
        2    0.000    0.000    0.000    0.000 iostream.py:86(_is_master_process)
        2    0.000    0.000    0.000    0.000 iostream.py:95(_check_mp_mode)
        1    0.000    0.000    0.002    0.002 profile:0(caller())
        0    0.000             0.000          profile:0(profiler)
        2    0.000    0.000    0.000    0.000 utf_8.py:15(decode)

两者使用相同数量的函数调用并且没有性能差异,这支持了我的说法,即在特定情况下进行测试确实很重要。

您可以看到我有一个未使用的 disassembly 模块导入。这是另一个有用的模块,可以让您查看您的代码在做什么(尝试 dis.dis(my_function))。我会 post 我生成的测试代码的配置文件,但它只会向您显示与解决问题或了解您的代码中实际发生的事情无关的更多详细信息。