其他答案中常见的模式是尝试将两个输入压缩在一起,然后在每对嵌套的“行”列表中遍历元素。我建议
反转这个过程,以获得更优雅的代码。正如Python之禅所说,“扁平比嵌套好”。
我采用以下方法设置了一个测试:
>>> class A:
... def __init__(self):
... self.isWhatever = True
...
>>>
>>> class B:
... def doSomething(self):
... pass
...
>>> alist = [[A() for _ in range(1000)] for _ in range(1000)]
>>> blist = [[B() for _ in range(1000)] for _ in range(1000)]
将原本在3.x中表现最佳的代码进行调整,这是解决方案
def brian_modern():
for a_row, b_row in zip(alist, blist):
for a_item, b_item in zip(a_row, b_row):
if a_item.isWhatever:
b_item.doSomething()
因为现在,zip
返回一个迭代器,而执行的是以前itertools.izip
的功能。
在我的平台上(Linux Mint 20.3上的Python 3.8.10; Intel(R) Core(TM) i5-4430 CPU @ 3.00GHz,具有8GB的DDR3 RAM @ 1600MT/s),我得到了这个时间结果:
>>> import timeit
>>> timeit.timeit(brian_modern, number=100)
10.740317705087364
与其重复使用zip函数,我的方法是先将每个输入可迭代对象展开,然后再进行zip操作。
from itertools import chain
def karl():
flatten = chain.from_iterable
for a_item, b_item in zip(flatten(alist), flatten(blist)):
if a_item.isWhatever:
b_item.doSomething()
这几乎提供了同样好的性能:
>>> karl()
>>> timeit.timeit(karl, number=100)
11.126002880046144
作为基准,让我们尝试将循环开销降到最低:
my_a = A()
my_b = B()
def baseline():
a = my_a
b = my_b
for i in range(1000000):
if a.isWhatever:
b.doSomething()
然后检查实际对象检查逻辑使用了多少时间:
>>> timeit.timeit(baseline, number=100)
9.41121925599873
因此,预扁平化方法确实会产生更多的开销(约为18%,而重复压缩方法约为14%)。但是,即使对于微不足道的循环体,它仍然是相当小的开销,并且还允许我们更优雅地编写代码。
在我的测试中,这是预扁平化最快的方法。将参数展开到
itertools.chain
中稍微慢一些,而使用生成器表达式来扁平化输入...
def karl_gen():
a_flat = (i for row in alist for i in row)
b_flat = (j for row in blist for j in row)
for a_item, b_item in zip(a_flat, b_flat):
if a_item.isWhatever:
b_item.doSomething()
...速度慢得多:
>>> timeit.timeit(karl_gen, number=100)
16.904560427879915
在这里切换到列表推导式几乎对速度与生成器没有什么影响,同时也会暂时将内存需求翻倍。因此,itertools.chain.from_iterable
是一个明显的赢家。