Obter o primeiro item de um iterável que corresponda a uma condição

303

Gostaria de obter o primeiro item de uma lista que corresponde a uma condição. É importante que o método resultante não processe a lista inteira, que pode ser bastante grande. Por exemplo, a seguinte função é adequada:

def first(the_iterable, condition = lambda x: True):
    for i in the_iterable:
        if condition(i):
            return i

Esta função pode ser usada algo como isto:

>>> first(range(10))
0
>>> first(range(10), lambda i: i > 3)
4

No entanto, não consigo pensar em um bom built-in / one-liner para me deixar fazer isso. Eu particularmente não quero copiar esta função, se não for necessário. Existe uma maneira interna de obter o primeiro item correspondente a uma condição?

Chris Phillips
fonte

Respostas:

476

No Python 2.6 ou mais recente:

Se você deseja StopIterationaumentar, se nenhum elemento correspondente for encontrado:

next(x for x in the_iterable if x > 3)

Se você deseja default_value(por exemplo None) retornar em vez disso:

next((x for x in the_iterable if x > 3), default_value)

Observe que você precisa de um par extra de parênteses em torno da expressão do gerador nesse caso - eles são necessários sempre que a expressão do gerador não for o único argumento.

Vejo a maioria das respostas ignorando resolutamente o nextbuilt-in e, portanto, presumo que, por algum motivo misterioso, eles estejam 100% focados nas versões 2.5 e anteriores - sem mencionar o problema da versão do Python (mas não vejo essa menção em as respostas que fazer mencionar o nextbuilt-in, que é por isso que eu pensei que é necessário dar uma resposta a mim mesmo - pelo menos a questão "versão correta" fica no registro desta forma ;-).

Na versão 2.5, o .next()método dos iteradores aumenta imediatamente StopIterationse o iterador terminar imediatamente - ou seja, para o seu caso de uso, se nenhum item no iterável atender à condição. Se você não se importa (você sabe que deve haver pelo menos um item satisfatório), basta usar .next()(melhor em um genexp, linha para o nextbuilt-in no Python 2.6 e melhor).

Se você fazer o cuidado, as coisas embrulho em uma função como você tinha indicado pela primeira vez em sua Q parece melhor, e enquanto a implementação da função que você propôs é muito bem, você pode usar como alternativa itertools, um for...: breakloop, ou um genexp, ou try/except StopIterationcomo o corpo da função , como várias respostas sugeridas. Não há muito valor agregado em nenhuma dessas alternativas, então eu usaria a versão extremamente simples que você propôs pela primeira vez.

Alex Martelli
fonte
6
Não funciona como você descreve. Ela levanta StopIterationquando nenhum elemento encontrado
Suor
Como isso aparece nos resultados de pesquisa, segui o comentário de @ Suor de 2011 e reformulei o primeiro parágrafo um pouco para tornar as coisas mais claras. Vá em frente e corrija minha edição, se necessário.
Kos
4
Como essa é a resposta selecionada, sinto-me compelido a compartilhar uma resposta para selecionar o primeiro elemento corretamente aqui . Em resumo: o uso de next não deve ser incentivado.
7776 guyarad
1
@guyarad como a solução proposta nessa resposta é menos "enigmática" do que apenas usar a seguir? O único argumento contra o próximo (nessa resposta) é que você deve lidar com uma exceção; realmente ?
Abraham TS
Minha opinião é um pouco diferente da época em que escrevi o comentário. Eu entendo o seu ponto. Dito isto, ter que lidar com StopIterationisso não é realmente bonito. Melhor usar um método.
precisa saber é
29

Como uma função reutilizável, documentada e testada

def first(iterable, condition = lambda x: True):
    """
    Returns the first item in the `iterable` that
    satisfies the `condition`.

    If the condition is not given, returns the first item of
    the iterable.

    Raises `StopIteration` if no item satysfing the condition is found.

    >>> first( (1,2,3), condition=lambda x: x % 2 == 0)
    2
    >>> first(range(3, 100))
    3
    >>> first( () )
    Traceback (most recent call last):
    ...
    StopIteration
    """

    return next(x for x in iterable if condition(x))

Versão com argumento padrão

O @zorf sugeriu uma versão dessa função em que você pode ter um valor de retorno predefinido se o iterável estiver vazio ou se não houver itens correspondentes à condição:

