Personalize a Iteração Usando Geradores

Beginner

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

Introdução

Neste laboratório, você aprenderá como personalizar a iteração usando geradores em Python. Você também implementará a funcionalidade de iterador em classes personalizadas e criará geradores para fontes de dados de streaming.

O arquivo structure.py será modificado, e um novo arquivo chamado follow.py será criado durante o experimento.

Compreendendo os Geradores Python

Geradores são um recurso poderoso em Python. Eles oferecem uma maneira simples e elegante de criar iteradores. Em Python, quando você lida com sequências de dados, os iteradores são muito úteis, pois permitem que você percorra uma série de valores um por um. Funções regulares normalmente retornam um único valor e, em seguida, param de executar. No entanto, os geradores são diferentes. Eles podem produzir uma sequência de valores ao longo do tempo, o que significa que podem produzir múltiplos valores de forma gradual.

O que é um Gerador?

Uma função geradora tem uma aparência semelhante a uma função regular. Mas a diferença fundamental reside em como ela retorna valores. Em vez de usar a instrução return para fornecer um único resultado, uma função geradora usa a instrução yield. A instrução yield é especial. Cada vez que é executada, o estado da função é pausado, e o valor que segue a palavra-chave yield é retornado ao chamador. Quando a função geradora é chamada novamente, ela retoma a execução exatamente de onde parou.

Vamos começar criando uma função geradora simples. A função range() embutida em Python não suporta passos fracionários. Então, criaremos uma função geradora que pode produzir uma faixa de números com um passo fracionário.

  1. Primeiro, você precisa abrir um novo terminal Python no WebIDE. Para fazer isso, clique no menu "Terminal" e selecione "Novo Terminal".
  2. Depois que o terminal estiver aberto, digite o seguinte código no terminal. Este código define uma função geradora e, em seguida, a testa.
def frange(start, stop, step):
    current = start
    while current < stop:
        yield current
        current += step

## Test the generator with a for loop
for x in frange(0, 2, 0.25):
    print(x, end=' ')

Neste código, a função frange é uma função geradora. Ela inicializa uma variável current com o valor start. Então, enquanto current for menor que o valor stop, ela produz o valor current e, em seguida, incrementa current pelo valor step. O loop for então itera sobre os valores produzidos pela função geradora frange e os imprime.

Você deve ver a seguinte saída:

0 0.25 0.5 0.75 1.0 1.25 1.5 1.75

A Natureza Única dos Geradores

Uma característica importante dos geradores é que eles são exauríveis. Isso significa que, uma vez que você iterou por todos os valores produzidos por um gerador, ele não pode ser usado novamente para produzir a mesma sequência de valores. Vamos demonstrar isso com o seguinte código:

## Create a generator object
f = frange(0, 2, 0.25)

## First iteration works fine
print("First iteration:")
for x in f:
    print(x, end=' ')
print("\n")

## Second iteration produces nothing
print("Second iteration:")
for x in f:
    print(x, end=' ')
print("\n")

Neste código, primeiro criamos um objeto gerador f usando a função frange. O primeiro loop for itera sobre todos os valores produzidos pelo gerador e os imprime. Após a primeira iteração, o gerador foi exaurido, o que significa que ele já produziu todos os valores que pode. Então, quando tentamos iterar sobre ele novamente no segundo loop for, ele não produz nenhum valor novo.

Saída:

First iteration:
0 0.25 0.5 0.75 1.0 1.25 1.5 1.75

Second iteration:

Observe que a segunda iteração não produziu nenhuma saída porque o gerador já estava exaurido.

Criando Geradores Reutilizáveis com Classes

Se você precisar iterar várias vezes sobre a mesma sequência de valores, pode encapsular o gerador em uma classe. Ao fazer isso, cada vez que você iniciar uma nova iteração, um novo gerador será criado.

class FRange:
    def __init__(self, start, stop, step):
        self.start = start
        self.stop = stop
        self.step = step

    def __iter__(self):
        n = self.start
        while n < self.stop:
            yield n
            n += self.step

## Create an instance
f = FRange(0, 2, 0.25)

## We can iterate multiple times
print("First iteration:")
for x in f:
    print(x, end=' ')
print("\n")

print("Second iteration:")
for x in f:
    print(x, end=' ')
print("\n")

Neste código, definimos uma classe FRange. O método __init__ inicializa os valores start, stop e step. O método __iter__ é um método especial em classes Python. Ele é usado para criar um iterador. Dentro do método __iter__, temos um gerador que produz valores de maneira semelhante à função frange que definimos anteriormente.

Quando criamos uma instância f da classe FRange e iteramos sobre ela várias vezes, cada iteração chama o método __iter__, que cria um novo gerador. Então, podemos obter a mesma sequência de valores várias vezes.

Saída:

First iteration:
0 0.25 0.5 0.75 1.0 1.25 1.5 1.75

