python workflow

Patrón de diseño de flujo de trabajo de Python



bpmn python (1)

Estoy trabajando en un diseño de software, y estoy atrapado entre no tener idea de lo que estoy haciendo y sentir que estoy reinventando la rueda.

Mi situación es la siguiente: estoy diseñando una utilidad científica con una interfaz de usuario interactiva. La entrada del usuario debe desencadenar retroalimentación visual (duh), parte de ella directamente, es decir, editar una geometría de dominio, y parte de ella tan pronto como sea posible, sin bloquear la interacción del usuario, por ejemplo, resolviendo algunas PDE sobre dicho dominio.

Si dibujo un diagrama de todas las operaciones que necesito realizar, obtendré este gráfico bastante denso, exponiendo todo tipo de oportunidades para el paralelismo y almacenando / reutilizando resultados parciales. Entonces, lo que quiero es principalmente explotar este paralelismo de una manera transparente (subtareas seleccionadas que se ejecutan en procesos separados, los resultados se "unen" de manera externa por las tareas posteriores que esperan que todas sus entradas estén listas), y solo es necesario recompensar esas ramas de entrada que en realidad han cambiado su entrada

pyutilib.workflow parece estar más cerca de ser lo que estoy buscando, excepto por supuesto que no lo es (para empezar, no parece hacer ningún subproceso). Eso parece bastante decepcionante; Aunque no soy un ingeniero de software, digo que no estoy pidiendo nada loco aquí.

Otro factor de complicación es la estrecha integración de interfaz de usuario que deseo, que otras soluciones de flujo de trabajo científico parecen no estar diseñadas para manejar. Por ejemplo, me gustaría pasar un evento de arrastrar y soltar a través de un nodo de transformación para su posterior procesamiento. El nodo de transformación tiene dos entradas; un puerto afín de entrada de estado de transformación y una clase de conjunto de puntos que sabe qué hacer con él. Si el puerto de entrada de transformación afín está "sucio" (a la espera de que se actualicen sus dependencias), el evento debe mantenerse hasta que esté disponible. Pero cuando el evento ha pasado el nodo, el puerto de entrada de evento debe marcarse como manejado, de modo que no se produzca un cambio cuando la transformación afín cambie debido a la entrada adicional del usuario. Ese es solo un ejemplo de uno de los muchos problemas que surgen que no veo en ningún lado. O qué hacer cuando una rama de unión de forking de larga duración recibe una nueva entrada mientras se encuentra en medio de una entrada previa.

Entonces, mi pregunta: ¿Conoce algunos buenos libros / artículos sobre patrones de diseño de flujos de trabajo que debería leer? ¿O estoy tratando de colocar una clavija cuadrada en un agujero redondo, y usted sabe de un patrón de diseño completamente diferente que debería conocer? ¿O un paquete de Python que hace lo que quiero que haga, independientemente de las palabras de moda en las que venga vestido?

He logrado una solución propia además de las actividades de entusiasmo, pero tampoco estoy del todo contento con eso, ya que se siente como una reinvención ruda y de mala calidad de la rueda. Excepto que parece que no puedo encontrar ninguna rueda en cualquier lugar en Internet.

NOTA: No estoy buscando webframeworks, diseñadores de flujos de trabajo gráficos ni herramientas especiales. Solo algo conceptual como pyutilib.workflow, pero que incluye documentación y un conjunto de características con las que puedo trabajar.

# # # EDITAR: aquí es donde estoy después de leer más y reflexionar sobre el tema: # # # #

Los requisitos que uno puede incluir en una ''arquitectura de flujo de trabajo'' son demasiado diversos para que haya un solo zapato que se ajuste a todos. ¿Desea una integración estrecha con el almacenamiento en disco, una integración estrecha con marcos web, asincronicidad, combinación en lógica de máquina de estados finitos personalizada para el envío de tareas? Todos ellos son requisitos válidos, y son en gran parte incompatibles, o hacen mezclas sin sentido.

Sin embargo, no todo está perdido. Buscar un sistema de flujo de trabajo genérico para resolver un problema arbitrario es como buscar un iterador genérico para resolver su problema de iteración personalizado. Los iteradores no son principalmente acerca de la reutilización; no puede reutilizar su iterador rojo-negro-árbol para iterar sobre su tensor. Su fuerza radica en una separación limpia de preocupaciones y en la definición de una interfaz uniforme.

Lo que estoy buscando (y he empezado a escribir por mí mismo; va a estar muy bien) se verá así: en su base se encuentra un mini-lenguaje de declaración de flujo de trabajo general, basado en decoradores y algo de metamagia. para transformar una declaración como la que se muestra a continuación en una declaración de flujo de trabajo que contenga toda la información requerida:

