Devolver valores desde funciones

PythonPythonBeginner
Practicar Ahora

This tutorial is from open-source community. Access the source code

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

En este laboratorio, aprenderás cómo devolver múltiples valores desde funciones en Python. También entenderás los valores de retorno opcionales y cómo manejar los errores de manera efectiva.

Además, explorarás el concepto de Futures (Futuros) para la programación concurrente. Aunque devolver un valor puede parecer sencillo, diferentes escenarios de programación presentan diversos patrones y consideraciones.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/DataStructuresGroup(["Data Structures"]) python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ErrorandExceptionHandlingGroup(["Error and Exception Handling"]) python(("Python")) -.-> python/AdvancedTopicsGroup(["Advanced Topics"]) python(("Python")) -.-> python/PythonStandardLibraryGroup(["Python Standard Library"]) python/DataStructuresGroup -.-> python/tuples("Tuples") python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/arguments_return("Arguments and Return Values") python/ErrorandExceptionHandlingGroup -.-> python/catching_exceptions("Catching Exceptions") python/AdvancedTopicsGroup -.-> python/threading_multiprocessing("Multithreading and Multiprocessing") python/PythonStandardLibraryGroup -.-> python/data_collections("Data Collections") subgraph Lab Skills python/tuples -.-> lab-132504{{"Devolver valores desde funciones"}} python/function_definition -.-> lab-132504{{"Devolver valores desde funciones"}} python/arguments_return -.-> lab-132504{{"Devolver valores desde funciones"}} python/catching_exceptions -.-> lab-132504{{"Devolver valores desde funciones"}} python/threading_multiprocessing -.-> lab-132504{{"Devolver valores desde funciones"}} python/data_collections -.-> lab-132504{{"Devolver valores desde funciones"}} end

Devolviendo múltiples valores desde funciones

En Python, cuando necesitas que una función devuelva más de un valor, hay una solución práctica: devolver una tupla. Una tupla es un tipo de estructura de datos en Python. Es una secuencia inmutable, lo que significa que una vez que creas una tupla, no puedes cambiar sus elementos. Las tuplas son útiles porque pueden contener múltiples valores de diferentes tipos en un solo lugar.

Vamos a crear una función para analizar líneas de configuración en el formato nombre=valor. El objetivo de esta función es tomar una línea en este formato y devolver tanto el nombre como el valor como elementos separados.

  1. Primero, necesitas crear un nuevo archivo de Python. Este archivo contendrá el código de nuestra función y el código de prueba. En el directorio del proyecto, crea un archivo llamado return_values.py. Puedes usar el siguiente comando en la terminal para crear este archivo:
touch ~/project/return_values.py
  1. Ahora, abre el archivo return_values.py en tu editor de código. Dentro de este archivo, escribiremos la función parse_line. Esta función toma una línea como entrada, la divide en el primer signo '=' y devuelve el nombre y el valor como una tupla.
def parse_line(line):
    """
    Parse a line in the format 'name=value' and return both the name and value.

    Args:
        line (str): Input line to parse in the format 'name=value'

    Returns:
        tuple: A tuple containing (name, value)
    """
    parts = line.split('=', 1)  ## Split at the first equals sign
    if len(parts) == 2:
        name = parts[0]
        value = parts[1]
        return (name, value)  ## Return as a tuple

En esta función, el método split se utiliza para dividir la línea de entrada en dos partes en el primer signo '='. Si la línea está en el formato correcto nombre=valor, extraemos el nombre y el valor y los devolvemos como una tupla.

  1. Después de definir la función, necesitamos agregar algún código de prueba para ver si la función funciona como se espera. El código de prueba llamará a la función parse_line con una entrada de muestra e imprimirá los resultados.
## Test the parse_line function
if __name__ == "__main__":
    result = parse_line('[email protected]')
    print(f"Result as tuple: {result}")

    ## Unpacking the tuple into separate variables
    name, value = parse_line('[email protected]')
    print(f"Unpacked name: {name}")
    print(f"Unpacked value: {value}")

En el código de prueba, primero llamamos a la función parse_line y almacenamos la tupla devuelta en la variable result. Luego imprimimos esta tupla. A continuación, usamos el desempaquetado de tuplas para asignar directamente los elementos de la tupla a las variables name y value e imprimirlos por separado.

  1. Una vez que hayas escrito la función y el código de prueba, guarda el archivo return_values.py. Luego, abre la terminal y ejecuta el siguiente comando para ejecutar el script de Python:
