arrow_back Volver
Inicio keyboard_arrow_right Artículos keyboard_arrow_right Artículo

Futuros en Python

Eduardo Ismael Garcia

Full Stack Developer at Código Facilito.

av_timer 5 Min. de lectura

remove_red_eye 10325 visitas

calendar_today 11 Noviembre 2020

En la versión 3.2 de Python se introdujo uno de los features más interesantes, desde mi punto de vista, que posee el lenguaje , me refiero a nada más y nada menos que los futures, o por su traducción al español, futuros. Un concepto el cual creo todos debemos comprender, y dominar, para sacarle el máximo provecho posible al lenguaje, más aun cuando nos encontramos trabajando de forma asíncrona. Es por ello que en esta ocasión me gustaría que habláramos un poco acerca de ellos, explicaremos exactamente que son, como utilizarlos, y por supuesto, trabajaremos con un par de ejemplo.

Bien, sin más dilación, comencemos.

Futures

Comencemos con la pregunta obligada ¿Qué es un futuro? Verás, si haz trabajado anteriormente con JavaScript podemos ver a los futuros como promesas. Un Futuro no es más que la abstracción de un resultado; un valor el cual, justo ahora, no se encuentra disponible, pero eventualmente puede hacerlo, dentro de un futuro. A grandes rasgos es eso. Sin duda es un concepto difícil de comprender, así que mejor veamos un ejemplo.

import time
import logging

from concurrent.futures import Future

logging.basicConfig(level=logging.DEBUG, format='%(message)s')

def callback(future):
    logging.info('Hola, soy un callback que se ejecuta hasta que el Futuro posea un valor!')
    logging.info(f'El valor del futuro es: {future.result()}')

if __name__ == '__main__':
    future = Future()

    logging.info('>> Futuro creado. No posee valor alguno.')

    future.add_done_callback(callback)

    logging.info('>>> Establecemos una acción a ejecutar una vez el Futuro posee valor alguno.')

    time.sleep(2)

    future.set_result('CódigoFacilito')

    logging.info('>>> Después de 2 segundos se asigna un valor al Futuro.')

En este ejemplo hay mucho código que analizar.

Para poder implementar un Futuro haremos uso del módulo concurrent, esto atraves de la clase Future.

Como podemos observar, en mi línea número 13, creo una instancia de un Futuro. La instancia se crea completamente vacía, sin valor alguno. El valor puede, o no, ser asignado más adelante.

Lo interesante de los Futuros es la posibilidad de definir callbacks, acciones que queremos se ejecuten una vez el futuro posea valor. Para definir qué acción queremos que se ejecute haremos uso del método add_done_callback. Este método recibe como argumento una función, la función a ejecutar cuando exista algún valor por parte del futuro.

Para definir un valor al futuro haremos uso del método set_result. Este método recibe como argumento cualquier valor.

Por otro lado, la función que usemos como callback debe, obligatoriamente, poseer el parámetro future, parámetro que no es más que el futuro mismo. Podremos acceder a su valor del futuro a través del método result.

Si ejecutamos obtendremos la siguiente salida.

>> Futuro creado. No posee valor alguno.
>>> Establecemos una acción a ejecutar una vez el Futuro posee valor alguno.
Hola, soy un callback que se ejecuta hasta que el Futuro posea un valor!
El valor del futuro es: CódigoFacilito
>>> Después de 2 segundos se asigna un valor al Futuro.

Observamos que el callback se ejecuta hasta que el futuro posea un valor, es decir, 2 segundos después de su creación.

Algo que me gustaría mencionar es que a los futuros les podemos asignar la n cantidad de callbacks que deseemos. Basta con que utilicemos, nuevamente, el método add_done_callback.

future.add_done_callback(
        lambda future: logging.info('Hola, soy una lambda!')
    )

Ok, esa sería una introducción muy básica de cómo implementar un futuro, ahora veamos un ejemplo un poco más real.

import logging
import requests
import threading
from concurrent.futures import Future

logging.basicConfig(level=logging.DEBUG, format='%(message)s')

def show_pokemon_name(response):
    if response.status_code == 200:
        response_json = response.json()
        name = response_json.get('forms')[0].get('name')

        logging.info(f'El nombre del pokemon es {name}')

