Para que você pode usar as funções do gerador Python?

213

Estou começando a aprender Python e deparei-me com funções geradoras, aquelas que possuem uma declaração de rendimento nelas. Quero saber que tipos de problemas essas funções são realmente boas para resolver.

quamrana
fonte
6
talvez uma pergunta melhor seria quando não devemos usá-los
cregox
1
Exemplo do mundo real aqui
Giri

Respostas:

239

Os geradores dão uma avaliação preguiçosa. Você os usa iterando sobre eles, explicitamente com 'for' ou implicitamente, passando-o para qualquer função ou construção que itere. Você pode pensar nos geradores como retornando vários itens, como se eles retornassem uma lista, mas, em vez de retorná-los todos de uma vez, eles os retornam um a um, e a função do gerador é pausada até o próximo item ser solicitado.

Os geradores são bons para calcular grandes conjuntos de resultados (em particular cálculos envolvendo loops) onde você não sabe se precisará de todos os resultados ou onde não deseja alocar a memória para todos os resultados ao mesmo tempo . Ou para situações em que o gerador usa outro gerador ou consome algum outro recurso, e é mais conveniente se isso acontecer o mais tarde possível.

Outro uso para geradores (que é realmente o mesmo) é substituir os retornos de chamada pela iteração. Em algumas situações, você deseja que uma função faça muito trabalho e, ocasionalmente, reporte ao chamador. Tradicionalmente, você usaria uma função de retorno de chamada para isso. Você passa esse retorno de chamada para a função de trabalho e periodicamente chamaria esse retorno de chamada. A abordagem do gerador é que a função de trabalho (agora um gerador) não sabe nada sobre o retorno de chamada e apenas cede sempre que deseja reportar algo. O chamador, em vez de escrever um retorno de chamada separado e passar para a função de trabalho, todos os relatórios funcionam em um pequeno loop 'for' em torno do gerador.

Por exemplo, digamos que você escreveu um programa de 'pesquisa de sistema de arquivos'. Você pode realizar a pesquisa em sua totalidade, coletar os resultados e exibi-los um de cada vez. Todos os resultados precisariam ser coletados antes da exibição do primeiro, e todos os resultados ficariam na memória ao mesmo tempo. Ou você pode exibir os resultados enquanto os encontra, o que seria mais eficiente em termos de memória e muito mais amigável para o usuário. O último pode ser feito passando a função de impressão de resultados para a função de pesquisa do sistema de arquivos, ou pode ser feito apenas tornando a função de pesquisa um gerador e iterando sobre o resultado.

Se você quiser ver um exemplo das duas últimas abordagens, consulte os.path.walk () (a antiga função de caminhar pelo sistema de arquivos com retorno de chamada) e os.walk () (o novo gerador de caminhada pelo sistema de arquivos). Obviamente, se você realmente queria coletar todos os resultados em uma lista, a abordagem do gerador é trivial para converter na abordagem da grande lista:

big_list = list(the_generator)
Thomas Wouters
fonte
Um gerador como o que produz listas de sistemas de arquivos executa ações em paralelo ao código que executa esse gerador em um loop? Idealmente, o computador executaria o corpo do loop (processando o último resultado) enquanto fazia simultaneamente o que o gerador deve fazer para obter o próximo valor.
9183 Steven Lu
@StevenLu: A menos que seja difícil iniciar manualmente os threads antes yielde joindepois para obter o próximo resultado, ele não é executado em paralelo (e nenhum gerador de biblioteca padrão faz isso; o lançamento secreto de threads é desaprovado). O gerador faz uma pausa em cada um yieldaté o próximo valor ser solicitado. Se o gerador estiver encapsulando E / S, o sistema operacional poderá armazenar em cache proativamente os dados do arquivo na suposição de que eles serão solicitados em breve, mas, como esse é o SO, o Python não está envolvido.
ShadowRanger
90

Uma das razões para usar o gerador é tornar a solução mais clara para algum tipo de solução.

O outro é tratar os resultados um de cada vez, evitando a criação de grandes listas de resultados que você processaria separados de qualquer maneira.

Se você tem uma função fibonacci-up-to-n como esta:

# function version
def fibon(n):
    a = b = 1
    result = []
    for i in xrange(n):
        result.append(a)
        a, b = b, a + b
    return result

Você pode escrever mais facilmente a função da seguinte maneira:

# generator version
def fibon(n):
    a = b = 1
    for i in xrange(n):
        yield a
        a, b = b, a + b

A função é mais clara. E se você usar a função assim:

for x in fibon(1000000):
    print x,

Neste exemplo, se você estiver usando a versão do gerador, a lista inteira de 1000000 itens não será criada, apenas um valor por vez. Esse não seria o caso ao usar a versão da lista, onde uma lista seria criada primeiro.

