如何测试多个值在列表中的存在性
我想测试两个或多个值是否在一个列表里,但我得到了一个意想不到的结果:
>>> 'a','b' in ['b', 'a', 'foo', 'bar']
('a', True)
那么,Python能否一次性测试多个值是否在列表中?这个结果是什么意思呢?
另见:如何找到列表的交集? 检查指定的值中是否有任何一个在列表里,相当于检查它们的交集是否非空。检查所有值是否在列表中,相当于检查它们是否是一个子集。
12 个回答
如果你想检查所有输入是否匹配,
>>> all(x in ['b', 'a', 'foo', 'bar'] for x in ['a', 'b'])
如果你想检查至少有一个匹配,
>>> any(x in ['b', 'a', 'foo', 'bar'] for x in ['a', 'b'])
还有一种方法可以做到这一点:
>>> set(['a','b']).issubset( ['b','a','foo','bar'] )
True
这个方法能满足你的需求,并且在几乎所有情况下都能正常工作:
>>> all(x in ['b', 'a', 'foo', 'bar'] for x in ['a', 'b'])
True
表达式 'a','b' in ['b', 'a', 'foo', 'bar']
不能按预期工作,因为Python把它当作一个元组来理解:
>>> 'a', 'b'
('a', 'b')
>>> 'a', 5 + 2
('a', 7)
>>> 'a', 'x' in 'xerxes'
('a', True)
其他选择
还有其他方法可以进行这个测试,但它们对不同类型的输入支持不如前面提到的方法广泛。正如Kabie指出的,你可以使用集合来解决这个问题……
>>> set(['a', 'b']).issubset(set(['a', 'b', 'foo', 'bar']))
True
>>> {'a', 'b'} <= {'a', 'b', 'foo', 'bar'}
True
……有时候:
>>> {'a', ['b']} <= {'a', ['b'], 'foo', 'bar'}
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
集合只能用可哈希的元素来创建。但是生成器表达式 all(x in container for x in items)
几乎可以处理任何类型的容器。唯一的要求是 container
必须是可重复迭代的(也就是说,不能是生成器)。items
可以是任何可迭代的对象。
>>> container = [['b'], 'a', 'foo', 'bar']
>>> items = (i for i in ('a', ['b']))
>>> all(x in [['b'], 'a', 'foo', 'bar'] for x in items)
True
速度测试
在很多情况下,子集测试会比 all
更快,但差别并不大——除非问题本身不相关,因为集合不是一个选项。为了进行这样的测试而把列表转换成集合并不总是值得。而把生成器转换成集合有时会非常浪费,可能会让程序变得慢得多。
这里有一些基准测试供参考。最大的差别出现在 container
和 items
都相对较小的情况下。在这种情况下,子集方法大约快一个数量级:
>>> smallset = set(range(10))
>>> smallsubset = set(range(5))
>>> %timeit smallset >= smallsubset
110 ns ± 0.702 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
>>> %timeit all(x in smallset for x in smallsubset)
951 ns ± 11.5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
这看起来差别很大。但是只要 container
是一个集合,all
在更大规模下仍然可以正常使用:
>>> bigset = set(range(100000))
>>> bigsubset = set(range(50000))
>>> %timeit bigset >= bigsubset
1.14 ms ± 13.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit all(x in bigset for x in bigsubset)
5.96 ms ± 37 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
使用子集测试仍然更快,但在这个规模下只快了大约5倍。这个速度提升是因为Python的 set
实现很快,但在这两种情况下,基本算法是相同的。
如果你的 items
已经因为其他原因存储在一个列表中,那么在使用子集测试方法之前,你需要先把它们转换成集合。这样速度提升会降到大约2.5倍:
>>> %timeit bigset >= set(bigsubseq)
2.1 ms ± 49.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
如果你的 container
是一个序列,并且需要先转换,那么速度提升会更小:
>>> %timeit set(bigseq) >= set(bigsubseq)
4.36 ms ± 31.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
我们只有在把 container
保持为序列时,结果才会变得非常慢:
>>> %timeit all(x in bigseq for x in bigsubseq)
184 ms ± 994 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
当然,只有在必须的情况下我们才会这样做。如果 bigseq
中的所有项都是可哈希的,那么我们会这样做:
>>> %timeit bigset = set(bigseq); all(x in bigset for x in bigsubseq)
7.24 ms ± 78 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
这比替代方法(set(bigseq) >= set(bigsubseq)
,上面测得为4.36)快了1.66倍。
所以一般来说,子集测试更快,但差别并不大。另一方面,我们来看看什么时候 all
更快。如果 items
有一千万个值,并且可能有一些值不在 container
中呢?
>>> %timeit hugeiter = (x * 10 for bss in [bigsubseq] * 2000 for x in bss); set(bigset) >= set(hugeiter)
13.1 s ± 167 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
>>> %timeit hugeiter = (x * 10 for bss in [bigsubseq] * 2000 for x in bss); all(x in bigset for x in hugeiter)
2.33 ms ± 65.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
在这种情况下,把生成器转换成集合会非常浪费。因为 set
构造函数需要消耗整个生成器。但是 all
的短路行为确保只需要消耗生成器的一小部分,所以它比子集测试快了四个数量级。
这确实是一个极端的例子。但正如它所示,你不能假设某种方法在所有情况下都会更快。
总结
大多数情况下,把 container
转换成集合是值得的,至少当它的所有元素都是可哈希的时。因为集合的 in
操作是 O(1),而序列的 in
操作是 O(n)。
另一方面,使用子集测试可能只有在某些情况下才值得。如果你的测试项已经存储在集合中,那就一定要使用它。否则,all
只慢一点,而且不需要额外的存储。它也可以与大型生成器一起使用,有时在这种情况下能提供巨大的速度提升。