Comprobación de tipos e interfaces

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á a mejorar su comprensión de la comprobación de tipos e interfaces en Python. Al extender un módulo de formato de tablas, implementará conceptos como clases base abstractas y validación de interfaces para crear un código más robusto y mantenible.

Este laboratorio se basa en conceptos de ejercicios anteriores, centrándose en la seguridad de tipos y patrones de diseño de interfaces. Sus objetivos incluyen implementar la comprobación de tipos para parámetros de función, crear y utilizar interfaces con clases base abstractas y aplicar el patrón de método de plantilla para reducir la duplicación de código. Modificará tableformat.py, un módulo para formatear datos en tablas, y reader.py, un módulo para leer archivos CSV.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ErrorandExceptionHandlingGroup(["Error and Exception Handling"]) python(("Python")) -.-> python/ControlFlowGroup(["Control Flow"]) python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python/ControlFlowGroup -.-> python/conditional_statements("Conditional Statements") python/FunctionsGroup -.-> python/function_definition("Function Definition") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/inheritance("Inheritance") python/ErrorandExceptionHandlingGroup -.-> python/catching_exceptions("Catching Exceptions") python/ErrorandExceptionHandlingGroup -.-> python/raising_exceptions("Raising Exceptions") subgraph Lab Skills python/conditional_statements -.-> lab-132497{{"Comprobación de tipos e interfaces"}} python/function_definition -.-> lab-132497{{"Comprobación de tipos e interfaces"}} python/classes_objects -.-> lab-132497{{"Comprobación de tipos e interfaces"}} python/inheritance -.-> lab-132497{{"Comprobación de tipos e interfaces"}} python/catching_exceptions -.-> lab-132497{{"Comprobación de tipos e interfaces"}} python/raising_exceptions -.-> lab-132497{{"Comprobación de tipos e interfaces"}} end

En este paso, vamos a mejorar la función print_table() en el archivo tableformat.py. Agregaremos una comprobación para verificar si el parámetro formatter es una instancia válida de TableFormatter. ¿Por qué necesitamos esto? Bueno, la comprobación de tipos es como una red de seguridad para tu código. Ayuda a asegurarse de que los datos con los que estás trabajando son del tipo correcto, lo que puede prevenir muchos errores difíciles de encontrar.

Comprender la comprobación de tipos en Python

La comprobación de tipos es una técnica realmente útil en la programación. Te permite detectar errores temprano en el proceso de desarrollo. En Python, a menudo trabajamos con diferentes tipos de objetos, y a veces esperamos que se pase un cierto tipo de objeto a una función. Para comprobar si un objeto es de un tipo específico o una subclase de él, podemos usar la función isinstance(). Por ejemplo, si tienes una función que espera una lista, puedes usar isinstance() para asegurarte de que la entrada es realmente una lista.

Primero, abre el archivo tableformat.py en tu editor de código. Desplázate hacia abajo hasta el final del archivo, y encontrarás la función print_table(). Así es como se ve inicialmente:

def print_table(data, columns, formatter):
    '''
    Print a table showing selected columns from a data source
    using the given formatter.
    '''
    formatter.headings(columns)
    for item in data:
        rowdata = [str(getattr(item, col)) for col in columns]
        formatter.row(rowdata)

Esta función recibe algunos datos, una lista de columnas y un formateador. Luego, utiliza el formateador para imprimir una tabla. Pero en este momento, no comprueba si el formateador es del tipo correcto.

Vamos a modificarla para agregar la comprobación de tipos. Usaremos la función isinstance() para comprobar si el parámetro formatter es una instancia de TableFormatter. Si no lo es, lanzaremos un TypeError con un mensaje claro. Aquí está el código modificado:

def print_table(data, columns, formatter):
    '''
    Print a table showing selected columns from a data source
    using the given formatter.
    '''
    if not isinstance(formatter, TableFormatter):
        raise TypeError("Expected a TableFormatter")

    formatter.headings(columns)
    for item in data:
        rowdata = [str(getattr(item, col)) for col in columns]
        formatter.row(rowdata)

Probar la implementación de la comprobación de tipos