def first(iterable, default = None, condition = lambda x: True):
    """
    Returns the first item in the `iterable` that
    satisfies the `condition`.

    If the condition is not given, returns the first item of
    the iterable.

    If the `default` argument is given and the iterable is empty,
    or if it has no items matching the condition, the `default` argument
    is returned if it matches the condition.

    The `default` argument being None is the same as it not being given.

    Raises `StopIteration` if no item satisfying the condition is found
    and default is not given or doesn't satisfy the condition.

    >>> first( (1,2,3), condition=lambda x: x % 2 == 0)
    2
    >>> first(range(3, 100))
    3
    >>> first( () )
    Traceback (most recent call last):
    ...
    StopIteration
    >>> first([], default=1)
    1
    >>> first([], default=1, condition=lambda x: x % 2 == 0)
    Traceback (most recent call last):
    ...
    StopIteration
    >>> first([1,3,5], default=1, condition=lambda x: x % 2 == 0)
    Traceback (most recent call last):
    ...
    StopIteration
    """

    try:
        return next(x for x in iterable if condition(x))
    except StopIteration:
        if default is not None and condition(default):
            return default
        else:
            raise
Caridorc
fonte
6
Se você estiver agrupando-o com um método, pegue pelo menos StopIteration e aumente o erro EmptySequence. Seria muito mais bonito quando não há elementos.
guyarad
@guyarad Isso é um tipo de ValueError?
Caridorc 28/01/19
2
@guyarad StopIterationé a exceção canônica "fora dos elementos" em python. Não vejo problema em ser lançado. Eu provavelmente usaria um padrão de "Nenhum", que pode ser passado como um parâmetro padrão para a função.
Baldrickk
1
Baldrickk Eu sinto que esse não é um método de iteração. Você não o chamará em uma competição de um iterador. Mas eu não estou me sentindo muito fortemente sobre ele :)
guyarad
1
Deve haver um argumento padrão opcional e, se esse argumento não for fornecido, somente crie uma exceção quando nenhum elemento na sequência atender à condição.
Zorf 26/02
28

Exceções malditas!

Eu amo essa resposta . No entanto, como next()gera uma StopIterationexceção quando não há itens, eu usaria o seguinte snippet para evitar uma exceção:

a = []
item = next((x for x in a), None)

Por exemplo,

a = []
item = next(x for x in a)

Irá gerar uma StopIterationexceção;

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
Jossef Harush
fonte
13

Semelhante ao uso ifilter, você pode usar uma expressão geradora:

>>> (x for x in xrange(10) if x > 5).next()
6

Em ambos os casos, você provavelmente quer pegar StopIteration , caso nenhum elemento atenda à sua condição.

Tecnicamente falando, suponho que você possa fazer algo assim:

>>> foo = None
>>> for foo in (x for x in xrange(10) if x > 5): break
... 
>>> foo
6

Evitaria ter que fazer um try/exceptbloco. Mas isso parece meio obscuro e abusivo para a sintaxe.

Matt Anderson
fonte
+1: não é obscuro nem abusivo. Tudo considerado, o último parece bastante limpo.
S.Lott
6
A última não é de todo limpa - for foo in genex: breaké apenas uma maneira de fazer foo = next(genex)sem deixar clara a tarefa e com a exceção que seria levantada se a operação não fizesse sentido ser esmagada. Terminar com um código de falha em vez de capturar uma exceção geralmente é uma coisa ruim no Python.
Mike Graham
13

A maneira mais eficiente no Python 3 é uma das seguintes (usando um exemplo semelhante):

Com estilo "compreensão" :

next(i for i in range(100000000) if i == 1000)

AVISO : A expressão também funciona com o Python 2, mas no exemplo é usado rangeque retorna um objeto iterável no Python 3 em vez de uma lista como o Python 2 (se você deseja construir um iterável no Python 2, usexrange ).

Observe que a expressão evita construir uma lista na expressão de compreensão next([i for ...]), que causaria a criação de uma lista com todos os elementos antes de filtrar os elementos e causaria o processamento de todas as opções, em vez de interromper a iteração uma vez i == 1000.

Com estilo "funcional" :

next(filter(lambda i: i == 1000, range(100000000)))

