Python性能:使用Try-except还是不使用?
在我的一个类里,有很多方法都需要从同一个字典里获取值。不过,如果某个方法想要访问一个不存在的值,它就得调用另一个方法来为这个键生成一个值。
我现在的实现方式是这样的,findCrackDepth(tonnage) 会给 self.lowCrackDepth[tonnage] 赋一个值。
if tonnage not in self.lowCrackDepth:
self.findCrackDepth(tonnage)
lcrack = self.lowCrackDepth[tonnage]
不过,我也可以这样做:
try:
lcrack = self.lowCrackDepth[tonnage]
except KeyError:
self.findCrackDepth(tonnage)
lcrack = self.lowCrackDepth[tonnage]
我想这两种方式在性能上是有区别的,主要是看值在字典里出现的频率。这个差别有多大呢?我需要生成几百万个这样的值(分布在很多字典和类的多个实例中),而每次值不存在的时候,可能有两次是存在的。
5 个回答
有疑问的时候,就去分析一下。
做个测试,看看在你的环境里,哪个运行得更快。
检查一个键是否存在的成本比直接获取它的成本要低,或者至少是一样的。所以使用 if not in 的方法会更简洁、更易读。
根据你的问题,键不存在并不是一种错误情况,所以没有必要让 Python 抛出异常(即使你马上就捕获了这个异常)。如果你使用 if not in 来检查,大家都能明白你的意图——要么获取现有的值,要么生成一个新的值。
这个问题的时间把握很微妙,因为你需要小心避免“持久性副作用”,而且性能的权衡取决于缺失键的比例。所以,考虑一个 dil.py
文件,如下所示:
def make(percentmissing):
global d
d = dict.fromkeys(range(100-percentmissing), 1)
def addit(d, k):
d[k] = k
def with_in():
dc = d.copy()
for k in range(100):
if k not in dc:
addit(dc, k)
lc = dc[k]
def with_ex():
dc = d.copy()
for k in range(100):
try: lc = dc[k]
except KeyError:
addit(dc, k)
lc = dc[k]
def with_ge():
dc = d.copy()
for k in range(100):
lc = dc.get(k)
if lc is None:
addit(dc, k)
lc = dc[k]
还有一系列的 timeit
调用,比如:
$ python -mtimeit -s'import dil; dil.make(10)' 'dil.with_in()'
10000 loops, best of 3: 28 usec per loop
$ python -mtimeit -s'import dil; dil.make(10)' 'dil.with_ex()'
10000 loops, best of 3: 41.7 usec per loop
$ python -mtimeit -s'import dil; dil.make(10)' 'dil.with_ge()'
10000 loops, best of 3: 46.6 usec per loop
这表明,当缺失键达到10%时,使用 in
检查是最快的方法。
$ python -mtimeit -s'import dil; dil.make(1)' 'dil.with_in()'
10000 loops, best of 3: 24.6 usec per loop
$ python -mtimeit -s'import dil; dil.make(1)' 'dil.with_ex()'
10000 loops, best of 3: 23.4 usec per loop
$ python -mtimeit -s'import dil; dil.make(1)' 'dil.with_ge()'
10000 loops, best of 3: 42.7 usec per loop
而当缺失键只有1%时,使用 exception
的方法是 稍微 快一些(而 get
方法在这两种情况下都是最慢的)。
所以,为了获得最佳性能,除非绝大多数(99%+)的查找都会成功,否则 in
方法是更好的选择。
当然,还有另一种优雅的可能性:添加一个字典子类,比如...:
class dd(dict):
def __init__(self, *a, **k):
dict.__init__(self, *a, **k)
def __missing__(self, k):
addit(self, k)
return self[k]
def with_dd():
dc = dd(d)
for k in range(100):
lc = dc[k]
但是...:
$ python -mtimeit -s'import dil; dil.make(1)' 'dil.with_dd()'
10000 loops, best of 3: 46.1 usec per loop
$ python -mtimeit -s'import dil; dil.make(10)' 'dil.with_dd()'
10000 loops, best of 3: 55 usec per loop
...虽然确实很炫,但这并不是性能上的赢家——它的性能与 get
方法差不多,甚至更慢,只是代码看起来更好用。(defaultdict
,在语义上与这个 dd
类似,如果适用的话,会在性能上有优势,因为在这种情况下,__missing__
特殊方法是用优化过的C代码实现的)。