¿El aplazamiento de Twisted es lo mismo que una promesa en JavaScript?

Comencé a usar Twisted en un proyecto que requiere progtwigción asíncrona y los documentos son bastante buenos.

Entonces, mi pregunta es: ¿Un aplazamiento en retorcido es lo mismo que una promesa en Javascript? Si no es así, ¿cuáles son las diferencias?

La respuesta a su pregunta es y No, según la razón por la que haga la pregunta.

Sí:

Tanto Twisted Deferred como Javascript Promise implementan un mecanismo para poner en cola los bloques de código síncronos que se ejecutarán en un orden dado mientras se desacoplan de otros bloques de código sincrónicos.

No:

Por lo tanto, la Promise de Javascript es en realidad más similar a la del Future de Python, y la mejor manera de explicar esto es hablar sobre la Promise y la Resolver se combinan para hacer un Deferred , y afirmar que esto afecta lo que puede hacer con las devoluciones de llamada.

Todo esto está muy bien y es bueno, es preciso, sin embargo, en realidad no aclara nada, y sin escribir miles de palabras en las que estoy casi seguro de cometer un error, probablemente sea mejor citar a alguien que sepa algo. algo pequeño acerca de Python.

Guido van Rossum sobre diferidos :

Este es mi bash de explicar las grandes ideas de Deferred (y hay muchas de ellas) a los usuarios avanzados de Python sin experiencia Twisted previa. También asumo que has pensado en llamadas asíncronas antes. Solo para molestar a Glyph, estoy usando un sistema de 5 estrellas para indicar la importancia de las ideas, donde 1 estrella es “buena idea pero bastante obvia” y 5 estrellas es “shiny”.

Estoy mostrando una gran cantidad de fragmentos de código, porque algunas ideas se expresan mejor de esa manera, pero intencionalmente omito muchos detalles, y algunas veces muestro código que tiene errores, si corregirlos reduciría la comprensión de la idea detrás del código. (Señalaré estos errores). Estoy usando Python 3.

Notas específicamente para Glyph: (a) Considere esto como un borrador para una publicación de blog. Estaría más que feliz de tomar correcciones y sugerencias para mejoras. (b) Esto no significa que voy a cambiar Tulipán a un modelo más de Aplazado; Pero eso es para un hilo diferente.

Idea 1: devolver un objeto especial en lugar de tomar un argumento de callback

Al diseñar API que producen resultados de forma asíncrona, usted encuentra que necesita un sistema para las devoluciones de llamada. Por lo general, el primer diseño que viene a la mente es pasar una función de callback que se llamará cuando se complete la operación asíncrona. Incluso he visto diseños en los que, si no se pasa una callback, la operación es sincrónica, eso es lo suficientemente malo como para darle cero estrellas. Pero incluso la versión de una estrella contamina todas las API con argumentos adicionales que deben transmitirse tediosamente. La primera gran idea de Twisted es que es mejor devolver un objeto especial al que la persona que llama puede agregar una callback después de recibirlo. Le doy a estas tres estrellas porque de ahí brotan muchas de las otras buenas ideas. Por supuesto, es similar a la idea que subyace a los Futuros y Promesas que se encuentran en muchos idiomas y bibliotecas, por ejemplo, los valores concurrentes de Python (PEP 3148, siguiendo de cerca los Futuros de Java, ambos de los cuales están destinados a un mundo con hilos) y ahora Tulipán (PEP 3156). , utilizando un diseño similar adaptado para la operación asíncrona sin hilos).

Idea 2: Pasar los resultados de callback a callback

Creo que es mejor mostrar un código primero:

 class Deferred: def __init__(self): self.callbacks = [] def addCallback(self, callback): self.callbacks.append(callback) # Bug here def callback(self, result): for cb in self.callbacks: result = cb(result) 

Los bits más interesantes son las dos últimas líneas: el resultado de cada callback se pasa a la siguiente. Esto es diferente de cómo funcionan las cosas en concurrent.futures y Tulip, donde el resultado (una vez establecido) se fija como un atributo del futuro. Aquí el resultado puede ser modificado por cada callback.