Ahora que hemos agregado la comprobación de tipos, necesitamos asegurarnos de que funcione. Vamos a crear un nuevo archivo Python llamado test_tableformat.py. Aquí está el código que debes poner en él:

import stock
import reader
import tableformat

## Read portfolio data
portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)

## Define a formatter that doesn't inherit from TableFormatter
class MyFormatter:
    def headings(self, headers):
        pass
    def row(self, rowdata):
        pass

## Try to use the non-compliant formatter
try:
    tableformat.print_table(portfolio, ['name', 'shares', 'price'], MyFormatter())
    print("Test failed - type checking not implemented")
except TypeError as e:
    print(f"Test passed - caught error: {e}")

En este código, primero leemos algunos datos de cartera. Luego definimos una nueva clase de formateador llamada MyFormatter que no hereda de TableFormatter. Intentamos usar este formateador no compatible en la función print_table(). Si nuestra comprobación de tipos está funcionando, debería lanzar un TypeError.

Para ejecutar la prueba, abre tu terminal y navega hasta el directorio donde se encuentra el archivo test_tableformat.py. Luego ejecuta el siguiente comando:

python test_tableformat.py

Si todo está funcionando correctamente, deberías ver una salida como esta:

Test passed - caught error: Expected a TableFormatter

Esta salida confirma que nuestra comprobación de tipos está funcionando como se esperaba. Ahora, la función print_table() solo aceptará un formateador que sea una instancia de TableFormatter o una de sus subclases.

✨ Revisar Solución y Practicar

Implementar una clase base abstracta

En este paso, vamos a convertir la clase TableFormatter en una clase base abstracta (ABC, por sus siglas en inglés) adecuada utilizando el módulo abc de Python. Pero primero, entendamos qué es una clase base abstracta y por qué la necesitamos.

Comprender las clases base abstractas

Una clase base abstracta es un tipo especial de clase en Python. Es una clase de la que no se puede crear un objeto directamente, lo que significa que no se puede instanciar. El propósito principal de una clase base abstracta es definir una interfaz común para sus subclases. Establece un conjunto de reglas que todas las subclases deben seguir. Específicamente, requiere que las subclases implementen ciertos métodos.

Aquí hay algunos conceptos clave sobre las clases base abstractas:

  • Usamos el módulo abc en Python para crear clases base abstractas.
  • Los métodos marcados con el decorador @abstractmethod son como reglas. Cualquier subclase que herede de una clase base abstracta debe implementar estos métodos.
  • Si intentas crear un objeto de una clase que hereda de una clase base abstracta pero no ha implementado todos los métodos requeridos, Python lanzará un error.

Ahora que entiendes los conceptos básicos de las clases base abstractas, veamos cómo podemos modificar la clase TableFormatter para que se convierta en una.

Modificar la clase TableFormatter

Abre el archivo tableformat.py. Vamos a hacer algunos cambios en la clase TableFormatter para que utilice el módulo abc y se convierta en una clase base abstracta.

  1. Primero, necesitamos importar las cosas necesarias del módulo abc. Agrega la siguiente declaración de importación en la parte superior del archivo:
## tableformat.py
from abc import ABC, abstractmethod

Esta declaración de importación trae dos cosas importantes: ABC, que es una clase base para todas las clases base abstractas en Python, y abstractmethod, que es un decorador que usaremos para marcar métodos como abstractos.

  1. A continuación, modificaremos la clase TableFormatter. Debería heredar de ABC para convertirse en una clase base abstracta, y marcaremos sus métodos como abstractos utilizando el decorador @abstractmethod. Así es como debería verse la clase modificada:
class TableFormatter(ABC):
    @abstractmethod
    def headings(self, headers):
        '''
        Emit the table headings.
        '''
        pass

    @abstractmethod
    def row(self, rowdata):
        '''
        Emit a single row of table data.
        '''
        pass

