Python命名参数、字典与NumPy数组传参的性能差异及优化问询
Python命名参数与字典/NumPy数组传参的性能差异及优化
问题背景
使用Python 3.9.12开发应用时,某需频繁调用的函数包含大量必要参数。最初采用带默认值的命名参数实现,速度最快;尝试用字典和NumPy数组优化参数组织后,二者速度均显著变慢。已移除不必要的辅助函数调用,性能趋势仍一致。需解决两个问题:
- 为何带默认值的命名参数版本速度至少快2.6倍?
- 如何在保留字典传参的良好组织性的同时不牺牲执行效率?
测试代码
import numpy as np import time as time def get_par_default(): return {'a':0.16, 'b':0.18, 'F0':0.0, 's':0.0, 'eps':3.0e-3, 'V':3.0, 'p0':0.30575896, 'p10':0.48998486,'q0':0.06468597,'q10':0.27093151} def get_par_arr_default(): return np.asarray([0.16,0.18,0.0,0.0,3.0e-3,3.0,0.30575896,0.48998486,0.06468597,0.27093151]) def namedargs(Ep,Eq,a=0.16,b=0.18,F0=0.12,s=0.0,eps=3.0e-3,V=3.0,p0=0.5,p10=0.956,q0=0.1,q10=0.306): # 模拟计算逻辑 ans = Ep*Eq*a*b*F0*s*eps*V*p0*p10*q0*q10 return ans def dictargs(Ep,Eq,pars=get_par_default()): # 模拟计算逻辑 ans = Ep*Eq*pars['a']*pars['b']*pars['F0']*pars['s']*pars['eps']*pars['V']*pars['p0']*pars['p10']*pars['q0']*pars['q10'] return ans def nparrargs(Ep,Eq,pars=get_par_arr_default()): # 模拟计算逻辑 ans = Ep*Eq*pars[0]*pars[1]*pars[2]*pars[3]*pars[4]*pars[5]*pars[6]*pars[7]*pars[8]*pars[9] return ans if __name__ == "__main__": Ep=20.0 Eq=10.0 start = time.time() for i in range(10000): namedargs(Ep,Eq) end = time.time() print('Evaluation Time: {:1.5f} sec.'.format(end-start)) start = time.time() args=get_par_default() for i in range(10000): dictargs(Ep,Eq,pars=args) end = time.time() print('Evaluation Time: {:1.5f} sec.'.format(end-start)) start = time.time() args=get_par_arr_default() for i in range(10000): nparrargs(Ep,Eq,pars=args) end = time.time() print('Evaluation Time: {:1.5f} sec.'.format(end-start))
运行结果
Evaluation Time: 0.00251 sec. Evaluation Time: 0.00394 sec. Evaluation Time: 0.01101 sec.
问题解答
1. 命名参数更快的核心原因
- 局部变量访问开销最低:带默认值的命名参数会直接存入函数的局部作用域,Python对局部变量的访问是通过栈帧索引直接定位的,几乎没有额外开销。
- 字典键查找有额外成本:字典传参需要先计算键的哈希值,再在哈希表中定位,还要处理可能的哈希冲突,每次
pars['a']这样的调用都要完成一次完整的查找流程,多次累加后开销明显。 - NumPy数组索引层级更多:NumPy数组的索引访问需要做边界检查、调用数组内部方法处理元素读取,加上数组本身的类型封装,比字典查找的执行层级更多,因此速度最慢。
2. 保留字典组织性且不牺牲效率的方案
方案一:字典解包到命名参数
直接复用原命名参数函数,调用时用**语法解包字典,既保留字典对参数的统一管理,又利用命名参数的高效访问:
args = get_par_default() # 解包字典调用命名参数函数 for i in range(10000): namedargs(Ep, Eq, **args)
这种方式的性能和直接调用命名参数几乎一致,是最简单的折中方案。
方案二:使用数据类(dataclasses)
Python 3.7+的dataclasses可以将参数组织成结构化实例,属性访问速度接近局部变量,比字典更高效:
from dataclasses import dataclass @dataclass class Params: a: float = 0.16 b: float = 0.18 F0: float = 0.12 s: float = 0.0 eps: float = 3.0e-3 V: float = 3.0 p0: float = 0.5 p10: float = 0.956 q0: float = 0.1 q10: float = 0.306 def dataclassargs(Ep, Eq, pars: Params = Params()): ans = Ep*Eq*pars.a*pars.b*pars.F0*pars.s*pars.eps*pars.V*pars.p0*pars.p10*pars.q0*pars.q10 return ans
数据类的属性访问无需哈希查找,性能接近命名参数,同时参数结构清晰易维护。
方案三:提前提取字典参数到局部变量
如果必须在函数内部使用字典传参,可以在函数开头一次性提取所有需要的参数到局部变量,减少重复查找的开销:
def dictargs_opt(Ep, Eq, pars=get_par_default()): # 一次性提取参数到局部变量 a = pars['a'] b = pars['b'] F0 = pars['F0'] s = pars['s'] eps = pars['eps'] V = pars['V'] p0 = pars['p0'] p10 = pars['p10'] q0 = pars['q0'] q10 = pars['q10'] ans = Ep*Eq*a*b*F0*s*eps*V*p0*p10*q0*q10 return ans
这样只需要一次字典查找,后续计算用局部变量,性能会大幅提升,接近命名参数的速度。
方案四:使用带__slots__的自定义类
自定义类并设置__slots__可以避免普通类的字典属性存储,属性访问直接通过偏移量定位,速度更快:
class ParamsSlots: __slots__ = ['a', 'b', 'F0', 's', 'eps', 'V', 'p0', 'p10', 'q0', 'q10'] def __init__(self): self.a = 0.16 self.b = 0.18 self.F0 = 0.12 self.s = 0.0 self.eps = 3.0e-3 self.V = 3.0 self.p0 = 0.5 self.p10 = 0.956 self.q0 = 0.1 self.q10 = 0.306 def slotsargs(Ep, Eq, pars: ParamsSlots = ParamsSlots()): ans = Ep*Eq*pars.a*pars.b*pars.F0*pars.s*pars.eps*pars.V*pars.p0*pars.p10*pars.q0*pars.q10 return ans
__slots__减少了内存开销和属性访问的间接层级,性能接近局部变量。
内容的提问来源于stack exchange,提问作者villaa
相关产品推荐
相关产品推荐

