在阅读Brett Slatkin的书“Effective Python”时,我注意到作者建议有时使用生成器函数构建列表并在生成的迭代器上调用list可能会产生更清晰,更易读的代码。
举个例子:
num_list = range(100)
def num_squared_iterator(nums):
for i in nums:
yield i**2
def get_num_squared_list(nums):
l = []
for i in nums:
l.append(i**2)
return l
用户可以打电话的地方
l = list(num_squared_iterator(num_list))
要么
l = get_num_squared_list(nums)
并得到相同的结果。
建议生成器函数具有较少的噪声,因为它较短并且没有用于创建列表和向其附加值的额外代码。
(请注意,对于这些简单的示例,列表理解或生成器表达式会更好,但让我们假设这是一个模式的简化,可以用于更复杂的代码,这在列表理解中是不明确的)
我的问题是,将生成器包装在列表中是否有成本?它在性能上与列表构建功能相同吗?
看到这个我决定做一个快速测试并编写并运行以下代码:
from functools import wraps
from time import time
TEST_DATA = range(100)
def timeit(func):
@wraps(func)
def wrapped(*args, **kwargs):
start = time()
func(*args, **kwargs)
end = time()
print(f'running time for {func.__name__} = {end-start}')
return wrapped
def num_squared_iterator(nums):
for i in nums:
yield i**2
@timeit
def get_num_squared_list(nums):
l = []
for i in nums:
l.append(i**2)
return l
@timeit
def get_num_squared_list_from_iterator(nums):
return list(num_squared_iterator(nums))
if __name__ == '__main__':
get_num_squared_list(TEST_DATA)
get_num_squared_list_from_iterator(TEST_DATA)
我多次运行测试代码并且每次运行(令我惊讶的是)get_num_squared_list_from_iterator函数实际上比get_num_squared_list函数更快地运行(分数)。
以下是我前几次运行的结果:
1. get_num_squared_list = 5.2928924560546875e-05的运行时间
get_num_squared_list_from_iterator = 5.0067901611328125e-05的运行时间
2. get_num_squared_list = 5.3882598876953125e-05的运行时间
get_num_squared_list_from_iterator = 4.982948303222656e-05的运行时间
3. get_num_squared_list = 5.1975250244140625e-05的运行时间
get_num_squared_list_from_iterator的运行时间= 4.76837158203125e-05
我猜这是由于在get_num_squared_list函数中循环的每次迭代中执行list.append的费用。
我发现这很有趣,因为不仅代码清晰而优雅,而且性能更高。
我可以确认你的发电机与list
示例更快:
In [4]: def num_squared_iterator(nums):
...: for i in nums:
...: yield i**2
...:
...: def get_num_squared_list(nums):
...: l = []
...: for i in nums:
...: l.append(i**2)
...: return l
...:
In [5]: %timeit list(num_squared_iterator(nums))
320 µs ± 4.57 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [6]: %timeit get_num_squared_list(nums)
370 µs ± 25.1 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [7]: nums = range(100000)
In [8]: %timeit list(num_squared_iterator(nums))
33.2 ms ± 461 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [9]: %timeit get_num_squared_list(nums)
36.3 ms ± 375 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
然而,故事还有更多。传统观念认为生成器比迭代其他类型的迭代更慢,生成器有很多开销。但是,使用list
将列表构建代码推送到C级,所以你有点看到了中间立场。注意,使用for循环可以这样优化:
In [10]: def get_num_squared_list_microoptimized(nums):
...: l = []
...: append = l.append
...: for i in nums:
...: append(i**2)
...: return l
...:
In [11]: %timeit list(num_squared_iterator(nums))
33.4 ms ± 427 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [12]: %timeit get_num_squared_list(nums)
36.5 ms ± 624 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [13]: %timeit get_num_squared_list_microoptimized(nums)
33.3 ms ± 487 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
现在你看到,如果你“内联”l.append
(这是list
构造函数避免的),方法中的许多差异可以得到改善。通常,Python中的方法解析速度很慢。在紧密循环中,上面的微优化是众所周知的,并且是使你的for循环更高效的第一步。