¿Cómo puedo llamar a una secuencia de funciones hasta que el valor de retorno cumpla alguna condición?

A veces me encuentro escribiendo código así:

def analyse(somedata): result = bestapproach(somedata) if result: return result else: result = notasgood(somedata) if result: return result else: result = betterthannothing(somedata) if result: return result else: return None 

Eso es bastante feo. Por supuesto, a algunas personas les gusta dejar algo de esa syntax:

 def analyse(somedata): result = bestapproach(somedata) if result: return result result = notasgood(somedata) if result: return result result = betterthannothing(somedata) if result: return result 

Pero eso no es una gran mejora; Todavía hay un montón de códigos duplicados aquí, y todavía es feo.

Busqué usar el iter() incorporado con un valor de centinela, pero en este caso el valor de None se usa para indicar que el bucle debe continuar, a diferencia de un centinela que se usa para indicar que el bucle debe terminar .

¿Hay otras técnicas (sanas) en Python para implementar este tipo de patrón de “sigue intentando hasta que encuentres algo que funcione”?

Debería aclarar que “el valor devuelto cumple con alguna condición” no se limita a los casos en que la condición es if bool(result) is True como en el ejemplo. Puede ser que la lista de posibles funciones de análisis produzca algún coeficiente que mida el grado de éxito (por ejemplo, un valor de R cuadrado), y que desee establecer un umbral mínimo para la aceptación. Por lo tanto, una solución general no debe basarse inherentemente en el valor de verdad del resultado.

Si el número de funciones no es demasiado alto, ¿por qué no usar el operador or ?

 d = 'somedata' result = f1(d) or f2(d) or f3(d) or f4(d) 

Solo aplicará las funciones hasta que una de ellas devuelva algo que no sea False .

Opción # 1: Usar or

Cuando el número de funciones totales es a) conocido, yb) pequeño, y la condición de prueba se basa completamente en el valor de verdad de la devolución, es posible simplemente usarla or como lo sugirió Grapsus:

 d = 'somedata' result = f1(d) or f2(d) or f3(d) or f4(d) 

Debido a que los operadores booleanos de Python tienen un cortocircuito , las funciones se ejecutan de derecha a izquierda hasta que uno de ellos produce un valor de retorno evaluado como True , momento en el que se realiza la asignación y las funciones restantes no se evalúan; o hasta que te quedes sin funciones, y el result se asigna como False .


Opción # 2: usar generadores

Cuando el número de funciones totales es a) desconocido, o b) muy grande, un método de comprensión de generador de una sola línea funciona, como sugirió Bitwise:

 result = (r for r in (f(somedata) for f in functions) if ).next() 

Esto tiene la ventaja adicional sobre la opción # 1 de que puede usar cualquier que desee, en lugar de confiar solo en el valor de verdad. Cada vez que se llama .next() :

  1. Le pedimos al generador exterior su siguiente elemento.
  2. El generador externo le pide al generador interno su siguiente elemento.
  3. El generador interno solicita una f de functions e intenta evaluar f(somedata)
  4. Si la expresión se puede evaluar (es decir, f es una función y somedata son un argumento válido), el generador interno genera el valor de retorno de f(somedata) al generador externo
  5. Si se cumple , el generador externo produce el valor de retorno de f(somedata) y lo asignamos como result
  6. Si no se cumplió en el paso 5, repita los pasos 2-4

Una debilidad de este método es que las comprensiones anidadas pueden ser menos intuitivas que sus equivalentes multilínea. Además, si el generador interno se agota sin haber .next() nunca la condición de prueba, .next() genera una StopIteration que debe manejarse (en un bloque try-except) o prevenirse (asegurando que la última función siempre “tendrá éxito”).


Opción # 3: Usando una función personalizada

Ya que podemos colocar funciones que se pueden llamar en una lista, una opción es enumerar explícitamente las funciones que desea “probar” en el orden en que se deben usar, y luego iterar a través de esa lista:

 def analyse(somedata): analysis_functions = [best, okay, poor] for f in analysis_functions: result = f(somedata) if result: return result 

Ventajas: soluciona el problema del código repetido, es más claro que está involucrado en un proceso iterativo y se cortocircuita (no continúa ejecutando funciones después de encontrar un resultado “bueno”).

Esto también podría escribirse con la syntax de Python’s for ... else : *

 def analyse(somedata): analysis_functions = [best, okay, poor] for f in analysis_functions: result = f(somedata) if result: break else: return None return result 

La ventaja aquí es que se identifican las diferentes maneras de salir de la función, lo que podría ser útil si desea que la función de analyse() falle completamente para devolver algo que None sea None o para generar una excepción. De lo contrario, es más largo y más esotérico.

* Como se describe en “Transformación de código en Python hermoso e idiomático” , comenzando a las 15: 50.

Esto es bastante python:

 result = (i for i in (f(somedata) for f in funcs) if i is not None).next() 

La idea es usar generadores para hacer una evaluación perezosa en lugar de evaluar todas las funciones. Tenga en cuenta que puede cambiar la condition / funcs para que sea lo que desee, por lo que es más robusta que la solución or solución propuesta por Grapsus.

Este es un buen ejemplo de por qué los generadores son poderosos en Python.

Una descripción más detallada de cómo funciona esto:

Le pedimos a este generador un solo elemento. Luego, el generador externo le pide al generador interno (f(d) for f in funcs) un solo elemento y lo evalúa. Si pasa la condición, entonces hemos terminado y sale, de lo contrario continúa pidiendo elementos al generador interno.