Esto permite un nuevo patrón cuando una función que devuelve un aplazado llama a otra y transforma su resultado, y esto es lo que le da a esta idea tres estrellas. Por ejemplo, supongamos que tenemos una función asíncrona que lee un conjunto de marcadores, y queremos escribir una función asíncrona que llame a esto y luego ordene los marcadores. En lugar de inventar un mecanismo mediante el cual una función asíncrona puede esperar a otra (lo cual haremos más adelante de todos modos :-), la segunda función asíncrona puede simplemente agregar una nueva callback al Aplazado devuelto por la primera:

 def read_bookmarks_sorted(): d = read_bookmarks() d.addCallback(sorted) return d 

El Aplazado devuelto por esta función representa una lista ordenada de marcadores. Si la persona que llama quiere imprimir esos marcadores, debe agregar otra callback:

 d = read_bookmarks_sorted() d.addCallback(print) 

En un mundo donde los resultados asíncronos están representados por futuros, este mismo ejemplo requeriría dos futuros separados: uno devuelto por read_bookmarks () que representa la lista sin clasificar, y un futuro separado devuelto por read_bookmarks_sorted () que representa la lista ordenada.

Hay un error no obvio en esta versión de la clase: si se llama a addCallback () después de que el Aplazado ya se haya disparado (es decir, se llamó a su método de callback), entonces nunca se llamará a la callback agregada por addCallback (). Es bastante fácil solucionarlo, pero es tedioso, y puedes buscarlo en el código fuente Twisted. Llevaré este error a través de ejemplos sucesivos: solo imagina que vives en un mundo donde el resultado nunca está listo demasiado pronto. También hay otros problemas con este diseño, pero prefiero llamar a las mejoras de las soluciones a las correcciones de errores.

Aparte: las malas elecciones de terminología de Twisted

No sé por qué, pero, comenzando con el nombre propio del proyecto, Twisted a menudo me frota el camino equivocado con la elección de los nombres de las cosas. Por ejemplo, me gusta mucho la pauta de que los nombres de clase deben ser sustantivos. Pero ‘Deferido’ es un adjetivo, y no solo un adjetivo, es un participio pasado del verbo (y uno demasiado largo en eso :-). ¿Y por qué está en un módulo llamado twisted.internet?

Luego está ‘callback’, que se usa para dos propósitos relacionados pero distintos: es el término preferido que se usa para una función que se llamará cuando el resultado esté listo, pero también es el nombre del método al que llama “disparo “el Aplazado, es decir, establecer el resultado (inicial).

No me hagas comenzar con el neologismo / portmanteau que es ‘errback’, lo que nos lleva a …

Idea 3: manejo integrado de errores

Esta idea solo tiene dos estrellas (que estoy seguro que decepcionará a muchos fanáticos de Twisted) porque me confundió mucho. También he notado que los documentos Twisted tienen algunos problemas para explicar cómo funciona. En este caso, en particular, descubrí que leer el código fue más útil que los documentos.

La idea básica es bastante simple: ¿qué pasa si no se puede cumplir la promesa de despedir al Aplazado con un resultado? Cuando escribimos

 d = pod_bay_doors.open() d.addCallback(lambda _: pod.launch()) 

¿Cómo se supone que HAL 9000 dice “Lo siento, Dave. Me temo que no puedo hacer eso”?

E incluso si no nos importa esa respuesta, ¿qué deberíamos hacer si una de las devoluciones de llamada genera una excepción?

La solución de Twisted es bifurcar cada callback en una callback y un ‘error’. Pero eso no es todo: para lidiar con las excepciones generadas por las devoluciones de llamada, también introduce una nueva clase, ‘Fallo’. De hecho, me gustaría presentar este último primero, sin introducir errores:

 class Failure: def __init__(self): self.exception = sys.exc_info()[1] 

(Por cierto, gran nombre de clase. Y quiero decir esto, no estoy siendo sarcástico).