python ~/project/return_values.py

Deberías ver una salida similar a:

Result as tuple: ('email', '[email protected]')
Unpacked name: email
Unpacked value: [email protected]

Explicación:

  • La función parse_line divide la cadena de entrada en el carácter '=' utilizando el método split. Este método divide la cadena en partes basadas en el separador especificado.
  • Devuelve ambas partes como una tupla utilizando la sintaxis return (name, value). Una tupla es una forma de agrupar múltiples valores juntos.
  • Cuando se llama a la función, tienes dos opciones. Puedes almacenar la tupla completa en una variable, como hicimos con la variable result. O puedes "desempaquetar" la tupla directamente en variables separadas utilizando la sintaxis name, value = parse_line(...). Esto facilita trabajar con los valores individuales.

Este patrón de devolver múltiples valores como una tupla es muy común en Python. Hace que las funciones sean más versátiles porque pueden proporcionar más de una pieza de información al código que las llama.

Devolviendo valores opcionales

En programación, hay ocasiones en las que una función puede no ser capaz de generar un resultado válido. Por ejemplo, cuando una función debe extraer información específica de una entrada, pero la entrada no tiene el formato esperado. En Python, una forma común de manejar estas situaciones es devolver None. None es un valor especial en Python que indica la ausencia de un valor de retorno válido.

Veamos cómo podemos modificar una función para manejar casos en los que la entrada no cumple con los criterios esperados. Trabajaremos en la función parse_line, que está diseñada para analizar una línea en el formato 'nombre=valor' y devolver tanto el nombre como el valor.

  1. Actualiza la función parse_line en tu archivo return_values.py:
def parse_line(line):
    """
    Parse a line in the format 'name=value' and return both the name and value.
    If the line is not in the correct format, return None.

    Args:
        line (str): Input line to parse in the format 'name=value'

    Returns:
        tuple or None: A tuple containing (name, value) or None if parsing failed
    """
    parts = line.split('=', 1)  ## Split at the first equals sign
    if len(parts) == 2:
        name = parts[0]
        value = parts[1]
        return (name, value)  ## Return as a tuple
    else:
        return None  ## Return None for invalid input

En esta función parse_line actualizada, primero dividimos la línea de entrada en el primer signo de igualdad utilizando el método split. Si la lista resultante tiene exactamente dos elementos, significa que la línea está en el formato correcto 'nombre=valor'. Luego extraemos el nombre y el valor y los devolvemos como una tupla. Si la lista no tiene dos elementos, significa que la entrada es inválida y devolvemos None.

  1. Agrega código de prueba para demostrar la función actualizada:
## Test the updated parse_line function
if __name__ == "__main__":
    ## Valid input
    result1 = parse_line('[email protected]')
    print(f"Valid input result: {result1}")

    ## Invalid input
    result2 = parse_line('invalid_line_without_equals_sign')
    print(f"Invalid input result: {result2}")

    ## Checking for None before using the result
    test_line = 'user_info'
    result = parse_line(test_line)
    if result is None:
        print(f"Could not parse the line: '{test_line}'")
    else:
        name, value = result
        print(f"Name: {name}, Value: {value}")

Este código de prueba llama a la función parse_line con entradas válidas e inválidas. Luego imprime los resultados. Observa que cuando se utiliza el resultado de la función parse_line, primero comprobamos si es None. Esto es importante porque si intentamos desempaquetar un valor None como si fuera una tupla, obtendremos un error.

  1. Guarda el archivo y ejecútalo:
python ~/project/return_values.py

Cuando ejecutes el script, deberías ver una salida similar a:

Valid input result: ('email', '[email protected]')
Invalid input result: None
Could not parse the line: 'user_info'

Explicación:

  • La función ahora comprueba si la línea contiene un signo de igualdad. Esto se hace dividiendo la línea en el signo de igualdad y comprobando la longitud de la lista resultante.
  • Si la línea no contiene un signo de igualdad, devuelve None para indicar que el análisis falló.
  • Cuando se utiliza una función de este tipo, es importante comprobar si el resultado es None antes de intentar usarlo. De lo contrario, es posible que encuentres errores al intentar acceder a elementos de un valor None.

