Diseño de pipelines asincrónicos para un procesamiento de datos eficiente
Nota. Este artículo ya asume que está familiarizado con las devoluciones de llamadas, las promesas y tiene una comprensión básica del paradigma asincrónico en JavaScript.
El mecanismo asincrónico es uno de los conceptos más importantes en JavaScript y en la programación en general. Permite que un programa ejecute tareas secundarias por separado en segundo plano sin bloquear el hilo actual para que no ejecute las tareas principales. Cuando se completa una tarea secundaria, se devuelve su resultado y el programa continúa ejecutándose normalmente. En este contexto, dichas tareas secundarias se denominan asincrónico.
Las tareas asincrónicas generalmente incluyen realizar solicitudes a entornos externos como bases de datos, API web o sistemas operativos.. Si el resultado de una operación asincrónica no afecta la lógica del programa principal, entonces en lugar de simplemente esperar a que se complete la tarea, es mucho mejor no perder este tiempo y continuar ejecutando las tareas principales.
Sin embargo, a veces el resultado de una operación asincrónica se utiliza inmediatamente en las siguientes líneas de código. En tales casos, las líneas de código siguientes no se deben ejecutar hasta que se complete la operación asincrónica.
Nota. Antes de llegar a la parte principal de este artículo, me gustaría proporcionar la motivación de por qué la asincronicidad se considera un tema importante en la ciencia de datos y por qué usé JavaScript en lugar de Python para explicarlo.
async / await
sintaxis.
Ingeniería de datos es una parte inseparable de la ciencia de datos, que consiste principalmente en diseñar canales de datos robustos y eficientes. Una de las tareas típicas de la ingeniería de datos incluye realizar llamadas periódicas a API, bases de datos u otras fuentes para recuperar datos, procesarlos y almacenarlos en algún lugar.
Imaginemos una fuente de datos que encuentra problemas de red y no puede devolver los datos solicitados inmediatamente. Si simplemente hacemos la solicitud en código a ese servicio, tendremos que esperar bastante tiempo, sin hacer nada. ¿No sería ¿Será mejor evitar perder el valioso tiempo del procesador y ejecutar otra función, por ejemplo? ¡Aquí es donde entra en juego el poder de la asincronicidad, que será el tema central de este artículo!
Nadie negará el hecho de que Python es la opción más popular en la actualidad para crear aplicaciones de ciencia de datos. Sin embargo, JavaScript es otro lenguaje con un enorme ecosistema que sirve para diversos propósitos de desarrollo, incluida la creación de aplicaciones web que procesan datos recuperados de otros servicios. Resulta que la asincronicidad juega uno de los papeles más fundamentales en JavaScript.
Además, en comparación con Python, JavaScript tiene un soporte integrado más completo para lidiar con la asincronicidad y generalmente sirve como un mejor ejemplo para profundizar en este tema.
Finalmente, Python tiene una función similar. async / await
Construcción. Por lo tanto, la información presentada en este artículo sobre JavaScript también se puede transferir a Python para diseñar canalizaciones de datos eficientes.
En las primeras versiones de JavaScript, el código asincrónico se escribía principalmente con devoluciones de llamadas. Desafortunadamente, esto llevó a los desarrolladores a un problema conocido llamado “El infierno de las devoluciones de llamadas”. Muchas veces, el código asincrónico escrito con devoluciones de llamadas sin formato conducía a varios ámbitos de código anidados que eran extremadamente difíciles de leer. Es por eso que en 2012, los creadores de JavaScript introdujeron Promesas.
// Example of the "callback hell" problemfunctionOne(function () {
functionTwo(function () {
functionThree(function () {
functionFour(function () {
...
});
});
});
});
Las promesas proporcionan una interfaz conveniente para el desarrollo de código asincrónico. Una promesa lleva a un constructor una función asincrónica que se ejecuta en un momento determinado en el futuro. Antes de que se ejecute la función, se dice que la promesa está en un pendiente estado. Dependiendo de si la función asincrónica se ha completado con éxito o no, la promesa cambia su estado a cumplido o rechazado respectivamente. Para los dos últimos estados, los programadores pueden encadenar .then()
y .catch()
métodos con la promesa de declarar la lógica de cómo debe manejarse el resultado de la función asincrónica en diferentes escenarios.
Aparte de eso, un grupo de promesas se puede encadenar mediante el uso de métodos de combinación como any()
, all()
, race()
etc.
A pesar de que las promesas se han convertido en una mejora significativa con respecto a las devoluciones de llamadas, todavía no son ideales por varias razones:
- VerbosidadLas promesas suelen requerir la escritura de una gran cantidad de código repetitivo. En algunos casos, crear una promesa con una funcionalidad simple requiere algunas líneas de código adicionales debido a su sintaxis verbosa.
- Legibilidad. Tener varias tareas que dependen unas de otras lleva a que las promesas se aniden unas dentro de otras. Este infame problema es muy similar al “El infierno de las devoluciones de llamadas” lo que dificulta la lectura y el mantenimiento del código. Además, cuando se trata de la gestión de errores, suele ser difícil seguir la lógica del código cuando un error se propaga a través de varias cadenas de promesas.
- DepuraciónAl verificar la salida del seguimiento de la pila, puede resultar difícil identificar la fuente de un error dentro de las promesas, ya que generalmente no brindan descripciones de errores claras.
- Integración con bibliotecas heredadasMuchas bibliotecas antiguas de JavaScript se desarrollaron en el pasado para funcionar con devoluciones de llamadas sin formato, lo que dificultaba su compatibilidad con las promesas. Si el código se escribe utilizando promesas, se deben crear componentes de código adicionales para proporcionar compatibilidad con bibliotecas antiguas.
En su mayor parte, la async / await
La construcción se agregó a JavaScript como azúcar sintética sobre las promesas. Como sugiere el nombre, introduce dos nuevas palabras clave de código:
async
se utiliza antes de la firma de la función y Marca la función como asincrónica, lo que siempre devuelve una promesa. (incluso si una promesa no se devuelve explícitamente, ya que se incluirá implícitamente).await
se utiliza dentro de funciones marcadas como asíncrono y se declara en el código antes de las operaciones asincrónicas que devuelven una promesa. Si una línea de código contiene elawait
palabra clave, entonces las siguientes líneas de código dentro de la función async no se ejecutarán hasta que se cumpla la promesa devuelta (ya sea en el cumplido o rechazado estado)Esto garantiza que si la lógica de ejecución de las siguientes líneas depende del resultado de la operación asincrónica, no se ejecutarán.
– El
await
La palabra clave se puede utilizar varias veces dentro de una función asíncrona.– Si
await
se utiliza dentro de una función que no está marcada como asíncrona,SyntaxError
será arrojado.– El resultado devuelto de una función marcada con
await
Es el valor resuelto de una promesa.
El async / await
Un ejemplo de uso se demuestra en el fragmento siguiente.
// Async / await example.
// The code snippet prints start and end words to the console.function getPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('end');
},
1000);
});
}
// since this function is marked as async, it will return a promise
async function printInformation() {
console.log('start');
const result = await getPromise();
console.log(result) // this line will not be executed until the promise is resolved
}
Es importante entender que await no bloquea la ejecución del subproceso principal de JavaScript. En cambio, solo suspende la función asincrónica que la contiene (mientras se puede ejecutar otro código de programa fuera de la función asincrónica).
Manejo de errores
El async / await
La construcción proporciona una forma estándar para el manejo de errores con try / catch
Palabras clave. Para manejar errores, es necesario encapsular todo el código que potencialmente puede causar un error (incluido await
declaraciones) en el try
bloquear y escribir los mecanismos de manejo correspondientes en el catch
bloquear.
En la práctica, el manejo de errores con
try / catch
Los bloques son más fáciles y legibles que lograr lo mismo en promesas con.catch()
encadenamiento de rechazo.
// Error handling template inside an async functionasync function functionOne() {
try {
...
const result = await functionTwo()
} catch (error) {
...
}
}
async / await
es una gran alternativa a las promesas. Eliminan las deficiencias mencionadas anteriormente de las promesas: el código escrito con async / await
Suele ser más legible y fácil de mantener y es la opción preferida por la mayoría de los ingenieros de software.
Sin embargo, sería incorrecto negar la importancia de las promesas en JavaScript: en algunas situaciones, son una mejor opción, especialmente cuando se trabaja con funciones que devuelven una promesa por defecto.
Intercambiabilidad de códigos
Veamos el mismo código escrito con async / await
y promesas. Supondremos que nuestro programa se conecta a una base de datos y en caso de establecerse una conexión solicita datos sobre los usuarios para mostrarlos posteriormente en la interfaz de usuario.
// Example of asynchronous requests handled by async / awaitasync function functionOne() {
try {
...
const result = await functionTwo()
} catch (error) {
...
}
}
Ambas solicitudes asincrónicas se pueden encapsular fácilmente mediante el uso de await
Sintaxis. En cada uno de estos dos pasos, el programa detendrá la ejecución del código hasta que se recupere la respuesta.
Dado que algo incorrecto puede suceder durante las solicitudes asincrónicas (conexión interrumpida, inconsistencia de datos, etc.), deberíamos encapsular todo el fragmento de código en un try / catch
bloque. Si se detecta un error, lo mostramos en la consola.
Ahora escribamos el mismo fragmento de código con promesas:
// Example of asynchronous requests handled by promisesfunction displayUsers() {
...
connectToDatabase()
.then((response) => {
...
return getData(data);
})
.then((users) => {
showUsers(users);
...
})
.catch((error) => {
console.log(`An error occurred: ${error.message}`);
...
});
}
Este código anidado parece más detallado y más difícil de leer. Además, podemos observar que cada declaración await se transformó en una instrucción correspondiente. then()
método y que el bloque catch ahora se encuentra dentro del .catch()
método de una promesa.
Siguiendo la misma lógica, cada
async / await
El código se puede reescribir con promesasEsta afirmación demuestra el hecho de queasync / await
Es solo azúcar sintético que cumple promesas.
El código escrito con async / await se puede transformar en la sintaxis de promesa donde cada declaración await correspondería a un método .then() separado y el manejo de excepciones se realizaría en el método .catch().
En esta sección veremos un ejemplo real de cómo async / await
obras.
Vamos a utilizar el API de países REST que proporciona información demográfica de un país solicitado en formato JSON mediante la siguiente dirección URL: https://restcountries.com/v3.1/name/$country
.
En primer lugar, declararemos una función que recuperará la información principal del JSON. Nos interesa recuperar información sobre el nombre del país, su capital, área y población. El JSON se devuelve en forma de matriz donde el primer objeto contiene toda la información necesaria. Podemos acceder a las propiedades mencionadas anteriormente accediendo a las claves del objeto con los nombres correspondientes.
const retrieveInformation = function (data) {
data = data(0)
return {
country: data("name")("common"),
capital: data("capital")(0),
area: `${data("area")} km`,
population: `{$data("population")} people`
};
};
Luego usaremos el API de búsqueda para realizar solicitudes HTTP. Fetch es una función asincrónica que devuelve una promesaDado que necesitamos inmediatamente los datos devueltos por fetch, debemos esperar hasta que fetch termine su trabajo antes de ejecutar las siguientes líneas de código. Para ello, utilizamos el await
Palabra clave antes de buscar.
// Fetch example with async / awaitconst getCountryDescription = async function (country) {
try {
const response = await fetch(
`https://restcountries.com/v3.1/name/${country}`
);
if (!response.ok) {
throw new Error(`Bad HTTP status of the request (${response.status}).`);
}
const data = await response.json();
console.log(retrieveInformation(data));
} catch (error) {
console.log(
`An error occurred while processing the request.\nError message: ${error.message}`
);
}
};
De manera similar, colocamos otro await
Antes de la .json()
Método para analizar los datos que se utilizan inmediatamente después en el código. En caso de un estado de respuesta incorrecto o de imposibilidad de analizar los datos, se genera un error que luego se procesa en el bloque catch.
Para fines de demostración, también reescribamos el fragmento de código utilizando promesas:
// Fetch example with promisesconst getCountryDescription = function (country) {
fetch(`https://restcountries.com/v3.1/name/${country}`)
.then((response) => {
if (!response.ok) {
throw new Error(`Bad HTTP status of the request (${response.status}).`);
}
return response.json();
})
.then((data) => {
console.log(retrieveInformation(data));
})
.catch((error) => {
console.log(
`An error occurred while processing the request. Error message: ${error.message}`
);
});
};
Al llamar a una función con un nombre de país proporcionado, se imprimirá su información principal:
// The result of calling getCountryDescription("Argentina"){
country: 'Argentina',
capital: 'Buenos Aires',
area: '27804000 km',
population: '45376763 people'
}
En este artículo, hemos cubierto los async / await
Construcción en JavaScript que apareció en el lenguaje en 2017. Habiendo surgido como una mejora a las promesas, permite escribir código asincrónico de manera sincrónica eliminando fragmentos de código anidados. Su uso correcto combinado con promesas da como resultado una combinación poderosa que hace que el código sea lo más limpio posible.
Por último, la información presentada en este artículo sobre JavaScript también es valiosa para Python., que tiene lo mismo async / await
Construcción. Personalmente, si alguien quiere profundizar en la asincronicidad, le recomendaría centrarse más en JavaScript que en Python. Conocer las abundantes herramientas que existen en JavaScript para desarrollar aplicaciones asincrónicas proporciona una comprensión más fácil de los mismos conceptos en otros lenguajes de programación.
Todas las imágenes, a menos que se indique lo contrario, son del autor.