Definir funciones de decorador simples

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 qué son los decoradores y cómo funcionan en Python. Los decoradores son una característica poderosa que te permite modificar el comportamiento de una función sin alterar el código fuente, y se utilizan ampliamente en los marcos (frameworks) y bibliotecas de Python.

También aprenderás a crear un decorador de registro (logging) simple e implementar uno más complejo para la validación de funciones. Los archivos involucrados en este laboratorio son logcall.py, sample.py y validate.py, y se modificará validate.py.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/BasicConceptsGroup(["Basic Concepts"]) python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/AdvancedTopicsGroup(["Advanced Topics"]) python/BasicConceptsGroup -.-> python/type_conversion("Type Conversion") python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/scope("Scope") python/AdvancedTopicsGroup -.-> python/decorators("Decorators") subgraph Lab Skills python/type_conversion -.-> lab-132514{{"Definir funciones de decorador simples"}} python/function_definition -.-> lab-132514{{"Definir funciones de decorador simples"}} python/scope -.-> lab-132514{{"Definir funciones de decorador simples"}} python/decorators -.-> lab-132514{{"Definir funciones de decorador simples"}} end

Creando tu primer decorador

¿Qué son los decoradores?

En Python, los decoradores son una sintaxis especial que puede ser muy útil para los principiantes. Permiten modificar el comportamiento de funciones o métodos. Puedes pensar en un decorador como una función que toma otra función como entrada y luego devuelve una nueva función. Esta nueva función a menudo extiende o cambia el comportamiento de la función original.

Los decoradores se aplican utilizando el símbolo @. Colocas este símbolo seguido del nombre del decorador directamente encima de la definición de una función. Esta es una forma sencilla de decirle a Python que quieres usar el decorador en esa función en particular.

Creando un decorador de registro (logging) simple

Vamos a crear un decorador simple que registre información cuando se llama a una función. El registro es una tarea común en aplicaciones del mundo real, y usar un decorador para esto es una excelente manera de entender cómo funcionan.

  1. Primero, abre el editor VSCode. En el directorio /home/labex/project, crea un nuevo archivo llamado logcall.py. Este archivo contendrá nuestra función decorador.

  2. Agrega el siguiente código a logcall.py:

## logcall.py

def logged(func):
    print('Adding logging to', func.__name__)
    def wrapper(*args, **kwargs):
        print('Calling', func.__name__)
        return func(*args, **kwargs)
    return wrapper

Analicemos lo que hace este código:

  • La función logged es nuestro decorador. Toma otra función, que llamamos func, como argumento. Esta func es la función a la que queremos agregar registro.
  • Cuando se aplica el decorador a una función, imprime un mensaje. Este mensaje nos dice que se está agregando registro a la función con el nombre dado.
  • Dentro de la función logged, definimos una función interna llamada wrapper. Esta función wrapper es la que reemplazará a la función original.
    • Cuando se llama a la función decorada, la función wrapper imprime un mensaje que dice que se está llamando a la función.
    • Luego llama a la función original (func) con todos los argumentos que se le pasaron. Los *args y **kwargs se utilizan para aceptar cualquier número de argumentos posicionales y de palabra clave.
    • Finalmente, devuelve el resultado de la función original.
  • La función logged devuelve la función wrapper. Esta función wrapper se utilizará ahora en lugar de la función original, agregando la funcionalidad de registro.

Usando el decorador

  1. Ahora, en el mismo directorio (/home/labex/project), crea otro archivo llamado sample.py con el siguiente código:
## sample.py

from logcall import logged

@logged
def add(x, y):
    return x + y

@logged
def sub(x, y):
    return x - y

La sintaxis @logged es muy importante aquí. Le dice a Python que aplique el decorador logged a las funciones add y sub. Entonces, cada vez que se llamen estas funciones, se ejecutará la funcionalidad de registro agregada por el decorador.

Probando el decorador

  1. Para probar tu decorador, abre una terminal en VSCode. Primero, cambia el directorio al directorio del proyecto usando el siguiente comando:
cd /home/labex/project

Luego, inicia el intérprete de Python:

python3
  1. En el intérprete de Python, importa el módulo sample y prueba las funciones decoradas:
