Cómo implementar el diseño por contrato en Python

PythonPythonBeginner
Practicar Ahora

💡 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

Este tutorial lo guiará a través del proceso de implementar el diseño por contrato (design by contract) en Python, una técnica de programación que ayuda a garantizar la confiabilidad y mantenibilidad del código. Exploraremos los principios fundamentales del diseño por contrato y profundizaremos en ejemplos prácticos y casos de uso para sus proyectos de Python.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/constructor("Constructor") python/ObjectOrientedProgrammingGroup -.-> python/inheritance("Inheritance") python/ObjectOrientedProgrammingGroup -.-> python/polymorphism("Polymorphism") python/ObjectOrientedProgrammingGroup -.-> python/encapsulation("Encapsulation") subgraph Lab Skills python/classes_objects -.-> lab-398022{{"Cómo implementar el diseño por contrato en Python"}} python/constructor -.-> lab-398022{{"Cómo implementar el diseño por contrato en Python"}} python/inheritance -.-> lab-398022{{"Cómo implementar el diseño por contrato en Python"}} python/polymorphism -.-> lab-398022{{"Cómo implementar el diseño por contrato en Python"}} python/encapsulation -.-> lab-398022{{"Cómo implementar el diseño por contrato en Python"}} end

Introducción al diseño por contrato

El diseño por contrato (Design by Contract, DbC) es una metodología de ingeniería de software que enfatiza la especificación formal del comportamiento de los componentes de software a través del uso de contratos. Un contrato es un acuerdo formal entre un cliente (el llamador de una función o método) y un proveedor (la implementación de la función o método) que especifica los derechos y obligaciones de ambas partes.

Los principios clave del diseño por contrato son:

Precondiciones

Las precondiciones son los requisitos que el cliente debe cumplir antes de llamar a una función o método. Definen el rango de entrada válido, el estado del sistema y cualquier otra condición necesaria para que la función o método se ejecute correctamente.

Postcondiciones

Las postcondiciones son las garantías que el proveedor ofrece al cliente después de que se haya ejecutado la función o método. Definen la salida esperada, el estado del sistema y cualquier otra propiedad que será verdadera una vez finalizada la ejecución de la función o método.

Invariantes de clase

Los invariantes de clase son condiciones que deben ser verdaderas para todas las instancias de una clase, tanto antes como después de la ejecución de cualquier método público. Aseguran la coherencia general y la validez del estado de la clase.

Al definir estos contratos, tanto el cliente como el proveedor pueden tener una comprensión clara del comportamiento esperado de los componentes de software, lo que puede conducir a un código más robusto, confiable y mantenible.

graph LR A[Client] -- Preconditions --> B[Function/Method] B -- Postconditions --> A B -- Class Invariants --> B

En la siguiente sección, exploraremos cómo implementar el diseño por contrato en Python.

Implementando el diseño por contrato en Python

Python no tiene soporte incorporado para el diseño por contrato (Design by Contract), pero hay varias bibliotecas y marcos de trabajo de terceros que se pueden utilizar para implementar esta metodología. Una opción popular es la biblioteca contracts, que proporciona una forma simple e intuitiva de definir y aplicar contratos en Python.

Usando la biblioteca contracts

Para usar la biblioteca contracts, puedes instalarla utilizando pip:

pip install contracts

Una vez instalada, puedes definir precondiciones, postcondiciones y invariantes de clase utilizando el decorador @contract proporcionado por la biblioteca.

Precondiciones

A continuación, se muestra un ejemplo de cómo definir una precondición utilizando el decorador @contract:

from contracts import contract

@contract(x='int,>=0', y='int,>=0')
def add_numbers(x, y):
    return x + y

En este ejemplo, el decorador @contract especifica que la función add_numbers espera dos argumentos enteros no negativos.

Postcondiciones

También puedes definir postcondiciones utilizando el decorador @contract:

from contracts import contract

@contract(x='int,>=0', y='int,>=0', returns='int,>=0')
def add_numbers(x, y):
    return x + y

En este ejemplo, el decorador @contract especifica que la función add_numbers debe devolver un entero no negativo.

Invariantes de clase

Para definir invariantes de clase, puedes utilizar el decorador @invariant proporcionado por la biblioteca contracts:

from contracts import contract, invariant