AVISO : Isso não funciona no Python 2, mesmo substituindo rangecom xrangedue que filtercria uma lista em vez de um iterador (ineficiente), e a nextfunção funciona apenas com iteradores.

Valor padrão

Conforme mencionado em outras respostas, você deve adicionar um parâmetro extra à função nextse desejar evitar uma exceção gerada quando a condição não for atendida.

"funcional"estilo :

next(filter(lambda i: i == 1000, range(100000000)), False)

estilo "compreensão" :

Com esse estilo, você precisa cercar a expressão de compreensão ()para evitar SyntaxError: Generator expression must be parenthesized if not sole argument:

next((i for i in range(100000000) if i == 1000), False)
Mariano Ruiz
fonte
7

Eu escreveria isso

next(x for x in xrange(10) if x > 3)
Mike Graham
fonte
Eu acho que i > 3deve ser x > 3no seu exemplo
Ricky Robinson
6

O itertoolsmódulo contém uma função de filtro para iteradores. O primeiro elemento do iterador filtrado pode ser obtido chamando next()-o:

from itertools import ifilter

print ifilter((lambda i: i > 3), range(10)).next()
sth
fonte
2
As expressões do gerador são mais simples.
Eric O Lebigot
1
( i) filtere ( i) mappodem fazer sentido para os casos em que as funções aplicadas já existem, mas em uma situação como essa, faz muito mais sentido usar uma expressão geradora.
Mike Graham
Esta é a melhor resposta. Evite a compreensão da lista xahlee.info/comp/list_comprehension.html
mit
6

Para versões mais antigas do Python, onde o próximo built-in não existe:

(x for x in range(10) if x > 3).next()
Menno Smits
fonte
5

Usando

(index for index, value in enumerate(the_iterable) if condition(value))

pode-se verificar a condição do valor do primeiro item noiterável e obter seu índice sem a necessidade de avaliar todos os itens no the_iterable .

A expressão completa a ser usada é

first_index = next(index for index, value in enumerate(the_iterable) if condition(value))

Aqui first_index assume o valor do primeiro valor identificado na expressão discutida acima.

nota azul
fonte
4

Esta pergunta já tem ótimas respostas. Só estou adicionando meus dois centavos porque cheguei aqui tentando encontrar uma solução para o meu próprio problema, que é muito semelhante ao OP.

Se você deseja encontrar o ÍNDICE do primeiro item que corresponde a um critério usando geradores, basta:

next(index for index, value in enumerate(iterable) if condition)
dangom
fonte
Veja também: stackoverflow.com/questions/1701211/…
dreftymac
0

Você também pode usar a argwherefunção Numpy. Por exemplo:

i) Encontre o primeiro "l" em "helloworld":

import numpy as np
l = list("helloworld") # Create list
i = np.argwhere(np.array(l)=="l") # i = array([[2],[3],[8]])
index_of_first = i.min()

ii) Encontre o primeiro número aleatório> 0,1

import numpy as np
r = np.random.rand(50) # Create random numbers
i = np.argwhere(r>0.1)
index_of_first = i.min()

iii) Encontre o último número aleatório> 0,1

import numpy as np
r = np.random.rand(50) # Create random numbers
i = np.argwhere(r>0.1)
index_of_last = i.max()
alvo
fonte
-1

No Python 3:

a = (None, False, 0, 1)
assert next(filter(None, a)) == 1

No Python 2.6:

a = (None, False, 0, 1)
assert next(iter(filter(None, a))) == 1

Edição: Eu pensei que era óbvio, mas aparentemente não: em vez de Nonevocê pode passar uma função (ou a lambda) com uma verificação para a condição:

a = [2,3,4,5,6,7,8]
assert next(filter(lambda x: x%2, a)) == 3
Berislav Lopac
fonte
-3

Oneliner:

thefirst = [i for i in range(10) if i > 3][0]

Se você não tiver certeza de que algum elemento será válido de acordo com os critérios, você deve anexá-lo, try/exceptpois isso [0]pode gerar um IndexError.

Mizipzor
fonte
TypeError: o objeto 'gerador' não é passível de assinatura
Josh Lee
Meu mal, deve ser a compreensão da lista não um gerador, fixo ... obrigado! :)
Mizipzor
2
Não há razão para avaliar todo o iterável (o que pode não ser possível). É mais robusto e eficiente usar uma das outras soluções fornecidas.
Mike Graham