我设置了一个简单的自定义函数,它接受一些默认参数(Python 3.5):
def foo(a=10, b=20, c=30, d=40): return a * b + c * d
并且在有或没有指定参数值的情况下定时调用它:
没有指定参数:
%timeit foo() The slowest run took 7.83 times longer than the fastest. This could mean that an intermediate result is being cached 1000000 loops, best of 3: 361 ns per loop
指定参数:
%timeit foo(a=10, b=20, c=30, d=40) The slowest run took 12.83 times longer than the fastest. This could mean that an intermediate result is being cached 1000000 loops, best of 3: 446 ns per loop
正如您所看到的,指定参数的调用和未指定参数的调用所需的时间会有一些明显的增加.在简单的一次性调用中,这可能是微不足道的,但是如果对函数进行大量调用,则开销会变得更加明显:
没有参数:
%timeit for i in range(10000): foo() 100 loops, best of 3: 3.83 ms per loop
随着参数:
%timeit for i in range(10000): foo(a=10, b=20, c=30, d=40) 100 loops, best of 3: 4.68 ms per loop
同样的情况存在,且在Python 2.7,其中这些调用之间的时间差实际上是一个有点大foo() -> 291ns
和foo(a=10, b=20, c=30, d=40) -> 410ns
为什么会这样?我通常应该尝试避免在调用期间指定参数值吗?
为什么会这样?我应该避免在通话期间指定参数值吗?
一般来说,没有.您能够看到这个的真正原因是因为您使用的功能根本不是计算密集型的.这样,可以通过定时检测在提供自变量的情况下发出的附加字节代码命令所需的时间.
例如,如果您具有更强大的表单功能:
def foo_intensive(a=10, b=20, c=30, d=40): [i * j for i in range(a * b) for j in range(c * d)]
它几乎没有显示出所需的时间差异:
%timeit foo_intensive() 10 loops, best of 3: 32.7 ms per loop %timeit foo_intensive(a=10, b=20, c=30, d=40) 10 loops, best of 3: 32.7 ms per loop
即使缩放到更多调用,执行函数体所需的时间也只是胜过附加字节码指令引入的小开销.
查看为每个调用案例发出的生成字节代码的一种方法是创建一个包装foo
并以不同方式调用它的函数.现在,让我们fooDefault
使用默认参数和fooKwargs()
指定关键字参数的函数创建调用:
# call foo without arguments, using defaults def fooDefault(): foo() # call foo with keyword arguments def fooKw(): foo(a=10, b=20, c=30, d=40)
现在dis
我们可以看到它们之间的字节代码差异.对于默认的版本中,我们可以看到,基本上是一个命令发出(忽略POP_TOP
这是目前在这两种情况下)的函数调用,CALL_FUNCTION
:
dis.dis(fooDefaults) 2 0 LOAD_GLOBAL 0 (foo) 3 CALL_FUNCTION 0 (0 positional, 0 keyword pair) 6 POP_TOP 7 LOAD_CONST 0 (None) 10 RETURN_VALUE
另一方面,在使用关键字的情况下,为了将参数名称和值加载到值堆栈中,还发出了8个LOAD_CONST
命令(即使在这种情况下加载数字可能非常快,因为它们被缓存):(a, b, c, d)
(10, 20, 30, 40)
< 256
dis.dis(fooKwargs) 2 0 LOAD_GLOBAL 0 (foo) 3 LOAD_CONST 1 ('a') # call starts 6 LOAD_CONST 2 (10) 9 LOAD_CONST 3 ('b') 12 LOAD_CONST 4 (20) 15 LOAD_CONST 5 ('c') 18 LOAD_CONST 6 (30) 21 LOAD_CONST 7 ('d') 24 LOAD_CONST 8 (40) 27 CALL_FUNCTION 1024 (0 positional, 4 keyword pair) 30 POP_TOP # call ends 31 LOAD_CONST 0 (None) 34 RETURN_VALUE
此外,对于关键字参数不为零的情况,通常需要一些额外的步骤.(例如ceval/_PyEval_EvalCodeWithName()
).
即使这些是非常快的命令,但它们总结了.越多的参数越大,并且当实际执行对函数的许多调用时,这些参数堆积起来导致执行时间的感觉差异.
这些的直接结果是我们指定的值越多,必须发出的命令越多,函数运行得越慢.此外,指定位置参数,解压缩位置参数和解压缩关键字参数都具有与之关联的不同开销量:
位置参数foo(10, 20, 30, 40)
:需要4个附加命令来加载每个值.
列表解包foo(*[10, 20, 30, 40])
:4个LOAD_CONST
命令和一个附加BUILD_LIST
命令.
使用列表可以foo(*l)
减少执行,因为我们提供了一个包含值的已构建列表.
字典解包foo(**{'a':10, 'b':20, 'c': 30, 'd': 40})
:8个LOAD_CONST
命令和a BUILD_MAP
.
与列表一样,解压缩foo(**d)
将减少执行,因为将提供内置列表.
总而言之,不同呼叫情况的执行时间顺序为:
defaults < positionals < keyword arguments < list unpacking < dictionary unpacking
我建议使用dis.dis
这些案例,看看他们的差异.
正如@goofd在评论中指出的那样,这确实是一个不应该担心的事情,它确实取决于用例.如果您经常从计算角度调用'轻型'功能,则指定默认值会略微提高速度.如果您经常提供不同的值,则几乎不会产生任何值.
因此,它可能是微不足道的,并试图从这样的模糊边缘案例中获得提升,这真的是推动它.如果你发现自己这样做,你可能想看看像PyPy
和的东西Cython
.