Houve uma certa quantidade de reclamações sobre a escolha do nome "decorador" para esse recurso. A mais frequente foi sobre o nome não ser consistente com seu uso no livro da GoF.[1] O nome decorator provavelmente se origina de seu uso no âmbito dos compiladores—uma árvore sintática é percorrida e anotada.
Decoradores de função nos permitem
"marcar" funções no código-fonte, para aprimorar de alguma forma seu
comportamento. É um mecanismo muito poderoso. Por exemplo, o decorador
@functools.cache armazena um mapeamento de argumentos para resultados, e
depois usa esse mapeamento para evitar computar novamente o resultado quando a
função é chamada com argumentos já vistos. Isso pode acelerar muito uma
aplicação.
Para dominar esse recurso, é preciso antes entender clausuras (closures)— nome dado à estrutura onde uma função captura variáveis presentes no escopo onde a função é definida, necessárias para a execução da função futuramente.[2]
A palavra reservada mais obscura de Python é nonlocal, introduzida no Python
3.0. É perfeitamente possível ter uma vida produtiva e lucrativa programando em
Python sem jamais usá-la, seguindo uma dieta estrita de orientação a objetos
centrada em classes. Entretanto, caso queira implementar seus próprios
decoradores de função, precisa entender clausuras, e então a necessidade de
nonlocal fica evidente.
Além de sua aplicação aos decoradores, clausuras também são essenciais para qualquer tipo de programação utilizando callbacks, e para codar em um estilo funcional quando isso fizer sentido.
O objetivo final deste capítulo é explicar exatamente como funcionam os decoradores de função, desde simples decoradores de registro até os complicados decoradores parametrizados. Mas antes de chegar a esse objetivo, precisamos tratar de:
-
Como Python analisa a sintaxe de decoradores
-
Como Python decide se uma variável é local
-
Por que clausuras existem e como elas funcionam
-
Qual problema é resolvido por
nonlocal
Após criar essa base, chegaremos aos decoradores:
-
Como implementar um decorador bem comportado
-
Decoradores poderosos da biblioteca padrão:
@cachee@singledispatch -
Como implementar um decorador parametrizado
Nesta edição,
apresento o decorador de caching functools.cache do Python
3.9 antes do functools.lru_cache, que é mais antigo.
A Usando o lru_cache apresenta também o uso de lru_cache
sem argumentos, uma novidade do Python 3.8.
Expandi a Funções genéricas com despacho único para incluir dicas de tipo, a sintaxe
recomendada para usar functools.singledispatch desde o Python 3.7.
A Decoradores parametrizados agora inclui o Módulo clockdeco_cls.py: decorador parametrizado clock, implementado como uma classe,
que usa uma classe e não uma clausura para implementar um decorador.
Começamos com a introdução aos decoradores mais suave que consegui imaginar.
Um decorador é um invocável que recebe outra função como um argumento (a função decorada).
Um decorador pode executar algum processamento com a função decorada, e pode devolver a mesma função ou substituí-la por outra função ou objeto invocável.[3]
Em outras palavras, supondo a existência de uma função decoradora
chamada decorate, este código:
@decorate
def target():
print('running target()')tem o mesmo efeito de:
def target():
print('running target()')
target = decorate(target)O resultado final é o mesmo: após a execução de qualquer um destes exemplos,
o nome target está vinculado a qualquer que seja a função devolvida por
decorate(target)—que tanto pode ser a função inicialmente chamada target
quanto uma outra função diferente.
Para confirmar que a função decorada é substituída, veja a sessão de console no Um decorador normalmente substitui uma função por outra, diferente.
>>> def deco(func):
... def inner():
... print('running inner()')
... return inner (1)
...
>>> @deco
... def target(): (2)
... print('running target()')
...
>>> target() (3)
running inner()
>>> target (4)
<function deco.<locals>.inner at 0x10063b598>-
decodevolve seu objeto funçãoinner. -
targeté decorada pordeco. -
Invocar a
targetdecorada causa, na verdade, a execução deinner. -
A inspeção revela que
targeté agora uma referência ainner.
Estritamente falando, decoradores são apenas açúcar sintático. Como vimos, é sempre possível chamar um decorador como um invocável normal, passando outra função como parâmetro. Algumas vezes isso inclusive é conveniente, especialmente quando estamos fazendo metaprogramação—mudando o comportamento de um programa durante a execução.
Três fatos essenciais sobre decoradores:
-
Um decorador é uma função ou outro invocável.
-
Um decorador pode, opcionalmente, substituir a função decorada por outra.
-
Decoradores são executados assim que um módulo é carregado.
Vamos agora nos concentrar nesse terceiro ponto.
Uma característica fundamental dos decoradores é serem executados logo após a função decorada ser definida. Isso normalmente acontece no momento da importação (import time), ou seja, quando um módulo é carregado pelo Python. Observe registration.py no O módulo registration.py.
link:../code/09-closure-deco/registration.py[role=include]-
registryvai armazenar referências para funções decoradas por@register. -
registerrecebe uma função como argumento. -
Exibe a função que está sendo decorada, para demonstração.
-
Insere
funcemregistry. -
É obrigatório devolver uma função; aqui devolvemos a mesma função recebida como argumento.
-
f1ef2são decoradas por@register. -
f3não é decorada. -
mainexiberegistry, depois chamaf1(),f2(), ef3(). -
main()só é invocada se registration.py for executado como um script.
O resultado da execução de registration.py é assim:
$ python3 registration.py running register(<function f1 at 0x100631bf8>) running register(<function f2 at 0x100631c80>) running main() registry -> [<function f1 at 0x100631bf8>, <function f2 at 0x100631c80>] running f1() running f2() running f3()
Observe que register roda (duas vezes) antes de qualquer outra função no
módulo. Quando register é chamada, ela recebe o objeto função a ser decorado
como argumento—por exemplo, <function f1 at 0x100631bf8>.
Após o carregamento do módulo, a lista registry contém referências para as
duas funções decoradas: f1 e f2. Essas funções, bem como f3, são executadas
apenas quando chamadas explicitamente por main.
Se registration.py for importado (e não executado como um script), a saída é essa:
>>> import registration
running register(<function f1 at 0x10063b1e0>)
running register(<function f2 at 0x10063b268>)Nesse momento, se você inspecionar registry, verá isso:
>>> registration.registry
[<function f1 at 0x10063b1e0>, <function f2 at 0x10063b268>]O ponto central do O módulo registration.py é enfatizar que decoradores de função são executados assim que o módulo é importado, mas as funções decoradas só rodam quando são invocadas explicitamente. Isso ressalta a diferença entre o momento da importação e o momento da execução na operação de um módulo em Python.
Considerando a forma como decoradores são normalmente usados em código do mundo real, o O módulo registration.py é incomum por dois motivos:
-
A função do decorador é definida no mesmo módulo das funções decoradas. Tipicamente, um decorador é definido em um módulo de uma biblioteca e aplicado a funções de outros módulos de bibliotecas ou aplicações.
-
O decorador
registerdevolve a mesma função recebida como argumento. Na prática, a maior parte dos decoradores define e devolve uma função interna.
Apesar do decorador register no O módulo registration.py devolver a função decorada
inalterada, esta técnica não é inútil. Decoradores parecidos são usados por muitos
frameworks Python para adicionar funções a um registro central—por exemplo, um
registro mapeando padrões de URLs para funções que geram respostas HTTP. Tais
decoradores de registro podem ou não modificar as funções decoradas.
Vamos ver um decorador de registro em ação na [decorated_strategy_sec] ([ch_design_patterns]).
A maioria dos decoradores modifica a função decorada. Eles normalmente fazem isso definindo e devolvendo uma função interna para substituir a função decorada. Código que usa funções internas quase sempre depende de clausuras para operar corretamente. Para entender as clausuras, precisamos dar um passo atrás e revisar como o escopo de variáveis funciona no Python.
No Função lendo uma variável local e uma variável global, definimos e testamos uma
função que lê duas variáveis: uma variável local a—definida como parâmetro de
função—e a variável b, que não é definida em lugar algum na função.
>>> def f1(a):
... print(a)
... print(b)
...
>>> f1(3)
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f1
NameError: global name 'b' is not definedO erro obtido não é surpreendente. Continuando do Função lendo uma variável local e uma variável global, se
atribuirmos um valor a um b global e então chamarmos f1, funciona:
>>> b = 6
>>> f1(3)
3
6Agora vamos ver um exemplo que pode ser surpreendente.
Leia com atenção a função f2, no A variável b é local, porque um valor é atribuído a ela no corpo da função. As primeiras duas linhas
são as mesmas da f1 do Função lendo uma variável local e uma variável global, e então ela faz uma atribuição a
b. Mas para com um erro no segundo print, antes da atribuição ser executada.
b é local, porque um valor é atribuído a ela no corpo da função>>> b = 6
>>> def f2(a):
... print(a)
... print(b)
... b = 9
...
>>> f2(3)
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f2
UnboundLocalError: local variable 'b' referenced before assignmentObserve que a saída começa com 3, provando que o comando print(a) foi
executado. Mas o segundo, print(b), nunca roda. Quando vi isso pela primeira
vez, me espantei. Achei que o 6 seria exibido, pois há uma variável
global b, e a atribuição para a variável local b ocorre após print(b).
Mas quando Python compila o corpo da função, ele decide que b é
uma variável local, por ser atribuída dentro da função. O bytecode gerado
reflete essa decisão. O código tentará acessar b no escopo local. Mais tarde, quando a
chamada f2(3) acontece, o corpo de f2 obtém e exibe o valor da variável
local a, mas ao tentar obter o valor da variável local b, descobre que b
não está vinculado a nada.
Isso não é um bug, mas uma escolha de projeto:
Python não exige que você declare variáveis,
mas assume que uma variável que recebe uma atribuição no corpo de uma função
é uma variável local.
Isso é muito melhor que o comportamento de JavaScript, que também não requer
declarações de variáveis, mas se você esquecer de declarar uma variável como
local (com var), pode acabar alterando uma variável global por acidente.
Se queremos que o interpretador trate b como uma variável global e também
atribuir um novo valor a ela dentro da função, usamos a declaração global:
>>> b = 6
>>> def f3(a):
... global b
... print(a)
... print(b)
... b = 9
...
>>> f3(3)
3
6
>>> b
9Nos exemplos anteriores, vimos dois escopos em ação:
- O escopo global do módulo
-
Composto por nomes atribuídos a valores fora de qualquer bloco de classe ou função.
- O escopo local da função f3
-
Composto por nomes atribuídos a valores como parâmetros, ou diretamente no corpo da função.
Há um outro escopo de onde variáveis podem vir, chamado nonlocal, e ele é fundamental para clausuras; vamos tratar disso em breve.
Agora que vimos como o escopo de variáveis funciona no Python, podemos
enfrentar as clausuras na Clausuras.
Se tiver curiosidade sobre as diferenças no bytecode das funções no Função lendo uma variável local e uma variável global
e no A variável b é local, porque um valor é atribuído a ela no corpo da função, veja o quadro a seguir.
O
módulo dis descompila o bytecode de funções.
Leia no Bytecode da função f1 do [ex_global_undef] e no Bytecode da função f2 do [ex_local_unbound] os
bytecodes de f1 e f2, do Função lendo uma variável local e uma variável global e do A variável b é local, porque um valor é atribuído a ela no corpo da função,
respectivamente.
f1 do [ex_global_undef]>>> from dis import dis
>>> dis(f1)
2 0 LOAD_GLOBAL 0 (print) (1)
3 LOAD_FAST 0 (a) (2)
6 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
9 POP_TOP
3 10 LOAD_GLOBAL 0 (print)
13 LOAD_GLOBAL 1 (b) (3)
16 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
19 POP_TOP
20 LOAD_CONST 0 (None)
23 RETURN_VALUE-
Carrega o nome global
print. -
Carrega o nome local
a. -
Carrega o nome global
b.
Compare o bytecode de f1, visto no Bytecode da função f1 do [ex_global_undef] acima, com o bytecode de f2 no Bytecode da função f2 do [ex_local_unbound].
f2 do [ex_local_unbound]>>> dis(f2)
2 0 LOAD_GLOBAL 0 (print)
3 LOAD_FAST 0 (a)
6 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
9 POP_TOP
3 10 LOAD_GLOBAL 0 (print)
13 LOAD_FAST 1 (b) (1)
16 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
19 POP_TOP
4 20 LOAD_CONST 1 (9)
23 STORE_FAST 1 (b)
26 LOAD_CONST 0 (None)
29 RETURN_VALUE-
Carrega o nome local
b. Isso mostra que o compilador considerabuma variável local, mesmo com uma atribuição abocorrendo mais tarde, porque a natureza da variável—se ela é ou não local—não pode mudar no corpo da função.
A máquina virtual (VM) do CPython que executa o bytecode é uma máquina de pilha
(stack machine), então as operações LOAD e POP se referem à pilha. A descrição
mais detalhada dos opcodes de Python está além da finalidade desse livro, mas
eles estão documentados com o módulo, em
"dis—Disassembler de bytecode de Python".
Na blogosfera, as clausuras são algumas vezes confundidas com funções anônimas. Muita gente confunde por causa da história paralela destes conceitos: definir funções dentro de outras funções se torna mais comum e conveniente quando existem funções anônimas. E clausuras só fazem sentido a partir do momento em que você tem funções aninhadas. Daí que muitos aprendem as duas ideias ao mesmo tempo.
Na verdade, uma clausura é uma função—vamos chamá-la de f—com um escopo
estendido, incorporando variáveis acessadas no corpo de f que não são nem
variáveis globais nem variáveis locais de f. Tais variáveis devem vir do
escopo local de uma função externa que englobe f.
Não interessa se a função é anônima ou não; o que importa é que ela pode acessar variáveis não-globais definidas fora de seu corpo.
É um conceito difícil de entender, melhor ilustrado por um exemplo.
Imagine uma função avg, para calcular a média de uma série de valores que
cresce continuamente; por exemplo, o preço diário de um produto
ao longo de toda a sua história. A cada dia, um novo preço é acrescentado,
e a média é computada levando em conta todos os preços até então.
Começando do zero, avg poderia ser usada assim:
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0Como avg é definida, e onde fica o histórico com os valores anteriores?
Para começar, o average_oo.py: uma classe para calcular uma média cumulativa mostra uma implementação baseada em uma classe.
class Averager():
def __init__(self):
self.series = []
def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total / len(self.series)A classe Averager cria instâncias invocáveis:
>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0O average.py: uma função de ordem superior para a calcular uma média cumulativa, a seguir, é uma implementação funcional, usando a função de
ordem superior make_averager.
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total / len(series)
return averagerQuando invocada, make_averager devolve um objeto função averager. Cada vez
que um averager é invocado, ele insere o argumento recebido na série, e
calcula a média atual, como mostra o Testando o [ex_average_fn].
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(15)
12.0Note as semelhanças entre os dois exemplos: chamamos Averager() ou
make_averager() para obter um objeto invocável avg, que atualizará a série
histórica e calculará a média atual. No average_oo.py: uma classe para calcular uma média cumulativa, avg é uma instância
de Averager, no average.py: uma função de ordem superior para a calcular uma média cumulativa é a função interna averager. Nos dois
casos, basta chamar avg(n) para incluir n na série e obter a média
atualizada.
É óbvio onde o avg da classe Averager armazena o histórico: no atributo de
instância self.series. Mas onde a função avg no average.py: uma função de ordem superior para a calcular uma média cumulativa encontra a
series?
Observe que series é uma variável local de make_averager, pois a atribuição
series = [] acontece no corpo daquela função. Mas quando avg(10) é chamada,
make_averager já retornou, e seu escopo local não existe mais.
Dentro de averager, series é uma variável livre: uma variável que é mencionada mas não é
um parâmetro, e não tem uma atribuição no escopo local.
Esse termo técnico é essencial para entender
uma clausura. Veja a A clausura de averager estende o escopo daquela função para incluir a vinculação da variável livre series..
averager estende o escopo daquela função para incluir a vinculação da variável livre series.Podemos inspecionar o objeto averager para ver como Python armazena os nomes
das variáveis locais e variáveis livres no atributo __code__,
que representa o corpo compilado da função. O Inspecionando a função criada por make_averager no [ex_average_fn] demonstra isso.
make_averager no [ex_average_fn]>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)O valor de series é armazenado no atributo __closure__ da função
avg. Cada item em avg.__closure__ corresponde a um nome em __code__.
Esses itens são cells, e têm um atributo chamado cell_contents, onde o valor
real pode ser encontrado. O Continuando do [ex_average_demo1] mostra esses atributos.
>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__
(<cell at 0x107a44f78: list object at 0x107a91a48>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]Resumindo: uma clausura é uma função que retém os vínculos das variáveis livres que existem quando a função é definida, de forma que elas possam ser usadas mais tarde, quando a função for invocada, mas o escopo de sua definição não puder mais ser acessado.
Note que a única situação na qual uma função pode ter de lidar com variáveis externas não-globais é quando ela estiver aninhada dentro de outra função, e aquelas variáveis sejam parte do escopo local da função externa.
A implementação anterior de make_averager funciona,
mas é ineficiente. No average.py: uma função de ordem superior para a calcular uma média cumulativa, armazenamos todos os valores na série
histórica e calculamos sua sum cada vez que averager é invocada. Uma
implementação melhor armazenaria apenas o total e a contagem de itens até aquele
momento, e calcularia a média com esses dois números.
O Função de ordem superior incorreta para calcular uma média cumulativa sem armazenar todo o histórico é uma implementação errada, apenas para ilustrar. Consegue ver onde o código quebra?
def make_averager():
count = 0
total = 0
def averager(new_value):
count += 1
total += new_value
return total / count
return averagerAo testar o Função de ordem superior incorreta para calcular uma média cumulativa sem armazenar todo o histórico, o resultado é um erro:
>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
...
UnboundLocalError: local variable 'count' referenced before assignment
>>>O problema é que a instrução count += 1 significa o mesmo que
count = count + 1, quando count é um número ou qualquer tipo imutável.
Então, estamos realmente atribuindo um valor a count no corpo de averager,
e isso a torna uma variável local. O mesmo problema afeta a variável total.
Não tivemos esse problema no average.py: uma função de ordem superior para a calcular uma média cumulativa, porque nunca atribuimos nada ao
nome series; apenas chamamos series.append e invocamos sum e len nele.
Nos valemos, então, do fato de listas serem mutáveis.
Mas com tipos imutáveis, como números, strings, tuplas, etc., só é possível ler,
nunca atualizar. Se você reatribuir, como em count += 1,
estará implicitamente criando uma variável local count. Ela não será mais uma
variável livre, e seu valor não será atualizado na clausura.
A palavra reservada nonlocal foi introduzida no Python 3 para contornar esse
problema. Ela permite declarar uma variável livre, mesmo quando
a variável é atribuída dentro da função. Se um novo valor é atribuído a uma variável
nonlocal, o valor armazenado na clausura é atualizado.
O Calcula uma média cumulativa sem armazenar todo o histórico é a implementação correta da versão otimizada de make_averager.
def make_averager():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averagerApós estudar o nonlocal, podemos resumir como a consulta de variáveis funciona
no Python.
Quando uma função é definida, o compilador de
bytecode de Python determina como encontrar uma variável x que aparece na
função, baseado nas seguintes regras:[4]
-
Se há uma declaração
global x, entãoxestá vinculada à variável globalxdo módulo.[5] -
Se há uma declaração
nonlocal x, entãoxestá vinculada à variável localxna função circundante mais próxima de ondexfor definida. -
Se
xé um parâmetro ou tem um valor atribuído a si no corpo da função, entãoxé uma variável local. -
Se
xé referenciada mas não atribuída, e não é um parâmetro:-
xserá procurada nos escopos locais do corpos das funções circundantes (os escopos não-locais). -
Se
xnão for encontrada nos escopos circundantes, será lida do escopo global do módulo. -
Se
xnão for encontrada no escopo global, será lida de__builtins__.__dict__.
-
Tendo visto as clausuras de Python, podemos agora implementar decoradores com funções aninhadas.
O clockdeco0.py: decorador simples que mostra o tempo de execução de funções é um decorador que cronometra cada invocação da função decorada e exibe o tempo decorrido, os argumentos passados, e o resultado da chamada.
link:../code/09-closure-deco/clock/clockdeco0.py[role=include]-
Define a função interna
clockedpara aceitar qualquer número de argumentos posicionais. -
Essa linha só funciona porque a clausura de
clockedengloba a variável livrefunc. -
Devolve a função interna para substituir a função decorada.
O Usando o decorador clock demonstra o uso do decorador clock.
clocklink:../code/09-closure-deco/clock/clockdeco_demo.py[role=include]O resultado da execução do Usando o decorador clock é o seguinte:
$ python3 clockdeco_demo.py
**************************************** Calling snooze(.123)
[0.12363791s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000095s] factorial(1) -> 1
[0.00002408s] factorial(2) -> 2
[0.00003934s] factorial(3) -> 6
[0.00005221s] factorial(4) -> 24
[0.00006390s] factorial(5) -> 120
[0.00008297s] factorial(6) -> 720
6! = 720Lembre-se de que esse código:
@clock
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)na verdade faz isso:
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
factorial = clock(factorial)Então, nos dois exemplos, clock recebe a função factorial como seu argumento
func (veja o clockdeco0.py: decorador simples que mostra o tempo de execução de funções). Ela então cria e devolve a função clocked,
que o interpretador Python atribui a factorial (no primeiro exemplo, por baixo
dos panos). De fato, se você importar o módulo clockdeco_demo e verificar o
__name__ de factorial, verá isso:
>>> import clockdeco_demo
>>> clockdeco_demo.factorial.__name__
'clocked'
>>>O nome factorial agora é uma referência para a função clocked. Daqui por
diante, cada vez que factorial(n) for invocada, clocked(n) será executada.
Essencialmente, clocked faz o seguinte:
-
Registra o tempo inicial
t0. -
Chama a função
factorialoriginal, salvando o resultado. -
Computa o tempo decorrido.
-
Formata e exibe os dados coletados.
-
Devolve o resultado salvo no passo 2.
Esse é o comportamento típico de um decorador: ele substitui a função decorada com uma nova função que aceita os mesmos argumentos e (normalmente) devolve o que quer que a função decorada deveria devolver, enquanto realiza também algum processamento adicional.
|
Tip
|
Em Padrões de Projetos, de Gamma et al., a descrição curta do padrão decorador começa assim: "Atribui dinamicamente responsabilidades adicionais a um objeto." Decoradores de função se encaixam nessa descrição. Mas, no nível da implementação, os decoradores de Python guardam pouca semelhança com o decorador clássico descrito no Padrões de Projetos original. No Ponto de vista escrevi mais sobre esse assunto. |
O decorador clock implementado no clockdeco0.py: decorador simples que mostra o tempo de execução de funções tem alguns defeitos: ele
não aceita argumentos nomeados, e encobre o __name__ e o __doc__ da
função decorada. O clockdeco.py: um decorador clock melhorado usa o decorador functools.wraps para
copiar os atributos relevantes de func para clocked.
Nesta nova versão, os argumentos nomeados também são tratados corretamente.
clock melhoradolink:../code/09-closure-deco/clock/clockdeco.py[role=include]O functools.wraps é apenas um dos decoradores prontos para uso da biblioteca
padrão. Na próxima seção, veremos o decorador mais útil oferecido por
functools: cache.
Python tem três funções embutidas projetadas para decorar
métodos: property, classmethod e staticmethod. Vamos discutir property
na [prop_validation_sec] e os outros na [classmethod_x_staticmethod_sec].
No clockdeco.py: um decorador clock melhorado vimos outro decorador importante: functools.wraps, um
auxiliar na criação de decoradores bem comportados. Três dos decoradores mais
interessantes da biblioteca padrão são cache, lru_cache e
singledispatch—todos do módulo functools. Falaremos deles a seguir.
O decorador functools.cache implementa
memoização:[6] uma técnica de otimização que
armazena os resultados de invocações de uma função dispendiosa em um cache,
evitando repetir o processamento para argumentos previamente utilizados.
Uma boa demonstração é aplicar @cache à função recursiva, e dolorosamente
lenta, que gera o enésimo número da sequência de Fibonacci, como mostra o
Algoritmo recursivo e ridiculamente dispendioso para calcular o enésimo número na série de Fibonacci.
link:../code/09-closure-deco/fibo_demo.py[role=include]Aqui está o resultado da execução de fibo_demo.py. Exceto pela última linha,
toda a saída é produzida pelo decorador clock:
$ python3 fibo_demo.py
[0.00000042s] fibonacci(0) -> 0
[0.00000049s] fibonacci(1) -> 1
[0.00006115s] fibonacci(2) -> 1
[0.00000031s] fibonacci(1) -> 1
[0.00000035s] fibonacci(0) -> 0
[0.00000030s] fibonacci(1) -> 1
[0.00001084s] fibonacci(2) -> 1
[0.00002074s] fibonacci(3) -> 2
[0.00009189s] fibonacci(4) -> 3
[0.00000029s] fibonacci(1) -> 1
[0.00000027s] fibonacci(0) -> 0
[0.00000029s] fibonacci(1) -> 1
[0.00000959s] fibonacci(2) -> 1
[0.00001905s] fibonacci(3) -> 2
[0.00000026s] fibonacci(0) -> 0
[0.00000029s] fibonacci(1) -> 1
[0.00000997s] fibonacci(2) -> 1
[0.00000028s] fibonacci(1) -> 1
[0.00000030s] fibonacci(0) -> 0
[0.00000031s] fibonacci(1) -> 1
[0.00001019s] fibonacci(2) -> 1
[0.00001967s] fibonacci(3) -> 2
[0.00003876s] fibonacci(4) -> 3
[0.00006670s] fibonacci(5) -> 5
[0.00016852s] fibonacci(6) -> 8
8O desperdício é óbvio: fibonacci(1) é chamada oito vezes, fibonacci(2) cinco vezes, etc.
Mas acrescentar apenas duas linhas, para usar cache, melhora muito o desempenho. Veja o Implementação mais rápida, usando caching.
link:../code/09-closure-deco/fibo_demo_cache.py[role=include]-
Essa linha funciona com Python 3.9 ou posterior. Veja a Usando o lru_cache para uma alternativa que suporta versões anteriores de Python.
-
Este é um exemplo de decoradores empilhados:
@cacheé aplicado à função devolvida por@clock.
|
Tip
|
Decoradores empilhados
Para entender os decoradores empilhados (stacked decorators),
lembre-se de que Este código… @alpha
@beta
def my_fn():
...…faz o mesmo que este: my_fn = alpha(beta(my_fn))Em outras palavras, o decorador |
Usando o cache no Implementação mais rápida, usando caching, a função fibonacci é chamada
apenas uma vez para cada valor de n:
$ python3 fibo_demo_lru.py
[0.00000043s] fibonacci(0) -> 0
[0.00000054s] fibonacci(1) -> 1
[0.00006179s] fibonacci(2) -> 1
[0.00000070s] fibonacci(3) -> 2
[0.00007366s] fibonacci(4) -> 3
[0.00000057s] fibonacci(5) -> 5
[0.00008479s] fibonacci(6) -> 8
8Em outro teste, para calcular fibonacci(30), o Implementação mais rápida, usando caching fez as
31 chamadas necessárias em 0,00017s (tempo total), enquanto o Algoritmo recursivo e ridiculamente dispendioso para calcular o enésimo número na série de Fibonacci
sem cache, demorou 12,09s em um notebook Intel Core i7, porque chamou
fibonacci(1) 832.040 vezes, num total de 2.692.537 chamadas!
Todos os argumentos recebidos pela função decorada devem ser hashable,
pois o cache usa um dict para armazenar os resultados, e as chaves
são formadas pelos argumentos posicionais e nomeados usados nas chamadas.
Além de tornar viáveis esses algoritmos recursivos tolos, @cache brilha de
verdade em aplicações que precisam buscar informações de APIs remotas.
|
Warning
|
O |
O decorador
functools.cache é, na realidade, um mero invólucro em torno da antiga função
functools.lru_cache, que é mais flexível e também compatível com
versões anteriores ao Python 3.9.
A maior vantagem de @lru_cache é a possibilidade de limitar seu uso de memória
através do parâmetro maxsize, que tem um default bastante pequeno: 128.
Isso significa que o cache pode armazenar no máximo 128 resultados.
LRU é a sigla de Least Recently Used ("Usado Menos Recentemente"). Significa que registros que há algum tempo não são lidos, são descartados para dar lugar a novos itens.
Desde o Python 3.8, lru_cache pode ser aplicado de duas formas.
Esta é a forma mais simples:
@lru_cache
def função_dispendiosa(a, b):
...A outra forma é invocá-lo como uma função,
com () (funciona desde o Python 3.2):
@lru_cache()
def função_dispendiosa(a, b):
...Nos dois casos, os parâmetros default seriam utilizados. São eles:
maxsize=128-
Estabelece o número máximo de registros a serem armazenados. Após o cache estar cheio, o registro menos recentemente usado é descartado, para dar lugar a cada novo item. Para um desempenho ótimo,
maxsizedeve ser uma potência de 2. Se você passarmaxsize=None, a lógica LRU é desabilitada e o cache funciona mais rápido, mas os itens nunca são descartados, podendo levar a um consumo excessivo de memória. É assim que o@functools.cachefunciona. typed=False-
Determina se os resultados de diferentes tipos de argumentos devem ser armazenados separadamente. Por exemplo, na configuração default, argumentos inteiros e de ponto flutuante considerados iguais são armazenados apenas uma vez. Assim, haverá apenas uma entrada para as chamadas
f(1)ef(1.0). Setyped=True, aqueles argumentos produziriam registros diferentes, possivelmente armazenando resultados distintos.
Eis um exemplo de invocação de @lru_cache com parâmetros diferentes dos defaults:
@lru_cache(maxsize=2**20, typed=True)
def costly_function(a, b):
...Vamos agora examinar outro decorador poderoso: functools.singledispatch.
Imagine que estamos criando uma ferramenta para depurar aplicações Web. Queremos gerar código HTML para tipos diferentes de objetos Python.
Poderíamos começar com uma função como essa:
import html
def htmlize(obj):
content = html.escape(repr(obj))
return f'<pre>{content}</pre>'Isso funcionará para qualquer tipo de objeto, mas agora queremos estender a função para gerar HTML específico para determinados tipos. Alguns exemplos seriam:
str-
Substituir os caracteres de mudança de linha na string por
'<br/>\n'e usar tags<p>em vez de<pre>. int-
Mostrar o número em formato decimal e hexadecimal (com um caso especial para
bool). list-
Gerar uma lista em HTML, formatando cada item de acordo com seu tipo.
floateDecimal-
Mostrar o valor como de costume, mas também na forma de fração (por que não?).
O comportamento que desejamos aparece no htmlize() gera HTML adaptado para diferentes tipos de objetos.
htmlize() gera HTML adaptado para diferentes tipos de objetoslink:../code/09-closure-deco/htmlizer.py[role=include]-
A função original é registrada para
object, então ela serve para capturar e tratar todos os tipos de argumentos que não foram capturados pelas outras implementações. -
Objetos
strtambém passam por escape de HTML, mas são cercados por<p></p>, com quebras de linha<br/>inseridas antes de cada'\n'. -
Um
inté exibido nos formatos decimal e hexadecimal, dentro de um bloco<pre></pre>. -
Cada item na lista é formatado de acordo com seu tipo, e a sequência inteira é apresentada como uma lista HTML.
-
Apesar de ser um subtipo de
int,boolrecebe um tratamento especial. -
Mostra
Fractioncomo uma fração. -
Mostra
floateDecimalcom a fração equivalente aproximada.
Como não temos no Python a sobrecarga de métodos ao estilo de Java, não podemos
simplesmente criar variações de htmlize com assinaturas diferentes para cada
tipo de dado que queremos tratar de forma distinta. Uma solução possível em
Python seria transformar htmlize em uma função de despacho, com uma cadeia de
if/elif/… ou match/case/… chamando funções especializadas como
htmlize_str, htmlize_int, etc. Isso não é extensível pelos usuários de nosso
módulo, e é desajeitado: com o tempo, a função htmlize ficaria muito longa,
e o acoplamento entre ela e as funções especializadas seria forte demais.
O decorador functools.singledispatch permite que diferentes
módulos contribuam para a solução geral, e que você forneça facilmente funções
especializadas, mesmo para tipos pertencentes a pacotes externos que não possam
ser editados. Se você decorar uma função simples com @singledispatch, ela se
torna o ponto de entrada para uma função genérica: um grupo de funções que
executam a mesma operação de formas diferentes, dependendo do tipo do primeiro
argumento. Este é o significado do termo single dispatch (despacho único).
Se mais argumentos fossem usados para selecionar a função específica,
teríamos despacho múltiplo (multiple dispatch), um recurso nativo em
linguagens como Common Lisp, Julia e C#.
O @singledispatch cria uma @htmlize.register customizada, para juntar várias funções em uma função genérica mostra como funciona o despacho único.
|
Warning
|
|
@singledispatch cria uma @htmlize.register customizada, para juntar várias funções em uma função genéricalink:../code/09-closure-deco/htmlizer.py[role=include]-
@singledispatchmarca a função base, que trata o tipoobject. -
Cada função especializada é decorada com
@«base».register. -
O tipo do primeiro argumento passado durante a execução determina quando essa definição de função em particular será utilizada. O nome das funções especializadas é irrelevante;
_é uma boa escolha para deixar isso claro.[7] -
Registra uma nova função para cada tipo que precisa de tratamento especial, com uma dica de tipo correspondente no primeiro parâmetro.
-
As ABCs em
numberssão úteis para uso em conjunto comsingledispatch.[8] -
boolé um subtipo-denumbers.Integral, mas a lógica desingledispatchbusca a implementação com o tipo correspondente mais específico, independente da ordem na qual eles aparecem no código. -
Se você não quiser ou não puder adicionar dicas de tipo à função decorada, você pode passar o tipo para o decorador
@«base».register. Essa sintaxe funciona em Python 3.4 ou posterior. -
O decorador
@«base».registerdevolve a função sem decoração, então é possível empilhá-los para registrar dois ou mais tipos na mesma implementação.[9]
Sempre que possível, registre as funções especializadas para tratar ABCs
(classes abstratas), como numbers.Integral e abc.MutableSequence, ao invés
das implementações concretas como int e list. Isso permite ao seu código
suportar uma variedade maior de tipos compatíveis. Por exemplo, uma extensão de
Python pode fornecer alternativas para o tipo int com número fixo de bits como
subclasses de numbers.Integral.[10]
|
Tip
|
Usar ABCs ou |
Uma qualidade notável do mecanismo de singledispatch é que você pode registrar
funções especializadas em qualquer lugar do sistema, em qualquer módulo. Se mais
tarde você adicionar um módulo com um novo tipo definido pelo usuário, é fácil
acrescentar uma nova função específica para tratar aquele tipo. É possível
também escrever funções customizadas para classes que você não escreveu e não
pode modificar.
O singledispatch foi uma adição muito bem pensada à biblioteca padrão, e
oferece outras facilidades que não cabem neste livro. Uma boa
referência é a PEP 443—Single-dispatch generic functions
mas ela não menciona o uso de dicas de tipo, que foram criadas depois.
A documentação do módulo functools foi melhorada e oferece um tratamento mais
atualizado, com vários exemplos na seção referente ao
singledispatch.
|
Note
|
O |
Vimos decoradores recebendo argumentos, como @lru_cache(maxsize=1024) e o
htmlize.register(float) criado por @singledispatch no
@singledispatch cria uma @htmlize.register customizada, para juntar várias funções em uma função genérica. A próxima seção mostra como criar decoradores com
parâmetros.
Ao analisar um
decorador no código-fonte, Python passa a função decorada como primeiro
argumento para a função do decorador. Mas como fazemos um decorador aceitar
outros argumentos? A resposta é: criar uma fábrica de decoradores que recebe
aqueles argumentos e devolve um decorador, que é então aplicado à função a ser
decorada. Complicado? Sim. Mas vamos começar com um exemplo baseado no
decorador mais simples que vimos: register no O módulo registration.py resumido, do [registration_ex], repetido aqui por conveniência.
link:../code/09-closure-deco/registration_abridged.py[role=include]Para tornar mais fácil a
habilitação ou desabilitação do registro executado por register, faremos esse
último aceitar um parâmetro opcional active que, se False, não registra a
função decorada. Conceitualmente, a nova função register não é um decorador,
mas uma fábrica de decoradores. Quando chamada, ela devolve o decorador que será
realmente aplicado à função alvo.
register precisa ser invocado como uma funçãolink:../code/09-closure-deco/registration_param.py[role=include]-
registryé agora umset, tornando mais rápido acrescentar ou remover funções. -
registerrecebe um argumento nomeado opcional. -
A função interna
decorateé o verdadeiro decorador; observe como ela aceita uma função como argumento. -
Registra
funcapenas se o argumentoactive(obtido da clausura) forTrue. -
Se
activeé falso, remove a função (sem efeito se a função não está noset). -
Como
decorateé um decorador, tem que devolver uma função. -
registeré nossa fábrica de decoradores, então devolvedecorate. -
A fábrica
@registerprecisa ser invocada como uma função, com os parâmetros desejados. -
Mesmo se nenhum parâmetro for passado, ainda assim
registerdeve ser invocada como uma função:@register()para criar e devolver o verdadeiro decorador,decorate.
O ponto central aqui é que register() devolve decorate, que então é aplicado
à função decorada.
O código do Para aceitar parâmetros, o novo decorador register precisa ser invocado como uma função está em um módulo registration_param.py.
Se o importarmos, veremos o seguinte:
>>> import registration_param
running register(active=False)->decorate(<function f1 at 0x10063c1e0>)
running register(active=True)->decorate(<function f2 at 0x10063c268>)
>>> registration_param.registry
[<function f2 at 0x10063c268>]Veja como apenas a função f2 aparece no registry; f1 não aparece porque
active=False foi passado para a fábrica de decoradores register, então o
decorate aplicado a f1 não adiciona essa função a registry.
Se, ao invés de usar a sintaxe @, usarmos register como uma função regular,
a sintaxe necessária para decorar uma função f seria register()(f), para
inserir f ao registry, ou register(active=False)(f), para não inseri-la
(ou removê-la). Veja o Usando o módulo registration_param listado no [registration_param_ex] para uma demonstração da
adição e remoção de funções do registry.
>>> from registration_param import *
running register(active=False)->decorate(<function f1 at 0x10073c1e0>)
running register(active=True)->decorate(<function f2 at 0x10073c268>)
>>> registry # (1)
{<function f2 at 0x10073c268>}
>>> register()(f3) # (2)
running register(active=True)->decorate(<function f3 at 0x10073c158>)
<function f3 at 0x10073c158>
>>> registry # (3)
{<function f3 at 0x10073c158>, <function f2 at 0x10073c268>}
>>> register(active=False)(f2) # (4)
running register(active=False)->decorate(<function f2 at 0x10073c268>)
<function f2 at 0x10073c268>
>>> registry # (5)
{<function f3 at 0x10073c158>}-
Quando o módulo é importado,
f2é inserida noregistry. -
A expressão
register()devolvedecorate, que então é aplicado af3. -
A linha anterior adicionou
f3aoregistry. -
Essa chamada remove
f2doregistry. -
Confirma que apenas
f3permanece noregistry.
O funcionamento de decoradores parametrizados é bastante complexo, e esse que acabamos de discutir é mais simples que a maioria. Decoradores parametrizados em geral substituem a função decorada, e sua construção exige um nível adicional de aninhamento. Vamos agora explorar a arquitetura de uma dessas pirâmides de funções.
Nesta seção vamos
revisitar o decorador clock, acrescentando um recurso: os usuários podem
passar uma string para formatar o relatório sobre a função cronometrada.
Veja o Módulo clockdeco_param.py: o decorador clock parametrizado.
|
Note
|
Para simplificar, o Módulo clockdeco_param.py: o decorador |
clock parametrizadolink:../code/09-closure-deco/clock/clockdeco_param.py[role=include]-
Formato padrão da saída citando variáveis locais da função
clocked. -
clocké a nossa fábrica de decoradores parametrizados. -
decorateé o verdadeiro decorador. -
clockedenvolve a função decorada. -
_resulté o resultado real da função decorada. -
_argsarmazena os verdadeiros argumentos declocked, enquantoargsé astrusada para exibição. -
resulté astrque representa_result, para uso no formato. -
Usar
**locals()aqui permite que qualquer variável local declockedseja referenciada emfmt.[11] -
clockedvai substituir a função decorada, então ela deve devolver o mesmo que aquela função devolve. -
decoratedevolveclocked. -
clockdevolvedecorate. -
Nesse auto-teste,
clock()é chamado sem argumentos, então o decorador aplicado usará o formato default,str.
Se você rodar o Módulo clockdeco_param.py: o decorador clock parametrizado no console, o resultado é o seguinte:
$ python3 clockdeco_param.py
[0.12412500s] snooze(0.123) -> None
[0.12411904s] snooze(0.123) -> None
[0.12410498s] snooze(0.123) -> NonePara exercitar a nova funcionalidade, veremos mais dois
módulos que usam o clockdeco_param,
o clockdeco_param_demo1.py e o
clockdeco_param_demo2.py, e as saídas que eles produzem.
link:../code/09-closure-deco/clock/clockdeco_param_demo1.py[role=include]Saída do clockdeco_param_demo1.py:
$ python3 clockdeco_param_demo1.py
snooze: 0.12414693832397461s
snooze: 0.1241159439086914s
snooze: 0.12412118911743164slink:../code/09-closure-deco/clock/clockdeco_param_demo2.py[role=include]Saída do clockdeco_param_demo2.py:
$ python3 clockdeco_param_demo2.py
snooze(0.123) dt=0.124s
snooze(0.123) dt=0.124s
snooze(0.123) dt=0.124s|
Note
|
Lennart Regebro—um dos revisores técnicos da primeira edição—argumenta que seria
melhor programar decoradores como classes implementando |
A próxima seção traz um exemplo no estilo recomendado por Regebro e Dumpleton.
Como um último exemplo,
o Módulo clockdeco_cls.py: decorador parametrizado clock, implementado como uma classe mostra a implementação de um decorador parametrizado clock,
programado como uma classe com __call__.
Compare o Módulo clockdeco_param.py: o decorador clock parametrizado com o Módulo clockdeco_cls.py: decorador parametrizado clock, implementado como uma classe.
Qual você prefere?
clock, implementado como uma classelink:../code/09-closure-deco/clock/clockdeco_cls.py[role=include]-
Ao invés de uma função externa
clock, a classeclocké nossa fábrica de decoradores parametrizados. Escreviclockcomcminúsculo para deixar claro que essa implementação substitui exatamente a funçãoclockno Módulo clockdeco_param.py: o decoradorclockparametrizado. -
O argumento passado em
clock(my_format)é atribuído ao parâmetrofmtaqui. O construtor da classe devolve uma instância declock, commy_formatarmazenado emself.fmt. -
__call__torna a instância declockinvocável. Quando chamada, a instância substitui a função decorada comclocked. -
clockedenvolve a função decorada.
Isso encerra nossa exploração dos decoradores de função. Veremos os decoradores de classe no [ch_class_metaprog].
Percorremos um terreno difícil neste capítulo. Tentei tornar a jornada tão suave quanto possível, mas entramos nos domínios da meta-programação, onde nada é simples.
Partimos de um decorador simples @register, sem uma função interna, e
terminamos com um @clock() parametrizado envolvendo dois níveis de funções
aninhadas.
Decoradores de registro, apesar de serem essencialmente simples, têm aplicações reais nos frameworks Python. Vamos aplicar a ideia de registro em uma implementação do padrão de projeto Estratégia, no [ch_design_patterns].
Entender como os decoradores realmente funcionam exigiu falar da diferença entre
momento de importação e momento de execução. Então mergulhamos no escopo de
variáveis, clausuras e a nova declaração nonlocal. Dominar as clausuras e
nonlocal é valioso não apenas para criar decoradores, mas também para escrever
programas orientados a eventos para GUIs ou E/S assíncrona com callbacks, e
para adotar um estilo funcional quando fizer sentido.
Decoradores parametrizados quase sempre implicam em pelo menos dois níveis de
funções aninhadas, talvez mais se você quiser usar @functools.wraps, e
produzir um decorador com um suporte melhor a técnicas mais avançadas. Uma
dessas técnicas é o empilhamento de decoradores, que vimos no
Implementação mais rápida, usando caching. Para decoradores mais sofisticados, uma implementação
baseada em classes pode ser mais fácil de ler e manter.
Como exemplos de decoradores parametrizados na biblioteca padrão, visitamos os
poderosos @cache e @singledispatch, do módulo functools.
O item #26 do livro
Effective Python, 2nd ed. (Addison-Wesley), de
Brett Slatkin, trata das melhores práticas para decoradores de função, e
recomenda sempre usar functools.wraps—que vimos no
clockdeco.py: um decorador clock melhorado.[12]
Graham Dumpleton tem, em seu blog, uma série de posts
abrangentes sobre técnicas para implementar decoradores bem comportados,
começando com How you implemented your Python decorator is
wrong (A forma como você implementou seu decorador em Python está errada).
Seus conhecimentos sobre o tema também aparecem
no módulo wrapt, que ele escreveu para simplificar a
implementação de decoradores e invólucros (wrappers) dinâmicos de função,
que suportam introspecção e se comportam de forma correta quando decorados
novamente, quando aplicados a métodos e quando usados como descritores de
atributos (o [ch_descriptors] é sobre descritores).
"Metaprogramming" (Metaprogramação) (EN), o capítulo 9 do
Python Cookbook, 3ª ed. de David Beazley e Brian K. Jones (O’Reilly), tem
várias receitas ilustrando desde decoradores elementares até alguns muito
sofisticados, incluindo um que pode ser invocado como um decorador regular ou
como uma fábrica de decoradores, por exemplo, @clock ou @clock(). É a
"Recipe 9.6. Defining a Decorator That Takes an Optional Argument" (Receita
9.6. Definindo um Decorador Que Recebe um Argumento Opcional) desse livro de
receitas.
Michele Simionato criou decorator, um pacote para "simplificar o uso de decoradores para o programador comum, e popularizar os decoradores através da apresentação de vários exemplos não-triviais", de acordo com sua documentação.
Criada quando os decoradores ainda eram um recurso novo no Python, a página wiki Python Decorator Library tem dezenas de exemplos. Como começou há muitos anos, algumas das técnicas apresentadas foram suplantadas, mas ela ainda é uma excelente fonte de inspiração.
Closures in Python é um post curto de Fredrik Lundh, explicando a terminologia das clausuras.
A PEP 3104—Access to Names in Outer Scopes (Acesso a Nomes
em Escopos Externos) descreve a introdução da declaração nonlocal.
Ela também inclui uma excelente revisão de como essa questão foi resolvida
em outras linguagens dinâmicas (Perl, Ruby, JavaScript, etc.)
e os prós e contras das opções de design disponíveis para Python.
Em um nível mais teórico, a PEP 227—Statically Nested Scopes (Escopos estaticamente Aninhados_) documenta a introdução do escopo léxico como um opção no Python 2.1 e como padrão no Python 2.2, explicando a justificativa e as opções de design para a implementação de clausuras no Python.
A PEP 443 traz a justificativa e uma descrição detalhada do mecanismo de funções genéricas de despacho único. Um post de Guido van Rossum de março de 2005 Five-Minute Multimethods in Python (Multi-métodos de cinco minutos em Python_), mostra os passos para uma implementação de funções genéricas (também chamadas multi-métodos) usando decoradores. O código de multi-métodos de Guido é interessante, mas é apenas um exemplo didático. Para conhecer uma implementação de funções genéricas de despacho múltiplo moderna e pronta para uso em produção, veja a Reg de Martijn Faassen–autor de Morepath, um framework Web guiado por modelos e orientado a REST.
Escopo dinâmico versus escopo léxico
O projetista de qualquer linguagem que contenha funções de primeira classe se depara com essa questão: sendo um objeto de primeira classe, uma função é definida dentro de um determinado escopo, mas pode ser invocada em outros escopos. O problema é: como avaliar as variáveis livres? A solução mais simples de implementar chama-se "escopo dinâmico". Isso significa que variáveis livres são avaliadas olhando para dentro do ambiente onde a função é invocada.
Se Python tivesse escopo dinâmico e não tivesse clausuras, poderíamos improvisar
avg (similar ao average.py: uma função de ordem superior para a calcular uma média cumulativa) desta forma:
>>> ### esta não é uma sessão real de Python! ###
>>> avg = make_averager()
>>> series = [] # (1)
>>> avg(10)
10.0
>>> avg(11) # (2)
10.5
>>> avg(12)
11.0
>>> series = [1] # (3)
>>> avg(5)
3.0-
Antes de usar
avg, precisamos definir por nós mesmosseries = [], então precisamos saber queaverager(dentro demake_averager) se refere a uma lista chamadaseries. -
Por trás da cortina,
seriesacumula os valores cuja média será calculada. -
Quando
series = [1]é executada, a lista anterior é perdida. Isso poderia ocorrer por acidente, ao computar duas médias cumulativas independentes ao mesmo tempo.
O ideal é que funções sejam opacas, sua implementação invisível para os usuários. Mas com escopo dinâmico, se a função usa variáveis livres, o programador precisa saber do funcionamento interno da função, para poder preparar um ambiente onde ela execute corretamente. Após anos lutando com a linguagem de preparação de documentos LaTeX, o excelente livro Practical LaTeX (LaTeX Prático), de George Grätzer (Springer), me ensinou que as variáveis no LaTeX usam escopo dinâmico. Por isso me confundiam tanto!
O Lisp do Emacs também usa escopo dinâmico, pelo menos como default. Veja Dynamic Binding (Vinculação Dinâmica) no manual do Emacs para uma breve explicação.
O escopo dinâmico é mais fácil de implementar, e essa foi provavelmente a razão de John McCarthy ter tomado esse caminho quando criou o Lisp, a primeira linguagem a ter funções de primeira classe. O texto de Paul Graham, The Roots of Lisp (As Raízes do Lisp) é uma explicação acessível do artigo original de John McCarthy sobre a linguagem Lisp, Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I (Funções Recursivas de Expressões Simbólicas e Sua Computação por Máquina). O artigo de McCarthy é uma obra prima no nível da Nona Sinfonia de Beethoven. Paul Graham o traduziu do jargão matemático para um inglês mais compreensível e código executável.
O comentário de Paul Graham explica como o escopo dinâmico é complexo. Citando The Roots of Lisp:
É um testemunho eloquente dos perigos do escopo dinâmico, que mesmo o primeiro exemplo de funções de ordem superior em Lisp estivesse errado por causa dele. Talvez, em 1960, McCarthy não estivesse inteiramente ciente das implicações do escopo dinâmico, que continuou presente nas implementações de Lisp por um tempo surpreendentemente longo—até Sussman e Steele desenvolverem o Scheme, em 1975. O escopo léxico não complica demais a definição de
eval, mas pode tornar mais difícil escrever compiladores.
Atualmente, o escopo léxico é o padrão: variáveis livres são avaliadas
considerando o ambiente onde a função foi definida. O escopo léxico complica a
implementação de linguagens com funções de primeira classe, pois requer o
suporte a clausuras. Por outro lado, o escopo léxico torna o código-fonte mais
fácil de ler. A maioria das linguagens inventadas desde o Algol tem escopo
léxico. Uma exceção notável é o JavaScript, onde a variável especial this é
confusa, pois pode ter escopo léxico ou dinâmico, dependendo
da forma como o código for escrito (EN).
Por muitos anos, o lambda de Python não implementava clausuras, contribuindo para
a má fama deste recurso entre os fãs da programação funcional na blogosfera.
Isso foi resolvido no Python 2.2 (de dezembro de 2001), mas a blogosfera nunca perdoa.
Desde então, lambda é triste apenas devido à sua sintaxe
limitada.
Os decoradores de Python e o padrão de projeto Decorator
Os decoradores de função de Python se encaixam na descrição geral dos decoradores de Gamma et al. em Padrões de Projeto: "Acrescenta responsabilidades adicionais a um objeto de forma dinâmica. Decoradores fornecem uma alternativa flexível à criação de subclasses para estender funcionalidade."
Ao nível da implementação, os decoradores de Python não lembram o padrão de projeto decorador clássico, mas é possível fazer uma analogia.
No padrão de projeto, Decorador e Componente são classes abstratas. Uma
instância de um decorador concreto envolve uma instância de um componente
concreto para adicionar comportamentos a ela. Citando Padrões de Projeto:
O decorador se adapta à interface do componente decorado, assim sua presença é transparente para os clientes do componente. O decorador encaminha requisições para o componente e pode executar ações adicionais (tal como desenhar uma borda) antes ou depois do encaminhamento. A transparência permite aninhar decoradores de forma recursiva, possibilitando assim um número ilimitado de responsabilidades adicionais. (p. 175 da edição em inglês)
No Python, a função decoradora faz o papel de uma subclasse concreta de
Decorador, e a função interna que ela devolve é uma instância do decorador. A
função devolvida envolve a função a ser decorada, que é análoga ao componente no
padrão de projeto. A função devolvida é transparente, pois se adapta à interface
do componente (ao aceitar os mesmos argumentos). Pegando emprestado da citação
anterior, podemos adaptar a última frase para dizer que "A transparência permite
empilhar decoradores, possibilitando assim um número ilimitado de comportamentos
adicionais".
Veja que não estou sugerindo que decoradores de função devam ser usados para implementar o padrão decorador em programas Python. Pode até ser possível em situações específicas, mas em geral o padrão decorador é melhor implementado com classes representando o decorador e os componentes que ela vai envolver.
numbers não foram descontinuadas, e você as encontra em código de Python 3.
@htmlize.register sem parâmetros, e uma dica de tipo usando Union. Mas quando tentei, Python gerou um TypeError com uma mensagem dizendo que Union não é uma classe. Então, apesar da sintaxe da PEP 484 ser suportada, a semântica ainda não chegou lá.
locals()." Sim, esse é mais um exemplo de como ferramentas estáticas de checagem desencorajam o uso dos recursos dinâmicos de Python que me atraíram (e a incontáveis outros programadores) quando adotei a linguagem. Para deixar o linter feliz, eu poderia escrever o nome de cada variável duas vezes na chamada: fmt.format(elapsed=elapsed, name=name, args=args, result=result). Prefiro não fazer isso. Se você usa ferramentas estáticas de checagem, é importante saber quando ignorá-las.