@composite_task(inputs(x=Int), outputs(z=Float)) class mycompositetask: @task(inputs(x=Int), outputs(y=Float)) def mytask1(x): return outputs( y = x*2 ) @task(inputs(x=Int, y=Float), outputs(z=Float)) def mytask2(x, y): return outputs( z = x+y ) mytask1.y = mytask2.y #redundant, but for illustration; inputs/outputs matching in name and metadata autoconnect

Lo que devuelven los decoradores es una clase de declaración task / compositetask / workflow. En lugar de solo restricciones de tipo, otros metadatos necesarios para el tipo de flujo de trabajo disponible se agregan fácilmente a la sintaxis.

Ahora, esta declaración concisa y pythonic puede incorporarse a una fábrica de instancias de flujo de trabajo que devuelve la instancia de flujo de trabajo real. Este lenguaje de declaración es bastante general y probablemente no deba cambiar mucho entre los diferentes requisitos de diseño, pero tal fábrica de creación de instancias de flujo de trabajo depende completamente de sus requisitos de diseño / imaginación, aparte de una interfaz común para la entrega / recuperación de entrada / salida.

En su encarnación más simple, tenemos algo como:

wf = workflow_factory(mycompositetask) wf.z = lambda result: print result #register callback on z-output socket wf.x = 1 #feed data into x input-socket

donde wf es una instancia de flujo de trabajo trivial, que no hace más que encadenar todos los cuerpos de funciones contenidos en el mismo hilo, una vez que todas las entradas están vinculadas. Una forma bastante detallada de encadenar dos funciones, pero ilustra la idea, y ya logra el objetivo de separar la preocupación de mantener la definición del flujo de información en un lugar central en lugar de difundir en todas las clases que preferirían no tener nada que ver. hazlo

Esa es más o menos la funcionalidad que he implementado hasta ahora, pero significa que puedo seguir trabajando en mi proyecto y, a su debido tiempo, agregaré soporte para fábricas de instancias de flujo de trabajo más sofisticadas. Por ejemplo, estoy pensando en analizar el gráfico de las dependencias para identificar las bifurcaciones y las uniones, y hacer un seguimiento de la actividad generada por cada entrada suministrada en el nivel de instancia de flujo de trabajo, para un equilibrio elegante de la carga y la cancelación de los efectos de las entradas específicas que han perdido. Su relevancia, pero todavía están acaparando recursos.

De cualquier manera, creo que el proyecto de separar la declaración de flujo de trabajo, la definición de la interfaz y la implementación de la creación de instancias es un esfuerzo que vale la pena. Una vez que tengo algunos tipos de instancias de flujo de trabajo no triviales que funcionan bien (necesito al menos dos para el proyecto en el que estoy trabajando, me di cuenta *), espero encontrar el tiempo para publicar esto como un proyecto público, porque a pesar de La diversidad de requisitos de diseño en los sistemas de flujo de trabajo, al tener esta base cubierta hace que la implementación de sus propios requisitos específicos sea mucho más sencilla. Y en lugar de un único marco de flujo de trabajo hinchado, una navaja suiza de soluciones personalizadas fácilmente intercambiables podría crecer alrededor de ese núcleo.

* al darme cuenta de que necesito dividir mi código en dos tipos diferentes de instancias de flujo de trabajo en lugar de intentar combinar todos mis requisitos de diseño en una sola solución, convertí la clavija cuadrada y el orificio redondo que tenía en mi mente en dos orificios y clavijas perfectamente complementarios.


Creo que ambos, correcto e incorrecto, dudan en volver a inventar la rueda. Tal vez diferentes niveles de pensamiento te dan una pista aquí.

¿Cómo comer un elefante?

Nivel A: diseño de software

En ese nivel, desearía atenerse a la mejor práctica de que no se realicen operaciones prolongadas en la interfaz de usuario (y el subproceso de la interfaz de usuario). Necesita una capa de UI que se centre solo en recopilar información (incluida la cancelación) y dibujar (incluida la visualización en progreso, como la barra de progreso o el reloj de arena). Esta capa debe estar separada de cualquier otra cosa como el anochecer y el amanecer. Cualquier llamada fuera de esta capa debe ser rápida si desea intuición y capacidad de respuesta.

En tareas tan complejas como las suyas, las llamadas fuera de la capa de UI suelen ser:

  1. Programe un poco de trabajo: el comando se debe poner en cola en la capa inteligente para que se active cada vez que lo reciba.
  2. Lea los resultados: los resultados deben ponerse en cola en la capa inteligente para que se puedan "extraer" y representar.
  3. Cancelar / detener / salir - simplemente levante una bandera. Capa inteligente debe marcar esta bandera de vez en cuando.

No se preocupe demasiado por el hecho de que algunas operaciones de los usuarios reaccionan demasiado lentamente: si tiene un núcleo de diseño sólido, puede ajustar las prioridades de la entrada del usuario más adelante. O agregar un reloj de arena a corto plazo o similar. O incluso cancelar todas las operaciones largas que se vuelven obsoletas después de una entrada específica del usuario.