>>> import sample
Adding logging to add
Adding logging to sub
>>> sample.add(3, 4)
Calling add
7
>>> sample.sub(2, 3)
Calling sub
-1
>>> exit()

Observa que cuando importas el módulo sample, se imprimen los mensajes "Adding logging to...". Esto se debe a que el decorador se aplica cuando se importa el módulo. Cada vez que llamas a una de las funciones decoradas, se imprime el mensaje "Calling...". Esto muestra que el decorador está funcionando como se esperaba.

Este decorador simple demuestra el concepto básico de los decoradores. Envuelve la función original con funcionalidad adicional (registro en este caso) sin cambiar el código de la función original. Esta es una característica poderosa en Python que puedes usar en muchos escenarios diferentes.

✨ Revisar Solución y Practicar

Construyendo un decorador de validación

En este paso, vamos a crear un decorador más práctico. Un decorador en Python es un tipo especial de función que puede modificar el comportamiento de otra función. El decorador que crearemos validará los argumentos de una función basándose en las anotaciones de tipo. Las anotaciones de tipo son una forma de especificar los tipos de datos esperados de los argumentos y el valor de retorno de una función. Este es un caso de uso común en aplicaciones del mundo real, ya que ayuda a garantizar que las funciones reciban los tipos de entrada correctos, lo que puede prevenir muchos errores.

Entendiendo las clases de validación

Ya hemos creado un archivo llamado validate.py para ti, y contiene algunas clases de validación. Las clases de validación se utilizan para comprobar si un valor cumple con ciertos criterios. Para ver lo que hay dentro de este archivo, debes abrirlo en el editor VSCode. Puedes hacer esto ejecutando los siguientes comandos en la terminal:

cd /home/labex/project
code validate.py

El archivo tiene tres clases:

  1. Validator - Esta es una clase base. Una clase base proporciona un marco general o estructura que otras clases pueden heredar. En este caso, proporciona la estructura básica para la validación.
  2. Integer - Esta clase de validador se utiliza para asegurarse de que un valor es un entero. Si pasas un valor que no es entero a una función que utiliza este validador, se generará un error.
  3. PositiveInteger - Esta clase de validador asegura que un valor es un entero positivo. Entonces, si pasas un entero negativo o cero, también se generará un error.

Agregando el decorador de validación

Ahora, vamos a agregar una función decorador llamada validated al archivo validate.py. Este decorador realizará varias tareas importantes:

  1. Inspeccionará las anotaciones de tipo de una función. Las anotaciones de tipo son como notas pequeñas que nos dicen qué tipo de datos espera la función.
  2. Validará los argumentos pasados a la función en contra de estas anotaciones de tipo. Esto significa que comprobará si los valores pasados a la función son del tipo correcto.
  3. También validará el valor de retorno de la función en contra de su anotación. Entonces, se asegura de que la función devuelva el tipo de datos que se supone que debe devolver.
  4. Si la validación falla, generará mensajes de error informativos. Estos mensajes te dirán exactamente qué salió mal, como qué argumento tenía el tipo incorrecto.

Agrega el siguiente código al final del archivo validate.py:

## Add to validate.py

import inspect
import functools

def validated(func):
    sig = inspect.signature(func)

    print(f'Validating {func.__name__} {sig}')

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        ## Bind arguments to the signature
        bound = sig.bind(*args, **kwargs)
        errors = []

        ## Validate each argument
        for name, value in bound.arguments.items():
            if name in sig.parameters:
                param = sig.parameters[name]
                if param.annotation != inspect.Parameter.empty:
                    try:
                        ## Create an instance of the validator and validate the value
                        if isinstance(param.annotation, type) and issubclass(param.annotation, Validator):
                            validator = param.annotation()
                            bound.arguments[name] = validator.validate(value)
                    except Exception as e:
                        errors.append(f'    {name}: {e}')

        ## If validation errors, raise an exception
        if errors:
            raise TypeError('Bad Arguments\n' + '\n'.join(errors))

        ## Call the function
        result = func(*bound.args, **bound.kwargs)

        ## Validate the return value
        if sig.return_annotation != inspect.Signature.empty:
            try:
                if isinstance(sig.return_annotation, type) and issubclass(sig.return_annotation, Validator):
                    validator = sig.return_annotation()
                    result = validator.validate(result)
            except Exception as e:
                raise TypeError(f'Bad return: {e}') from None

        return result

    return wrapper

