Python常见问题:“异常有多快?”
我刚刚在看Python的常见问题解答,因为在另一个问题中提到了它。之前我从来没有仔细看过,结果发现了一个问题:“异常处理有多快?”:
使用try/except块是非常高效的。实际上,捕获一个异常是比较耗费资源的。在Python 2.0之前的版本中,通常会使用这样的写法:
try: value = mydict[key] except KeyError: mydict[key] = getvalue(key) value = mydict[key]
我对“捕获异常是比较耗费资源的”这句话有点惊讶。这是指那些在except
中真的把异常保存到一个变量里的情况,还是说所有的except
情况(包括上面的例子)?
我一直认为使用这样的写法是非常符合Python风格的,特别是在Python中有一句话:“请求原谅比请求许可要容易”。而且在Stack Overflow上,很多回答通常也遵循这个想法。
捕获异常的性能真的那么差吗?在这种情况下,是否应该更倾向于“先看再跳”(LBYL)?
(注意,我并不是直接在讨论常见问题解答中的例子;还有很多其他例子是你只关注异常,而不是在之前检查类型。)
3 个回答
如果找不到键的情况发生得很频繁,那我建议你使用'get'方法,因为它在所有情况下都能保持稳定的速度:
s.append('''\
x = D.get('key', None)
''')
s.append('''\
x = D.get('xxx', None)
''')
成本当然跟具体的实现有关,但我觉得你不需要太担心这个。其实这并不会太影响你的工作。标准协议在一些奇怪的地方会抛出异常(比如说StopIteration
),所以无论你愿不愿意,你都会在处理抛出和捕获异常的过程中。
在选择使用LBYL(先检查再执行)和EAFP(先执行再检查)时,应该更关注代码的可读性,而不是纠结于微小的优化。我建议尽量避免类型检查,因为这可能会降低代码的通用性。
捕捉异常是比较耗费资源的,但异常应该是“例外情况”(也就是说,不应该经常发生)。如果异常很少发生,那么使用 try/catch
的效率比 LBYL 更高。
下面的例子比较了在字典中查找键时,使用异常和 LBYL 的速度,分别在键存在和不存在的情况下:
import timeit
s = []
s.append('''\
try:
x = D['key']
except KeyError:
x = None
''')
s.append('''\
x = D['key'] if 'key' in D else None
''')
s.append('''\
try:
x = D['xxx']
except KeyError:
x = None
''')
s.append('''\
x = D['xxx'] if 'xxx' in D else None
''')
for i,c in enumerate(s,1):
t = timeit.Timer(c,"D={'key':'value'}")
print('Run',i,'=',min(t.repeat()))
输出结果
Run 1 = 0.05600167960596991 # try/catch, key exists
Run 2 = 0.08530091918578364 # LBYL, key exists (slower)
Run 3 = 0.3486251291120652 # try/catch, key doesn't exist (MUCH slower)
Run 4 = 0.050621117060586585 # LBYL, key doesn't exist
当正常情况下没有异常时,try/catch
的效率与 LBYL 相比是“极其高效”的。