Observa algunas cosas sobre esta clase modificada:

  • La clase ahora hereda de ABC, lo que significa que es oficialmente una clase base abstracta.
  • Tanto el método headings como el método row están decorados con @abstractmethod. Esto le dice a Python que cualquier subclase de TableFormatter debe implementar estos métodos.
  • Reemplazamos el NotImplementedError con pass. El decorador @abstractmethod se encarga de asegurarse de que las subclases implementen estos métodos, por lo que ya no necesitamos el NotImplementedError.

Probar tu clase base abstracta

Ahora que hemos convertido la clase TableFormatter en una clase base abstracta, probemos si funciona correctamente. Crearemos un archivo llamado test_abc.py con el siguiente código:

from tableformat import TableFormatter

## Test case 1: Define a class with a misspelled method
try:
    class NewFormatter(TableFormatter):
        def headers(self, headings):  ## Misspelled 'headings'
            pass
        def row(self, rowdata):
            pass

    f = NewFormatter()
    print("Test 1 failed - abstract method enforcement not working")
except TypeError as e:
    print(f"Test 1 passed - caught error: {e}")

## Test case 2: Define a class that properly implements all methods
try:
    class ProperFormatter(TableFormatter):
        def headings(self, headers):
            pass
        def row(self, rowdata):
            pass

    f = ProperFormatter()
    print("Test 2 passed - proper implementation works")
except TypeError as e:
    print(f"Test 2 failed - error: {e}")

En este código, tenemos dos casos de prueba. El primer caso de prueba define una clase NewFormatter que intenta heredar de TableFormatter pero tiene un nombre de método mal escrito. El segundo caso de prueba define una clase ProperFormatter que implementa correctamente todos los métodos requeridos.

Para ejecutar la prueba, abre tu terminal y ejecuta el siguiente comando:

python test_abc.py

Deberías ver una salida similar a esta:

Test 1 passed - caught error: Can't instantiate abstract class NewFormatter with abstract methods headings
Test 2 passed - proper implementation works

Esta salida confirma que nuestra clase base abstracta está funcionando como se esperaba. El primer caso de prueba falla porque la clase NewFormatter no implementó correctamente el método headings. El segundo caso de prueba pasa porque la clase ProperFormatter implementó todos los métodos requeridos.

✨ Revisar Solución y Practicar

Creación de clases de plantilla de algoritmo

En este paso, vamos a utilizar clases base abstractas para implementar un patrón de método plantilla. El objetivo es reducir la duplicación de código en la funcionalidad de análisis de archivos CSV. La duplicación de código puede dificultar el mantenimiento y la actualización de tu código. Al utilizar el patrón de método plantilla, podemos crear una estructura común para nuestro código de análisis de CSV y permitir que las subclases manejen los detalles específicos.

Comprender el patrón de método plantilla

El patrón de método plantilla es un patrón de diseño de comportamiento. Es como un plano para un algoritmo. En un método, define la estructura general o el "esqueleto" de un algoritmo. Sin embargo, no implementa completamente todos los pasos. En lugar de eso, pospone algunos de los pasos a las subclases. Esto significa que las subclases pueden redefinir ciertas partes del algoritmo sin cambiar su estructura general.

En nuestro caso, si miras el archivo reader.py, notarás que las funciones read_csv_as_dicts() y read_csv_as_instances() tienen mucho código similar. La principal diferencia entre ellas es cómo crean registros a partir de las filas del archivo CSV. Al utilizar el patrón de método plantilla, podemos evitar escribir el mismo código varias veces.

Agregar la clase base CSVParser

Comencemos agregando una clase base abstracta para nuestro análisis de CSV. Abre el archivo reader.py. Agregaremos la clase base abstracta CSVParser justo en la parte superior del archivo, después de las declaraciones de importación.

## reader.py
import csv
from abc import ABC, abstractmethod

class CSVParser(ABC):
    def parse(self, filename):
        records = []
        with open(filename) as f:
            rows = csv.reader(f)
            headers = next(rows)
            for row in rows:
                record = self.make_record(headers, row)
                records.append(record)
        return records

    @abstractmethod
    def make_record(self, headers, row):
        pass