Discusión sobre el diseño:
Un enfoque alternativo para manejar entradas inválidas es lanzar una excepción. Este enfoque es adecuado en ciertas situaciones:

  1. La entrada inválida es realmente excepcional y no un caso esperado. Por ejemplo, si se espera que la entrada provenga de una fuente de confianza y siempre esté en el formato correcto.
  2. Quieres forzar al llamador a manejar el error. Al lanzar una excepción, el flujo normal del programa se interrumpe y el llamador tiene que manejar el error explícitamente.
  3. Necesitas proporcionar información detallada sobre el error. Las excepciones pueden llevar información adicional sobre el error, lo que puede ser útil para la depuración.

Ejemplo de un enfoque basado en excepciones:

def parse_line_with_exception(line):
    """Parse a line and raise an exception for invalid input."""
    parts = line.split('=', 1)
    if len(parts) != 2:
        raise ValueError(f"Invalid format: '{line}' does not contain '='")
    return (parts[0], parts[1])

La elección entre devolver None y lanzar excepciones depende de las necesidades de tu aplicación:

  • Devuelve None cuando la ausencia de un resultado es común y esperada. Por ejemplo, cuando se busca un elemento en una lista y puede que no esté allí.
  • Lanza excepciones cuando el fallo es inesperado y debe interrumpir el flujo normal. Por ejemplo, cuando se intenta acceder a un archivo que siempre debe existir.

Trabajando con Futures para programación concurrente

En Python, cuando necesitas ejecutar funciones al mismo tiempo, es decir, de forma concurrente, el lenguaje ofrece herramientas útiles como hilos (threads) y procesos. Pero aquí está un problema común que enfrentarás: ¿cómo puedes obtener el valor que devuelve una función cuando se está ejecutando en un hilo diferente? Aquí es donde el concepto de un Future se vuelve muy importante.

Un Future es como un marcador de posición para un resultado que estará disponible más adelante. Es una forma de representar un valor que una función producirá en el futuro, incluso antes de que la función haya terminado de ejecutarse. Entendamos mejor este concepto con un ejemplo sencillo.

Paso 1: Crear un nuevo archivo

Primero, necesitas crear un nuevo archivo de Python. Lo llamaremos futures_demo.py. Puedes usar el siguiente comando en tu terminal para crear este archivo:

touch ~/project/futures_demo.py

Paso 2: Agregar código básico de la función

Ahora, abre el archivo futures_demo.py y agrega el siguiente código de Python. Este código define una función sencilla y muestra cómo funciona una llamada a función normal.

import time
import threading
from concurrent.futures import Future, ThreadPoolExecutor

def worker(x, y):
    """A function that takes time to complete"""
    print('Starting work...')
    time.sleep(5)  ## Simulate a time-consuming task
    print('Work completed')
    return x + y

## Part 1: Normal function call
print("--- Part 1: Normal function call ---")
result = worker(2, 3)
print(f"Result: {result}")

En este código, la función worker toma dos números, los suma, pero primero simula una tarea que consume tiempo deteniéndose durante 5 segundos. Cuando llamas a esta función de forma normal, el programa espera a que la función termine y luego obtiene el valor de retorno.

Paso 3: Ejecutar el código básico

Guarda el archivo y ejecútalo usando el siguiente comando en tu terminal:

python ~/project/futures_demo.py

Deberías ver una salida como esta:

--- Part 1: Normal function call ---
Starting work...
Work completed
Result: 5

Esto muestra que una llamada a función normal espera a que la función termine y luego devuelve el resultado.

Paso 4: Ejecutar la función en un hilo separado

A continuación, veamos qué sucede cuando ejecutamos la función worker en un hilo separado. Agrega el siguiente código al archivo futures_demo.py:

## Part 2: Running in a separate thread (problem: no way to get result)
print("\n--- Part 2: Running in a separate thread ---")
t = threading.Thread(target=worker, args=(2, 3))
t.start()
print("Main thread continues while worker runs...")
t.join()  ## Wait for the thread to complete
print("Worker thread finished, but we don't have its return value!")

Aquí, estamos usando la clase threading.Thread para iniciar la función worker en un nuevo hilo. El hilo principal no espera a que la función worker termine y continúa su ejecución. Sin embargo, cuando el hilo worker termina, no tenemos una forma fácil de obtener el valor de retorno.

Paso 5: Ejecutar el código con hilos

Guarda el archivo nuevamente y ejecútalo usando el mismo comando:

python ~/project/futures_demo.py

Notarás que el hilo principal continúa, el hilo worker se ejecuta, pero no podemos acceder al valor de retorno de la función worker.

Paso 6: Usar un Future manualmente

