Verificação de Tipos e Interfaces

Beginner

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

Introdução

Neste laboratório, você aprenderá a aprimorar sua compreensão de verificação de tipos (type checking) e interfaces em Python. Ao estender um módulo de formatação de tabelas, você implementará conceitos como classes base abstratas (abstract base classes) e validação de interfaces para criar um código mais robusto e sustentável.

Este laboratório se baseia em conceitos de exercícios anteriores, com foco na segurança de tipos e padrões de design de interfaces. Seus objetivos incluem a implementação de verificação de tipos para parâmetros de função, a criação e o uso de interfaces com classes base abstratas e a aplicação do padrão de método de modelo (template method pattern) para reduzir a duplicação de código. Você modificará tableformat.py, um módulo para formatar dados como tabelas, e reader.py, um módulo para ler arquivos CSV.

Este é um Lab Guiado, que fornece instruções passo a passo para ajudá-lo a aprender e praticar. Siga as instruções cuidadosamente para completar cada etapa e ganhar experiência prática. Dados históricos mostram que este é um laboratório de nível iniciante com uma taxa de conclusão de 92%. Recebeu uma taxa de avaliações positivas de 90% dos estudantes.

Nesta etapa, vamos aprimorar a função print_table() no arquivo tableformat.py. Adicionaremos uma verificação para ver se o parâmetro formatter é uma instância válida de TableFormatter. Por que precisamos disso? Bem, a verificação de tipos é como uma rede de segurança para o seu código. Ela ajuda a garantir que os dados com os quais você está trabalhando sejam do tipo correto, o que pode evitar muitos bugs difíceis de encontrar.

Compreendendo a Verificação de Tipos em Python

A verificação de tipos é uma técnica realmente útil na programação. Ela permite que você detecte erros no início do processo de desenvolvimento. Em Python, frequentemente lidamos com diferentes tipos de objetos, e às vezes esperamos que um determinado tipo de objeto seja passado para uma função. Para verificar se um objeto é de um tipo específico ou uma subclasse dele, podemos usar a função isinstance(). Por exemplo, se você tiver uma função que espera uma lista, pode usar isinstance() para garantir que a entrada seja de fato uma lista.

Primeiro, abra o arquivo tableformat.py no seu editor de código. Role para baixo até o final do arquivo e você encontrará a função print_table(). Veja como ela se parece 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 função recebe alguns dados, uma lista de colunas e um formatador. Em seguida, ela usa o formatador para imprimir uma tabela. Mas, no momento, ela não verifica se o formatador é do tipo correto.

Vamos modificá-la para adicionar a verificação de tipos. Usaremos a função isinstance() para verificar se o parâmetro formatter é uma instância de TableFormatter. Se não for, lançaremos um TypeError com uma mensagem clara. Aqui está o 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)

Testando sua Implementação de Verificação de Tipos

Agora que adicionamos a verificação de tipos, precisamos garantir que ela funcione. Vamos criar um novo arquivo Python chamado test_tableformat.py. Aqui está o código que você deve colocar nele:

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}")

Neste código, primeiro lemos alguns dados de portfólio. Em seguida, definimos uma nova classe formatadora chamada MyFormatter que não herda de TableFormatter. Tentamos usar este formatador não compatível na função print_table(). Se nossa verificação de tipos estiver funcionando, ela deverá lançar um TypeError.

Para executar o teste, abra seu terminal e navegue até o diretório onde o arquivo test_tableformat.py está localizado. Em seguida, execute o seguinte comando:

python test_tableformat.py

Se tudo estiver funcionando corretamente, você deverá ver uma saída como esta:

Test passed - caught error: Expected a TableFormatter

Esta saída confirma que nossa verificação de tipos está funcionando como esperado. Agora, a função print_table() aceitará apenas um formatador que seja uma instância de TableFormatter ou uma de suas subclasses.

Implementando uma Classe Base Abstrata

Nesta etapa, vamos converter a classe TableFormatter em uma classe base abstrata (CBA) adequada usando o módulo abc do Python. Mas, primeiro, vamos entender o que é uma classe base abstrata e por que precisamos dela.

Compreendendo as Classes Base Abstratas

Uma classe base abstrata é um tipo especial de classe em Python. É uma classe da qual você não pode criar um objeto diretamente, o que significa que você não pode instanciá-la. O principal objetivo de uma classe base abstrata é definir uma interface comum para suas subclasses. Ela define um conjunto de regras que todas as subclasses devem seguir. Especificamente, ela exige que as subclasses implementem certos métodos.

Aqui estão alguns conceitos-chave sobre classes base abstratas:

  • Usamos o módulo abc em Python para criar classes base abstratas.
  • Métodos marcados com o decorador @abstractmethod são como regras. Qualquer subclasse que herde de uma classe base abstrata deve implementar esses métodos.
  • Se você tentar criar um objeto de uma classe que herda de uma classe base abstrata, mas não implementou todos os métodos necessários, o Python lançará um erro.

Agora que você entende o básico das classes base abstratas, vamos ver como podemos modificar a classe TableFormatter para se tornar uma.

Modificando a Classe TableFormatter

Abra o arquivo tableformat.py. Vamos fazer algumas alterações na classe TableFormatter para que ela use o módulo abc e se torne uma classe base abstrata.

  1. Primeiro, precisamos importar as coisas necessárias do módulo abc. Adicione a seguinte instrução de importação no topo do arquivo:
## tableformat.py
from abc import ABC, abstractmethod

Esta instrução de importação traz duas coisas importantes: ABC, que é uma classe base para todas as classes base abstratas em Python, e abstractmethod, que é um decorador que usaremos para marcar métodos como abstratos.

  1. Em seguida, modificaremos a classe TableFormatter. Ela deve herdar de ABC para se tornar uma classe base abstrata, e marcaremos seus métodos como abstratos usando o decorador @abstractmethod. Veja como a classe modificada deve ser:
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

