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.
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.
Respostas:
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)
yield
e join
depois 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 yield
até 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.
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.
list(fibon(5))
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.
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 function
onde paramos. É aqui que o conceito de generators
é introduzido e a yield
instrução é retomada de onde function
parou.
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 return
eyield
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 yield
instruçã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).
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 domains
e o nome da coluna sejadomain
.
Se você usar, SELECT domain FROM domains
ele 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 yield
os resultados. Se você usasse a palavra-chave em return
vez 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()
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.
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 for
loops e outras construções de iteração tradicionais.
A explicação simples: considere uma for
declaração
for item in iterable:
do_stuff()
Na maioria das vezes, todos os itens iterable
não precisam estar lá desde o início, mas podem ser gerados rapidamente, conforme necessário. Isso pode ser muito mais eficiente nos dois
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.
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.
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.
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_corners
e 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 for
expressão.
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.
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 yield
explicações, geradores para processamento paralelo, escapando do limite de recursão etc.)
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.
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)