nosklo
fonte
18
e se você precisa de uma lista, você sempre pode fazerlist(fibon(5))
endolith
41

Veja a seção "Motivação" no PEP 255 .

Um uso não óbvio de geradores está criando funções interruptíveis, o que permite que você faça coisas como atualizar a interface do usuário ou executar vários trabalhos "simultaneamente" (intercalados, na verdade) sem usar threads.

Nickolay
fonte
1
A seção Motivação é boa, pois possui um exemplo específico: "Quando uma função de produtor tem um trabalho bastante difícil que exige a manutenção do estado entre os valores produzidos, a maioria das linguagens de programação não oferece uma solução agradável e eficiente além de adicionar uma função de retorno de chamada ao argumento do produtor. lista ... Por exemplo, tokenize.py na biblioteca padrão leva esta abordagem"
Ben Creasy
38

Encontro essa explicação que tira minha dúvida. Porque existe a possibilidade de uma pessoa que não conheceGenerators também não conheceryield

Retorna

A instrução de retorno é onde todas as variáveis ​​locais são destruídas e o valor resultante é devolvido (retornado) ao chamador. Caso a mesma função seja chamada algum tempo depois, ela receberá um novo conjunto de variáveis.

Produção

Mas e se as variáveis ​​locais não forem descartadas quando sairmos de uma função? Isso implica que podemos de resume the functiononde paramos. É aqui que o conceito de generatorsé introduzido e a yieldinstrução é retomada de onde functionparou.

  def generate_integers(N):
    for i in xrange(N):
    yield i

    In [1]: gen = generate_integers(3)
    In [2]: gen
    <generator object at 0x8117f90>
    In [3]: gen.next()
    0
    In [4]: gen.next()
    1
    In [5]: gen.next()

Então essa é a diferença entre returneyield instruções no Python.

A declaração de rendimento é o que torna uma função uma função geradora.

Portanto, os geradores são uma ferramenta simples e poderosa para criar iteradores. Eles são escritos como funções regulares, mas usam a yieldinstrução sempre que desejam retornar dados. Cada vez que next () é chamado, o gerador continua de onde parou (lembra todos os valores de dados e qual instrução foi executada pela última vez).

Miragem
fonte
33

Exemplo do mundo real

Digamos que você tenha 100 milhões de domínios em sua tabela MySQL e gostaria de atualizar o ranking Alexa para cada domínio.

A primeira coisa que você precisa é selecionar seus nomes de domínio no banco de dados.

Digamos que o nome da sua tabela seja domainse o nome da coluna sejadomain .

Se você usar, SELECT domain FROM domainsele retornará 100 milhões de linhas, o que consumirá muita memória. Portanto, seu servidor pode falhar.

Então você decidiu executar o programa em lotes. Digamos que o tamanho do nosso lote seja 1000.

Em nosso primeiro lote, consultaremos as primeiras 1000 linhas, verificaremos a classificação Alexa para cada domínio e atualizaremos a linha do banco de dados.

Em nosso segundo lote, trabalharemos nas próximas 1000 linhas. Em nosso terceiro lote, será de 2001 a 3000 e assim por diante.

Agora precisamos de uma função geradora que gere nossos lotes.

Aqui está a nossa função de gerador:

def ResultGenerator(cursor, batchsize=1000):
    while True:
        results = cursor.fetchmany(batchsize)
        if not results:
            break
        for result in results:
            yield result

Como você pode ver, nossa função mantém yieldos resultados. Se você usasse a palavra-chave em returnvez de yield, toda a função seria encerrada assim que atingisse o retorno.

return - returns only once
yield - returns multiple times

Se uma função usa a palavra-chave yield - , é um gerador.

Agora você pode iterar assim:

db = MySQLdb.connect(host="localhost", user="root", passwd="root", db="domains")
cursor = db.cursor()
cursor.execute("SELECT domain FROM domains")
for result in ResultGenerator(cursor):
    doSomethingWith(result)
db.close()
Giri
fonte
seria mais prático, se o rendimento pudesse ser explicado em termos de programação recursiva / dinâmica!
igaurav
27

Carregando. Quando é eficiente buscar dados em grandes pedaços, mas processá-los em pequenos pedaços, um gerador pode ajudar:

def bufferedFetch():
  while True:
     buffer = getBigChunkOfData()
     # insert some code to break on 'end of data'
     for i in buffer:    
          yield i

O exposto acima permite separar facilmente o buffer do processamento. A função consumidor agora pode obter os valores um por um sem se preocupar com o buffer.

