Gerenciadores de contexto podem vir a ser quase tão importantes quanto a própria sub-rotina. Só arranhamos a superfície das possibilidades. […] Basic tem uma instrução
with
, há instruçõeswith
em várias linguagens. Mas elas não fazem a mesma coisa, todas fazem algo muito raso, economizam consultas a atributos com o operador ponto (.
), elas não configuram e desfazem ambientes. Não pense que é a mesma coisa só porque o nome é igual. A instruçãowith
é muito mais que isso.[1] (EN)
um eloquente evangelista de Python
Este capítulo é sobre mecanismos de controle de fluxo não muito comuns em outras linguagens e que, por essa razão, podem ser ignorados ou subutilizados em Python. São eles:
-
A instrução
with
e o protocolo de gerenciamento de contexto -
A instrução
match/case
para pattern matching (casamento de padrões) -
A cláusula
else
nas instruçõesfor
,while
, etry
A instrução with
cria um contexto temporário e o destrói com segurança, sob o controle de um objeto gerenciador de contexto. Isso previne erros e reduz código repetitivo, tornando as APIs ao mesmo tempo mais seguras e mais fáceis de usar. Programadores Python estão encontrando muitos usos para blocos with
além do fechamento automático de arquivos.
Já estudamos pattern matching em capítulos anteriores, mas aqui veremos como a gramática de uma linguagem de programação pode ser expressa como padrões de sequências.
Por isso match/case
é uma ferramenta eficiente para criar processadores de linguagem fáceis de entender e de estender. Vamos examinar um interpretador completo para um pequeno (porém funcional) subconjunto da linguagem Scheme. As mesmas ideias poderiam ser aplicadas no desenvolvimento de uma linguagem de templates ou uma DSL (Domain-Specific Language, literalmente Linguagem de Domínio Específico) para codificar regras de negócio em um sistema maior.
A cláusula else
não é grande coisa, mas ajuda a transmitir a intenção por trás do código quando usada corretamente junto com for
, while
e try
.
Também atualizei a Utilitários do contextlib para incluir alguns recursos do módulo contextlib
adicionados desde o Python 3.6,
e os novos gerenciadores de contexto "parentizados", introduzidos no Python 3.10.
Vamos começar com a poderosa instrução with
.
Objetos gerenciadores de contexto
existem para controlar uma instrução with
, da mesma forma que iteradores existem para controlar uma instrução for
.
A instrução with
foi projetada para simplificar alguns usos comuns de try/finally
,
que garantem que alguma operação seja realizada após um bloco de código,
mesmo que o bloco termine com um return
, uma exceção, ou uma chamada sys.exit()
.
O código no bloco finally
normalmente libera um recurso crítico ou restaura um estado anterior que havia sido temporariamente modificado.
A comunidade Python está encontrando novos usos criativos para gerenciadores de contexto. Alguns exemplos, da biblioteca padrão, são:
-
Gerenciar transações no módulo
sqlite3
— veja "Usando a conexão como gerenciador de contexto". -
Manipular travas, condições e semáforos de forma segura—como descrito na documentação do módulo
threading
(EN). -
Configurar ambientes personalizados para operações aritméticas com objetos
Decimal
—veja a documentação dedecimal.localcontext
(EN). -
Remendar (patch) objetos para testes—veja a função
unittest.mock.patch
(EN).
A interface gerenciador de contexto consiste dos métodos __enter__
and __exit__
.
No topo do with
, o Python chama o método __enter__
do objeto gerenciador de contexto. Quando o bloco with
encerra ou termina por qualquer razão, o Python chama
__exit__
no objeto gerenciador de contexto.
O exemplo mais comum é se assegurar que um objeto arquivo seja fechado. O Exemplo 1 é uma demonstração detalhada do uso do with
para fechar um arquivo.
>>> with open('mirror.py') as fp: # (1)
... src = fp.read(60) # (2)
...
>>> len(src)
60
>>> fp # (3)
<_io.TextIOWrapper name='mirror.py' mode='r' encoding='UTF-8'>
>>> fp.closed, fp.encoding # (4)
(True, 'UTF-8')
>>> fp.read(60) # (5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file.
-
fp
está vinculado ao arquivo de texto aberto, pois o método__enter__
do arquivo devolveself
. -
Lê
60
caracteres Unicode defp
. -
A variável
fp
ainda está disponível—blocoswith
não definem um novo escopo, como fazem as funções. -
Podemos ler os atributos do objeto
fp
. -
Mas não podemos ler mais texto de
fp
pois, no final do blocowith
, o métodoTextIOWrapper.__exit__
foi chamado, e isso fechou o arquivo.
A primeira explicação no Exemplo 1 transmite uma informação sutil porém crucial:
o objeto gerenciador de contexto é o resultado da avaliação da expressão após o with
, mas o valor vinculado à variável alvo (na cláusula as
) é o resultado devolvido pelo método __enter__
do objeto gerenciador de contexto.
E acontece que a função open()
devolve uma instância de TextIOWrapper
, e o método __enter__
dessa classe devolve self
.
Mas em uma classe diferente, o método __enter__
também pode devolver algum outro objeto em vez do gerenciador de contexto.
Quando o fluxo de controle sai do bloco with
de qualquer forma, o método __exit__
é invocado no objeto gerenciador de contexto, e não no que quer que __enter__
tenha devolvido.
A cláusula as
da instrução with
é opcional. No caso de open
, sempre precisamos obter uma referência para o arquivo, para podermos chamar seus métodos. Mas alguns gerenciadores de contexto devolvem None
, pois não tem nenhum objeto útil para entregar ao usuário.
O Exemplo 2 mostra o funcionamento de um gerenciador de contexto perfeitamente frívolo, projetado para ressaltar a diferença entre o gerenciador de contexto e o objeto devolvido por seu método __enter__
.
LookingGlass
link:code/18-with-match/mirror.py[role=include]
-
O gerenciador de contexto é uma instância de
LookingGlass
; o Python chama__enter__
no gerenciador de contexto e o resultado é vinculado awhat
. -
Exibe uma
str
, depois o valor da variável alvowhat
. A saída de cadaprint
será invertida. -
Agora o bloco
with
terminou. Podemos ver que o valor devolvido por__enter__
, armazenado emwhat
, é a string'JABBERWOCKY'
. -
A saída do programa não está mais invertida.
O Exemplo 3 mostra a implementação de LookingGlass
.
LookingGlass
link:code/18-with-match/mirror.py[role=include]
-
O Python invoca
__enter__
sem argumentos além deself
. -
Armazena o método
sys.stdout.write
original, para podermos restaurá-lo mais tarde. -
Faz um monkey-patch em
sys.stdout.write
, substituindo-o com nosso próprio método. -
Devolve a string
'JABBERWOCKY'
, apenas para termos algo para colocar na variável alvowhat
. -
Nosso substituto de
sys.stdout.write
inverte o argumentotext
e chama a implementação original. -
Se tudo correu bem, o Python chama
__exit__
comNone, None, None
; se ocorreu uma exceção, os três argumentos recebem dados da exceção, como descrito a seguir, logo após esse exemplo. -
Restaura o método original em
sys.stdout.write
. -
Se a exceção não é
None
e seu tipo éZeroDivisionError
, exibe uma mensagem… -
…e devolve
True
, para informar o interpretador que a exceção foi tratada. -
Se
__exit__
devolveNone
ou qualquer valor falso, qualquer exceção levantada dentro do blocowith
será propagada.
Tip
|
Quando aplicações reais tomam o controle da saída padrão, elas frequentemente desejam substituir |
O interpretador chama o método __enter__
sem qualquer argumento—além do self
implícito. Os três argumentos passados a __exit__
são:
exc_type
-
A classe da exceção (por exemplo,
ZeroDivisionError
). exc_value
-
A instância da exceção. Algumas vezes, parâmetros passados para o construtor da exceção—tal como a mensagem de erro—podem ser encontrados em
exc_value.args
. traceback
-
Um objeto
traceback
.[2]
Para uma visão detalhada de como funciona um gerenciador de contexto, vejamos o Exemplo 4, onde LookingGlass
é usado fora de um bloco with
, de forma que podemos chamar manualmente seus métodos
__enter__
e __exit__
.
LookingGlass
sem um bloco with
link:code/18-with-match/mirror.py[role=include]
-
Instancia e inspeciona a instância de
manager
. -
Chama o método
__enter__
do manager e guarda o resultado emmonster
. -
monster
é a string'JABBERWOCKY'
. O identificadorTrue
aparece invertido, porque toda a saída viastdout
passa pelo métodowrite
, que modificamos em__enter__
. -
Chama
manager.__exit__
para restaurar ostdout.write
original.
Tip
|
Gerenciadores de contexto entre parênteses
O Python 3.10 adotou um novo parser (analisador sintático), mais poderoso que o antigo parser LL(1). Isso permitiu introduzir novas sintaxes que não eram viáveis anteriormente. Uma melhoria na sintaxe foi permitir gerenciadores de contexto agrupados entre parênteses, assim: with (
CtxManager1() as example1,
CtxManager2() as example2,
CtxManager3() as example3,
):
... Antes do 3.10, as linhas acima teriam que ser escritas como blocos |
A biblioteca padrão inclui o pacote contextlib
, com funções, classe e decoradores muito convenientes para desenvolver, combinar e usar gerenciadores de contexto.
Antes de desenvolver suas próprias classes gerenciadoras de contexto, dê uma olhada em
contextlib
—"Utilities for with-statement contexts" ("Utilitários para contextos da instrução with), na documentação do Python.
Pode ser que você esteja prestes a escrever algo que já existe, ou talvez exista uma classe ou algum invocável que tornará seu trabalho mais fácil.
Além do gerenciador de contexto redirect_stdout
mencionado logo após o Exemplo 3, o redirect_stderr
foi acrescentado no Python 3.5—ele faz o mesmo que seu par mais antigo, mas com as saídas direcionadas para stderr
.
O pacote contextlib
também inclui:
closing
-
Uma função para criar gerenciadores de contexto a partir de objetos que forneçam um método
close()
mas não implementam a interface__enter__/__exit__
. suppress
-
Um gerenciador de contexto para ignorar temporariamente exceções passadas como parâmetros.
nullcontext
-
Um gerenciador de contexto que não faz nada, para simplificar a lógica condicional em torno de objetos que podem não implementar um gerenciador de contexto adequado. Ele serve como um substituto quando o código condicional antes do bloco
with
pode ou não fornecer um gerenciador de contexto para a instruçãowith
. Adicionado no Python 3.7.
O módulo contextlib
fornece classes e um decorador que são mais largamente aplicáveis que os decoradores mencionados acima:
@contextmanager
-
Um decorador que permite construir um gerenciador de contexto a partir de um simples função geradora, em vez de criar uma classe e implementar a interface. Veja a Usando o @contextmanager.
AbstractContextManager
-
Uma ABC que formaliza a interface gerenciador de contexto, e torna um pouco mais fácil criar classes gerenciadoras de contexto, através de subclasses—adicionada no Python 3.6.
ContextDecorator
-
Uma classe base para definir gerenciadores de contexto baseados em classes que podem também ser usadas como decoradores de função, rodando a função inteira dentro de um contexto gerenciado.
ExitStack
-
Um gerenciador de contexto que permite entrar em um número variável de gerenciadores de contexto. Quando o bloco with termina, ExitStack chama os métodos
__exit__
dos gerenciadores de contexto empilhados na ordem LIFO (Last In, First Out, Último a Entrar, Primeiro a Sair). Use essa classe quando você não sabe de antemão em quantos gerenciadores de contexto será necessário entrar no blocowith
; por exemplo, ao abrir ao mesmo tempo todos os arquivos de uma lista arbitrária de arquivos.
Com o Python 3.7, contextlib
acrescentou AbstractAsyncContextManager
, @asynccontextmanager
, e AsyncExitStack
.
Eles são similares aos utilitários equivalentes sem a parte async
no nome, mas projetados para uso com a nova instrução async with
, tratado no [async_ch].
Desses todos, o utilitário mais amplamente usado é o decorador @contextmanager
, então ele merece mais atenção. Esse decorador também é interessante por mostrar um uso não relacionado a iteração para a instrução yield
.
O decorador @contextmanager
é uma ferramenta elegante e prática, que une três recursos distintos do Python: um decorador de função, um gerador, e a instrução with
.
Usar o @contextmanager
reduz o código repetitivo na criação de um gerenciador de contexto: em vez de escrever toda uma classe com métodos __enter__/__exit__
, você só precisa implementar um gerador com uma única instrução yield
, que deve produzir o que o método __enter__
deveria devolver.
Em um gerador decorado com @contextmanager
, o yield
divide o corpo da função em duas partes: tudo que vem antes do yield
será executado no início do bloco with
, quando o interpretador chama __enter__
; o código após o yield
será executado quando __exit__
é chamado, no final do bloco.
link:code/18-with-match/mirror_gen.py[role=include]
-
Aplica o decorador
contextmanager
. -
Preserva o método
sys.stdout.write
original. -
reverse_write
pode chamaroriginal_write
mais tarde, pois ele está disponível em sua clausura (closure). -
Substitui
sys.stdout.write
porreverse_write
. -
Produz o valor que será vinculado à variável alvo na cláusula
as
da instruçãowith
. O gerador se detem nesse ponto, enquanto o corpo dowith
é executado. -
Quando o fluxo de controle sai do bloco
with
, a execução continua após oyield
; neste ponto osys.stdout.write
original é restaurado.
O Exemplo 6 mostra a função looking_glass
em operação.
looking_glass
link:code/18-with-match/mirror_gen.py[role=include]
-
A única diferença do Exemplo 2 é o nome do gerenciador de contexto:`looking_glass` em vez de
LookingGlass
.
O decorador contextlib.contextmanager
envolve a função em uma classe que implementa os métodos __enter__
e __exit__
.[3]
O método __enter__
daquela classe:
-
Chama a função geradora para obter um objeto gerador—vamos chamá-lo de
gen
. -
Chama
next(gen)
para acionar com ele a palavra reservadayield
. -
Devolve o valor produzido por
next(gen)
, para permitir que o usuário o vincule a uma variável usando o formatowith/as
.
Quando o bloco with
termina, o método __exit__
:
-
Verifica se uma exceção foi passada como
exc_type
; em caso afirmativo,gen.throw(exception)
é invocado, fazendo com que a exceção seja levantada na linhayield
, dentro do corpo da função geradora. -
Caso contrário,
next(gen)
é chamado, retomando a execução do corpo da função geradora após oyield
.
O Exemplo 5 tem um defeito:
Se uma exceção for levantada no corpo do bloco with
,
o interpretador Python vai capturá-la e levantá-la novamente na expressão yield
dentro de looking_glass
.
Mas não há tratamento de erro ali, então o gerador looking_glass
vai terminar sem nunca restaurar o método sys.stdout.write
original, deixando o sistema em um estado inconsistente.
O Exemplo 7 acrescenta o tratamento especial da exceção ZeroDivisionError
, tornando esse gerenciador de contexto funcionalmente equivalente ao Exemplo 3, baseado em uma classe.
link:code/18-with-match/mirror_gen_exc.py[role=include]
-
Cria uma variável para uma possível mensagem de erro; essa é a primeira mudança em relação a Exemplo 5.
-
Trata
ZeroDivisionError
, fixando uma mensagem de erro. -
Desfaz o monkey-patching de
sys.stdout.write
. -
Mostra a mensagem de erro, se ela foi determinada.
Lembre-se que o método __exit__
diz ao interpretador que ele tratou a exceção ao devolver um valor verdadeiro; nesse caso, o interpretador suprime a exceção.
Por outro lado, se __exit__
não devolver explicitamente um valor, o interpretador recebe o habitual None
, e propaga a exceção.
Com o @contextmanager
, o comportamento default é invertido: o método __exit__
fornecido pelo decorador assume que qualquer exceção enviada para o gerador está tratada e deve ser suprimida.
Tip
|
Ter um |
Um recurso pouco conhecido do @contextmanager
é que os geradores decorados com ele podem ser usados eles mesmos como decoradores.[5]
Isso ocorre porque @contextmanager
é implementado com a classe contextlib.ContextDecorator
.
O Exemplo 8 mostra o gerenciador de contexto looking_glass
do Exemplo 5 sendo usado como um decorador.
looking_glass
também funciona como um decorador.link:code/18-with-match/mirror_gen.py[role=include]
-
looking_glass
faz seu trabalho antes e depois do corpo deverse
rodar. -
Isso confirma que o
sys.write
original foi restaurado.
Um interessante exemplo real do uso do @contextmanager
fora da biblioteca padrão é a reescrita de arquivo no mesmo lugar usando um gerenciador de contexto de Martijn Pieters. O Exemplo 9 mostra como ele é usado.
import csv
with inplace(csvfilename, 'r', newline='') as (infh, outfh):
reader = csv.reader(infh)
writer = csv.writer(outfh)
for row in reader:
row += ['new', 'columns']
writer.writerow(row)
A função inplace
é um gerenciador de contexto que fornece a você dois identificadores—no exemplo, infh
e outfh
—para o mesmo arquivo, permitindo que seu código leia e escreva ali ao mesmo tempo. Isso é mais fácil de usar que a função fileinput.input
(EN) da biblioteca padrão (que, por sinal, também fornece um gerenciador de contexto).
Se você quiser estudar o código-fonte do inplace
de Martijn (listado no post) (EN), encontre a palavra reservada yield
: tudo antes dela lida com configurar o contexto, que implica criar um arquivo de backup, então abrir e produzir referências para os identificadores de arquivo de leitura e escrita que serão devolvidos pela chamada a __enter__
. O processamento do __exit__
após o yield
fecha os identificadores do arquivo e, se algo deu errado, restaura o arquivo do backup.
Isso conclui nossa revisão da instrução with
e dos gerenciadores de contexto. Vamos agora olhar o match/case
, no contexto de um exemplo completo.
Na [pattern_matching_seq_interp_sec], vimos exemplos de sequências de padrões extraídos da funcão evaluate
do interpretador lis.py de Peter Norvig, portado para o Python 3.10.
Nessa seção quero dar um visão geral do funcionamento do lis.py, e também explorar todas as cláusulas case
de evaluate
, explicando não apenas os padrões mas também o que o interpretador faz em cada case
.
Além de mostrar mais pattern matching, escrevi essa seção por três razões:
-
O lis.py de Norvig é um lindo exemplo de código Python idiomático.
-
A simplicidade do Scheme é uma aula magna de design de linguagens.
-
Aprender como um interpretador funciona me deu um entendimento mais profundo sobre o Python e sobre linguagens de programação em geral—interpretadas ou compiladas.
Antes de olhar o código Python, vamos ver um pouquinho de Scheme, para você poder entender este estudo de caso—pensando em quem nunca viu Scheme e Lisp antes.
No Scheme não há distinção entre expressões e instruções, como temos em Python. Também não existem operadores infixos. Todas as expressões usam a notação prefixa, como (+ x 13)
em vez de x + 13
.
A mesma notação prefixa é usada para chamadas de função—por exemplo, (gcd x 13)
—e formas especiais—por exemplo, (define x 13)
, que em Python escreveríamos como uma declaração de atribuição x = 13
.
A notação usada no Scheme e na maioria dos dialetos de Lisp é conhecida como S-expression (Expressão-S).[6]
O Exemplo 10 mostra um exemplo simples em Scheme.
(define (mod m n)
(- m (* n (quotient m n))))
(define (gcd m n)
(if (= n 0)
m
(gcd n (mod m n))))
(display (gcd 18 45))
O Exemplo 10 mostra três expressões em Scheme:
duas definições de função—mod
e gcd
—e uma chamada a display
,
que vai devolver 9, o resultado de (gcd 18 45)
.
O Exemplo 11 é o mesmo código em Python (menor que a explicação em português do
algoritmo recursivo de Euclides).
def mod(m, n):
return m - (m // n * n)
def gcd(m, n):
if n == 0:
return m
else:
return gcd(n, mod(m, n))
print(gcd(18, 45))
Em Python idiomático, eu usaria o operador %
em vez de reinventar mod
, e seria mais eficiente usar um loop while
em vez de recursão. Mas queria mostrar duas definições de função, e fazer os exemplos o mais similares possível, para ajudar você a ler o código Scheme.
O Scheme não tem instruções iterativas de controle de fluxo como while
ou for
.
A iteração é feita com recursão.
Observe que não há atribuições nos exemplos em Python e Scheme. O uso extensivo de recursão e o uso mínimo de atribuição são marcas registradas do estilo funcional de programação.[7]
Agora vamos revisar o código da versão Python 3.10 do lis.py. O código fonte completo, com testes, está no diretório 18-with-match/lispy/py3.10/, do repositório fluentpython/example-code-2e no Github.
O Exemplo 12 mostra as primeiras linhas do lis.py.
O uso do TypeAlias
e do operador de união de tipos |
exige o Python 3.10.
link:code/18-with-match/lispy/py3.10/lis.py[role=include]
Os tipos definidos são:
Symbol
-
Só um alias para
str
. Em lis.py,Symbol
é usado para identificadores; não há um tipo de dados string, com operações como fatiamento (slicing), divisão (splitting), etc.[8] Atom
-
Um elemento sintático simples, tal como um número ou um
Symbol
—ao contrário de uma estrutura complexa, composta por vários elementos distintos, como uma lista. Expression
-
Os componentes básicos de programas Scheme são expressões feitas de átomos e listas, possivelmente aninhados.
O parser (analisador sintático) de Norvig tem 36 linhas de código que exibem o poder do Python aplicado ao tratamento da sintaxe recursiva simples das expressões-S—sem strings, comentários, macros e outros recursos que tornam a análise sintática do Scheme padrão mais complicada (Exemplo 13).
link:code/18-with-match/lispy/py3.10/lis.py[role=include]
# mais código do analisador omitido na listagem do livro
A principal função desse grupo é parse
, que recebe uma expressão-S em forma de str
e devolve um objeto Expression
, como definido no Exemplo 12:
um Atom
ou uma list
que pode conter mais átomos e listas aninhadas.
Norvig usa um truque elegante em tokenize
:
ele acrescenta espaços antes e depois de cada parênteses na entrada, e então a recorta,
resultando em uma lista de símbolos sintáticos (tokens) com '('
e ')'
como símbolos separados
Esse atalho funciona porque não há um tipo string no pequeno Scheme de lis.py, então todo '('
ou ')'
é um delimitador de expressão.
O código recursivo do analisador está em read_from_tokens
,
uma função de 14 linhas que você pode ler no repositório
fluentpython/example-code-2e.
Vou pular isso, pois quero me concentrar em outras partes do interpretador.
Aqui estão alguns doctests estraídos do lispy/py3.10/examples_test.py:
link:code/18-with-match/lispy/py3.10/examples_test.py[role=include]
As regras de avaliação para esse subconjunto do Scheme são simples:
-
Um símbolo sintático que se pareça com um número é tratado como um
float
ou umint
. -
Todo o resto que não seja um
'('
ou um')'
é considerado umSymbol
—umastr
, a ser usado como um identificador. Isso inclui texto no código-fonte como+
,set!
, emake-counter
, que são identificadores válidos em Scheme, mas não em Python. -
Expressões dentro de
'('
e')'
são avaliadas recursivamente como listas contendo átomos ou listas aninhadas que podem conter átomos ou mais listas aninhadas.
Usando a terminologia do interpretador Python, a saída de parse
é uma AST (Abstract Syntax Tree—Árvore Sintática Abstrata):
uma representação conveniente de um programa Scheme como listas aninhadas formando uma estrutura similar a uma árvore, onde a lista mais externa é o tronco, listas internas são os galhos, e os átomos são as folhas (Figura 1).
A classe Environment
estende collections.ChainMap
, acrescentando o método change
, para atualizar um valor dentro de um dos dicts encadeados que as instâncias de ChainMap
mantém em uma lista de mapeamentos: o atributo self.maps
.
O método change
é necessário para suportar a forma (set! …)
do Scheme, descrita mais tarde; veja o Exemplo 14.
Environment
link:code/18-with-match/lispy/py3.10/lis.py[role=include]
Observe que o método change
só atualiza chaves existentes.[9]
Tentar mudar uma chave não encontrada causa um KeyError
.
Esse doctest mostra como Environment
funciona:
link:code/18-with-match/lispy/py3.10/examples_test.py[role=include]
-
Ao ler os valores,
Environment
funciona comoChainMap
: as chaves são procuradas nos mapeamentos aninhados da esquerda para a direita. Por isso o valor dea
noouter_env
é encoberto pelo valor eminner_env
. -
Atribuir com
[]
sobrescreve ou insere novos itens, mas sempre no primeiro mapeamento,inner_env
nesse exemplo. -
env.change('b', 333)
busca a chaveb
e atribui a ela um novo valor no mesmo lugar, noouter_env
A seguir temos a função standard_env()
, que constrói e devolve um Environment
carregado com funções pré-definidas, similar ao módulo __builtins__
do Python, que está sempre disponível (Exemplo 15).
standard_env()
constrói e devolve o ambiente globallink:code/18-with-match/lispy/py3.10/lis.py[role=include]
# omitted here: more operator definitions
link:code/18-with-match/lispy/py3.10/lis.py[role=include]
# omitted here: more function definitions
link:code/18-with-match/lispy/py3.10/lis.py[role=include]
Resumindo, o mapeamento env
é carregado com:
-
Todas as funções do módulo
math
do Python -
Operadores selecionados do módulo
op
do Python -
Funções simples porém poderosas construídas com o
lambda
do Python -
Estruturas e entidades embutidas do Python, ou renomeadas, como
callable
paraprocedure?
, ou mapeadas diretamente, comoround
O REPL (read-eval-print-loop, loop-lê-calcula-imprime ) de Norvig é fácil de entender mas não é amigável ao usuário (veja o Exemplo 16).
Se nenhum argumento de linha de comando é passado a lis.py,
a função repl()
é invocada por main()
—definida no final do módulo.
No prompt de lis.py>
, devemos digitar expressões corretas e completas; se esquecemos de fechar um só parênteses, lis.py se encerra.[10]
link:code/18-with-match/lispy/py3.10/lis.py[role=include]
Segue uma breve explicação sobre essas duas funções:
repl(prompt: str = 'lis.py> ') → NoReturn
-
Chama
standard_env()
para provisionar as funções embutidas para o ambiente global, então entra em um loop infinito, lendo e avaliando cada linha de entrada, calculando-a no ambiente global, e exibindo o resultado—a menos que sejaNone
. Oglobal_env
pode ser modificado porevaluate
. Por exemplo, quando o usuário define uma nova variável global ou uma função nomeada, ela é armazenada no primeiro mapeamento do ambiente—odict
vazio na chamada ao construtor deEnvironment
na primeira linha derepl
. lispstr(exp: object) → str
-
A função inversa de
parse
: dado um objeto Python representando uma expressão,lispstr
devolve o código-fonte para ela. Por exemplo, dado['', 2, 3]`, o resultado é `'( 2 3)'
.
Agora podemos apreciar a beleza do avaliador de expressões de Norvig—tornado um pouco mais bonito com match/case
.
A função evaluate
no Exemplo 17 recebe uma Expression
(construída por parse
) e um Environment
.
O corpo de evaluate
é composto por uma única instrução match
com uma expressão exp
como sujeito.
Os padrões de case
expressam a sintaxe e a semântica do Scheme com uma clareza impressionante.
evaluate
recebe uma expressão e calcula seu valorlink:code/18-with-match/lispy/py3.10/lis.py[role=include]
Vamos estudar cada cláusula case
e o que cada uma faz.
Em algumas ocasiões eu acrescentei comentários, mostrando uma expressão-S que casaria com padrão quando transformado em uma lista do Python.
Os doctests extraídos de
examples_test.py demonstram cada case
.
case int(x) | float(x):
return x
- Padrão:
-
Instância de
int
oufloat
. - Ação:
-
Devolve o próprio valor.
- Exemplo:
-
link:code/18-with-match/lispy/py3.10/examples_test.py[role=include]
case Symbol(var):
return env[var]
- Padrão:
-
Instância de
Symbol
, isto é, umastr
usada como identificador. - Ação:
-
Consulta
var
emenv
e devolve seu valor. - Exemplos:
-
link:code/18-with-match/lispy/py3.10/examples_test.py[role=include]
A forma especial quote
trata átomos e listas como dados em vez de expressões a serem avaliadas.
# (quote (99 bottles of beer))
case ['quote', x]:
return x
- Padrão:
-
Lista começando com o símbolo
'quote'
, seguido de uma expressãox
. - Ação:
-
Devolve
x
sem avaliá-la. - Exemplos:
-
link:code/18-with-match/lispy/py3.10/examples_test.py[role=include]
Sem quote
, cada expressão no teste geraria um erro:
-
no-such-name
seria buscado no ambiente, gerando umKeyError
-
(99 bottles of beer)
não pode ser avaliado, pois o número 99 não é umSymbol
nomeando uma forma especial, um operador ou uma função -
(/ 10 0)
geraria umZeroDivisionError
Apesar de ser simples,
quote
não pode ser implementada como uma função em Scheme.
Seu poder especial é impedir que o interpretador avalie (f 10)
na expressão (quote (f 10))
:
o resultado é apenas uma lista com um Symbol
e um int
.
Por outro lado, em uma chamada de função como (abs (f 10))
,
o interpretador primeiro calcula o resultado de (f 10)
antes de invocar abs
.
Por isso quote
é uma palavra reservada: ela precisa ser tratada como uma forma especial.
De modo geral, palavras reservadas são necessárias para:
-
Introduzir regras especiais de avaliação, como
quote
elambda
—que não avaliam nenhuma de suas sub-expressões -
Mudar o fluxo de controle, como em
if
e chamadas de função—que também tem regras especiais de avaliação -
Para gerenciar o ambiente, como em
define
eset
Por isso também o Python, e linguagens de programação em geral, precisam de palavras reservadas.
Pense em def
, if
, yield
, import
, del
, e o que elas fazem em Python.
# (if (< x 0) 0 x)
case ['if', test, consequence, alternative]:
if evaluate(test, env):
return evaluate(consequence, env)
else:
return evaluate(alternative, env)
- Padrão:
-
Lista começando com
'if'
seguida de três expressões:test
,consequence
, ealternative
. - Ação:
-
Avalia
test
:-
Se verdadeira, avalia
consequence
e devolve seu valor. -
Caso contrário, avalia
alternative
e devolve seu valor.
-
- Exemplos:
-
link:code/18-with-match/lispy/py3.10/examples_test.py[role=include]
Os ramos consequence
e alternative
devem ser expressões simples.
Se mais de uma expressão for necessária em um ramo, você pode combiná-las com (begin exp1 exp2…)
, fornecida como uma função em lis.py—veja o Exemplo 15.
A forma lambda
do Scheme define funções anônimas.
Ela não sofre das limitações da lambda
do Python:
qualquer função que pode ser escrita em Scheme pode ser escrita usando a sintaxe (lambda …)
.
# (lambda (a b) (/ (+ a b) 2))
case ['lambda' [*parms], *body] if body:
return Procedure(parms, body, env)
- Padrão:
-
Lista começando com
'lambda'
, seguida de:-
Lista de zero ou mais nomes de parâmetros
-
Uma ou mais expressões coletadas em
body
(a expressão guarda assegura quebody
não é vazio).
-
- Ação:
-
Cria e devolve uma nova instância de
Procedure
com os nomes de parâmetros, a lista de expressões como o corpo da função, e o ambiente atual. - Exemplo:
-
link:code/18-with-match/lispy/py3.10/examples_test.py[role=include]
A classe Procedure
implementa o conceito de uma closure (clausura):
um objeto invocável contendo nomes de parâmetros, um corpo de função,
e uma referência ao ambiente no qual a Procedure
está sendo instanciada.
Vamos estudar o código de Procedure
daqui a pouco.
A palavra reservada define
é usada de duas formas sintáticas diferentes.
A mais simples é:
# (define half (/ 1 2))
case ['define', Symbol(name), value_exp]:
env[name] = evaluate(value_exp, env)
- Padrão:
-
Lista começando com
'define'
, seguido de umSymbol
e uma expressão. - Ação:
-
Avalia a expressão e coloca o valor resultante em
env
, usandoname
como chave. - Exemplo:
-
link:code/18-with-match/lispy/py3.10/examples_test.py[role=include]
O doctest para esse case
cria um global_env
, para podermos verificar que evaluate
coloca answer
dentro daquele Environment
.
Podemos usar primeira forma de define
para criar variáveis ou para vincular nomes a funções anônimas, usando (lambda …)
como o value_exp
.
A segunda forma de define
é um atalho para definir funções nomeadas.
# (define (average a b) (/ (+ a b) 2))
case ['define', [Symbol(name), *parms], *body] if body:
env[name] = Procedure(parms, body, env)
- Padrão:
-
Lista começando com
'define'
, seguida de:-
Uma lista começando com um
Symbol(name)
, seguida de zero ou mais itens agrupados em uma lista chamadaparms
. -
Uma ou mais expressões agrupadas em
body
(a expressão guarda garante quebody
não esteja vazio)
-
- Ação:
-
-
Cria uma nova instância de
Procedure
com os nomes dos parâmetros, a lista de expressões como o corpo, e o ambiente atual. -
Insere a
Procedure
emenv
, usandoname
como chave.
-
O doctest no Exemplo 18 define e coloca no global_env
uma função chamada %
, que calcula uma porcentagem.
%
, que calcula uma porcentagemlink:code/18-with-match/lispy/py3.10/examples_test.py[role=include]
Após chamar evaluate
, verificamos que %
está vinculada a uma Procedure
que recebe dois argumentos numéricos e devolve uma porcentagem.
O padrão para o segundo define
não obriga os itens em parms
a serem todos instâncias de Symbol
.
Eu teria que verificar isso antes de criar a Procedure
, mas não o fiz—para manter o código aqui tão fácil de acompanhar quanto o de Norvig.
A forma set!
muda o valor de uma variável previamente definida.[11]
# (set! n (+ n 1))
case ['set!', Symbol(name), value_exp]:
env.change(name, evaluate(value_exp, env))
- Padrão:
-
Lista começando com
'set!'
, seguida de umSymbol
e de uma expressão. - Ação:
-
Atualiza o valor de
name
emenv
com o resultado da avaliação da expressão.
O método Environment.change
atravessa os ambientes encadeados de local para global,
e atualiza a primeira ocorrência de name
com o novo valor.
Se não estivéssemos implementando a palavra reservada 'set!'
,
esse interpretador poderia usar apenas o ChainMap
do Python para implementar env
,
sem precisar da nossa classe Environment
.
O uso da forma set!
está relacionado ao uso da palavra reservada nonlocal
em Python:
declarar nonlocal x
permite a x = 10
atualizar uma variável x
anteriormente definida fora do escopo local.
Sem a declaração nonlocal x
, x = 10
vai sempre criar uma variável local em Python, como vimos na [nonlocal_sec].
De forma similar, (set! x 10)
atualiza um x
anteriormente definido que pode estar fora do ambiente local da função.
Por outro lado, a variável x
em (define x 10)
é sempre uma variável local, criada ou atualizada no ambiente local.
Ambos, nonlocal
e (set! …)
, são necessários para atualizar o estados do programas mantidos em variáveis dentro de uma clausura (closure). O [ex_average_fixed] demonstrou o uso de nonlocal
para implementar uma função que calcula uma média contínua, mantendo itens count
e total
em uma clausura.
Aqui está a mesma ideia, escrita no subconjunto de Scheme de lis.py:
(define (make-averager)
(define count 0)
(define total 0)
(lambda (new-value)
(set! count (+ count 1))
(set! total (+ total new-value))
(/ total count)
)
)
(define avg (make-averager)) # (1)
(avg 10) # (2)
(avg 11) # (3)
(avg 15) # (4)
-
Cria uma nova clausura com a função interna definida por
lambda
e as variáveiscount
etotal
, inicialziadas com 0; vincula a clausura aavg
. -
Devolve 10.0.
-
Devolve 10.5.
-
Devolve 12.0.
O código acima é um dos testes em lispy/py3.10/examples_test.py.
Agora chegamos a uma chamada de função.
# (gcd (* 2 105) 84)
case [func_exp, *args] if func_exp not in KEYWORDS:
proc = evaluate(func_exp, env)
values = [evaluate(arg, env) for arg in args]
return proc(*values)
- Padrão:
-
Lista com um ou mais itens.
A expressão guarda garante que
func_exp
não é um de['quote', 'if', 'define', 'lambda', 'set!']
—listados logo antes deevaluate
no Exemplo 17.O padrão casa com qualquer lista com uma ou mais expressões, vinculando a primeira expressão a
func_exp
e o restante aargs
como uma lista, que pode ser vazia. - Ação:
-
-
Avaliar
func_exp
para obter umaproc
da função. -
Avaliar cada item em
args
para criar uma lista de valores dos argumentos. -
Chamar
proc
com os valores como argumentos separados, devolvendo o resultado.
-
- Exemplo:
-
link:code/18-with-match/lispy/py3.10/examples_test.py[role=include]
Esse doctest continua do Exemplo 18:
ele assume que global_env
contém uma função chamada %
.
Os argumentos passados a %
são expressões aritméticas,
para enfatizar que eles são avaliados antes da função ser chamada.
A expressão guarda nesse case
é necessária porque [func_exp, *args]
casa com qualquer sequência sujeito com um ou mais itens.
Entretanto, se func_exp
é uma palavra reservada e o
sujeito não casou com nenhum dos case
anteriores,
então isso é de fato um erro de sintaxe.
Se o sujeito exp
não casa com nenhum dos case
anteriores,
o case
"pega tudo" gera um SyntaxError
:
case _:
raise SyntaxError(lispstr(exp))
Aqui está um exemplo de um (lambda …)
malformado, identificado como um SyntaxError
:
link:code/18-with-match/lispy/py3.10/examples_test.py[role=include]
Se o case
para chamada de função não tivesse aquela expressão guarda rejeitando palavras reservadas, a expressão (lambda is not like this)
teria sido tratada como uma chamada de função,
que geraria um KeyError
, pois 'lambda'
não é parte do ambiente—da mesma forma que lambda
em Python não é uma função embutida.
A classe Procedure
poderia muito bem se chamar Closure
, porque é isso que ela representa:
uma definição de função junto com um ambiente.
A definição de função inclui o nome dos parâmetros e as expressões que compõe o corpo da funcão.
O ambiente é usado quando a função é chamada, para fornecer os valores das variáveis livres: variáveis que aparecem no corpo da função mas não são parâmetros, variáveis locais ou variáveis globais.
Vimos os conceitos de clausura e de variáveis livres na [closures_sec].
Aprendemos como usar clausuras em Python, mas agora podemos mergulhar mais fundo e ver como uma clausura é implementada em lis.py:
link:code/18-with-match/lispy/py3.10/lis.py[role=include]
-
Chamada quando uma função é definida pelas formas
lambda
oudefine
. -
Salva os nomes dos parâmetros, as expressões no corpo e o ambiente, para uso posterior.
-
Chamada por
proc(*values)
na última linha da cláusulacase [func_exp, *args]
. -
Cria
local_env
, mapeandoself.parms
como nomes de variáveis locais e osargs
passados como valores. -
Cria um novo
env
combinado, colocandolocal_env
primeiro e entãoself.env
—o ambiente que foi salvo quando a função foi definida. -
Itera sobre cada expressão em
self.body
, avaliando-as noenv
combinado. -
Devolve o resultado da última expressão avaliada.
Há um par de funções simples após evaluate
em lis.py:
run
lê um programa Scheme completo e o executa,
e main
chama run
ou repl
, dependendo da linha de comando—parecido com o modo como o Python faz.
Não vou descrever essas funções, pois não há nada novo ali.
Meus objetivos aqui eram compartilhar com vocês a beleza do pequeno interpretador de Norvig,
explicar melhor como as clausuras funcionam,
e mostrar como match/case
foi uma ótima adição ao Python.
Para fechar essa seção estendida sobre pattern matching, vamos formalizar o conceito de um OR-pattern (padrão-OU).
Uma série de padrões separados por |
formam um
OR-pattern (EN):
ele tem êxito se qualquer dos sub-padrões tiver êxito.
O padrão em avaliando números é um OR-pattern:
case int(x) | float(x):
return x
Todos os sub-padrões em um OR-pattern devem usar as mesmas variáveis.
Essa restrição é necessária para garantir que
as variáveis estejam disponíveis para a expressão de guarda e para o corpo do case
,
independente de qual sub-padrão tenha sido bem sucedido.
Warning
|
No contexto de uma cláusula |
Um OR-pattern não está limitado a aparecer no nível superior de um padrão.
|
pode também ser usado em sub-padrões.
Por exemplo, se quiséssemos que o lis.py aceitasse a letra grega λ (lambda)[12]
além da palavra reservada lambda
, poderíamos reescrever o padrão assim:
# (λ (a b) (/ (+ a b) 2) )
case ['lambda' | 'λ', [*parms], *body] if body:
return Procedure(parms, body, env)
Agora podemos passar para o terceiro e último assunto deste capítulo:
lugares incomuns onde a cláusula else
pode aparecer no Python.
Isso não é segredo,
mas é um recurso pouco conhecido em Python:
a cláusula else
pode ser usada não apenas com instruções if
, mas também com as instruções for
, while
, e try
.
A semântica para for/else
, while/else
, e try/else
é semelhante, mas é muito diferente do if/else
.
No início, a palavra else
na verdade atrapalhou meu entendimento desses recursos, mas no fim acabei me acostumando.
Aqui estão as regras:
for
-
O bloco
else
será executado apenas se e quando o loopfor
rodar até o fim (isto é, não rodará se ofor
for interrompido com umbreak
). while
-
O bloco
else
será executado apenas se e quando o loopwhile
terminar pela condição se tornar falsa (novamente, não rodará se owhile
for interrompido por umbreak
) try
-
O bloco
else
será executado apenas se nenhuma exceção for gerada no blocotry
. A documentação oficial também afirma: "Exceções na cláusulaelse
não são tratadas pela cláusulaexcept
precedente."
Em todos os casos, a cláusula else
também será ignorada se uma exceção ou uma instrução return
, break
ou continue
fizer com que o fluxo de controle saia do bloco principal da instrução composta.
No caso do try
, esta é a diferença importante entre else
e finally
:
o bloco finally
será executado sempre, ocorrendo ou não uma exceção,
e até mesmo se o fluxo de execução sair do bloco try
por uma instrução como return
.
Note
|
Não tenho nada contra o funcionamento dessas cláusulas |
Usar else
com essas instruções muitas vezes torna o código mais fácil de ler e evita o transtorno de configurar flags de controle ou acrescentar instruções if
extras ao código.
O uso de else
em loops em geral segue o padrão desse trecho:
for item in my_list:
if item.flavor == 'banana':
break
else:
raise ValueError('No banana flavor found!')
No caso de blocos try/except
, o else
pode parecer redundante à primeira vista.
Afinal, a after_call()
no trecho a seguir só será executado se a dangerous_call()
não gerar uma exceção, correto?
try:
dangerous_call()
after_call()
except OSError:
log('OSError...')
Entretanto, isso coloca a after_call()
dentro do bloco try
sem um bom motivo.
Por clareza e correção, o corpo de um bloco try
deveria conter apenas instruções que podem gerar as exceções esperadas. Isso é melhor:
try:
dangerous_call()
except OSError:
log('OSError...')
else:
after_call()
Agora fica claro que o bloco try
está de guarda contra possíveis erros na dangerous_call()
, e não em after_call()
.
Também fica explícito que after_call()
só será executada se nenhuma exceção for gerada no bloco try
.
Em Python, try/except
é frequentemene usado para controle de fluxo, não apenas para tratamento de erro. Há inclusive um acrônimo/slogan para isso, documentado no glossário oficial do Python:
- EAFP
Iniciais da expressão em inglês “easier to ask for forgiveness than permission” que significa “é mais fácil pedir perdão que permissão”. Este estilo de codificação comum em Python assume a existência de chaves ou atributos válidos e captura exceções caso essa premissa se prove falsa. Este estilo limpo e rápido se caracteriza pela presença de várias instruções
try
eexcept
. A técnica diverge do estilo LBYL, comum em outras linguagens como C, por exemplo.
O glossário então define LBYL:
- LBYL
Iniciais da expressão em inglês “look before you leap”, que significa algo como “olhe antes de pisar”[NT: ou "olhe antes de pular"]. Este estilo de codificação testa as pré-condições explicitamente antes de fazer chamadas ou buscas. Este estilo contrasta com a abordagem EAFP e é caracterizada pela presença de muitas instruções
if
. Em um ambiente multithread, a abordagem LBYL pode arriscar a introdução de uma condição de corrida entre “o olhar” e “o pisar”. Por exemplo, o códigoif key in mapping: return mapping[key]
pode falhar se outra thread removerkey
domapping
após o teste, mas antes da olhada. Esse problema pode ser resolvido com bloqueios [travas] ou usando a abordagem EAFP.
Dado o estilo EAFP, faz mais sentido conhecer e usar os blocos else
corretamente nas instruções try/except
.
Note
|
Quando a [inclusão da] instrução |
Agora vamos resumir o capítulo.
Este capítulo começou com gerenciadores de contexto e o significado da instrução with
, indo rapidamente além de uso comum (o fechamento automático de arquivos abertos). Implementamos um gerenciador de contexto personalizado: a classe LookingGlass
, usando os métodos
__enter__/__exit__
, e vimos como tratar exceções no método __exit__
. Uma ideia fundamental apontada por Raymond Hettinger, na palestra de abertura da Pycon US 2013, é que with
não serve apenas para gerenciamento de recursos; ele é uma ferramenta para fatorar código comum de configuração e de finalização, ou qualquer par de operações que precisem ser executadas antes e depois de outro procedimento.[14]
Revisamos funções no módulo contextlib
da biblioteca padrão. Uma delas, o decorador @contextmanager
, permite implementar um gerenciador de contexto usando apenas um mero gerador com um yield—uma solução menos trabalhosa que criar uma classe com pelo menos dois métodos. Reimplementamos a LookingGlass como uma função geradora looking_glass
, e discutimos como fazer tratamento de exceções usando o @contextmanager
.
Nós então estudamos o elegante interpretador Scheme de Peter Norvig, o lis.py, escrito em Python idiomático e refatorado para usar match/case
em evaluate
—a função central de qualquer interpretador.
Entender o funcionamenteo de evaluate
exigiu revisar um pouco de Scheme, um parser para expressões-S, um REPL simples e a construção de escopos aninhados através de Environment
, uma subclasse de collection.ChainMap
.
No fim, lys.py se tornou um instrumento para explorarmos muito mais que pattern matching. Ele mostra como diferentes partes de um interpretador trabalham juntas, jogando luz sobre recursos fundamentais do próprio Python: porque palavras reservadas são necessárias, como as regras de escopo funcionam, e como clausuras são criadas e usadas.
O Capítulo 8, "Instruções Compostas," em A Referência da Linguagem Python diz praticamente tudo que há para dizer sobre cláusulas else
em instruções if
, for
, while
e try
. Sobre o uso pythônico de try/except
, com ou sem else
, Raymond Hettinger deu uma resposta brilhante para a pergunta "Is it a good practice to use try-except-else in Python?" (É uma boa prática usar try-except-else em Python?) (EN) no StackOverflow. O Python in a Nutshell, 3rd ed., by Martelli et al., tem um capítulo sobre exceções com uma excelente discussão sobre o estilo EAFP, atribuindo à pioneira da computação Grace Hopper a criação da frase "É mais fácil pedir perdão que pedir permissão."
O capítulo 4 de A Biblioteca Padrão do Python, "Tipos Embutidos", tem uma seção dedicada a "Tipos de Gerenciador de Contexto". Os métodos especiais __enter__/__exit__
também estão documentados em A Referência da Linguagem Python, em "Gerenciadores de Contexto da Instrução with".[15] Os gerenciadores de contexto foram introduzidos na PEP 343—The "with" Statement (EN).
Raymond Hettinger apontou a instrução with
como um "recurso maravilhoso da linguagem" em sua palestra de abertura da PyCon US 2013 (EN). Ele também mostrou alguns usos interessantes de gerenciadores de contexto em sua apresentação "Transforming Code into Beautiful, Idiomatic Python" ("Transformando Código em Lindo Python Idiomático") (EN), na mesma conferência.
O post de Jeff Preshing em seu blog, "The Python 'with' Statement by Example" "A Instrução 'with' do Python através de Exemplos"(EN) é interessante pelos exemplos de uso de gerenciadores de contexto com a biblioteca gráfica pycairo
.
A classe contextlib.ExitStack
foi baseada em uma ideia original de Nikolaus Rath, que escreveu um post curto explicando porque ela é útil:
"On the Beauty of Python’s ExitStack" "Sobre a Beleza do ExitStack do Python". No texto, Rath propõe que ExitStack
é similar, mas mais flexível que a instrução defer
em Go—que acho uma das melhores ideias naquela linguagem.
Beazley and Jones desenvolveram gerenciadores de contexto para propósitos muito diferentes em seu livro, Python Cookbook, (EN) 3rd ed. A "Recipe 8.3. Making Objects Support the Context-Management Protocol" (Receita 8.3. Fazendo Objetos Suportarem o Protocolo Gerenciador de Contexto) implementa uma classe LazyConnection
, cujas instâncias são gerenciadores de contexto que abrem e fecham conexões de rede automaticamente, em blocos with
. A "Recipe 9.22. Defining Context Managers the Easy Way" (Receita 9.22. O Jeito Fácil de Definir Gerenciadores de Contexto) introduz um gerenciador de contexto para código de cronometragem, e outro para realizar mudanças transacionais em um objeto list
: dentro do bloco with
é criada um cópia funcional da instância de list
, e todas as mudanças são aplicadas àquela cópia funcional. Apenas quando o bloco with
termina sem uma exceção a cópia funcional substitui a original. Simples e genial.
Peter Norvig descreve seu pequeno interpretador Scheme nos posts "(How to Write a (Lisp) Interpreter (in Python))" "(_Como Escrever um Interpretador (Lisp) (em Python))_" (EN) e "(An ((Even Better) Lisp) Interpreter (in Python))" "_(Um Interpretador (Lisp (Ainda Melhor)) (em Python))_" (EN). O código-fonte de lis.py e lispy.py está no repositório norvig/pytudes. Meu repositório, fluentpython/lispy, inclui a versão mylis do lis.py, atualizado para o Python 3.10, com um REPL melhor, integraçào com a linha de comando, exemplos, mais testes e referências para aprender mais sobre Scheme. O melhor ambiente e dialeto de Scheme para aprender e experimentar é o Racket.
Fatorando o pão
Em sua palestra de abertura na PyCon US 2013, "What Makes Python Awesome" ("O que torna o Python incrível"),
Raymond Hettinger diz que quando viu a proposta da instrução with
, pensou que era "um pouquinho misteriosa." Inicialmente tive uma reação similar. As PEPs são muitas vezes difíceis de ler, e a PEP 343 é típica nesse sentido.
Mas aí—nos contou Hettinger—ele teve uma ideia: as sub-rotinas são a invenção mais importante na história das linguagens de computador. Se você tem sequências de operações, como A;B;C e P;B;Q, você pode fatorar B em uma sub-rotina. É como fatorar o recheio de um sanduíche: usar atum com tipos de diferentes de pão. Mas e se você quiser fatorar o pão, para fazer sanduíches com pão de trigo integral usando recheios diferentes a cada vez? É isso que a instrução with
oferece. Ela é o complemento da sub-rotina. Hettinger continuou:
A instrução
with
é algo muito importante. Encorajo vocês a irem lá e olharem para a ponta desse iceberg, e daí cavarem mais fundo. Provavelmente é possível fazer coisas muito profundas com a instruçãowith
. Seus melhores usos ainda estão por ser descobertos. Espero que, se vocês fizerem bom uso dela, ela será copiada para outras linguagens, e todas as linguagens futuras vão incluí-la. Vocês podem ser parte da descoberta de algo quase tão profundo quanto a invenção da própria sub-rotina.
Hettinger admite que está tentando muito vender a instrução with
.
Mesmo assim, é um recurso bem útil.
Quando ele usou a analogia do sanduíche para explicar como with
é o complemento da sub-rotina, muitas possibilidades se abriram na minha mente.
Se você precisa convencer alguém que o Python é maravilhoso, assista a palestra de abertura de Hettinger. A parte sobre gerenciadores de contexto fica entre 23:00 to 26:15. Mas a palestra inteira é excelente.
Recursão eficiente com chamadas de cauda apropriadas
As implementações padrão de Scheme são obrigadas a oferecer chamadas de cauda apropriadas (PTC, sigla em inglês para proper tail calls), para tornar a iteração por recursão uma alternativa prática aos loops while
das linguagens imperativas.
Alguns autores se referem às PTC como otimização de chamadas de cauda (TCO, sigla em inglês para tail call optimization);
para outros, TCO é uma coisa diferente.
Para mais detalhes, leia "Chamadas recursivas de cauda na Wikipedia em português e
"Tail call" (EN), mais aprofundado, na Wikipedia em inglês, e
"Tail call optimization in ECMAScript 6" (EN).
Uma chamada de cauda é quando uma função devolve o resultado de uma chamada de função, que pode ou não ser a ela mesma (a função que está devolvendo o resultado).
Os exemplos gcd
no Exemplo 10 e no Exemplo 11 fazem chamadas de cauda (recursivas) no lado falso do if
.
Por outro lado, essa factorial
não faz uma chamada de cauda:
def factorial(n):
if n < 2:
return 1
return n * factorial(n - 1)
A chamada para factorial
na última linha não é uma chamada de cauda, pois o valor de return
não é somente o resultado de uma chamada recursiva:
o resultado é multiplicado por n
antes de ser devolvido.
Aqui está uma alternativa que usa uma chamada de cauda, e é portanto recursiva de cauda:
def factorial_tc(n, product=1):
if n < 1:
return product
return factorial_tc(n - 1, product * n)
O Python não tem PTC
então não há vantagem em escrever funções recursivas de cauda.
Neste caso, a primeira versão é, na minha opinião, mais curta e mais legível.
Para usos na vida real, não se esqueça que o Python tem o math.factorial
,
escrito em C sem recursão.
O ponto é que, mesmo em linguagens que implementam PTC, isso não beneficia toda função recursiva,
apenas aquelas cuidadosamente escritas para fazerem chamadas de cauda.
Se PTC são suportadas pela linguagem, quando o interpretador vê uma chamada de cauda, ele pula para dentro do corpo da função chamada sem criar um novo stack frame, economizando memória. Há também linguagens compiladas que implementam PTC, por vezes como uma otimização que pode ser ligada e desligada.
Não existe um consenso universal sobre a definição de TCO ou sobre o valor das PTC em linguagens que não foram projetadas como linguagens funcionais desde o início, como Python e Javascript. Em linguagens funcionais, PTC é um recurso esperado, não apenas uma otimização boa de ter à mão. Se a linguagem não tem outro mecanismo de iteração além da recursão, então PTC é necessário para tornar prático o uso da linguagem. O lis.py de Norvig não implementa PTC, mas seu interpretador mais elaborado, o lispy.py, implementa.
Os argumentos contra chamadas de cauda apropriadas em Python e Javascript
O CPython não implementa PTC, e provavelmente nunca o fará. Guido van Rossum escreveu "Final Words on Tail Calls" ("Últimas Palavras sobre Chamadas de Cauda") para explicar o motivo. Resumindo, aqui está uma passagem fundamental de seu post:
Pessoalmente, acho que é um bom recurso para algumas linguagens, mas não acho que se encaixe no Python: a eliminação dos registros do stack para algumas chamadas mas não para outras certamente confundiria muitos usuários, que não foram criados na religião das chamadas de cauda, mas podem ter aprendido sobre a semântica das chamadas restreando algumas chamadas em um depurador.
Em 2015, PTC foram incluídas no padrão ECMAScript 6 para JavaScript. Em outubro de 2021 o interpretador no WebKit as implementa (EN). O WebKit é usado pelo Safari. Os interpretadores JS em todos os outros navegadores populares não tem PTC, assim como o Node.js, que depende da engine V8 que o Google mantém para o Chrome. Transpiladores e polyfills (injetores de código) voltados para o JS, como o TypeScript, o ClojureScript e o Babel, também não suportam PTC, de acordo com essa " Tabela de compatibilidade com ECMAScript 6" (EN).
Já vi várias explicações para a rejeição das PTC por parte dos implementadores, mas a mais comum é a mesma que Guido van Rossum mencionou: PTC tornam a depuração mais difícil para todo mundo, e beneficiam apenas uma minoria que prefere usar recursão para fazer iteração. Para mais detalhes, veja "What happened to proper tail calls in JavaScript?" "O que aconteceu com as chamadas de cauda apropriadas em Javascript?" de Graham Marlow.
Há casos em que a recursão é a melhor solução, mesmo no Python sem PTC. Em um post anterior sobre o assunto, Guido escreveu:
[…] uma implementação típica de Python permite 1000 recursões, o que é bastante para código não-recursivo e para código que usa recursão para atravessar, por exemplo, um árvore de parsing típica, mas não o bastante para um loop escrito de forma recursiva sobre uma lista grande.
Concordo com Guido e com a maioria dos implementadores de Javascript.
A falta de PTC é a maior restrição ao desenvolvimento de programas Python em um estilo funcional—mais que a sintaxe limitada de lambda
.
Se você estiver curioso em ver como PTC funciona em um interpretador com menos recursos (e menos código) que o lispy.py de Norvig, veja o mylis_2.
O truque é iniciar com o loop infinito em evaluate
e o código no case
para chamadas de função:
essa combinação faz o interpretador pular para dentro do corpo da próxima Procedure
sem chamar evaluate
recursivamente durante a chamada de cauda.
Esses pequenos interpretadores demonstram o poder da abstração:
apesar do Python não implementar PTC, é possível e não muito difícil escrever um interpretador, em Python, que implementa PTC.
Aprendi a fazer isso lendo o código de Peter Norvig.
Obrigado por compartilhar, professor!
A opinião de Norvig sobre evaluate() com pattern matching
Eu compartilhei o código da versão Python 3.10 de lis.py com Peter Norvig.
Ele gostou do exemplo usando pattern matching, mas sugeriu uma solução diferente:
em vez de usar os guardas que escrevi,
ele teria exatamente um case
por palavra reservada,
e teria testes dentro de cada case
, para fornecer mensagens de SyntaxError
mais específicas—por exemplo, quando o corpo estiver vazio.
Isso também tornaria o guarda em case [func_exp, *args] if func_exp not in KEYWORDS:
desnecessário,
pois todas as palavras reservadas teriam sido tratadas antes do case
para chamadas de função.
Provavelmente seguirei o conselho do professor Norvig quando acrescentar mais funcionalidades ao
mylis.
Mas a forma como estruturei evaluate
no Exemplo 17 tem algumas vantagens didáticas nesse livro:
o exemplo é paralelo à implementação com if/elif/…
([ex_norvigs_eval]),
as cláusulas case
demonstram mais recursos de pattern matching
e o código é mais conciso.
with
começa em 23:00 e termina em 26:15.
self
são exatamente o que você obtém se chama sys.exc_info()
no bloco finally
de uma instrução try/finally
. Isso faz sentido, considerando que a instrução with
tem por objetivo substituir a maioria dos usos de try/finally
, e chamar sys.exc_info()
é muitas vezes necessário para determinar que ação de limpeza é necessária.
_GeneratorContextManager
. Se você quiser saber exatamente como ela funciona, leia seu código fonte na Lib/contextlib.py do Python 3.10.
(f …)
para chamadas de função e formas especiais como (define …)
, (if …)
e (quote …)
, que de forma alguma se comportam como chamadas de função
# type: ignore[index]
está ali por causa do issue #6042 no typeshed, que segue sem resolução quando esse capítulo está sendo revisado. ChainMap
é anotado como MutableMapping
, mas a dica de tipo no atributo maps
diz que ele é uma lista de Mapping
, indiretamente tornando todo o ChainMap
imutável até onde o Mypy entende.
…
) até entrarmos uma expressão ou instrução completa, que possa ser analisada e avaliada. O mylis também trata alguns erros de forma graciosa, mas ele ainda é fácil de quebrar. Não é nem de longe tão robusto quanto o REPL do Python.
set!
só aparece na página 220 do mais conhecido livro de Scheme, Structure and Interpretation of Computer Programs (A Estrutura e a Interpretação de Programas de Computador), 2nd ed., de Abelson et al. (MIT Press), também conhecido como SICP ou "Wizard Book" (Livro do Mago). Programas em estilo funcional podem nos levar muito longe sem as mudanças de estado típicas da programação imperativa e da programação orientada a objetos.
else
foi a falta de consenso sobre como indentá-lo dentro do match
: o else
deveria ser indentedo no mesmo nível do match
ou no mesmo nível do case
?