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_function
aquí simplemente pasa por cada uno de los elementos de la ns
lista 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_function
está 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_function
es 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_function
y eliminar todas las print
declaraciones 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 print
declaraciones, 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_function
siguiente:
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_function
así:
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.
logging
módulo de Pythonlogging
módulo ayudaría aquí. Aunque mi pregunta usaprint
declaraciones 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 ellogging
módulo ni siquiera sería aplicable.logging
demuestran las sugerencias de uso ), pero no cómo separar el código arbitrario.Respuestas:
Si necesita funcionalidad fuera de la función para usar datos desde dentro de la función, entonces debe haber algún sistema de mensajería dentro de la función para admitir esto. No hay forma de evitar esto. Las variables locales en funciones están totalmente aisladas del exterior.
El módulo de registro es bastante bueno para configurar un sistema de mensajes. No solo se limita a imprimir los mensajes de registro: con los controladores personalizados, puede hacer cualquier cosa.
Agregar un sistema de mensajes es similar a su ejemplo de devolución de llamada, excepto que los lugares donde se manejan las 'devoluciones de llamada' (controladores de registro) se pueden especificar en cualquier lugar dentro del
example_function
(enviando los mensajes al registrador). Las variables que necesitan los manejadores de registro se pueden especificar cuando envía el mensaje (aún puede usarlocals()
, pero es mejor declarar explícitamente las variables que necesita).Un nuevo
example_function
podría verse así:Esto especifica tres ubicaciones donde se pueden manejar los mensajes. Por sí solo, esto
example_function
no hará nada más que la funcionalidad deexample_function
sí mismo. No imprimirá nada ni realizará ninguna otra funcionalidad.Para agregar funcionalidad adicional al
example_function
, deberá agregar controladores al registrador.Por ejemplo, si desea imprimir algunas de las variables enviadas (similar a su
debugging
ejemplo), defina el controlador personalizado y agréguelo alexample_function
registrador:Si desea trazar los resultados en un gráfico, simplemente defina otro controlador:
Puede definir y agregar los controladores que desee. Estarán totalmente separados de la funcionalidad de
example_function
, y solo pueden usar las variables queexample_function
les da.Aunque el registro se puede usar como un sistema de mensajería, podría ser mejor pasar a un sistema de mensajería completo, como PyPubSub , para que no interfiera con ningún registro real que pueda estar haciendo:
fuente
logging
módulo es de hecho más organizado y mantenible que lo que propuse usarprint
yif
declaraciones. Sin embargo, no desacopla la funcionalidad de impresión de la funcionalidad principal de laexample_function
función. Es decir, el problema principal de tener queexample_function
hacer dos cosas a la vez sigue siendo, lo que hace que su código sea más complicado de lo que me gustaría que fuera.example_function
ahora solo tiene una funcionalidad, y las cosas de impresión (o cualquier otra funcionalidad que nos gustaría tener) sucede fuera de ella.example_function
está desacoplado de la funcionalidad de impresión: la única funcionalidad agregada a la función es enviar los mensajes. Es similar a su ejemplo de devolución de llamada, excepto que solo envía variables específicas que desea, en lugar de todaslocals()
. Depende de los manejadores de registros (que adjuntas al registrador en otro lugar) hacer la funcionalidad adicional (impresión, gráficos, etc.). No necesita adjuntar ningún controlador, en cuyo caso no pasará nada cuando se envíen los mensajes. He actualizado mi publicación para aclarar esto.example_function
. ¡Gracias por dejarlo más claro ahora! Realmente me gusta esta respuesta, el único precio que se paga es la complejidad adicional de pasar mensajes, lo que, como mencionó, parece inevitable. Gracias también por la referencia a PyPubSub, que me llevó a leer sobre el patrón de observación .Si desea seguir con solo las declaraciones de impresión, puede usar un decorador que agrega un argumento que enciende / apaga la impresión en la consola.
Aquí hay un decorador que agrega el argumento de solo palabras clave y el valor predeterminado de
verbose=False
cualquier función, actualiza la cadena de documentos y la firma. Llamar a la función tal cual devuelve el resultado esperado. Llamar a la función converbose=True
activará las declaraciones de impresión y devolverá el resultado esperado. Esto tiene el beneficio adicional de no tener que prefacio cada impresión con unif debug:
bloque.Ajustar su función ahora le permite activar / desactivar las funciones de impresión usando
verbose
.Ejemplos:
Cuando inspeccione
example_function
, verá también la documentación actualizada. Como su función no tiene una cadena de documentación, es justo lo que está en el decorador.En cuanto a la filosofía de codificación. Tener una función que no incurre en efectos secundarios es un paradigma de programación funcional. Python puede ser un lenguaje funcional, pero no está diseñado para ser exclusivamente de esa manera. Siempre diseño mi código con el usuario en mente.
Si agregar la opción de imprimir los pasos de cálculo es un beneficio para el usuario, entonces NO HAY nada malo en hacerlo. Desde el punto de vista del diseño, se quedará atascado agregando los comandos de impresión / registro en alguna parte.
fuente
print
yif
declaraciones. Además, se las arregla para desacoplar parte de la funcionalidad de impresión deexample_function
la funcionalidad principal, lo cual fue muy agradable (también me gustó que el decorador se agregue automáticamente a la cadena de documentos, un buen toque). Sin embargo, no desacopla completamente la funcionalidad de impresión de la funcionalidad principal deexample_function
: todavía tiene que agregar lasprint
declaraciones y la lógica que lo acompaña al cuerpo de la función.example_function
cuerpo del cuerpo, de modo que su complejidad solo esté asociada a la complejidad de su funcionalidad principal. En mi aplicación de la vida real de todo esto, tengo una función principal que ya es significativamente compleja. Agregar declaraciones de impresión / trazado / registro a su cuerpo hace que se convierta en una bestia que ha sido bastante difícil de mantener y depurar.Puede definir una función que encapsule la
debug_mode
condición y pasar la función opcional deseada y sus argumentos a esa función (como se sugiere aquí ):Tenga en cuenta que
debug_mode
obviamente se le debe haber asignado un valor antes de llamarDEBUG
.Por supuesto, es posible invocar funciones que no sean
print
.También podría ampliar este concepto a varios niveles de depuración utilizando un valor numérico para
debug_mode
.fuente
if
declaraciones en todas partes y también facilita la activación y desactivación de la impresión. Sin embargo, no desacopla la funcionalidad de impresión de la funcionalidad principal deexample_function
. Compare esto con, por ejemplo, mi sugerencia de devolución de llamada. Usando devoluciones de llamada, example_function ahora solo tiene una funcionalidad, y las cosas de impresión (o cualquier otra funcionalidad que nos gustaría tener) sucede fuera de ella.He actualizado mi respuesta con una simplificación: la función
example_function
pasa una única devolución de llamada o enlace con un valor predeterminado de modo queexample_function
ya no necesita probar para ver si se pasó o no:Lo anterior es una expresión lambda que devuelve
None
yexample_function
puede llamar a este valor predeterminadohook
con cualquier combinación de parámetros posicionales y de palabras clave en varios lugares dentro de la función.En el siguiente ejemplo, sólo estoy interesado en las
"end_iteration"
y los"result
"eventos.Huellas dactilares:
La función de enlace puede ser tan simple o tan elaborada como desee. Aquí está haciendo una verificación del tipo de evento y haciendo una impresión simple. Pero podría obtener una
logger
instancia y registrar el mensaje. Puede tener toda la riqueza del registro si lo necesita, pero simplicidad si no lo necesita.fuente
example_function
.if
declaraciones :)