Personalizando a Iteração com Funções Geradoras

Beginner

This tutorial is from open-source community. Access the source code

Introdução

Esta seção aborda como você pode personalizar a iteração usando uma função geradora (generator function).

Um problema

Suponha que você queira criar seu próprio padrão de iteração personalizado.

Por exemplo, uma contagem regressiva.

>>> for x in countdown(10):
...   print(x, end=' ')
...
10 9 8 7 6 5 4 3 2 1
>>>

Há uma maneira fácil de fazer isso.

Geradores (Generators)

Um gerador (generator) é uma função que define a iteração.

def countdown(n):
    while n > 0:
        yield n
        n -= 1

Por exemplo:

>>> for x in countdown(10):
...   print(x, end=' ')
...
10 9 8 7 6 5 4 3 2 1
>>>

Um gerador (generator) é qualquer função que usa a instrução yield.

O comportamento dos geradores (generators) é diferente de uma função normal. Chamar uma função geradora (generator function) cria um objeto gerador (generator object). Ele não executa a função imediatamente.

def countdown(n):
    ## Added a print statement
    print('Counting down from', n)
    while n > 0:
        yield n
        n -= 1
>>> x = countdown(10)
## There is NO PRINT STATEMENT
>>> x
## x is a generator object
<generator object at 0x58490>
>>>

A função só executa na chamada __next__().

>>> x = countdown(10)
>>> x
<generator object at 0x58490>
>>> x.__next__()
Counting down from 10
10
>>>

yield produz um valor, mas suspende a execução da função. A função é retomada na próxima chamada para __next__().

>>> x.__next__()
9
>>> x.__next__()
8

Quando o gerador (generator) finalmente retorna, a iteração levanta um erro.

>>> x.__next__()
1
>>> x.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in ? StopIteration
>>>

Observação: Uma função geradora (generator function) implementa o mesmo protocolo de baixo nível que as instruções for usam em listas, tuplas, dicionários, arquivos, etc.

Exercício 6.4: Um Gerador Simples

Se você se encontrar querendo personalizar a iteração, você sempre deve pensar em funções geradoras (generator functions). Elas são fáceis de escrever---crie uma função que execute a lógica de iteração desejada e use yield para emitir valores.

Por exemplo, experimente este gerador que pesquisa um arquivo por linhas contendo uma substring correspondente:

>>> def filematch(filename, substr):
        with open(filename, 'r') as f:
            for line in f:
                if substr in line:
                    yield line

>>> for line in open('portfolio.csv'):
        print(line, end='')

name,shares,price
"AA",100,32.20
"IBM",50,91.10
"CAT",150,83.44
"MSFT",200,51.23
"GE",95,40.37
"MSFT",50,65.10
"IBM",100,70.44
>>> for line in filematch('portfolio.csv', 'IBM'):
        print(line, end='')

"IBM",50,91.10
"IBM",100,70.44
>>>

Isso é interessante---a ideia de que você pode esconder um monte de processamento personalizado em uma função e usá-la para alimentar um loop for. O próximo exemplo analisa um caso mais incomum.

Exercício 6.5: Monitorando uma fonte de dados de streaming

Geradores (generators) podem ser uma maneira interessante de monitorar fontes de dados em tempo real, como arquivos de log ou feeds do mercado de ações. Nesta parte, exploraremos essa ideia. Para começar, siga as próximas instruções cuidadosamente.

O programa stocksim.py é um programa que simula dados do mercado de ações. Como saída, o programa escreve constantemente dados em tempo real em um arquivo stocklog.csv. Em uma janela de comando separada, entre no diretório `` e execute este programa:

$ python3 stocksim.py

Se você estiver no Windows, basta localizar o programa stocksim.py e clicar duas vezes nele para executá-lo. Agora, esqueça este programa (apenas deixe-o rodando). Usando outra janela, observe o arquivo stocklog.csv sendo escrito pelo simulador. Você deve ver novas linhas de texto sendo adicionadas ao arquivo a cada poucos segundos. Novamente, apenas deixe este programa rodando em segundo plano---ele rodará por várias horas (você não deve precisar se preocupar com isso).

Depois que o programa acima estiver rodando, vamos escrever um pequeno programa para abrir o arquivo, procurar o final e observar a nova saída. Crie um arquivo follow.py e coloque este código nele:

## follow.py
import os
import time

f = open('stocklog.csv')
f.seek(0, os.SEEK_END)   ## Move file pointer 0 bytes from end of file

while True:
    line = f.readline()
    if line == '':
        time.sleep(0.1)   ## Sleep briefly and retry
        continue
    fields = line.split(',')
    name = fields[0].strip('"')
    price = float(fields[1])
    change = float(fields[4])
    if change < 0:
        print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')

Se você executar o programa, verá um ticker de ações em tempo real. Por baixo dos panos, este código é parecido com o comando Unix tail -f que é usado para observar um arquivo de log.

Observação: O uso do método readline() neste exemplo é um tanto incomum, pois não é a maneira usual de ler linhas de um arquivo (normalmente você usaria um loop for). No entanto, neste caso, estamos usando-o para sondar repetidamente o final do arquivo para ver se mais dados foram adicionados (readline() retornará novos dados ou uma string vazia).

Exercício 6.6: Usando um gerador para produzir dados

Se você olhar para o código no Exercício 6.5, a primeira parte do código está produzindo linhas de dados, enquanto as instruções no final do loop while estão consumindo os dados. Uma característica importante das funções geradoras (generator functions) é que você pode mover todo o código de produção de dados para uma função reutilizável.

Modifique o código no Exercício 6.5 para que a leitura do arquivo seja realizada por uma função geradora follow(filename). Faça com que o seguinte código funcione:

>>> for line in follow('stocklog.csv'):
          print(line, end='')

... Deve ver linhas de saída produzidas aqui ...

Modifique o código do ticker de ações para que ele se pareça com isto:

if __name__ == '__main__':
    for line in follow('stocklog.csv'):
        fields = line.split(',')
        name = fields[0].strip('"')
        price = float(fields[1])
        change = float(fields[4])
        if change < 0:
            print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')

Exercício 6.7: Monitorando seu portfólio

Modifique o programa follow.py para que ele monitore o fluxo de dados de ações e imprima um ticker mostrando informações apenas para as ações em um portfólio. Por exemplo:

if __name__ == '__main__':
    import report

    portfolio = report.read_portfolio('portfolio.csv')

    for line in follow('stocklog.csv'):
        fields = line.split(',')
        name = fields[0].strip('"')
        price = float(fields[1])
        change = float(fields[4])
        if name in portfolio:
            print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')

Observação: Para que isso funcione, sua classe Portfolio deve suportar o operador in. Consulte o Exercício 6.3 e certifique-se de implementar o operador __contains__().

Discussão

Algo muito poderoso acabou de acontecer aqui. Você moveu um padrão de iteração interessante (lendo linhas no final de um arquivo) para sua própria pequena função. A função follow() agora é uma utilidade de propósito completamente geral que você pode usar em qualquer programa. Por exemplo, você pode usá-la para monitorar logs de servidor, logs de depuração e outras fontes de dados semelhantes. Isso é muito legal.

Resumo

Parabéns! Você concluiu o laboratório de Personalização de Iteração. Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.