为什么会这样?我应该避免在调用期间指定参数值吗?
一般来说,不会。 您能够看到这一点的真正原因是因为您使用的函数根本不是计算密集型。因此,在提供参数的情况下发出额外的字节码命令所需的时间可以通过计时来检测。
例如,如果你有一个更密集的表单函数:
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 命令和一个BUILD_MAP。
- 与列表解包一样,
foo(**d) 将减少执行,因为将提供已构建的列表。
总而言之,不同调用案例的执行时间排序为:
defaults < positionals < keyword arguments < list unpacking < dictionary unpacking
我建议在这些情况下使用dis.dis 并查看它们的差异。
总结:
正如@goofd 在评论中指出的那样,这确实是人们不应该担心的事情,它确实取决于用例。如果您经常从计算的角度调用“轻”函数,则指定默认值会稍微提高速度。如果您经常提供不同的值,这几乎不会产生任何结果。
因此,它可能可以忽略不计,并且试图从像这样的模糊边缘案例中获得提升确实是在推动它。如果您发现自己正在这样做,您可能需要查看 PyPy 和 Cython 之类的内容。