Para resolver el problema de obtener el valor de retorno de un hilo, podemos usar un objeto Future. Agrega el siguiente código al archivo futures_demo.py:

## Part 3: Using a Future to get the result
print("\n--- Part 3: Using a Future manually ---")

def do_work_with_future(x, y, future):
    """Wrapper that sets the result in the Future"""
    result = worker(x, y)
    future.set_result(result)

## Create a Future object
fut = Future()

## Start a thread that will set the result in the Future
t = threading.Thread(target=do_work_with_future, args=(2, 3, fut))
t.start()

print("Main thread continues...")
print("Waiting for the result...")
## Block until the result is available
result = fut.result()  ## This will wait until set_result is called
print(f"Got the result: {result}")

En este código, creamos un objeto Future y lo pasamos a una nueva función do_work_with_future. Esta función llama a la función worker y luego establece el resultado en el objeto Future. El hilo principal luego puede usar el método result() del objeto Future para obtener el resultado cuando esté disponible.

Paso 7: Ejecutar el código con Future

Guarda el archivo y ejecútalo nuevamente:

python ~/project/futures_demo.py

Ahora verás que podemos obtener con éxito el valor de retorno de la función que se está ejecutando en el hilo.

Paso 8: Usar ThreadPoolExecutor

La clase ThreadPoolExecutor en Python hace que trabajar con tareas concurrentes sea aún más fácil. Agrega el siguiente código al archivo futures_demo.py:

## Part 4: Using ThreadPoolExecutor (easier way)
print("\n--- Part 4: Using ThreadPoolExecutor ---")
with ThreadPoolExecutor() as executor:
    ## Submit the work to the executor
    future = executor.submit(worker, 2, 3)

    print("Main thread continues after submitting work...")
    print("Checking if the future is done:", future.done())

    ## Get the result (will wait if not ready)
    result = future.result()
    print("Now the future is done:", future.done())
    print(f"Final result: {result}")

El ThreadPoolExecutor se encarga de crear y administrar los objetos Future por ti. Solo necesitas enviar la función y sus argumentos, y devolverá un objeto Future que puedes usar para obtener el resultado.

Paso 9: Ejecutar el código completo

Guarda el archivo por última vez y ejecútalo:

python ~/project/futures_demo.py

Explicación

  1. Llamada a función normal: Cuando llamas a una función de forma normal, el programa espera a que la función termine y obtiene directamente el valor de retorno.
  2. Problema con los hilos: Ejecutar una función en un hilo separado tiene una desventaja. No hay una forma incorporada de obtener el valor de retorno de la función que se está ejecutando en ese hilo.
  3. Future manual: Al crear un objeto Future y pasarlo al hilo, podemos establecer el resultado en el Future y luego obtener el resultado desde el hilo principal.
  4. ThreadPoolExecutor: Esta clase simplifica la programación concurrente. Se encarga de la creación y administración de los objetos Future por ti, lo que hace que sea más fácil ejecutar funciones de forma concurrente y obtener sus valores de retorno.

Los objetos Future tienen varios métodos útiles:

  • result(): Este método se utiliza para obtener el resultado de la función. Si el resultado aún no está listo, esperará hasta que lo esté.
  • done(): Puedes usar este método para comprobar si el cálculo de la función está completo.
  • add_done_callback(): Este método te permite registrar una función que se llamará cuando el resultado esté listo.

Este patrón es muy importante en la programación concurrente, especialmente cuando necesitas obtener resultados de funciones que se ejecutan en paralelo.

Resumen

En este laboratorio, has aprendido varios patrones clave para devolver valores desde funciones en Python. En primer lugar, las funciones de Python pueden devolver múltiples valores empaquetándolos en una tupla, lo que permite una devolución y desempaquetado de valores limpios y legibles. En segundo lugar, para las funciones que no siempre pueden producir resultados válidos, devolver None es una forma común de indicar la ausencia de un valor, y también se presentó el lanzamiento de excepciones como una alternativa.

Por último, en la programación concurrente, un Future actúa como un marcador de posición para un resultado futuro, lo que te permite obtener valores de retorno de funciones que se ejecutan en hilos o procesos separados. Comprender estos patrones mejorará la robustez y la flexibilidad de tu código Python. Para una práctica adicional, experimenta con diferentes estrategias de manejo de errores, utiliza Future con otros tipos de ejecución concurrente y explora su aplicación en la programación asíncrona con async/await.