Introdução
Neste laboratório, você aprenderá como gerenciar o que acontece nas instruções yield em Python. Você obterá uma compreensão de como lidar com as operações e comportamentos associados a essas instruções de forma eficaz.
Além disso, você aprenderá sobre o ciclo de vida (lifetime) de geradores e tratamento de exceções em geradores. Os arquivos follow.py e cofollow.py serão modificados durante este processo de aprendizado.
Compreendendo o Ciclo de Vida (Lifetime) e o Fechamento de Geradores
Nesta etapa, vamos explorar o ciclo de vida (lifetime) dos geradores Python e aprender como fechá-los corretamente. Geradores em Python são um tipo especial de iterador que permite gerar uma sequência de valores em tempo real (on-the-fly), em vez de calculá-los todos de uma vez e armazená-los na memória. Isso pode ser muito útil ao lidar com grandes conjuntos de dados ou sequências infinitas.
O que é o Gerador follow()?
Vamos começar analisando o arquivo follow.py no diretório do projeto. Este arquivo contém uma função geradora chamada follow(). Uma função geradora é definida como uma função normal, mas em vez de usar a palavra-chave return, ela usa yield. Quando uma função geradora é chamada, ela retorna um objeto gerador, que você pode iterar para obter os valores que ele produz.
A função geradora follow() lê continuamente linhas de um arquivo e produz cada linha à medida que é lida. Isso é semelhante ao comando Unix tail -f, que monitora continuamente um arquivo em busca de novas linhas.
Abra o arquivo follow.py no editor WebIDE:
import os
import time
def follow(filename):
with open(filename,'r') as f:
f.seek(0,os.SEEK_END)
while True:
line = f.readline()
if line == '':
time.sleep(0.1) ## Sleep briefly to avoid busy wait
continue
yield line
Neste código, a instrução with open(filename, 'r') as f abre o arquivo no modo de leitura e garante que ele seja fechado corretamente quando o bloco for finalizado. A linha f.seek(0, os.SEEK_END) move o ponteiro do arquivo para o final do arquivo, para que o gerador comece a ler a partir do final. O loop while True lê continuamente linhas do arquivo. Se a linha estiver vazia, significa que ainda não há novas linhas, então o programa dorme por 0,1 segundos para evitar uma espera ocupada (busy wait) e, em seguida, continua para a próxima iteração. Se a linha não estiver vazia, ela é produzida.
Este gerador é executado em um loop infinito, o que levanta uma questão importante: o que acontece quando paramos de usar o gerador ou queremos terminá-lo antecipadamente?
Modificando o Gerador para Lidar com o Fechamento
Precisamos modificar a função follow() em follow.py para lidar com o caso em que o gerador é fechado corretamente. Para fazer isso, adicionaremos um bloco try-except que captura a exceção GeneratorExit. A exceção GeneratorExit é lançada quando um gerador é fechado, seja pela coleta de lixo (garbage collection) ou pela chamada do método close().
import os
import time
def follow(filename):
try:
with open(filename,'r') as f:
f.seek(0,os.SEEK_END)
while True:
line = f.readline()
if line == '':
time.sleep(0.1) ## Sleep briefly to avoid busy wait
continue
yield line
except GeneratorExit:
print('Following Done')
Neste código modificado, o bloco try contém a lógica principal do gerador. Se uma exceção GeneratorExit for lançada, o bloco except a captura e imprime a mensagem 'Following Done'. Esta é uma maneira simples de realizar ações de limpeza quando o gerador é fechado.
Salve o arquivo após fazer essas alterações.
Experimentando com o Fechamento de Geradores
Agora, vamos conduzir alguns experimentos para ver como os geradores se comportam quando são coletados pelo coletor de lixo (garbage collected) ou fechados explicitamente.
Abra um terminal e execute o interpretador Python:
cd ~/project
python3
Experimento 1: Coleta de Lixo (Garbage Collection) de um Gerador em Execução
>>> from follow import follow
>>> ## Experiment: Garbage collection of a running generator
>>> f = follow('stocklog.csv')
>>> next(f)
'"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314\n'
>>> del f ## Delete the generator object
Following Done ## This message appears because of our GeneratorExit handler
Neste experimento, primeiro importamos a função follow do arquivo follow.py. Em seguida, criamos um objeto gerador f chamando follow('stocklog.csv'). Usamos a função next() para obter a próxima linha do gerador. Finalmente, excluímos o objeto gerador usando a instrução del. Quando o objeto gerador é excluído, ele é fechado automaticamente, o que aciona nosso manipulador de exceção GeneratorExit, e a mensagem 'Following Done' é impressa.
Experimento 2: Fechando Explicitamente um Gerador
>>> f = follow('stocklog.csv')
>>> for line in f:
... print(line, end='')
... if 'IBM' in line:
... f.close() ## Explicitly close the generator
...
"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
"VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
"HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169
"GM",31.45,"6/11/2007","09:34.31",0.45,31.00,31.50,31.45,582429
"IBM",102.86,"6/11/2007","09:34.44",-0.21,102.87,102.86,102.77,147550
Following Done
>>> for line in f:
... print(line, end='') ## No output: generator is closed
...
Neste experimento, criamos um novo objeto gerador f e iteramos sobre ele usando um loop for. Dentro do loop, imprimimos cada linha e verificamos se a linha contém a string 'IBM'. Se contiver, chamamos o método close() no gerador para fechá-lo explicitamente. Quando o gerador é fechado, a exceção GeneratorExit é lançada, e nosso manipulador de exceção imprime a mensagem 'Following Done'. Depois que o gerador é fechado, se tentarmos iterar sobre ele novamente, não haverá saída porque o gerador não está mais ativo.
Experimento 3: Saindo e Retomando um Gerador
>>> f = follow('stocklog.csv')
>>> for line in f:
... print(line, end='')
... if 'IBM' in line:
... break ## Break out of the loop, but don't close the generator
...
"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
"VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
"HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169
"GM",31.45,"6/11/2007","09:34.31",0.45,31.00,31.50,31.45,582429
"IBM",102.86,"6/11/2007","09:34.44",-0.21,102.87,102.86,102.77,147550
>>> ## Resume iteration - the generator is still active
>>> for line in f:
... print(line, end='')
... if 'IBM' in line:
... break
...
"CAT",78.36,"6/11/2007","09:37.19",-0.16,78.32,78.36,77.99,237714
"VZ",42.99,"6/11/2007","09:37.20",-0.08,42.95,42.99,42.78,268459
"IBM",102.91,"6/11/2007","09:37.31",-0.16,102.87,102.91,102.77,190859
>>> del f ## Clean up
Following Done
Neste experimento, criamos um objeto gerador f e iteramos sobre ele usando um loop for. Dentro do loop, imprimimos cada linha e verificamos se a linha contém a string 'IBM'. Se contiver, usamos a instrução break para sair do loop. Sair do loop não fecha o gerador, então o gerador ainda está ativo. Podemos então retomar a iteração iniciando um novo loop for sobre o mesmo objeto gerador. Finalmente, excluímos o objeto gerador para limpar, o que aciona o manipulador de exceção GeneratorExit.
Principais Conclusões
- Quando um gerador é fechado (seja por coleta de lixo ou chamando
close()), uma exceçãoGeneratorExité lançada dentro do gerador. - Você pode capturar essa exceção para realizar ações de limpeza quando o gerador é fechado.
- Sair da iteração de um gerador (com
break) não fecha o gerador, permitindo que ele seja retomado mais tarde.
Saia do interpretador Python digitando exit() ou pressionando Ctrl+D.
Lidando com Exceções em Geradores
Nesta etapa, vamos aprender como lidar com exceções em geradores e corrotinas. Mas, primeiro, vamos entender o que são exceções. Uma exceção é um evento que ocorre durante a execução de um programa e interrompe o fluxo normal das instruções do programa. Em Python, podemos usar o método throw() para lidar com exceções em geradores e corrotinas.
Compreendendo Corrotinas
Uma corrotina é um tipo especial de gerador. Ao contrário dos geradores regulares que principalmente produzem valores, as corrotinas podem consumir valores (usando o método send()) e produzir valores. O arquivo cofollow.py tem uma implementação simples de uma corrotina.
Vamos abrir o arquivo cofollow.py no editor WebIDE. Aqui está o código dentro:
def consumer(func):
def start(*args,**kwargs):
c = func(*args,**kwargs)
next(c)
return c
return start
@consumer
def printer():
while True:
item = yield
print(item)
Agora, vamos analisar este código. O consumer é um decorador. Um decorador é uma função que recebe outra função como argumento, adiciona alguma funcionalidade a ela e, em seguida, retorna a função modificada. Neste caso, o decorador consumer move automaticamente o gerador para sua primeira instrução yield. Isso é importante porque torna o gerador pronto para receber valores.
A corrotina printer() é definida com o decorador @consumer. Dentro da função printer(), temos um loop while infinito. A instrução item = yield é onde a mágica acontece. Ela pausa a execução da corrotina e espera receber um valor. Quando um valor é enviado para a corrotina, ela retoma a execução e imprime o valor recebido.
Adicionando Tratamento de Exceções à Corrotina
Agora, vamos modificar a corrotina printer() para lidar com exceções. Vamos atualizar a função printer() em cofollow.py assim:
@consumer
def printer():
while True:
try:
item = yield
print(item)
except Exception as e:
print('ERROR: %r' % e)
O bloco try contém o código que pode lançar uma exceção. Em nosso caso, é o código que recebe e imprime o valor. Se uma exceção ocorrer no bloco try, a execução salta para o bloco except. O bloco except captura a exceção e imprime uma mensagem de erro. Após fazer essas alterações, salve o arquivo.
Experimentando com Tratamento de Exceções em Corrotinas
Vamos começar a experimentar o lançamento de exceções na corrotina. Abra um terminal e execute o interpretador Python usando os seguintes comandos:
cd ~/project
python3
Experimento 1: Uso Básico de Corrotina
>>> from cofollow import printer
>>> p = printer()
>>> p.send('hello') ## Send a value to the coroutine
hello
>>> p.send(42) ## Send another value
42
Aqui, primeiro importamos a corrotina printer do módulo cofollow. Em seguida, criamos uma instância da corrotina printer chamada p. Usamos o método send() para enviar valores para a corrotina. Como você pode ver, a corrotina processa os valores que enviamos para ela sem problemas.
Experimento 2: Lançando uma Exceção na Corrotina
>>> p.throw(ValueError('It failed')) ## Throw an exception into the coroutine
ERROR: ValueError('It failed')
Neste experimento, usamos o método throw() para injetar uma exceção ValueError na corrotina. O bloco try-except na corrotina printer() captura a exceção e imprime uma mensagem de erro. Isso mostra que nosso tratamento de exceção está funcionando como esperado.
Experimento 3: Lançando uma Exceção Real na Corrotina
>>> try:
... int('n/a') ## This will raise a ValueError
... except ValueError as e:
... p.throw(e) ## Throw the caught exception into the coroutine
...
ERROR: ValueError("invalid literal for int() with base 10: 'n/a'")
Aqui, primeiro tentamos converter a string 'n/a' em um inteiro, o que lança um ValueError. Capturamos essa exceção e, em seguida, usamos o método throw() para passá-la para a corrotina. A corrotina captura a exceção e imprime a mensagem de erro.
Experimento 4: Verificando se a Corrotina Continua em Execução
>>> p.send('still working') ## The coroutine continues to run after handling exceptions
still working
Após lidar com as exceções, enviamos outro valor para a corrotina usando o método send(). A corrotina ainda está ativa e pode processar o novo valor. Isso mostra que nossa corrotina pode continuar em execução mesmo após encontrar erros.
Principais Conclusões
- Geradores e corrotinas podem lidar com exceções no ponto da instrução
yield. Isso significa que podemos capturar e lidar com erros que ocorrem quando a corrotina está esperando ou processando um valor. - O método
throw()permite que você injete exceções em um gerador ou corrotina. Isso é útil para testes e para lidar com erros que ocorrem fora da corrotina. - Lidar adequadamente com exceções em geradores permite que você crie geradores robustos e tolerantes a erros que podem continuar em execução mesmo quando ocorrem erros. Isso torna seu código mais confiável e fácil de manter.
Para sair do interpretador Python, você pode digitar exit() ou pressionar Ctrl+D.
Aplicações Práticas do Gerenciamento de Geradores
Nesta etapa, vamos explorar como aplicar os conceitos que aprendemos sobre o gerenciamento de geradores e o tratamento de exceções em geradores a cenários do mundo real. Compreender essas aplicações práticas o ajudará a escrever um código Python mais robusto e eficiente.
Criando um Sistema Robusto de Monitoramento de Arquivos
Vamos construir uma versão mais confiável do nosso sistema de monitoramento de arquivos. Este sistema será capaz de lidar com diferentes situações, como timeouts e solicitações do usuário para parar.
Primeiro, abra o editor WebIDE e crie um novo arquivo chamado robust_follow.py. Aqui está o código que você precisa escrever neste arquivo:
import os
import time
import signal
class TimeoutError(Exception):
pass
def timeout_handler(signum, frame):
raise TimeoutError("Operation timed out")
def follow(filename, timeout=None):
"""
A generator that yields new lines in a file.
With timeout handling and proper cleanup.
"""
try:
## Set up timeout if specified
if timeout:
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(timeout)
with open(filename, 'r') as f:
f.seek(0, os.SEEK_END)
while True:
line = f.readline()
if line == '':
## No new data, wait briefly
time.sleep(0.1)
continue
yield line
except TimeoutError:
print(f"Following timed out after {timeout} seconds")
except GeneratorExit:
print("Following stopped by request")
finally:
## Clean up timeout alarm if it was set
if timeout:
signal.alarm(0)
print("Follow generator cleanup complete")
Neste código, primeiro definimos uma classe TimeoutError personalizada. A função timeout_handler é usada para lançar esse erro quando ocorre um timeout. A função follow é um gerador que lê um arquivo e produz novas linhas. Se um timeout for especificado, ele configura um alarme usando o módulo signal. Se não houver novos dados no arquivo, ele espera por um curto período de tempo antes de tentar novamente. O bloco try - except - finally é usado para lidar com diferentes exceções e garantir a limpeza adequada.
Após escrever o código, salve o arquivo.
Experimentando com o Sistema Robusto de Monitoramento de Arquivos
Agora, vamos testar nosso sistema de monitoramento de arquivos aprimorado. Abra um terminal e execute o interpretador Python com os seguintes comandos:
cd ~/project
python3
Experimento 1: Uso Básico
No interpretador Python, testaremos a funcionalidade básica do nosso gerador follow. Aqui está o código a ser executado:
>>> from robust_follow import follow
>>> f = follow('stocklog.csv')
>>> for i, line in enumerate(f):
... print(f"Line {i+1}: {line.strip()}")
... if i >= 2: ## Just read a few lines for the example
... break
...
Line 1: "MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
Line 2: "VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
Line 3: "HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169
Aqui, importamos a função follow do nosso arquivo robust_follow.py. Em seguida, criamos um objeto gerador f que acompanha o arquivo stocklog.csv. Usamos um loop for para iterar sobre as linhas produzidas pelo gerador e imprimir as três primeiras linhas.
Experimento 2: Usando Timeout
Vamos ver como o recurso de timeout funciona. Execute o seguinte código no interpretador Python:
>>> ## Create a generator that will time out after 3 seconds
>>> f = follow('stocklog.csv', timeout=3)
>>> for line in f:
... print(line.strip())
... time.sleep(1) ## Process each line slowly
...
"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
"VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
"HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169
Following timed out after 3 seconds
Follow generator cleanup complete
Neste experimento, criamos um gerador com um timeout de 3 segundos. Processamos cada linha lentamente, dormindo por 1 segundo entre cada linha. Após cerca de 3 segundos, o gerador lança uma exceção de timeout, e o código de limpeza no bloco finally é executado.
Experimento 3: Fechamento Explícito
Vamos testar como o gerador lida com um fechamento explícito. Execute o seguinte código:
>>> f = follow('stocklog.csv')
>>> for i, line in enumerate(f):
... print(f"Line {i+1}: {line.strip()}")
... if i >= 1:
... print("Explicitly closing the generator...")
... f.close()
...
Line 1: "MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
Line 2: "VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
Explicitly closing the generator...
Following stopped by request
Follow generator cleanup complete
Aqui, criamos um gerador e começamos a iterar sobre suas linhas. Após processar duas linhas, fechamos explicitamente o gerador usando o método close. O gerador então lida com a exceção GeneratorExit e realiza a limpeza necessária.
Criando um Pipeline de Processamento de Dados com Tratamento de Erros
Em seguida, criaremos um pipeline de processamento de dados simples usando corrotinas. Este pipeline será capaz de lidar com erros em diferentes estágios.
Abra o editor WebIDE e crie um novo arquivo chamado pipeline.py. Aqui está o código para escrever neste arquivo:
def consumer(func):
def start(*args,**kwargs):
c = func(*args,**kwargs)
next(c)
return c
return start
@consumer
def grep(pattern, target):
"""Filter lines containing pattern and send to target"""
try:
while True:
line = yield
if pattern in line:
target.send(line)
except Exception as e:
target.throw(e)
@consumer
def printer():
"""Print received items"""
try:
while True:
item = yield
print(f"PRINTER: {item}")
except Exception as e:
print(f"PRINTER ERROR: {repr(e)}")
def follow_and_process(filename, pattern):
"""Follow a file and process its contents"""
import time
import os
output = printer()
filter_pipe = grep(pattern, output)
try:
with open(filename, 'r') as f:
f.seek(0, os.SEEK_END)
while True:
line = f.readline()
if not line:
time.sleep(0.1)
continue
filter_pipe.send(line)
except KeyboardInterrupt:
print("Processing stopped by user")
finally:
filter_pipe.close()
output.close()
Neste código, o decorador consumer é usado para inicializar corrotinas. A corrotina grep filtra as linhas que contêm um padrão específico e as envia para outra corrotina. A corrotina printer imprime os itens recebidos. A função follow_and_process lê um arquivo, filtra suas linhas usando a corrotina grep e imprime as linhas correspondentes usando a corrotina printer. Ele também lida com a exceção KeyboardInterrupt e garante a limpeza adequada.
Após escrever o código, salve o arquivo.
Testando o Pipeline de Processamento de Dados
Vamos testar nosso pipeline de processamento de dados. Em um terminal, execute o seguinte comando:
cd ~/project
python3 -c "from pipeline import follow_and_process; follow_and_process('stocklog.csv', 'IBM')"
Você deve ver uma saída semelhante a esta:
PRINTER: "IBM",102.86,"6/11/2007","09:34.44",-0.21,102.87,102.86,102.77,147550
PRINTER: "IBM",102.91,"6/11/2007","09:37.31",-0.16,102.87,102.91,102.77,190859
PRINTER: "IBM",102.95,"6/11/2007","09:39.44",-0.12,102.87,102.95,102.77,225350
Esta saída mostra que o pipeline está funcionando corretamente, filtrando e imprimindo linhas que contêm o padrão "IBM".
Para interromper o processo, pressione Ctrl+C. Você deve ver a seguinte mensagem:
Processing stopped by user
Principais Conclusões
- O tratamento adequado de exceções em geradores permite que você crie sistemas robustos que podem lidar com erros de forma elegante. Isso significa que seus programas não travarão inesperadamente quando algo der errado.
- Você pode usar técnicas como timeouts para evitar que os geradores sejam executados indefinidamente. Isso ajuda a gerenciar os recursos do sistema e garante que seu programa não fique preso em um loop infinito.
- Geradores e corrotinas podem formar pipelines de processamento de dados poderosos, onde os erros podem ser propagados e tratados no nível apropriado. Isso facilita a construção de sistemas complexos de processamento de dados.
- O bloco
finallyem geradores garante que as operações de limpeza sejam executadas, independentemente de como o gerador termina. Isso ajuda a manter a integridade do seu programa e evita vazamentos de recursos.
Resumo
Neste laboratório, você aprendeu técnicas essenciais para gerenciar as instruções yield em geradores e corrotinas Python. Você explorou o gerenciamento do ciclo de vida do gerador, incluindo o tratamento da exceção GeneratorExit durante o fechamento ou coleta de lixo (garbage collection) e o controle da interrupção e retomada da iteração. Além disso, você aprendeu sobre o tratamento de exceções em geradores, como o uso do método throw() e a escrita de geradores robustos para lidar com exceções de forma elegante.
Essas técnicas são fundamentais para a construção de aplicações Python robustas e de fácil manutenção. Elas são úteis para processamento de dados, operações assíncronas e gerenciamento de recursos. Ao gerenciar adequadamente o ciclo de vida do gerador e lidar com exceções, você pode criar sistemas resilientes que lidam com erros de forma elegante e limpam os recursos quando eles não são mais necessários.