Ahora podemos reescribir el método de callback () de la siguiente manera:

 def callback(self, result): for cb in self.callbacks: try: result = cb(result) except: result = Failure() 

Esto en sí mismo le daría dos estrellas; la callback puede usar isinstance (resultado, falla) para diferenciar los resultados regulares de las fallas.

Por cierto, en Python 3 podría ser posible eliminar las excepciones de encapsulación de la clase de Fallos, y simplemente usar la clase BaseException incorporada. Desde la lectura de los comentarios en el código, la clase de fallas de Twisted existe principalmente para que pueda contener toda la información devuelta por sys.exc_info (), es decir, la clase / tipo de excepción, la instancia de excepción y el rastreo, pero en Python 3, los objetos de excepción ya tienen una referencia al rastreo. Hay algunas cosas de depuración que la clase de fallas de Twisted hace, pero las excepciones estándar no lo hacen, pero aún así, creo que la mayoría de las razones para introducir una clase separada se han abordado.

Pero no olvidemos los errores. Cambiamos la lista de devoluciones de llamada a una lista de pares de funciones de callback y volvemos a escribir el método de callback () de la siguiente manera:

 def callback(self, result): for (cb, eb) in self.callbacks: if isinstance(result, Failure): cb = eb # Use errback try: result = cb(result) except: result = Failure() 

Para mayor comodidad, también agregamos un método errback ():

 def errback(self, fail=None): if fail is None: fail = Failure() self.callback(fail) 

(La función real errback () tiene algunos casos más especiales, se puede llamar con una excepción o un error como argumento, y la clase de errores toma un argumento de excepción opcional para evitar que use sys.exc_info (). Pero ninguno de eso es esencial y hace que los fragmentos de código sean más complicados.)

Para garantizar que self.callbacks sea una lista de pares, también debemos actualizar addCallback () (aún no funciona correctamente cuando se llama después de que se apague el Aplazado):

 def addCallback(self, callback, errback=None): if errback is None: errback = lambda r: r self.callbacks.append((callback, errback)) 

Si esto se llama solo con una función de callback, el error será un dummy que pasa el resultado (es decir, una instancia de fallo) sin cambios. Esto conserva la condición de error para un controlador de errores posterior. Para facilitar la adición de un controlador de errores sin que también se resuelva un resultado regular, agregamos addErrback (), de la siguiente manera:

 def addErrback(self, errback): self.addCallback(lambda r: r, errback) 

En este caso, la mitad de la callback del par pasará el resultado (sin fallo) sin cambios hasta la siguiente callback.

Si desea la motivación completa, lea Introducción a los diferidos de Twisted; Solo terminaré observando un error y sustituiré un resultado normal por un error simplemente devolviendo un valor de no error (incluido ninguno).

Antes de pasar a la siguiente idea, permítame señalar que hay más detalles en la verdadera clase de Aplazado. Por ejemplo, puede especificar argumentos adicionales que se pasarán a la callback y errback. Pero en caso de necesidad, puede hacer esto con las lambdas, así que lo dejo afuera, porque el código adicional para hacer la administración no aclara las ideas básicas.

Idea 4: Encadenamiento de diferidos

Esta es una idea de cinco estrellas! A veces es realmente necesario que una callback espere un evento asíncrono adicional antes de que pueda producir el resultado deseado. Por ejemplo, supongamos que tenemos dos operaciones asíncronas básicas, read_bookmarks () y sync_bookmarks (), y queremos una operación combinada. Si este fuera un código síncrono, podríamos escribir:

 def sync_and_read_bookmarks(): sync_bookmarks() return read_bookmarks() 

Pero, ¿cómo escribimos esto si todas las operaciones devuelven diferidos? Con la idea de encadenar, podemos hacerlo de la siguiente manera:

 def sync_and_read_bookmarks(): d = sync_bookmarks() d.addCallback(lambda unused_result: read_bookmarks()) return d 

La lambda es necesaria porque todas las devoluciones de llamada se llaman con un valor de resultado, pero read_bookmarks () no tiene argumentos.