Esta clase CSVParser sirve como una plantilla para el análisis de CSV. El método parse contiene los pasos comunes para leer un archivo CSV, como abrir el archivo, obtener los encabezados y recorrer las filas. La lógica específica para crear un registro a partir de una fila se abstrae en el método make_record(). Dado que es un método abstracto, cualquier clase que herede de CSVParser debe implementar este método.

Implementar las clases de analizador concretas

Ahora que tenemos nuestra clase base, necesitamos crear las clases de analizador concretas. Estas clases implementarán la lógica específica de creación de registros.

class DictCSVParser(CSVParser):
    def __init__(self, types):
        self.types = types

    def make_record(self, headers, row):
        return { name: func(val) for name, func, val in zip(headers, self.types, row) }

class InstanceCSVParser(CSVParser):
    def __init__(self, cls):
        self.cls = cls

    def make_record(self, headers, row):
        return self.cls.from_row(row)

La clase DictCSVParser se utiliza para crear registros como diccionarios. Toma una lista de tipos en su constructor. El método make_record utiliza estos tipos para convertir los valores de la fila y crear un diccionario.

La clase InstanceCSVParser se utiliza para crear registros como instancias de una clase. Toma una clase en su constructor. El método make_record llama al método from_row de esa clase para crear una instancia a partir de la fila.

Refactorizar las funciones originales

Ahora, refactoricemos las funciones originales read_csv_as_dicts() y read_csv_as_instances() para utilizar estas nuevas clases.

def read_csv_as_dicts(filename, types):
    '''
    Read a CSV file into a list of dictionaries with appropriate type conversion.
    '''
    parser = DictCSVParser(types)
    return parser.parse(filename)

def read_csv_as_instances(filename, cls):
    '''
    Read a CSV file into a list of instances of a class.
    '''
    parser = InstanceCSVParser(cls)
    return parser.parse(filename)

Estas funciones refactorizadas tienen la misma interfaz que las originales. Pero internamente, utilizan las nuevas clases de analizador que acabamos de crear. De esta manera, hemos separado la lógica común de análisis de CSV de la lógica específica de creación de registros.

Probar tu implementación

Veamos si nuestro código refactorizado funciona correctamente. Crea un archivo llamado test_reader.py y agrega el siguiente código a él.

import reader
import stock

## Test the refactored read_csv_as_instances function
portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
print("First stock:", portfolio[0])

## Test the refactored read_csv_as_dicts function
portfolio_dicts = reader.read_csv_as_dicts('portfolio.csv', [str, int, float])
print("First stock as dict:", portfolio_dicts[0])

## Test direct use of a parser
parser = reader.DictCSVParser([str, int, float])
portfolio_dicts2 = parser.parse('portfolio.csv')
print("First stock from direct parser:", portfolio_dicts2[0])

Para ejecutar la prueba, abre tu terminal y ejecuta el siguiente comando:

python test_reader.py

Deberías ver una salida similar a esta:

First stock: Stock('AA', 100, 32.2)
First stock as dict: {'name': 'AA', 'shares': 100, 'price': 32.2}
First stock from direct parser: {'name': 'AA', 'shares': 100, 'price': 32.2}

Si ves esta salida, significa que tu código refactorizado está funcionando correctamente. Tanto las funciones originales como el uso directo de los analizadores están produciendo los resultados esperados.

✨ Revisar Solución y Practicar

Resumen

En este laboratorio, has aprendido varios conceptos clave de programación orientada a objetos para mejorar el código de Python. En primer lugar, implementaste la comprobación de tipos en la función print_table(), lo que garantiza que solo se utilicen formateadores válidos, mejorando así la robustez del código. En segundo lugar, transformaste la clase TableFormatter en una clase base abstracta, obligando a las subclases a implementar métodos específicos.

Además, aplicaste el patrón de método plantilla creando la clase base abstracta CSVParser y sus implementaciones concretas. Esto reduce la duplicación de código mientras se mantiene una estructura de algoritmo consistente. Estas técnicas son cruciales para crear código de Python más mantenible y robusto, especialmente en aplicaciones a gran escala. Para profundizar en tu aprendizaje, explora las sugerencias de tipos en Python (PEP 484), las clases de protocolo y los patrones de diseño en Python.