Second iteration:
0 0.25 0.5 0.75 1.0 1.25 1.5 1.75

Desta vez, podemos iterar várias vezes porque o método __iter__() cria um novo gerador cada vez que é chamado.

Adicionando Iteração a Classes Personalizadas

Agora que você compreendeu os conceitos básicos de geradores, vamos usá-los para adicionar recursos de iteração a classes personalizadas. Em Python, se você deseja tornar uma classe iterável, precisa implementar o método especial __iter__(). Uma classe iterável permite que você percorra seus elementos, assim como você pode percorrer uma lista ou uma tupla. Este é um recurso poderoso que torna suas classes personalizadas mais flexíveis e fáceis de trabalhar.

Compreendendo o Método __iter__()

O método __iter__() é uma parte crucial para tornar uma classe iterável. Ele deve retornar um objeto iterador. Um iterador é um objeto que pode ser iterado (percorrido em loop). Uma maneira simples e eficaz de conseguir isso é definindo __iter__() como uma função geradora. Uma função geradora usa a palavra-chave yield para produzir uma sequência de valores, um de cada vez. Cada vez que a instrução yield é encontrada, a função pausa e retorna o valor. Na próxima vez que o iterador for chamado, a função retoma de onde parou.

Modificando a Classe Structure

Na configuração deste laboratório, fornecemos uma classe base Structure. Outras classes, como Stock, podem herdar desta classe Structure. Herança é uma maneira de criar uma nova classe que herda as propriedades e métodos de uma classe existente. Ao adicionar um método __iter__() à classe Structure, podemos tornar todas as suas subclasses iteráveis. Isso significa que qualquer classe que herde de Structure terá automaticamente a capacidade de ser percorrida em loop.

  1. Abra o arquivo structure.py no WebIDE:
cd ~/project

Este comando altera o diretório de trabalho atual para o diretório project, onde o arquivo structure.py está localizado. Você precisa estar no diretório correto para acessar e modificar o arquivo.

  1. Observe a implementação atual da classe Structure:
class Structure(metaclass=StructureMeta):
    _fields = []
    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError(f'Expected {len(self._fields)} arguments')
        for name, val in zip(self._fields, args):
            setattr(self, '_'+name, val)

A classe Structure possui uma lista _fields que armazena os nomes dos atributos. O método __init__() é o construtor da classe. Ele inicializa os atributos do objeto verificando se o número de argumentos passados é igual ao número de campos. Caso contrário, ele levanta um TypeError. Caso contrário, ele define os atributos usando a função setattr().

  1. Adicione um método __iter__() que produza cada valor de atributo em ordem:
def __iter__(self):
    for name in self._fields:
        yield getattr(self, name)

Este método __iter__() é uma função geradora. Ele percorre a lista _fields e usa a função getattr() para obter o valor de cada atributo. A palavra-chave yield então retorna o valor um por um.

O arquivo structure.py completo agora deve ser assim:

class StructureMeta(type):
    def __new__(cls, name, bases, clsdict):
        fields = clsdict.get('_fields', [])
        for name in fields:
            clsdict[name] = property(lambda self, name=name: getattr(self, '_'+name))
        return super().__new__(cls, name, bases, clsdict)

class Structure(metaclass=StructureMeta):
    _fields = []
    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError(f'Expected {len(self._fields)} arguments')
        for name, val in zip(self._fields, args):
            setattr(self, '_'+name, val)

    def __iter__(self):
        for name in self._fields:
            yield getattr(self, name)

Esta classe Structure atualizada agora possui o método __iter__(), o que a torna e suas subclasses iteráveis.

  1. Salve o arquivo.
    Depois de fazer alterações no arquivo structure.py, você precisa salvá-lo para que as alterações sejam aplicadas.

  2. Agora, vamos testar a capacidade de iteração criando uma instância Stock e iterando sobre ela:

python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print('Iterating over Stock:'); [print(val) for val in s]"

Este comando cria uma instância da classe Stock, que herda da classe Structure. Em seguida, itera sobre a instância usando uma compreensão de lista e imprime cada valor.

Você deve ver uma saída como esta:

Iterating over Stock:
GOOG
100
490.1

Agora, qualquer classe que herde de Structure será automaticamente iterável, e a iteração produzirá os valores dos atributos na ordem definida pela lista _fields. Isso significa que você pode facilmente percorrer os atributos de qualquer subclasse de Structure sem ter que escrever código adicional para iteração.

Aprimorando Classes com Recursos de Iteração

Agora, tornamos nossa classe Structure e suas subclasses compatíveis com iteração. Iteração é um conceito poderoso em Python que permite que você percorra uma coleção de itens um por um. Quando uma classe suporta iteração, ela se torna mais flexível e pode funcionar com muitos recursos embutidos do Python. Vamos explorar como esse suporte à iteração possibilita muitos recursos poderosos em Python.