Rafał Dowgird
fonte
3
Se getBigChuckOfData não for preguiçoso, não entendo qual o benefício que o rendimento tem aqui. O que é um caso de uso para esta função?
Sean Geoffrey Pietz
1
Mas o ponto é que, IIUC, bufferedFetch está preguiçosamente pressionando a chamada para getBigChunkOfData. Se getBigChunkOfData já era preguiçoso, o bufferedFetch seria inútil. Cada chamada para bufferedFetch () retornará um elemento buffer, mesmo que um BigChunk já tenha sido lido. E você não precisa manter explicitamente a contagem do próximo elemento a retornar, porque a mecânica do rendimento faz exatamente isso implicitamente.
hmijail lamenta os demitidos 26/10
21

Descobri que os geradores são muito úteis para limpar seu código e oferecer uma maneira muito única de encapsular e modularizar o código. Em uma situação em que você precisa cuspir valores constantemente com base em seu próprio processamento interno e quando algo precisa ser chamado de qualquer lugar do seu código (e não apenas dentro de um loop ou bloco, por exemplo), os geradores são o recurso para usar.

Um exemplo abstrato seria um gerador de números de Fibonacci que não vive dentro de um loop e quando é chamado de qualquer lugar sempre retornará o próximo número na sequência:

def fib():
    first = 0
    second = 1
    yield first
    yield second

    while 1:
        next = first + second
        yield next
        first = second
        second = next

fibgen1 = fib()
fibgen2 = fib()

Agora você tem dois objetos geradores de números de Fibonacci que você pode chamar de qualquer lugar do seu código e eles sempre retornarão números cada vez maiores de Fibonacci na sequência da seguinte maneira:

>>> fibgen1.next(); fibgen1.next(); fibgen1.next(); fibgen1.next()
0
1
1
2
>>> fibgen2.next(); fibgen2.next()
0
1
>>> fibgen1.next(); fibgen1.next()
3
5

O mais adorável dos geradores é que eles encapsulam o estado sem ter que passar pelos bastidores da criação de objetos. Uma maneira de pensar sobre eles é como "funções" que lembram seu estado interno.

Eu recebi o exemplo de Fibonacci do Python Generators - O que são? e com um pouco de imaginação, você pode criar muitas outras situações em que os geradores são uma ótima alternativa para forloops e outras construções de iteração tradicionais.

Andz
fonte
19

A explicação simples: considere uma fordeclaração

for item in iterable:
   do_stuff()

Na maioria das vezes, todos os itens iterablenão precisam estar lá desde o início, mas podem ser gerados rapidamente, conforme necessário. Isso pode ser muito mais eficiente nos dois

  • espaço (você nunca precisa armazenar todos os itens simultaneamente) e
  • tempo (a iteração pode terminar antes que todos os itens sejam necessários).

Outras vezes, você nem conhece todos os itens antes do tempo. Por exemplo:

for command in user_input():
   do_stuff_with(command)

Você não tem como conhecer todos os comandos do usuário de antemão, mas pode usar um loop agradável como esse se tiver um gerador entregando comandos:

def user_input():
    while True:
        wait_for_command()
        cmd = get_command()
        yield cmd

Com os geradores, você também pode ter iteração em infinitas seqüências, o que obviamente não é possível ao iterar sobre contêineres.

dF.
fonte
... e uma sequência infinita pode ser gerada alternando repetidamente sobre uma pequena lista, retornando ao início após o término. Eu uso isso para selecionar cores em gráficos ou produzir throbbers ou spinners ocupados em texto.
Andrej Panjkov 31/10/12
@ mataap: Há um itertoolpara isso - veja cycles.
martineau
12

Meus usos favoritos são operações de "filtro" e "redução".

Digamos que estamos lendo um arquivo e queremos apenas as linhas que começam com "##".

def filter2sharps( aSequence ):
    for l in aSequence:
        if l.startswith("##"):
            yield l

Podemos então usar a função do gerador em um loop adequado

source= file( ... )
for line in filter2sharps( source.readlines() ):
    print line
source.close()

O exemplo de redução é semelhante. Digamos que temos um arquivo em que precisamos localizar blocos de <Location>...</Location>linhas. [Não tags HTML, mas linhas que parecem com tags.]

def reduceLocation( aSequence ):
    keep= False
    block= None
    for line in aSequence:
        if line.startswith("</Location"):
            block.append( line )
            yield block
            block= None
            keep= False
        elif line.startsWith("<Location"):
            block= [ line ]
            keep= True
        elif keep:
            block.append( line )
        else:
            pass
    if block is not None:
        yield block # A partial block, icky

Novamente, podemos usar esse gerador em um loop for apropriado.

source = file( ... )
for b in reduceLocation( source.readlines() ):
    print b
source.close()

A idéia é que uma função geradora nos permita filtrar ou reduzir uma sequência, produzindo uma outra sequência, um valor por vez.