class BankAccount:
    @invariant('balance >= 0')
    def __init__(self, initial_balance):
        self.balance = initial_balance

    @contract(amount='int,>=0')
    def deposit(self, amount):
        self.balance += amount

    @contract(amount='int,>=0', returns='bool')
    def withdraw(self, amount):
        if self.balance >= amount:
            self.balance -= amount
            return True
        else:
            return False

En este ejemplo, el decorador @invariant asegura que el atributo balance de la clase BankAccount siempre sea no negativo.

Al utilizar la biblioteca contracts, puedes implementar el diseño por contrato en tus proyectos de Python, lo que conduce a un código más robusto y mantenible.

Ejemplos prácticos y casos de uso

El diseño por contrato (Design by Contract) se puede aplicar a una amplia gama de proyectos de Python, desde pequeños scripts hasta aplicaciones a gran escala. A continuación, se presentan algunos ejemplos prácticos y casos de uso:

Validación de datos

Un caso de uso común del diseño por contrato es la validación de datos. Al definir precondiciones y postcondiciones, se puede garantizar que las funciones y métodos solo operen con datos de entrada válidos y que los datos de salida cumplan con ciertos requisitos.

Por ejemplo, considere una función que calcula el promedio de una lista de números:

from contracts import contract

@contract(numbers='list[N](float,>=0)', returns='float,>=0')
def calculate_average(numbers):
    return sum(numbers) / len(numbers)

En este ejemplo, el decorador @contract especifica que la función calculate_average espera una lista no vacía de números de punto flotante no negativos y que debe devolver un número de punto flotante no negativo.

Diseño de API

El diseño por contrato también puede ser útil al diseñar API, ya que ayuda a definir claramente el comportamiento esperado de las funciones y métodos de la API. Esto puede hacer que la API sea más intuitiva y fácil de usar, y también puede ayudar a detectar errores y casos extremos desde el principio del proceso de desarrollo.

Por ejemplo, considere una API simple para una aplicación de lista de tareas:

from contracts import contract

class TodoList:
    @invariant('len(tasks) >= 0')
    def __init__(self):
        self.tasks = []

    @contract(task='str,len(x)>0')
    def add_task(self, task):
        self.tasks.append(task)

    @contract(index='int,>=0,<len(tasks)', returns='str,len(x)>0')
    def get_task(self, index):
        return self.tasks[index]

    @contract(index='int,>=0,<len(tasks)')
    def remove_task(self, index):
        del self.tasks[index]

En este ejemplo, la clase TodoList define varios métodos con precondiciones y postcondiciones que garantizan que la API se comporte como se espera. Por ejemplo, el método add_task requiere una cadena no vacía como argumento, y el método get_task devuelve una cadena no vacía.

Pruebas unitarias

El diseño por contrato también puede ser útil para escribir pruebas unitarias más efectivas. Al definir el comportamiento esperado de las funciones y métodos utilizando contratos, se pueden escribir más fácilmente casos de prueba que cubran todo el rango de posibles entradas y salidas.

Por ejemplo, considere la siguiente prueba unitaria para la función calculate_average:

from contracts import new_contract
from unittest import TestCase

new_contract('non_empty_list', 'list[N](float,>=0) and len(x) > 0')

class TestCalculateAverage(TestCase):
    @contract(numbers='non_empty_list')
    def test_calculate_average(self, numbers):
        expected_average = sum(numbers) / len(numbers)
        actual_average = calculate_average(numbers)
        self.assertAlmostEqual(expected_average, actual_average)

En este ejemplo, la función new_contract se utiliza para definir un tipo de contrato personalizado llamado non_empty_list, que luego se utiliza en el método test_calculate_average para garantizar que la lista de números de entrada no esté vacía.

Al utilizar el diseño por contrato en sus proyectos de Python, puede crear un código más robusto, confiable y mantenible, y mejorar la calidad general y la capacidad de prueba de su software.

Resumen

En este completo tutorial de Python, has aprendido cómo implementar el diseño por contrato (design by contract), una poderosa técnica de programación que ayuda a garantizar la confiabilidad y mantenibilidad del código. Al entender los principios del diseño por contrato y aplicarlos a tus proyectos de Python, puedes escribir un código más robusto y bien documentado, lo que facilita la colaboración, la depuración y el mantenimiento a lo largo del tiempo.