Aproveitando a Iteração para Conversões de Sequência

Em Python, existem funções embutidas como list() e tuple(). Essas funções são muito úteis porque podem receber qualquer objeto iterável como entrada. Um objeto iterável é algo que você pode percorrer em loop, como uma lista, uma tupla ou, agora, nossas instâncias da classe Structure. Como nossa classe Structure agora suporta iteração, podemos facilmente converter instâncias dela em listas ou tuplas.

  1. Vamos tentar essas operações com uma instância Stock. A classe Stock é uma subclasse de Structure. Execute o seguinte comando em seu terminal:
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print('As list:', list(s)); print('As tuple:', tuple(s))"

Este comando primeiro importa a classe Stock, cria uma instância dela e, em seguida, converte essa instância em uma lista e uma tupla usando as funções list() e tuple(), respectivamente. A saída mostrará a instância representada como uma lista e uma tupla:

As list: ['GOOG', 100, 490.1]
As tuple: ('GOOG', 100, 490.1)

Desempacotamento (Unpacking)

Python tem um recurso muito útil chamado desempacotamento (unpacking). Desempacotamento permite que você pegue um objeto iterável e atribua seus elementos a variáveis individuais de uma só vez. Como nossa instância Stock é iterável, podemos usar esse recurso de desempacotamento nela.

python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); name, shares, price = s; print(f'Name: {name}, Shares: {shares}, Price: {price}')"

Neste código, criamos uma instância Stock e, em seguida, desempacotamos seus elementos em três variáveis: name, shares e price. Em seguida, imprimimos essas variáveis. A saída mostrará os valores dessas variáveis:

Name: GOOG, Shares: 100, Price: 490.1

Adicionando Recursos de Comparação

Quando uma classe suporta iteração, torna-se mais fácil implementar operações de comparação. Operações de comparação são usadas para verificar se dois objetos são iguais ou não. Vamos adicionar um método __eq__() à nossa classe Structure para comparar instâncias.

  1. Abra o arquivo structure.py novamente. O método __eq__() é um método especial em Python que é chamado quando você usa o operador == para comparar dois objetos. Adicione o seguinte código à classe Structure no arquivo structure.py:
def __eq__(self, other):
    return isinstance(other, type(self)) and tuple(self) == tuple(other)

Este método primeiro verifica se o objeto other é uma instância da mesma classe que self usando a função isinstance(). Em seguida, ele converte tanto self quanto other em tuplas e verifica se essas tuplas são iguais.

O arquivo structure.py completo agora deve ser assim:

class StructureMeta(type):
    def __new__(cls, name, bases, clsdict):
        fields = clsdict.get('_fields', [])
        for name in fields:
            clsdict[name] = property(lambda self, name=name: getattr(self, '_'+name))
        return super().__new__(cls, name, bases, clsdict)

class Structure(metaclass=StructureMeta):
    _fields = []
    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError(f'Expected {len(self._fields)} arguments')
        for name, val in zip(self._fields, args):
            setattr(self, '_'+name, val)

    def __iter__(self):
        for name in self._fields:
            yield getattr(self, name)

    def __eq__(self, other):
        return isinstance(other, type(self)) and tuple(self) == tuple(other)
  1. Depois de adicionar o método __eq__(), salve o arquivo structure.py.

  2. Vamos testar a capacidade de comparação. Execute o seguinte comando em seu terminal:

python3 -c "from stock import Stock; a = Stock('GOOG', 100, 490.1); b = Stock('GOOG', 100, 490.1); c = Stock('AAPL', 200, 123.4); print(f'a == b: {a == b}'); print(f'a == c: {a == c}')"

Este código cria três instâncias Stock: a, b e c. Em seguida, ele compara a com b e a com c usando o operador ==. A saída mostrará os resultados dessas comparações:

a == b: True
a == c: False
  1. Agora, para garantir que tudo esteja funcionando corretamente, precisamos executar os testes unitários. Testes unitários são um conjunto de código que verifica se diferentes partes do seu programa estão funcionando conforme o esperado. Execute o seguinte comando em seu terminal:
python3 teststock.py

Se tudo estiver funcionando corretamente, você deverá ver uma saída indicando que os testes foram aprovados:

..
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

Ao adicionar apenas dois métodos simples (__iter__() e __eq__()), aprimoramos significativamente nossa classe Structure com recursos que a tornam mais Pythonica e fácil de usar.

Criando um Gerador para Dados de Streaming

Em programação, geradores são uma ferramenta poderosa, especialmente ao lidar com problemas do mundo real, como monitorar uma fonte de dados de streaming. Nesta seção, aprenderemos como aplicar o que aprendemos sobre geradores a um cenário tão prático. Vamos criar um gerador que fica de olho em um arquivo de log e nos fornece novas linhas à medida que são adicionadas ao arquivo.

