在一个序列中找到第一个满足条件的元素

278

我希望找到一种习惯用法来查找与谓词相匹配的列表中的第一个元素。

当前的代码相当丑陋:

[x for x in seq if predicate(x)][0]

我考虑过将它改为:

from itertools import dropwhile
dropwhile(lambda x: not predicate(x), seq).next()

但肯定有更优雅的解决方案...并且如果没有匹配到结果,返回None值而不是抛出异常会更好。

我知道我可以定义一个函数,例如:

def get_first(predicate, seq):
    for i in seq:
        if predicate(i): return i
    return None

如果已经有内置函数可以提供同样的功能,那么像这样在代码中添加公用函数就显得有些无味(人们可能不会注意到它们已经存在,因此随着时间的推移它们往往会被重复)。


6
除了比“python sequence find function”晚被提出外,这个问题的标题要好得多。 - Wolf
4个回答

440

要找到序列 seq 中与给定的 predicate 匹配的第一个元素:

next(x for x in seq if predicate(x))

或者简单地说:

Python 2:

next(itertools.ifilter(predicate, seq))

Python 3:

next(filter(predicate, seq))

如果谓词与任何元素不匹配,这些代码会引发 StopIteration 异常。


如果没有这样的元素,则返回None

next((x for x in seq if predicate(x)), None)

或:

next(filter(predicate, seq), None)

37
你可以给next函数提供第二个“默认”参数,代替抛出异常。 - Karl Knechtel
2
@fortran: next() 自 Python 2.6 起可用。您可以阅读新特性页面快速熟悉它的新特性。 - jfs
1
一个变量使用了 ifilter,而另一个没有使用,这并没有起到帮助作用。请考虑在两个变量中都使用生成器表达式以提高代码的清晰度。 - Piotr Dobrogost
2
@geekazoid: seq.find(&method(:predicate)) 或者更简洁的实例方法,例如:[1,1,4].find(&:even?) - jfs
16
在Python 3中,ifilter已更名为filter - tsauerwein
显示剩余10条评论

105

你可以使用带有默认值的生成器表达式,然后使用next函数:

next((x for x in seq if predicate(x)), None)

对于这个一行代码,你需要使用Python >= 2.6。

这篇相当受欢迎的文章进一步讨论了这个问题:最干净的Python查找列表函数?


12

我认为你在问题中提出的两种解决方案都没有问题。

不过,在我的代码中,我会这样实现:

(x for x in seq if predicate(x)).next()

()语法创建一个生成器,它比一次性使用[]生成整个列表更有效率。


而且不仅如此 - 如果迭代器永远不会结束或其元素难以创建,那么使用[]可能会遇到问题,后者越来越糟... - glglgl
7
在Python 3中,'generator' object has no attribute 'next' 表示生成器对象没有 next 属性。 - jfs
@glglgl - 关于第一个观点(永远不会结束),我表示怀疑,因为参数是一个有限的序列[更准确地说,根据OP的问题,是一个列表]。至于第二个观点:同样地,由于提供的参数是一个序列,这些对象应该在调用此函数之前已经被创建和存储了...或者我漏掉了什么? - mac
@J.F.Sebastian - 谢谢,我之前不知道这个! :) 出于好奇,这个选择背后的设计原则是什么? - mac
@mac - 为了与其他特殊方法的双下划线保持一致。请参阅http://www.python.org/dev/peps/pep-3114/。 - Chewie
对于给定的情况,您是正确的 - 但在没有这些限制的一般情况下,可能会出现问题。 - glglgl

4

J.F. Sebastian的回答很优雅,但需要Python 2.6,如fortran所指出的。

对于Python版本< 2.6,这是我能想到的最好办法:

from itertools import repeat,ifilter,chain
chain(ifilter(predicate,seq),repeat(None)).next()

如果您需要稍后使用列表(列表处理StopIteration),或者您需要不止第一个但仍不是全部的结果,您可以使用islice来实现:

from itertools import islice,ifilter
list(islice(ifilter(predicate,seq),1))

更新: 虽然我个人使用了一个叫做first()的预定义函数来捕获StopIteration并返回None,但是以下是可能对上面示例的改进:避免使用filter / ifilter:

from itertools import islice,chain
chain((x for x in seq if predicate(x)),repeat(None)).next()

20
哎呀!如果没别的办法,我就只能用简单的“for”循环和其中的“if”语句了——这样更易于阅读。 - Nick Perkins

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接