Este código utiliza el módulo inspect de Python. El módulo inspect nos permite obtener información sobre objetos en tiempo de ejecución, como funciones. Aquí, lo usamos para examinar la firma de la función y validar los argumentos basados en las anotaciones de tipo. También usamos functools.wraps. Esta es una función auxiliar que preserva los metadatos de la función original, como su nombre y docstring. Los metadatos son como información adicional sobre la función que nos ayuda a entender lo que hace.

Probando el decorador de validación

Vamos a crear un archivo para probar nuestro decorador de validación. Crearemos un nuevo archivo llamado test_validate.py y agregaremos el siguiente código a él:

## test_validate.py

from validate import Integer, PositiveInteger, validated

@validated
def add(x: Integer, y: Integer) -> Integer:
    return x + y

@validated
def pow(x: Integer, y: Integer) -> Integer:
    return x ** y

## Test with a class
class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def cost(self):
        return self.shares * self.price

    @validated
    def sell(self, nshares: PositiveInteger):
        self.shares -= nshares

Ahora, probaremos nuestro decorador en el intérprete de Python. Primero, navega al directorio del proyecto y inicia el intérprete de Python ejecutando estos comandos en la terminal:

cd /home/labex/project
python3

Luego, en el intérprete de Python, podemos ejecutar el siguiente código para probar nuestro decorador:

>>> from test_validate import add, pow, Stock
Validating add (x: validate.Integer, y: validate.Integer) -> validate.Integer
Validating pow (x: validate.Integer, y: validate.Integer) -> validate.Integer
Validating sell (self, nshares: validate.PositiveInteger) -> <class 'inspect._empty'>
>>>
>>> ## Test with valid inputs
>>> add(2, 3)
5
>>>
>>> ## Test with invalid inputs
>>> add('2', '3')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/labex/project/validate.py", line 75, in wrapper
    raise TypeError('Bad Arguments\n' + '\n'.join(errors))
TypeError: Bad Arguments
    x: Expected <class 'int'>
    y: Expected <class 'int'>
>>>
>>> ## Test valid power
>>> pow(2, 3)
8
>>>
>>> ## Test with negative exponent (produces non - integer result)
>>> pow(2, -1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/labex/project/validate.py", line 83, in wrapper
    raise TypeError(f'Bad return: {e}') from None
TypeError: Bad return: Expected <class 'int'>
>>>
>>> ## Test with a class
>>> s = Stock("GOOG", 100, 490.1)
>>> s.sell(50)
>>> s.shares
50
>>>
>>> ## Test with invalid shares
>>> s.sell(-10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/labex/project/validate.py", line 75, in wrapper
    raise TypeError('Bad Arguments\n' + '\n'.join(errors))
TypeError: Bad Arguments
    nshares: Expected value > 0
>>> exit()

Como puedes ver, nuestro decorador validated ha aplicado con éxito la comprobación de tipos en los argumentos y valores de retorno de las funciones. Esto es muy útil porque hace que nuestro código sea más robusto. En lugar de dejar que los errores de tipo se propaguen más profundo en el código y causen errores difíciles de encontrar, los capturamos en los límites de las funciones.

✨ Revisar Solución y Practicar

Resumen

En este laboratorio (lab), has aprendido sobre los decoradores en Python, incluyendo qué son y cómo funcionan. También has dominado la creación de un decorador de registro (logging) simple para agregar comportamiento a las funciones y has construido uno más complejo para validar los argumentos de las funciones basado en las anotaciones de tipo. Además, has aprendido a usar el módulo inspect para analizar las firmas de las funciones y functools.wraps para preservar los metadatos de las funciones.

Los decoradores son una característica poderosa de Python que permite escribir código más mantenible y reutilizable. Se utilizan comúnmente en los marcos (frameworks) y bibliotecas de Python para preocupaciones transversales como el registro (logging), el control de acceso y el almacenamiento en caché (caching). Ahora puedes aplicar estas técnicas en tus propios proyectos de Python para obtener un código más limpio y mantenible.