Observe algumas coisas sobre esta classe modificada:

  • A classe agora herda de ABC, o que significa que ela é oficialmente uma classe base abstrata.
  • Os métodos headings e row são decorados com @abstractmethod. Isso informa ao Python que qualquer subclasse de TableFormatter deve implementar esses métodos.
  • Substituímos o NotImplementedError por pass. O decorador @abstractmethod se encarrega de garantir que as subclasses implementem esses métodos, então não precisamos mais do NotImplementedError.

Testando sua Classe Base Abstrata

Agora que tornamos a classe TableFormatter uma classe base abstrata, vamos testar se ela funciona corretamente. Criaremos um arquivo chamado test_abc.py com o seguinte 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}")

Neste código, temos dois casos de teste. O primeiro caso de teste define uma classe NewFormatter que tenta herdar de TableFormatter, mas tem um nome de método com erro de ortografia. O segundo caso de teste define uma classe ProperFormatter que implementa corretamente todos os métodos necessários.

Para executar o teste, abra seu terminal e execute o seguinte comando:

python test_abc.py

Você deve ver uma saída semelhante a esta:

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

Esta saída confirma que nossa classe base abstrata está funcionando como esperado. O primeiro caso de teste falha porque a classe NewFormatter não implementou o método headings corretamente. O segundo caso de teste passa porque a classe ProperFormatter implementou todos os métodos necessários.

Criando Classes de Modelo de Algoritmo

Nesta etapa, vamos usar classes base abstratas para implementar um padrão de método de modelo (template method pattern). O objetivo é reduzir a duplicação de código na funcionalidade de análise de CSV. A duplicação de código pode tornar seu código mais difícil de manter e atualizar. Ao usar o padrão de método de modelo, podemos criar uma estrutura comum para nosso código de análise de CSV e deixar que as subclasses lidem com os detalhes específicos.

Compreendendo o Padrão de Método de Modelo

O padrão de método de modelo é um padrão de design comportamental. É como um projeto para um algoritmo. Em um método, ele define a estrutura geral ou o "esqueleto" de um algoritmo. No entanto, ele não implementa totalmente todas as etapas. Em vez disso, ele adia algumas das etapas para as subclasses. Isso significa que as subclasses podem redefinir certas partes do algoritmo sem alterar sua estrutura geral.

Em nosso caso, se você olhar o arquivo reader.py, notará que as funções read_csv_as_dicts() e read_csv_as_instances() têm muito código semelhante. A principal diferença entre elas é como elas criam registros a partir das linhas no arquivo CSV. Ao usar o padrão de método de modelo, podemos evitar escrever o mesmo código várias vezes.

Adicionando a Classe Base CSVParser

Vamos começar adicionando uma classe base abstrata para nossa análise de CSV. Abra o arquivo reader.py. Adicionaremos a classe base abstrata CSVParser logo no início do arquivo, logo após as instruções de importação.

## 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 classe CSVParser serve como um modelo para a análise de CSV. O método parse contém as etapas comuns para ler um arquivo CSV, como abrir o arquivo, obter os cabeçalhos e iterar sobre as linhas. A lógica específica para criar um registro a partir de uma linha é abstraída no método make_record(). Como é um método abstrato, qualquer classe que herde de CSVParser deve implementar este método.

Implementando as Classes Parser Concretas

Agora que temos nossa classe base, precisamos criar as classes parser concretas. Essas classes implementarão a lógica específica de criação 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)

A classe DictCSVParser é usada para criar registros como dicionários. Ela recebe uma lista de tipos em seu construtor. O método make_record usa esses tipos para converter os valores na linha e criar um dicionário.

A classe InstanceCSVParser é usada para criar registros como instâncias de uma classe. Ela recebe uma classe em seu construtor. O método make_record chama o método from_row dessa classe para criar uma instância a partir da linha.

Refatorando as Funções Originais

Agora, vamos refatorar as funções originais read_csv_as_dicts() e read_csv_as_instances() para usar essas novas classes.

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)

Essas funções refatoradas têm a mesma interface que as originais. Mas internamente, elas usam as novas classes parser que acabamos de criar. Dessa forma, separamos a lógica comum de análise de CSV da lógica específica de criação de registros.

Testando sua Implementação

Vamos verificar se nosso código refatorado funciona corretamente. Crie um arquivo chamado test_reader.py e adicione o seguinte código a ele.

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 executar o teste, abra seu terminal e execute o seguinte comando:

python test_reader.py

Você deve ver uma saída semelhante 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}

Se você vir esta saída, significa que seu código refatorado está funcionando corretamente. Tanto as funções originais quanto o uso direto de parsers estão produzindo os resultados esperados.

Resumo

Neste laboratório, você aprendeu vários conceitos-chave de programação orientada a objetos (object-oriented programming) para aprimorar o código Python. Primeiro, você implementou a verificação de tipos (type checking) na função print_table(), que garante que apenas formatadores válidos sejam usados, melhorando assim a robustez do código. Em segundo lugar, você transformou a classe TableFormatter em uma classe base abstrata (abstract base class), forçando as subclasses a implementar métodos específicos.

Além disso, você aplicou o padrão de método de modelo (template method pattern) criando a classe base abstrata CSVParser e suas implementações concretas. Isso reduz a duplicação de código, mantendo uma estrutura de algoritmo consistente. Essas técnicas são cruciais para criar um código Python mais sustentável e robusto, especialmente em aplicações em larga escala. Para aprofundar seu aprendizado, explore as dicas de tipo (type hints) em Python (PEP 484), classes de protocolo e padrões de design em Python.