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_function
aqui está simplesmente analisando cada um dos elementos da ns
lista 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_function
estã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_function
e 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 print
instruçõ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_function
seguinte:
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_function
assim:
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.
logging
módulologging
módulo ajudaria aqui. Embora minha pergunta useprint
instruçõ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 ologging
módulo nem seria aplicável.logging
demonstram as sugestões de uso ), mas não como separar o código arbitrário.Respostas:
Se você precisar de uma funcionalidade externa à função para usar dados de dentro da função, será necessário haver algum sistema de mensagens dentro da função para suportar isso. Não há como contornar isso. Variáveis locais em funções são totalmente isoladas do exterior.
O módulo de registro é muito bom em configurar um sistema de mensagens. Não se restringe apenas à impressão das mensagens de log - usando manipuladores personalizados, você pode fazer qualquer coisa.
A adição de um sistema de mensagens é semelhante ao seu exemplo de retorno de chamada, exceto que os locais onde os 'retornos de chamada' (manipuladores de log) são manipulados podem ser especificados em qualquer lugar dentro do
example_function
(enviando as mensagens para o logger). Quaisquer variáveis necessárias aos manipuladores de log podem ser especificadas quando você envia a mensagem (você ainda pode usarlocals()
lo, mas é melhor declarar explicitamente as variáveis necessárias).Um novo
example_function
pode se parecer com:Isso especifica três locais onde as mensagens podem ser tratadas. Por si só, isso
example_function
não fará outra coisa senão a funcionalidade doexample_function
próprio. Não imprimirá nada ou fará qualquer outra funcionalidade.Para adicionar funcionalidade extra ao
example_function
, você precisará adicionar manipuladores ao criador de logs.Por exemplo, se você quiser imprimir algumas variáveis enviadas (semelhante ao seu
debugging
exemplo), defina o manipulador personalizado e adicione-o aoexample_function
criador de logs:Se você deseja plotar os resultados em um gráfico, basta definir outro manipulador:
Você pode definir e adicionar os manipuladores que desejar. Eles serão totalmente separados da funcionalidade do
example_function
, e só podem usar as variáveis que osexample_function
fornece.Embora o registro possa ser usado como um sistema de mensagens, talvez seja melhor mudar para um sistema de mensagens completo, como o PyPubSub , para que não interfira em nenhum registro real que você possa estar fazendo:
fonte
logging
módulo é realmente mais organizado e sustentável do que o que eu propus usarprint
eif
instruções. No entanto, não separa a funcionalidade de impressão da principal funcionalidade daexample_function
função. Ou seja, o principal problema deexample_function
fazer duas coisas ao mesmo tempo ainda permanece, tornando o código mais complicado do que eu gostaria que fosse.example_function
agora só há uma funcionalidade, e o material de impressão (ou qualquer outra funcionalidade que gostaríamos de ter) acontece fora dela.example_function
está dissociado da funcionalidade de impressão - a única funcionalidade adicionada à função é enviar as mensagens. É semelhante ao seu exemplo de retorno de chamada, exceto que ele envia apenas variáveis específicas desejadas, e não todaslocals()
. Cabe aos manipuladores de log (que você anexa ao logger em outro lugar) executar a funcionalidade extra (impressão, gráficos, etc.). Você não precisa anexar nenhum manipulador; nesse caso, nada acontecerá quando as mensagens forem enviadas. Atualizei minha postagem para deixar isso mais claro.example_function
. Obrigado por torná-lo mais claro agora! Gosto muito dessa resposta, o único preço pago é a complexidade adicional da transmissão de mensagens, que, como você mencionou, parece inevitável. Obrigado também pela referência ao PyPubSub, que me levou a ler sobre o padrão do observador .Se você deseja manter apenas as instruções de impressão, pode usar um decorador que adicione um argumento que ativa / desativa a impressão no console.
Aqui está um decorador que adiciona o argumento somente palavra-chave e o valor padrão de
verbose=False
qualquer função, atualiza a sequência de caracteres e a assinatura. Chamar a função como está retorna a saída esperada. Chamar a função comverbose=True
ativará as instruções de impressão e retornará a saída esperada. Isso tem o benefício adicional de não precisar preceder todas as impressões com umif debug:
bloco.A quebra da sua função agora permite ativar / desativar as funções de impressão usando
verbose
.Exemplos:
Quando você inspeciona
example_function
, também verá a documentação atualizada. Como sua função não possui uma sequência de caracteres, é exatamente o que está no decorador.Em termos de filosofia de codificação. Ter uma função que não produza efeitos colaterais é um paradigma de programação funcional. Python pode ser uma linguagem funcional, mas não foi projetada para ser exclusivamente dessa maneira. Eu sempre desenvolvo meu código com o usuário em mente.
Se adicionar a opção de imprimir as etapas de cálculo for um benefício para o usuário, não há NADA errado nisso. Do ponto de vista do design, você ficará preso ao adicionar os comandos de impressão / registro em algum lugar.
fonte
print
eif
afirmações. Além disso, ele consegue desacoplar parte da funcionalidade da impressão daexample_function
funcionalidade principal, o que foi muito bom (eu também gostei que o decorador anexasse automaticamente à docstring, toque agradável). No entanto, ele não dissocia totalmente a funcionalidade de impressão da funcionalidade principal deexample_function
: você ainda precisa adicionar asprint
instruções e qualquer lógica que acompanha o corpo da função.example_function
corpo do, para que sua complexidade permaneça associada apenas à complexidade de sua funcionalidade principal. Na minha aplicação na vida real de tudo isso, tenho uma função principal que já é significativamente complexa. A adição de instruções de impressão / plotagem / registro em seu corpo faz com que ele se torne um animal que tem sido bastante desafiador para manter e depurar.Você pode definir uma função que encapsule a
debug_mode
condição e passar a função opcional desejada e seus argumentos para essa função (conforme sugerido aqui ):Observe que
debug_mode
obviamente deve ter sido atribuído um valor antes de chamarDEBUG
.É claro que é possível invocar funções diferentes de
print
.Você também pode estender esse conceito para vários níveis de depuração usando um valor numérico para
debug_mode
.fonte
if
declarações em todo o lugar e também facilita a ativação e desativação da impressão. No entanto, não separa a funcionalidade de impressão da funcionalidade principal deexample_function
. Compare isso com, por exemplo, minha sugestão de retorno de chamada. Usando retornos de chamada, example_function agora possui apenas uma funcionalidade, e o material de impressão (ou qualquer outra funcionalidade que gostaríamos de ter) acontece fora dele.Atualizei minha resposta com uma simplificação: a função
example_function
recebe um único retorno de chamada ou gancho com um valor padrão queexample_function
não precisa mais testar para verificar se foi aprovada ou não:A descrição acima é uma expressão lambda que retorna
None
eexample_function
pode chamar esse valor padrão parahook
qualquer combinação de parâmetros posicionais e de palavras-chave em vários locais da função.No exemplo abaixo, estou interessado apenas nos eventos "
"end_iteration"
e"result
".Impressões:
A função de gancho pode ser tão simples ou elaborada quanto você desejar. Aqui, é feita uma verificação do tipo de evento e uma impressão simples. Mas poderia obter uma
logger
instância e registrar a mensagem. Você pode ter toda a riqueza do registro, se precisar, mas a simplicidade, se não precisar.fonte
example_function
.if
declarações :)