def generate_request(url):
    future = Future()

    thread = threading.Thread(target=(
        lambda: future.set_result(requests.get(url))
    ))
    thread.start()

    return future

if __name__ == '__main__':
    future = generate_request('https://pokeapi.co/api/v2/pokemon/1/')
    future.add_done_callback(
        lambda future: show_pokemon_name(future.result())
    )

    while not future.done():
        logging.info('A la espera de un resultado')

    logging.info('Finalizamos el programa!')

En este caso complicando un poco el ejemplo, realizamos una petición asíncrona a un API. Como sabemos este tipo de peticiones no son inmediatas, y le toman un par de segundos a nuestro programa completarlas. Es por ello que mediante un futuro imprimimos en consola el mensaje: A la espera de un resultado. Este mensaje se estará mostrando hasta que exista un valor para el futuro.

Mediante un Thread programó el futuro. Su valor no será más que el resultado por parte del servidor. El callback asignado al Futuro mostrará en consola el nombre del Pokémon, claro, siempre y cuando la petición se haya realizado exitosamente.

Este es un ejemplo un poco rebuscado debo admitir, pero sin duda nos deja en claro el potencial de los futuros.

Ahora vemos otro ejemplo, un ejemplo un poco más real; ya que verás, el potencial de los futuros lo encontraremos cuando trabajemos con un Pool de Threads.

import time
import logging
import requests
import threading

from concurrent.futures import ThreadPoolExecutor

logging.basicConfig(level=logging.DEBUG, format='%(threadName)s: %(message)s',)

URLS = [
    'https://codigofacilito.com/',
    'https://twitter.com/home',
    'https://www.google.com/',
    'https://es.stackoverflow.com/',
    'https://stackoverflow.com/',
    'https://about.gitlab.com/',
    'https://github.com/',
    'https://www.youtube.com/'
]

def generate_request(url):
    return requests.get(url)

def check_status_code(future):
    response = future.result()
    logging.info(f'>>> La respuesta del servidor es: {response.status_code}')

if __name__ == '__main__':
    with ThreadPoolExecutor(max_workers=2) as executor:

        for url in URLS:
            futuro = executor.submit(generate_request, url)
            futuro.add_done_callback(check_status_code)

Como mencionamos en una entrega anterior el método submit de un pool de thread retornará un futuro, al cual facilmente podemos asignarle un callback. De esta forma trabajaremos de forma concurrente reciclando Threads y definiendo acciones a realizar.

Podemos cambiar un poco la implementación.

def check_status_code(response):
    logging.info(f'>>> La respuesta del servidor es: {response.status_code}')

if __name__ == '__main__':
    with ThreadPoolExecutor(max_workers=2) as executor:

        futuros = [ executor.submit(generate_request, url) for url un URLS ]

        for futuro in futuros:
            future.add_done_callback(
                lambda future: check_status_code(
                    future.result()
                )
            ) 

Creo que queda mucho mejor, así la función check_status_code se deslinda de conocer cómo se realizó la petición.

Ya para finalizar me gustaría mencionar el dentro del módulo concurren encontraremos la función as_completed, función que nos permite conocer si un futuro a finalizado o no.

from concurrent.futures import as_completed

if __name__ == '__main__':
    with ThreadPoolExecutor(max_workers=2) as executor:

        futuros = [ executor.submit(generate_request, url) for url in URLS ]

        for futuro in as_completed(futuros):
            check_status_code(futuro.result())

Ya para finalizar me gustaría mencionar que implementar futuros es una excelente idea siempre que trabajemos con tareas las cuales puedan llegar a bloquear nuestro programar. Por otro lado no es muy recomendable trabajar futuros y Threads cuando realicemos calculos complejos, para ello mejor apoyarnos de procesos, por que sí, los futuros también pueden ser utilizados junto con procesos.


Y bien de esta forma es como podemos implementar los Futuros en Python. Recordemos, un Futuro no es más que la abstracción de un valor que justo ahora no se encuentra disponible, pero posiblemente lo haga dentro de un futuro.

Si te interesa saber más acerca de los futuros o de cómo trabajar con Python de forma concurrente o paralela me gustaría recomendarte el Curso de Programación concurrente en Python. Curso donde se cubren este y otros temas más que sin duda te serán de mucha utilidad en tu ambiente profesional.