¿Es posible construir programáticamente un marco de pila Python y comenzar la ejecución en un punto arbitrario en el código?
serialization stack (7)
¿Es posible construir programáticamente una pila (uno o más marcos de pila) en CPython y comenzar la ejecución en un punto de código arbitrario? Imagine el siguiente escenario:
Usted tiene un motor de flujo de trabajo donde los flujos de trabajo pueden ser generados por scripts en Python con algunas construcciones (por ejemplo, bifurcación, espera / unión) que son llamadas al motor de flujo de trabajo.
Una llamada de bloqueo, como esperar o unir establece una condición de oyente en un motor de envío de eventos con un almacén de respaldo persistente de algún tipo.
Tiene un script de flujo de trabajo, que llama a la condición de Espera en el motor, esperando alguna condición que se señalizará más tarde. Esto configura el oyente en el motor de envío de eventos.
El estado del script de flujo de trabajo, los marcos de pila relevantes, incluido el contador del programa (o estado equivalente), se conservan, ya que la condición de espera puede ocurrir días o meses después.
Mientras tanto, el motor de flujo de trabajo podría detenerse y reiniciarse, lo que significa que debe ser posible almacenar y reconstruir programáticamente el contexto del script de flujo de trabajo.
El motor de envío de eventos activa el evento de que la condición de espera se activa.
El motor de flujo de trabajo lee el estado serializado y apila y reconstruye un hilo con la pila. A continuación, continúa la ejecución en el punto donde se llamó al servicio de espera.
La pregunta
¿Se puede hacer esto con un intérprete de Python sin modificaciones? Mejor aún, ¿alguien puede indicarme algún tipo de documentación que pueda cubrir este tipo de cosas o un ejemplo de código que construye programáticamente un marco de pila y comienza la ejecución en algún lugar en medio de un bloque de código?
Editar: para aclarar el "intérprete de Python no modificado", no me importa usar la API de C (¿hay suficiente información en un PyThreadState para hacer esto?), Pero no quiero hurgar en las partes internas del intérprete de Python y tener para construir uno modificado.
Actualización: a partir de una investigación inicial, uno puede obtener el contexto de ejecución con PyThreadState_Get()
. Esto devuelve el estado del hilo en un PyThreadState
(definido en pystate.h
), que tiene una referencia al marco de la pila en el frame
. Un marco de pila se mantiene en una estructura typedef''d a PyFrameObject
, que se define en frameobject.h
. PyFrameObject
tiene un campo f_lasti
(props to bobince ) que tiene un contador de programa expresado como un desplazamiento desde el principio del bloque de código.
Esta última es una especie de buena noticia, porque significa que siempre que conserve el bloque de código compilado real, debería ser capaz de reconstruir los locales para tantos marcos de pila como sea necesario y reiniciar el código. Yo diría que esto significa que es teóricamente posible sin tener que hacer un intérprete de pitón modificado, aunque significa que el código probablemente todavía esté complicado y estrechamente vinculado a versiones específicas del intérprete.
Los tres problemas restantes son:
El estado de la transacción y la reversión ''saga'', que probablemente se pueden lograr mediante el tipo de pirateo de metaclases que se usaría para construir un asignador O / R. Construí un prototipo una vez, así que tengo una buena idea de cómo se puede lograr esto.
Seriado robusto de estado de transacciones y locals arbitrarios. Esto se puede lograr leyendo
__locals__
(que está disponible desde el marco de la pila) y construyendo programáticamente una llamada a pickle. Sin embargo, no sé qué podría haber aquí, si es que hay alguno.Control de versiones y actualización de flujos de trabajo. Esto es un poco más complicado, ya que el sistema no proporciona ningún anclaje simbólico para los nodos de flujo de trabajo. Todo lo que tenemos es el ancla Para hacer esto, uno debería identificar las compensaciones de todos los puntos de entrada y asignarlos a la nueva versión. Probablemente sea factible hacerlo manualmente, pero sospecho que sería difícil automatizarlo. Este es probablemente el mayor obstáculo si desea soportar esta capacidad.
Actualización 2: PyCodeObject
( code.h
) tiene una lista de addr ( f_lasti
) -> asignaciones de número de línea en PyCodeObject.co_lnotab
( PyCodeObject.co_lnotab
si está mal aquí). Esto podría usarse para facilitar un proceso de migración para actualizar los flujos de trabajo a una nueva versión, ya que los punteros de instrucciones congelados podrían asignarse al lugar apropiado en el nuevo guión, hecho en términos de los números de línea. Aún bastante desordenado, pero un poco más prometedor.
Actualización 3: Creo que la respuesta a esto podría ser Stackless Python. Puede suspender tareas y serializarlas. No he averiguado si esto también funcionará con la pila.
¿Qué hay de usar joblib ?
No estoy muy seguro de que esto sea lo que quieres, pero parece ajustarse a la idea de tener un flujo de trabajo cuyas etapas puedan persistir. El caso de uso de Joblib parece ser evitar el recálculo, no estoy seguro de si esto es lo que estás tratando de hacer aquí o algo más complicado.
Con CPython estándar, esto se complica por la mezcla de datos de C y Python en la pila. La reconstrucción de la pila de llamadas requeriría la reconstrucción de la pila C al mismo tiempo. Esto realmente lo coloca en la cesta demasiado difícil, ya que podría unir estrechamente la implementación a versiones específicas de CPython.
Stackless Python permite escanear escalas, lo que proporciona la mayor parte de la capacidad requerida de fábrica.
Lo que generalmente desea son las continuas, que veo ya son una etiqueta en esta pregunta.
Si tiene la capacidad de trabajar con todo el código en el sistema, puede intentar hacerlo de esta manera en lugar de tratar con la parte interna de la pila del intérprete. No estoy seguro de cuán fácilmente esto persistirá.
http://www.ps.uni-sb.de/~duchier/python/continuations.html
En la práctica, estructuraría su motor de flujo de trabajo para que su secuencia de comandos envíe objetos de acción a un administrador. El administrador podría resumir el conjunto de acciones en cualquier punto y permitir que se carguen y comenzar la ejecución nuevamente (al reanudar el envío de acciones).
En otras palabras: crea tu propio stack, nivel de aplicación.
Los enlaces expat python incluidos en la distribución normal de Python están construyendo cuadros de pila de forma programática. Sin embargo, ten en cuenta que se basa en API privadas e indocumentadas.
http://svn.python.org/view/python/trunk/Modules/pyexpat.c?rev=64048&view=auto
Podría agarrar el marco de pila existente lanzando una excepción y retrocediendo un cuadro a lo largo de la trazabilidad. El problema es que no hay forma de proporcionar reanudar la ejecución en el medio (frame.f_lasti) del bloque de código.
Las "excepciones reanudables" son una idea de lenguaje realmente interesante, aunque es difícil pensar en una forma razonable de que puedan interactuar con los bloques ''try / finally'' y ''with'' existentes de Python.
Por el momento, la forma normal de hacer esto es simplemente usar hilos para ejecutar su flujo de trabajo en un contexto separado de su controlador. (O coroutines / greenlets si no te importa compilarlos).
Stackless python es probablemente el mejor ... si no te importa pasar totalmente a una distribución de pitón diferente. stackless
puede serializar todo en python, más sus tasklets. Si quieres permanecer en la distribución estándar de Python, entonces usaré dill , que puede serializar casi cualquier cosa en Python.
>>> import dill
>>>
>>> def foo(a):
... def bar(x):
... return a*x
... return bar
...
>>> class baz(object):
... def __call__(self, a,x):
... return foo(a)(x)
...
>>> b = baz()
>>> b(3,2)
6
>>> c = baz.__call__
>>> c(b,3,2)
6
>>> g = dill.loads(dill.dumps(globals()))
>>> g
{''dill'': <module ''dill'' from ''/Library/Frameworks/Python.framework/Versions/7.2/lib/python2.7/site-packages/dill-0.2a.dev-py2.7.egg/dill/__init__.pyc''>, ''c'': <unbound method baz.__call__>, ''b'': <__main__.baz object at 0x4d61970>, ''g'': {...}, ''__builtins__'': <module ''__builtin__'' (built-in)>, ''baz'': <class ''__main__.baz''>, ''_version'': ''2'', ''__package__'': None, ''__name__'': ''__main__'', ''foo'': <function foo at 0x4d39d30>, ''__doc__'': None}
Dill registra sus tipos en el registro de pickle
, por lo que si tiene un código de caja negra que usa pickle
y no puede editarlo realmente, entonces la importación de eneldo puede hacer que funcione mágicamente sin utilizar el código de terceros.
Aquí está el dill
decapado de toda la sesión de intérprete ...
>>> # continuing from above
>>> dill.dump_session(''foobar.pkl'')
>>>
>>> ^D
dude@sakurai>$ python
Python 2.7.5 (default, Sep 30 2013, 20:15:49)
[GCC 4.2.1 (Apple Inc. build 5566)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import dill
>>> dill.load_session(''foobar.pkl'')
>>> c(b,3,2)
6
dill
también tiene algunas buenas herramientas para ayudarlo a comprender qué es lo que está causando que su decapado falle cuando falla su código.
¿También preguntó dónde se usa para guardar el estado del intérprete?
IPython puede usar dill
para guardar la sesión del intérprete en un archivo. https://nbtest.herokuapp.com/github/ipython/ipython/blob/master/examples/parallel/Using%20Dill.ipynb
klepto usa dill
para admitir el almacenamiento en memoria caché en memoria, en disco o en la base de datos que evita el recálculo. https://github.com/uqfoundation/klepto/blob/master/tests/test_cache_info.py
mystic usa dill
para guardar los puntos de control para grandes trabajos de optimización guardando el estado del optimizador mientras está en progreso. https://github.com/uqfoundation/mystic/blob/master/tests/test_solver_state.py
Hay otros dos paquetes que usan dill
para guardar el estado de objetos o sesiones.
Tengo el mismo tipo de problema para resolver. Me pregunto qué decidió hacer el poster original.
stackless afirma que puede recuperar tareas siempre que no haya una pila C asociada ''gravada'' (grabada es mi elección de fraseo).
Probablemente usaré eventlet y descubriré alguna forma de demarcar ''estado'', realmente no quiero escribir una máquina de estado explícita aunque ...