S.Lott
fonte
8
fileobj.readlines()leria o arquivo inteiro em uma lista na memória, derrotando o propósito de usar geradores. Como os objetos de arquivo já são iteráveis, você pode usá-lo for b in your_generator(fileobject):. Dessa forma, seu arquivo será lido uma linha por vez, para evitar a leitura do arquivo inteiro.
Nosklo 19/09/08
reduzirLocalização é bem estranho produzindo uma lista, por que não apenas produzir cada linha? Além disso, filtrar e reduzir são internos com comportamentos esperados (consulte a ajuda no ipython etc.), seu uso de "reduzir" é o mesmo que o filtro.
James Antill
Bom argumento sobre as linhas de leitura (). Normalmente, percebo que os arquivos são iteradores de primeira classe durante o teste de unidade.
S.Lott
Na verdade, a "redução" está combinando várias linhas individuais em um objeto composto. Ok, é uma lista, mas ainda é uma redução retirada da fonte.
S.Lott
9

Um exemplo prático em que você poderia usar um gerador é se você tiver algum tipo de forma e quiser percorrer os cantos, bordas ou o que for. Para o meu próprio projeto (código fonte aqui ), eu tinha um retângulo:

class Rect():

    def __init__(self, x, y, width, height):
        self.l_top  = (x, y)
        self.r_top  = (x+width, y)
        self.r_bot  = (x+width, y+height)
        self.l_bot  = (x, y+height)

    def __iter__(self):
        yield self.l_top
        yield self.r_top
        yield self.r_bot
        yield self.l_bot

Agora eu posso criar um retângulo e fazer um loop nos cantos:

myrect=Rect(50, 50, 100, 100)
for corner in myrect:
    print(corner)

Em vez de __iter__você poderia ter um método iter_cornerse chamar isso com for corner in myrect.iter_corners(). É apenas mais elegante de usar, __iter__desde então, podemos usar o nome da instância da classe diretamente na forexpressão.

Pithikos
fonte
Eu adorava a idéia de passar campos de classe semelhantes como um gerador
eusoubrasileiro
7

Evitando basicamente as funções de retorno de chamada ao iterar sobre o estado de manutenção da entrada.

Veja aqui e aqui uma visão geral do que pode ser feito usando geradores.

MvdD
fonte
4

Algumas boas respostas aqui, no entanto, eu também recomendaria uma leitura completa do tutorial de Programação Funcional do Python, que ajuda a explicar alguns dos casos de uso mais potentes dos geradores.

songololo
fonte
3

Como o método de envio de um gerador não foi mencionado, aqui está um exemplo:

def test():
    for i in xrange(5):
        val = yield
        print(val)

t = test()

# Proceed to 'yield' statement
next(t)

# Send value to yield
t.send(1)
t.send('2')
t.send([3])

Mostra a possibilidade de enviar um valor para um gerador em execução. Um curso mais avançado sobre geradores no vídeo abaixo (incluindo yieldexplicações, geradores para processamento paralelo, escapando do limite de recursão etc.)

David Beazley em geradores na PyCon 2014

John Damen
fonte
2

Eu uso geradores quando nosso servidor da web está atuando como um proxy:

  1. O cliente solicita uma URL com proxy do servidor
  2. O servidor começa a carregar o URL de destino
  3. O servidor cede para retornar os resultados ao cliente assim que os obtém
Brian
fonte
1

Pilhas de coisas. Sempre que você quiser gerar uma sequência de itens, mas não precisará 'materializar' todos eles em uma lista de uma só vez. Por exemplo, você pode ter um gerador simples que retorna números primos:

def primes():
    primes_found = set()
    primes_found.add(2)
    yield 2
    for i in itertools.count(1):
        candidate = i * 2 + 1
        if not all(candidate % prime for prime in primes_found):
            primes_found.add(candidate)
            yield candidate

Você pode usar isso para gerar os produtos de números primos subsequentes:

def prime_products():
    primeiter = primes()
    prev = primeiter.next()
    for prime in primeiter:
        yield prime * prev
        prev = prime

Estes são exemplos bastante triviais, mas você pode ver como pode ser útil para processar conjuntos de dados grandes (potencialmente infinitos!) Sem gerá-los com antecedência, que é apenas um dos usos mais óbvios.

Nick Johnson
fonte
se não houver (% nobre candidato para privilegiada no primes_found) deve ser se todos (% nobre candidato para privilegiada no primes_found)
rjmunro
Sim, eu pretendia escrever "se não houver (candidato% nobre == 0 para privilegiada no primes_found) O seu é um pouco mais limpa, embora :)..
Nick Johnson
Eu acho que você esqueceu de apagar o 'não' de se não todos (% nobre candidato para privilegiada no primes_found)
Thava
0

Também é bom para imprimir os números primos até n:

def genprime(n=10):
    for num in range(3, n+1):
        for factor in range(2, num):
            if num%factor == 0:
                break
        else:
            yield(num)

for prime_num in genprime(100):
    print(prime_num)
Sébastien Wieckowski
fonte