Contexto
Supongamos que tengo el siguiente código de 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_functionaquí simplemente pasa por cada uno de los elementos de la nslista y los divide por la mitad 3 veces, mientras acumula los resultados. El resultado de ejecutar este script es simplemente:
2.0
Desde 1 / (2 ^ 3) * (1 + 3 + 12) = 2.
Ahora, digamos que (por cualquier motivo, tal vez depuración o registro), me gustaría mostrar algún tipo de información sobre los pasos intermedios que example_functionestá tomando. Quizás entonces reescribiría esta función en algo como esto:
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 ahora, cuando se llama con los mismos argumentos que antes, genera lo siguiente:
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
Esto logra exactamente lo que pretendía. Sin embargo, esto va un poco en contra del principio de que una función solo debe hacer una cosa, y ahora el código example_functiones un poco más largo y complejo. Para una función tan simple, esto no es un problema, pero en mi contexto tengo funciones bastante complicadas que se llaman entre sí, y las declaraciones de impresión a menudo implican pasos más complicados que los que se muestran aquí, lo que resulta en un aumento sustancial en la complejidad de mi código (para uno ¡De mis funciones había más líneas de código relacionadas con el registro que líneas relacionadas con su propósito real!).
Además, si luego decido que ya no quiero imprimir ninguna declaración en mi función, tendría que revisar example_functiony eliminar todas las printdeclaraciones manualmente, junto con cualquier variable relacionada con esta funcionalidad, un proceso que es tedioso y erróneo -propenso.
La situación empeora aún más si quisiera tener siempre la posibilidad de imprimir o no imprimir durante la ejecución de la función, lo que me lleva a declarar dos funciones extremadamente similares (una con las printdeclaraciones, una sin), que es terrible para mantener, o 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
lo que resulta en una función hinchada y (con suerte) innecesariamente complicada, incluso en el caso simple de nuestro example_function.
Pregunta
¿Existe una forma pitónica de "desacoplar" la funcionalidad de impresión de la funcionalidad original de la example_function?
En términos más generales, ¿hay una manera pitónica de desacoplar la funcionalidad opcional del propósito principal de una función?
Lo que he probado hasta ahora:
La solución que he encontrado en este momento es usar devoluciones de llamada para el desacoplamiento. Por ejemplo, uno puede reescribir lo example_functionsiguiente:
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
y luego definiendo una función de devolución de llamada que realice la funcionalidad de impresión que desee:
def print_callback(locals):
print(locals['number'])
y llamando example_functionasí:
ns = [1, 3, 12]
example_function(ns, 3, callback=print_callback)
que luego produce:
0.5
0.25
0.125
1.5
0.75
0.375
6.0
3.0
1.5
2.0
Esto desacopla con éxito la funcionalidad de impresión de la funcionalidad base de example_function. Sin embargo, el principal problema con este enfoque es que la función de devolución de llamada solo se puede ejecutar en una parte específica de example_function(en este caso, justo después de reducir a la mitad el número actual), y toda la impresión tiene que suceder exactamente allí. Esto a veces obliga al diseño de la función de devolución de llamada a ser bastante complicado (y hace que algunos comportamientos sean imposibles de lograr).
Por ejemplo, si a uno le gustaría lograr exactamente el mismo tipo de impresión que hice en una parte anterior de la pregunta (que muestra qué número se está procesando, junto con sus mitades correspondientes) la devolución de llamada resultante sería:
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 da como resultado exactamente el mismo resultado que 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
pero es un dolor de escribir, leer y depurar.
loggingmódulo ayudaría aquí. Aunque mi pregunta usa printdeclaraciones cuando configuro el contexto, en realidad estoy buscando una solución sobre cómo desacoplar cualquier tipo de funcionalidad opcional del propósito principal de una función. Por ejemplo, tal vez quiero una función para trazar las cosas mientras se ejecuta. En ese caso, creo que el loggingmódulo ni siquiera sería aplicable.
loggingdemuestran las sugerencias de uso ), pero no cómo separar el código arbitrario.
loggingmódulo de Python