Hoje vamos falar sobre como melhorar um padrão que você provavelmente já viu em muito código:
f = open(...)
....
f.close()
a = A(...)
a.start(...)
...
a.end()
c = Coisa(...)
c.cria(...)
....
c.destroi()
test = Test()
test.setUp()
test.run()
test.tearDown()
Os trechos de código acima possuem em comum o que chamamos acoplamento temporal (do inglês, temporal coupling).
Acoplamento mede o quanto uma coisa depende de outra em software, e é comumente medida entre módulos ou componentes – tipo, o quanto certas classes ou funções dependem uma da outra. Em geral, é desejável que haja poucas interdependências, para evitar que a complexidade se espalhe em um projeto.
O acoplamento temporal acontece quando uma coisa precisa ser feita depois de outra, mesmo que seja dentro do mesmo módulo ou função. A relação de interdependência é com o tempo, isto é, o momento em que as coisas precisam acontecer. Alguns exemplos seriam: fechar um arquivo depois de terminar de carregar o conteúdo, liberar memória quando acabar de usar, etc.
Reduzindo o acoplamento temporal
Desde a versão 2.5, Python possui o comando with para lidar exatamente com este tipo de situação. Com ele, podemos fazer:
with open(...) as f:
dados = f.read()
# processa dados aqui
Ao usar o bloco with para abrir um arquivo, o método close() é chamado por trás dos panos pelo gerenciador de contexto incondicionalmente (isto é, mesmo que ocorra alguma exceção no código de dentro do bloco).
O código equivalente seria:
f = open(...)
try:
dados = f.read()
finally;
f.close()
Repare como o primeiro código é mais curto e mais simples que o segundo, pois há menos interdependências entre as coisas que estão acontecendo.
A ideia é não precisar lembrar de ter que escrever try/finally e chamar o close(), utilizando por trás dos panos um código que se certifique de que as coisas aconteçam na ordem esperada. A este código, chamamos de gerenciadores de contexto, ou context managers.
Implementando um gerenciador de contexto
A maneira mais simples de implementar um gerenciador de contexto Python é utilizar o decorator contextlib.contextmanager. Vejamos um exemplo:
import os
import contextlib
@contextlib.contextmanager
def roda_em_dir(dir):
orig_dir = os.getcwd()
os.chdir(dir)
try:
yield
finally:
os.chdir(orig_dir)
Esse gerenciador de contexto nos permite rodar um código em um diretório qualquer, e ao fim dele voltar para o diretório que estávamos antes.
A função os.getcwd() devolve o diretório atual e a função os.chdir() entra no diretório passado como argumento, e depois os.chdir() é usada novamente para voltar para o diretório original.
Veja como usá-lo:
print('Comecei no diretorio: %s' % os.getcwd())
with roda_em_dir('/etc'):
print('Agora estou no diretorio: %s' % os.getcwd())
print('E agora, de volta no diretorio: %s' % os.getcwd())
Rodando esse código na minha máquina, a saída é:
Comecei no diretorio: /home/elias Agora estou no diretorio: /etc E agora, de volta no diretorio: /home/elias
A função roda_em_dir() é o que chamamos de uma corotina, pois utiliza o comando yield para “pausar” sua execução, entregando-a para o código que está dirigindo-a. Neste caso, isso é o trabalho do decorator contextlib.contextmanager, que entrega a execução para o código que está dentro do bloco with até que este termine, o que devolverá a execução para a corotina roda_em_dir(), que irá executar o código dentro do finally.
Não se preocupe se for um pouco difícil de entender como a coisa toda funciona, estamos passando por cima de alguns tópicos avançados aqui (decorators, corotinas, etc). O importante é que você se dê conta de que pode implementar um gerenciador de contexto rapidamente usando o contextlib.contextmanager com uma função que faça yield dentro de um bloco try/finally.
Vejamos um outro exemplo, desta vez vamos fazer um gerenciador de contexto que cria um arquivo temporário para utilizarmos em um código de teste, e deleta o arquivo automaticamente ao fim do bloco with:
import os
import contextlib
import tempfile
@contextlib.contextmanager
def arquivo_temp():
_, arq = tempfile.mkstemp()
try:
yield arq
finally:
os.remove(arq)
Repare como desta vez, o comando yield não está mais sozinho, desta vez ele está enviando a variável arq que contém o nome do arquivo temporário para ser usado no with, como segue:
with arquivo_temp() as arq:
print('Usando arquivo temporario %s' % arq)
print('Arquivo existe? %s' % os.path.exists(arq))
print('E agora, arquivo existe? %s' % os.path.exists(arq))
Rodando na minha máquina, a saída ficou:
Usando arquivo temporario /tmp/tmp2IUF7H Arquivo existe? True E agora, arquivo existe? False
Note como a variável arq pode ser usada depois do with também: isto mostra que o contexto está sendo gerenciado de maneira especial, mas o espaço de nomes de variáveis ainda é o mesmo (ou seja, o comando with é mais parecido com if e for, do que com o comando def, por exemplo).
Para concluir
Bem, apesar do mecanismo por trás ser um pouquinho complicado de entender inicialmente, você pode perceber que implementar um gerenciador de contexto não é muito difícil. Você precisa usar o decorator contextlib.contextmanager em uma função geradora fazendo yield dentro de um bloco try/finally – moleza!
Você também pode implementar um gerenciador de contexto escrevendo uma classe que implemente o protocolo do comando with, que envolve basicamente implementar dois métodos especiais chamados __enter__ e __exit__ que sinalizam respectivamente entrar e sair do bloco with.
Em geral é mais conveniente utilizar o @contextlib.contextmanager, mas em alguns casos é melhor implementar os métodos. Por exemplo, caso queira compartilhar o próprio objeto gerenciador do contexto dentro do with, você pode usar return self no método __enter__.
Agora, vá refatorar aqueles códigos com acoplamento temporal e se divirta!