Encontre o primeiro elemento em uma sequência que corresponde a um predicado

171

Eu quero uma maneira idiomática de encontrar o primeiro elemento em uma lista que corresponde a um predicado.

O código atual é bastante feio:

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

Eu pensei em mudar para:

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

Mas deve haver algo mais elegante ... E seria bom se ele retornasse um Nonevalor em vez de gerar uma exceção se nenhuma correspondência fosse encontrada.

Eu sei que eu poderia apenas definir uma função como:

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

Mas é bastante insípido começar a preencher o código com funções utilitárias como esta (e as pessoas provavelmente não perceberão que elas já estão lá, então elas tendem a ser repetidas ao longo do tempo) se houver itens incorporados que já forneçam o mesmo.

fortran
fonte
3
Além de ser perguntado mais tarde que " função de localização de sequência python ", esta pergunta tem um título muito melhor .
Wolf

Respostas:

250

Para encontrar o primeiro elemento em uma sequência seqque corresponde a predicate:

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

Ou ( itertools.ifilterno Python 2) :

next(filter(predicate, seq))

Aumenta StopIterationse não houver.


Para retornar Nonese não houver esse elemento:

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

Ou:

next(filter(predicate, seq), None)
jfs
fonte
27
Ou você pode fornecer um segundo argumento "padrão" para nextesse argumento, em vez de gerar a exceção.
quer
2
@ fortran: next()está disponível desde o Python 2.6 Você pode ler a página O que há de novo para se familiarizar rapidamente com os novos recursos.
JFS
1
Sou novato em python e leio os documentos e o ifilter usa o método "yield". Suponho que isso significa que o predicado é avaliado preguiçosamente à medida que avançamos. ou seja, nós não correr o predicado toda a lista porque eu tenho uma função predicado que é um pouco caro e eu quero apenas iterate até o ponto em que encontrar um item
Kannan Ekanath
2
@geekazoid: seq.find(&method(:predicate))ou ainda mais concisa para os métodos de instância, por exemplo:[1,1,4].find(&:even?)
jfs
16
ifilterfoi renomeado para filterno Python 3.
tsauerwein
92

Você pode usar uma expressão de gerador com um valor padrão e, em seguida, nextela:

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

Embora para essa linha única você precise usar Python> = 2.6.

Este artigo bastante popular discute ainda mais esse problema: Função de busca na lista do Python mais limpa? .

Chewie
fonte
8

Não acho que haja algo errado nas soluções que você propôs na sua pergunta.

No meu próprio código, eu o implementaria assim:

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

A sintaxe com ()cria um gerador, que é mais eficiente do que gerar toda a lista de uma só vez [].

Mac
fonte
E não apenas isso - com []você pode ter problemas se o iterador nunca terminar ou se for difícil criar seus elementos, mais tarde ele
ficará
6
'generator' object has no attribute 'next'em Python 3.
jfs
@glglgl - Quanto ao primeiro ponto (nunca acaba), duvido, pois o argumento é uma sequência finita [mais precisamente uma lista de acordo com a pergunta do OP]. Quanto ao segundo: novamente, como o argumento fornecido é uma sequência, os objetos já devem ter sido criados e armazenados no momento em que essa função é chamada ... ou estou perdendo alguma coisa?
mac
@JFSebastian - Obrigado, eu não estava ciente disso! :) Por curiosidade, qual é o princípio do design por trás dessa escolha?
mac
@mac - para obter consistência com o sublinhado duplo de outros métodos especiais. Veja python.org/dev/peps/pep-3114
Chewie
1

A resposta de JF Sebastian é mais elegante, mas requer o python 2.6, como fortran apontou.

Para a versão Python <2.6, aqui está o melhor que posso apresentar:

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

Como alternativa, se você precisar de uma lista posteriormente (a lista lida com o StopIteration) ou se precisar de mais do que apenas o primeiro, mas ainda não todos, poderá fazê-lo com o islice:

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

ATUALIZAÇÃO: Embora eu pessoalmente esteja usando uma função predefinida chamada first () que captura uma StopIteration e retorna None, aqui está uma possível melhoria em relação ao exemplo acima: evite usar filter / ifilter:

from itertools import islice,chain
chain((x for x in seq if predicate(x)),repeat(None)).next()
parity3
fonte
11
Caramba! se resume a isso, gostaria apenas de fazer o simples "para" loop com um "se" dentro dele - mais fácil tanto para ler
Nick Perkins