Nivel B: la capa inteligente de trabajo pesado

No existe un "mejor" marco para "cualquier" tipo de trabajo duro .

Por lo tanto, le sugiero que diseñe la alimentación (por UI) de esta capa de la manera más sencilla posible, sin marcos involucrados.

Internamente, puede implementarlo utilizando algunos marcos, pero en el futuro tendrá la capacidad de rediseñar los elementos de trabajo duro según sea necesario. Por ejemplo, en el futuro usted podría:

  • dar algunas matemáticas a GPU
  • compartir tareas con las granjas de servidores
  • involucrar a la computación en la nube

Para tareas complejas, elegir un marco en el nivel superior de diseño podría ser un obstáculo en el futuro. Específicamente, puede limitar su libertad de aplicar otras tecnologías.

Es difícil decirlo con seguridad, pero me parece que no tiene un marco de bala de plata para su tarea. Por lo tanto, debe encontrar herramientas sólidas (por ejemplo, hilos y colas) para implementar buenas prácticas de diseño (por ejemplo, desacoplamiento).

EDITAR como respuesta a su edición

Su última edición resalta a la perfección los difíciles desafíos que enfrenta un diseñador de software. Para su caso, la aceptación de que no hay bala de plata . Te sugiero que lo aceptes antes, mejor que después ...

La sugerencia reside en que ofreció la tarea más genérica que definirían Int''s y Float''s. Esto podría hacerte feliz por hoy, pero fracasará mañana. Exactamente como encerrar en un marco super-abstracto.

El camino es correcto: tener una base de "tarea" pesada en su diseño. Pero no debe definir Int o Flotante. Concéntrese en el mencionado "inicio", "leer" y "detener" en su lugar. Si no ves el tamaño del elefante que estás comiendo, puedes dejar de comerlo y terminar muriendo de hambre :)

Desde el nivel A: perspectiva de diseño, puede definir una tarea para contener algo como esto:

class AnySuperPowerfulTask: def run(): scheduleForAThreadToRunMe() def cancel(): doTheSmartCancellationSoNobodyWouldCrash()

Esto le da la base: neutral, pero limpio y desacoplado según la perspectiva del Nivel A (diseño).

Sin embargo, necesitarías algún tipo de configuración de la tarea y obtener el resultado real, ¿verdad? Claro, eso caería en el nivel B de pensamiento. Sería específico para una tarea (o para un grupo de tareas implementadas como una base intermedia). La tarea final podría ser algo en este sentido:

class CalculatePossibilitiesToSaveAllThePandas(SuperPowerfulTask): def __init__(someInt, someFloat, anotherURL, configPath): anythingSpecificToThisKindOfTasks() def getResults(): return partiallyCalculated4DimensionalEvolutionGraphOfPandasInOptimisticEnvoronment()

(Las muestras son incorrectamente intencionadas por Python para enfocarse en el diseño , no en la sintaxis).

Nivel C - abstracción-nirvana

Parece que este nivel debería ser mencionado en esta publicación.

Sí, hay tal escollo que muchos buenos diseñadores pueden confirmar. El estado en el que podría (y sin ningún resultado) buscar una "solución genérica" ​​(es decir, la bala de plata ). Te sugiero que eches un vistazo a esto y luego salgas rápido antes de que sea demasiado tarde;) caer en este escollo no es una pena, es una etapa normal de desarrollo de los mejores diseñadores. Al menos estoy tratando de creerlo :)

Editar 2

Usted dijo: "Estoy trabajando en una pieza de diseño de software, y estoy atascado entre no tener idea de lo que estoy haciendo y sentir que estoy reinventando la rueda".

Cualquier diseñador de software puede atascarse. Tal vez el siguiente nivel de pensamiento pueda ayudarte. Aquí viene:

Nivel D - estoy atascado

Sugerencia. Dejar el edificio Entra en la cafetería que está al lado, pide el mejor café y siéntate. Hágase la pregunta "¿Qué necesito?". Tenga en cuenta que es diferente de la pregunta "¿Qué quiero?". Haz ping hasta que hayas eliminado las respuestas incorrectas y comienza a observar las correctas :

Respuestas erróneas :

  1. Necesito un framework que haga X, Y y Z.
  2. Necesito un destornillador que pueda funcionar a 200 mph y cosechar bosque en una granja cercana.
  3. Necesito una estructura interna increíble que mi usuario nunca verá.

Respuestas correctas (perdóname si entendí mal tu problema):

  1. Necesito usuario para poder dar entrada al software.
  2. Necesito que el usuario vea que los cálculos están en progreso.
  3. Necesito que el usuario vea visualmente el resultado de los cálculos.