Configurando a Fonte de Dados

Antes de começarmos a criar o gerador, precisamos configurar uma fonte de dados. Neste caso, usaremos um programa de simulação que gera dados do mercado de ações.

Primeiro, você precisa abrir um novo terminal no WebIDE. É aqui que você executará comandos para iniciar a simulação.

Depois de abrir o terminal, você executará o programa de simulação de ações. Aqui estão os comandos que você precisa inserir:

cd ~/project
python3 stocksim.py

O primeiro comando cd ~/project altera o diretório atual para o diretório project em seu diretório home. O segundo comando python3 stocksim.py executa o programa de simulação de ações. Este programa gerará dados do mercado de ações e os escreverá em um arquivo chamado stocklog.csv no diretório atual. Deixe este programa rodando em segundo plano enquanto trabalhamos no código de monitoramento.

Criando um Monitor de Arquivo Simples

Agora que temos nossa fonte de dados configurada, vamos criar um programa que monitora o arquivo stocklog.csv. Este programa exibirá quaisquer alterações de preço que sejam negativas.

  1. Primeiro, crie um novo arquivo chamado follow.py no WebIDE. Para fazer isso, você precisa alterar o diretório para o diretório project usando o seguinte comando no terminal:
cd ~/project
  1. Em seguida, adicione o seguinte código ao arquivo follow.py. Este código abre o arquivo stocklog.csv, move o ponteiro do arquivo para o final do arquivo e, em seguida, verifica continuamente novas linhas. Se uma nova linha for encontrada e representar uma alteração de preço negativa, ela imprime o nome da ação, o preço e a alteração.
## 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('%10s %10.2f %10.2f' % (name, price, change))
  1. Depois de adicionar o código, salve o arquivo. Em seguida, execute o programa usando o seguinte comando no terminal:
python3 follow.py

Você deve ver uma saída que mostra ações com alterações de preço negativas. Pode ser algo parecido com isto:

      AAPL     148.24      -1.76
      GOOG    2498.45      -1.55

Se você quiser parar o programa, pressione Ctrl+C no terminal.

Convertendo para uma Função Geradora

Embora o código anterior funcione, podemos torná-lo mais reutilizável e modular convertendo-o em uma função geradora. Uma função geradora é um tipo especial de função que pode ser pausada e retomada, e ela produz valores um de cada vez.

  1. Abra o arquivo follow.py novamente e modifique-o para usar uma função geradora. Aqui está o código atualizado:
## follow.py
import os
import time

def follow(filename):
    """
    Generator function that yields new lines in a file as they are added.
    Similar to the 'tail -f' Unix command.
    """
    f = open(filename)
    f.seek(0, os.SEEK_END)   ## Move to the end of the file

    while True:
        line = f.readline()
        if line == '':
            time.sleep(0.1)   ## Sleep briefly and retry
            continue
        yield line

## Example usage - monitor stocks with negative price changes
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('%10s %10.2f %10.2f' % (name, price, change))

A função follow agora é uma função geradora. Ela abre o arquivo, move para o final e, em seguida, verifica continuamente novas linhas. Quando uma nova linha é encontrada, ela produz essa linha.

  1. Salve o arquivo e execute-o novamente usando o comando:
python3 follow.py

A saída deve ser a mesma de antes. Mas agora, a lógica de monitoramento de arquivos está bem encapsulada na função geradora follow. Isso significa que podemos reutilizar esta função em outros programas que precisam monitorar um arquivo.

Compreendendo o Poder dos Geradores

Ao converter nosso código de leitura de arquivos em uma função geradora, tornamos ele muito mais flexível e reutilizável. A função follow() pode ser usada em qualquer programa que precise monitorar um arquivo, não apenas para dados de ações.

Por exemplo, você pode usá-la para monitorar logs de servidor, logs de aplicativos ou qualquer outro arquivo que seja atualizado ao longo do tempo. Isso mostra como os geradores são uma ótima maneira de lidar com fontes de dados de streaming de forma limpa e modular.

Resumo

Neste laboratório, você aprendeu como personalizar a iteração em Python usando geradores. Você criou geradores simples com a instrução yield para gerar sequências de valores, adicionou suporte à iteração a classes personalizadas implementando o método __iter__(), aproveitou a iteração para conversões de sequência, desempacotamento (unpacking) e comparação, e construiu um gerador prático para monitorar uma fonte de dados de streaming.

Geradores são um recurso poderoso do Python que permite criar iteradores com código mínimo. Eles são especialmente úteis para processar grandes conjuntos de dados, trabalhar com dados de streaming, criar pipelines de dados e implementar padrões de iteração personalizados. Usar geradores permite que você escreva um código mais limpo e com maior eficiência de memória, que transmite claramente sua intenção.