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.
- Primeiro, você precisa abrir um novo terminal Python no WebIDE. Para fazer isso, clique no menu "Terminal" e selecione "Novo Terminal".
- 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.
- Abra o arquivo
structure.pyno 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.
- 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().
- 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.
Salve o arquivo. Depois de fazer alterações no arquivo
structure.py, você precisa salvá-lo para que as alterações sejam aplicadas.Agora, vamos testar a capacidade de iteração criando uma instância
Stocke 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.
- Vamos tentar essas operações com uma instância
Stock. A classeStocké uma subclasse deStructure. 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.
- Abra o arquivo
structure.pynovamente. 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 à classeStructureno arquivostructure.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)
Depois de adicionar o método
__eq__(), salve o arquivostructure.py.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
- 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.
- Primeiro, crie um novo arquivo chamado
follow.pyno WebIDE. Para fazer isso, você precisa alterar o diretório para o diretórioprojectusando o seguinte comando no terminal:
cd ~/project
- Em seguida, adicione o seguinte código ao arquivo
follow.py. Este código abre o arquivostocklog.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))
- 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.
- Abra o arquivo
follow.pynovamente 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.
- 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.