代码解剖 - 编写高性能的Python代码
编写简洁高效的Python代码并不总是容易或直接。然而,我们经常看到一段代码,却没有意识到它背后的思考过程。我们将看一下difference片段,它返回两个可迭代对象之间的差异,以了解其结构。
根据片段功能的描述,我们可以天真地这样写:
def difference(a, b):
return [item for item in a if item not in b]
这个实现可能工作得足够好,但没有考虑到b
中的重复项。这使得在第二个列表中有许多重复项的情况下,代码所需的时间更长。为了解决这个问题,我们可以使用set()
方法,它只会保留列表中的唯一值:
def difference(a, b):
return [item for item in a if item not in set(b)]
这个版本,虽然看起来是一种改进,但实际上可能比之前的版本更慢。如果你仔细观察,你会发现set()
在每个a
中的item
上都被调用,导致set(b)
的结果每次都被计算。下面是一个示例,我们在另一个方法中包装了set()
,以更好地展示这个问题:
def difference(a, b):
return [item for item in a if item not in make_set(b)]
def make_set(itr):
print('Making set...')
return set(itr)
print(difference([1, 2, 3], [1, 2, 4]))
# Making set...
# Making set...
# Making set...
# [3]
解决这个问题的方法是在列表推导式之前调用set()
一次,并将结果存储起来以加快处理速度:
def difference(a, b):
_b = set(b)
return [item for item in a if item not in _b]
在性能方面,另一个值得一提的选择是使用列表推导式而不是filter()
和list()
。使用后者选项实现相同的代码将会得到类似这样的结果:
def difference(a, b):
_b = set(b)
return list(filter(lambda item: item not in _b, a))
使用timeit
来分析最后两个代码示例的性能,很明显使用列表推导式比替代方案快上十倍。这是因为它是一种类似于简单的for
循环的本地语言特性,没有额外函数调用的开销。这解释了为什么我们更喜欢它,除了可读性之外。
这几乎适用于大多数数学列表操作片段,比如difference、symmetric_difference和intersection。