Contexto
Suponha que eu tenha o seguinte código Python:
def example_function(numbers, n_iters):
sum_all = 0
for number in numbers:
for _ in range(n_iters):
number = halve(number)
sum_all += number
return sum_all
ns = [1, 3, 12]
print(example_function(ns, 3))
example_functionaqui está simplesmente analisando cada um dos elementos da nslista e dividindo-os pela metade 3 vezes, acumulando os resultados. A saída da execução desse script é simplesmente:
2.0
Como 1 / (2 ^ 3) * (1 + 3 + 12) = 2.
Agora, digamos que (por qualquer motivo, talvez depuração ou log), eu gostaria de exibir algum tipo de informação sobre as etapas intermediárias que example_functionestão sendo executadas. Talvez eu reescrevesse essa função em algo assim:
def example_function(numbers, n_iters):
sum_all = 0
for number in numbers:
print('Processing number', number)
for i_iter in range(n_iters):
number = number/2
print(number)
sum_all += number
print('sum_all:', sum_all)
return sum_all
que agora, quando chamado com os mesmos argumentos de antes, gera o seguinte:
Processing number 1
0.5
0.25
0.125
sum_all: 0.125
Processing number 3
1.5
0.75
0.375
sum_all: 0.5
Processing number 12
6.0
3.0
1.5
sum_all: 2.0
Isso atinge exatamente o que eu pretendia. No entanto, isso contraria um pouco o princípio de que uma função deve fazer apenas uma coisa, e agora o código para example_functioné ligeiramente mais longo e mais complexo. Para uma função tão simples, isso não é um problema, mas, no meu contexto, tenho funções bastante complicadas que se chamam, e as instruções de impressão geralmente envolvem etapas mais complicadas do que as mostradas aqui, resultando em um aumento substancial na complexidade do meu código (por exemplo, das minhas funções, havia mais linhas de código relacionadas ao log do que linhas relacionadas à sua real finalidade!).
Além disso, se eu decidir mais tarde que não quero mais nenhuma declaração de impressão em minha função, teria que passar example_functione excluir todas asprint instruções manualmente, juntamente com quaisquer variáveis relacionadas a essa funcionalidade, um processo que é tedioso e com erros. -propenso.
A situação fica ainda pior se eu gostaria de sempre ter a possibilidade de imprimir ou não imprimir durante a execução da função, levando-me a declarar duas funções extremamente semelhantes (uma com as printinstruções e outra sem), o que é terrível para manter ou para definir algo como:
def example_function(numbers, n_iters, debug_mode=False):
sum_all = 0
for number in numbers:
if debug_mode:
print('Processing number', number)
for i_iter in range(n_iters):
number = number/2
if debug_mode:
print(number)
sum_all += number
if debug_mode:
print('sum_all:', sum_all)
return sum_all
o que resulta em uma função inchada e (espero) desnecessariamente complicada, mesmo no caso simples de nossa example_function.
Questão
Existe uma maneira pitônica de "dissociar" a funcionalidade de impressão da funcionalidade original do example_function?
De maneira mais geral, existe uma maneira pitônica de dissociar a funcionalidade opcional do objetivo principal de uma função?
O que eu tentei até agora:
A solução que encontrei no momento está usando retornos de chamada para a dissociação. Por exemplo, pode-se reescrever o example_functionseguinte:
def example_function(numbers, n_iters, callback=None):
sum_all = 0
for number in numbers:
for i_iter in range(n_iters):
number = number/2
if callback is not None:
callback(locals())
sum_all += number
return sum_all
e, em seguida, definindo uma função de retorno de chamada que execute a funcionalidade de impressão que eu desejar:
def print_callback(locals):
print(locals['number'])
e chamando example_functionassim:
ns = [1, 3, 12]
example_function(ns, 3, callback=print_callback)
que então gera:
0.5
0.25
0.125
1.5
0.75
0.375
6.0
3.0
1.5
2.0
Isso dissocia com êxito a funcionalidade de impressão da funcionalidade básica de example_function. No entanto, o principal problema dessa abordagem é que a função de retorno de chamada só pode ser executada em uma parte específica do example_function(neste caso, logo após reduzir pela metade o número atual), e toda a impressão deve ocorrer exatamente lá. Às vezes, isso força o design da função de retorno de chamada a ser bastante complicado (e impossibilita a realização de alguns comportamentos).
Por exemplo, se alguém gostaria de obter exatamente o mesmo tipo de impressão que eu fiz na parte anterior da pergunta (mostrando qual número está sendo processado, juntamente com as respectivas metades correspondentes), o retorno de chamada resultante seria:
def complicated_callback(locals):
i_iter = locals['i_iter']
number = locals['number']
if i_iter == 0:
print('Processing number', number*2)
print(number)
if i_iter == locals['n_iters']-1:
print('sum_all:', locals['sum_all']+number)
que resulta exatamente na mesma saída de antes:
Processing number 1.0
0.5
0.25
0.125
sum_all: 0.125
Processing number 3.0
1.5
0.75
0.375
sum_all: 0.5
Processing number 12.0
6.0
3.0
1.5
sum_all: 2.0
mas é difícil escrever, ler e depurar.
loggingmódulo ajudaria aqui. Embora minha pergunta use printinstruções ao configurar o contexto, na verdade, estou procurando uma solução para dissociar qualquer tipo de funcionalidade opcional do objetivo principal de uma função. Por exemplo, talvez eu queira uma função para plotar as coisas à medida que é executada. Nesse caso, acredito que o loggingmódulo nem seria aplicável.
loggingdemonstram as sugestões de uso ), mas não como separar o código arbitrário.
loggingmódulo