Проверка типов и интерфейсы

PythonPythonBeginner
Практиковаться сейчас

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

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

В этом практическом занятии (лабораторной работе) вы научитесь углублять свое понимание проверки типов и интерфейсов в Python. Расширив модуль форматирования таблиц, вы реализуете такие концепции, как абстрактные базовые классы и валидация интерфейсов, чтобы создать более надежный и поддерживаемый код.

Эта лабораторная работа основана на концепциях предыдущих упражнений и сосредоточена на безопасности типов и шаблонах проектирования интерфейсов. Ваши задачи включают в себя реализацию проверки типов для параметров функций, создание и использование интерфейсов с помощью абстрактных базовых классов, а также применение шаблона "Шаблонный метод" для уменьшения дублирования кода. Вы будете модифицировать tableformat.py, модуль для форматирования данных в виде таблиц, и reader.py, модуль для чтения CSV-файлов.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python(("Python")) -.-> python/ErrorandExceptionHandlingGroup(["Error and Exception Handling"]) python(("Python")) -.-> python/ControlFlowGroup(["Control Flow"]) 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{{"Проверка типов и интерфейсы"}} python/function_definition -.-> lab-132497{{"Проверка типов и интерфейсы"}} python/classes_objects -.-> lab-132497{{"Проверка типов и интерфейсы"}} python/inheritance -.-> lab-132497{{"Проверка типов и интерфейсы"}} python/catching_exceptions -.-> lab-132497{{"Проверка типов и интерфейсы"}} python/raising_exceptions -.-> lab-132497{{"Проверка типов и интерфейсы"}} end

На этом этапе мы улучшим функцию print_table() в файле tableformat.py. Мы добавим проверку, является ли параметр formatter допустимым экземпляром TableFormatter. Почему это необходимо? Проверка типов - это своего рода страховка для вашего кода. Она помогает убедиться, что данные, с которыми вы работаете, имеют правильный тип, что может предотвратить множество трудноуловимых ошибок.

Понимание проверки типов в Python

Проверка типов - это очень полезная техника в программировании. Она позволяет выявлять ошибки на ранних этапах разработки. В Python мы часто работаем с разными типами объектов, и иногда мы ожидаем, что в функцию будет передан объект определенного типа. Чтобы проверить, является ли объект определенного типа или его подклассом, мы можем использовать функцию isinstance(). Например, если у вас есть функция, которая ожидает список, вы можете использовать isinstance(), чтобы убедиться, что входные данные действительно являются списком.

Сначала откройте файл tableformat.py в вашем редакторе кода. Прокрутите файл до конца, и вы найдете функцию print_table(). Вот как она выглядит изначально:

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)

Эта функция принимает некоторые данные, список столбцов и форматер. Затем она использует форматер для вывода таблицы. Но в данный момент она не проверяет, является ли форматер правильного типа.

Давайте изменим ее, чтобы добавить проверку типов. Мы будем использовать функцию isinstance(), чтобы проверить, является ли параметр formatter экземпляром TableFormatter. Если это не так, мы вызовем исключение TypeError с четким сообщением. Вот модифицированный код:

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)

Тестирование реализации проверки типов

Теперь, когда мы добавили проверку типов, нам нужно убедиться, что она работает. Давайте создадим новый Python-файл с именем test_tableformat.py. Вот код, который вы должны поместить в него:

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

В этом коде мы сначала считываем данные о портфеле. Затем мы определяем новый класс форматера с именем MyFormatter, который не наследуется от TableFormatter. Мы пытаемся использовать этот несоответствующий форматер в функции print_table(). Если наша проверка типов работает, она должна вызвать исключение TypeError.

Чтобы запустить тест, откройте терминал и перейдите в каталог, где находится файл test_tableformat.py. Затем выполните следующую команду:

python test_tableformat.py

Если все работает правильно, вы должны увидеть вывод, похожий на следующий:

Test passed - caught error: Expected a TableFormatter

Этот вывод подтверждает, что наша проверка типов работает как ожидалось. Теперь функция print_table() будет принимать только форматеры, которые являются экземплярами TableFormatter или его подклассов.

✨ Проверить решение и практиковаться

Реализация абстрактного базового класса

На этом этапе мы преобразуем класс TableFormatter в правильный абстрактный базовый класс (ABC) с использованием модуля abc в Python. Но сначала разберемся, что такое абстрактный базовый класс и зачем он нужен.

Понимание абстрактных базовых классов

Абстрактный базовый класс - это особый тип класса в Python. Это класс, из которого нельзя напрямую создавать объекты, то есть его нельзя инстанцировать. Основная цель абстрактного базового класса - определить общий интерфейс для своих подклассов. Он устанавливает набор правил, которым должны следовать все подклассы. В частности, он требует от подклассов реализовать определенные методы.

Вот несколько ключевых концепций, связанных с абстрактными базовыми классами:

  • В Python для создания абстрактных базовых классов используется модуль abc.
  • Методы, помеченные декоратором @abstractmethod, являются своего рода правилами. Любой подкласс, наследующийся от абстрактного базового класса, должен реализовать эти методы.
  • Если вы попытаетесь создать объект класса, который наследуется от абстрактного базового класса, но не реализует все обязательные методы, Python вызовет ошибку.

Теперь, когда вы понимаете основы абстрактных базовых классов, посмотрим, как можно изменить класс TableFormatter, чтобы он стал таким классом.

Изменение класса TableFormatter

Откройте файл tableformat.py. Мы внесем некоторые изменения в класс TableFormatter, чтобы он использовал модуль abc и стал абстрактным базовым классом.

  1. Сначала нам нужно импортировать необходимые элементы из модуля abc. Добавьте следующую инструкцию импорта в начало файла:
## tableformat.py
from abc import ABC, abstractmethod

Эта инструкция импорта добавляет два важных элемента: ABC, который является базовым классом для всех абстрактных базовых классов в Python, и abstractmethod, который является декоратором, с помощью которого мы будем помечать методы как абстрактные.

  1. Затем мы изменим класс TableFormatter. Он должен наследоваться от ABC, чтобы стать абстрактным базовым классом, и мы пометим его методы как абстрактные с использованием декоратора @abstractmethod. Вот как должен выглядеть измененный класс:
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

Обратите внимание на несколько аспектов этого измененного класса:

  • Теперь класс наследуется от ABC, что означает, что он официально стал абстрактным базовым классом.
  • Оба метода headings и row помечены декоратором @abstractmethod. Это сообщает Python, что любой подкласс TableFormatter должен реализовать эти методы.
  • Мы заменили NotImplementedError на pass. Декоратор @abstractmethod отвечает за то, чтобы убедиться, что подклассы реализуют эти методы, поэтому нам больше не нужен NotImplementedError.

Тестирование абстрактного базового класса

Теперь, когда мы сделали класс TableFormatter абстрактным базовым классом, проверим, работает ли он правильно. Мы создадим файл с именем test_abc.py со следующим кодом:

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

В этом коде есть два тестовых случая. В первом тестовом случае определен класс NewFormatter, который пытается наследоваться от TableFormatter, но имеет ошибку в имени метода. Во втором тестовом случае определен класс ProperFormatter, который правильно реализует все обязательные методы.

Чтобы запустить тест, откройте терминал и выполните следующую команду:

python test_abc.py

Вы должны увидеть вывод, похожий на следующий:

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

Этот вывод подтверждает, что наш абстрактный базовый класс работает как ожидалось. Первый тестовый случай завершается неудачей, потому что класс NewFormatter не реализовал метод headings правильно. Второй тестовый случай завершается успешно, потому что класс ProperFormatter реализовал все обязательные методы.

✨ Проверить решение и практиковаться

Создание классов - шаблонов алгоритмов

На этом этапе мы будем использовать абстрактные базовые классы для реализации шаблонного метода (template method pattern). Цель - уменьшить дублирование кода в функциональности парсинга CSV - файлов. Дублирование кода делает его сложнее в поддержке и обновлении. Используя шаблонный метод, мы можем создать общую структуру для кода парсинга CSV и позволить подклассам обрабатывать конкретные детали.

Понимание шаблонного метода

Шаблонный метод - это поведенческий паттерн проектирования. Он похож на чертеж алгоритма. В методе он определяет общую структуру или "скелет" алгоритма. Однако он не полностью реализует все шаги. Вместо этого он откладывает некоторые шаги на подклассы. Это означает, что подклассы могут переопределить определенные части алгоритма, не меняя его общую структуру.

В нашем случае, если вы посмотрите на файл reader.py, вы заметите, что функции read_csv_as_dicts() и read_csv_as_instances() имеют много схожего кода. Основное различие между ними - это то, как они создают записи из строк в CSV - файле. Используя шаблонный метод, мы можем избежать многократной записи одного и того же кода.

Добавление базового класса CSVParser

Начнем с добавления абстрактного базового класса для парсинга CSV. Откройте файл reader.py. Мы добавим абстрактный базовый класс CSVParser сразу в начало файла, сразу после инструкций импорта.

## 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

Класс CSVParser служит шаблоном для парсинга CSV. Метод parse содержит общие шаги для чтения CSV - файла, такие как открытие файла, получение заголовков и итерация по строкам. Конкретная логика создания записи из строки абстрагирована в метод make_record(). Поскольку это абстрактный метод, любой класс, наследующийся от CSVParser, должен реализовать этот метод.

Реализация конкретных классов парсеров

Теперь, когда у нас есть базовый класс, нам нужно создать конкретные классы парсеров. Эти классы будут реализовывать конкретную логику создания записей.

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)

Класс DictCSVParser используется для создания записей в виде словарей. Он принимает список типов в конструкторе. Метод make_record использует эти типы для преобразования значений в строке и создания словаря.

Класс InstanceCSVParser используется для создания записей в виде экземпляров класса. Он принимает класс в конструкторе. Метод make_record вызывает метод from_row этого класса для создания экземпляра из строки.

Рефакторинг исходных функций

Теперь давайте выполним рефакторинг исходных функций read_csv_as_dicts() и read_csv_as_instances() для использования этих новых классов.

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)

Эти рефакторингнутые функции имеют тот же интерфейс, что и исходные. Но внутри они используют новые классы парсеров, которые мы только что создали. Таким образом, мы разделили общую логику парсинга CSV от конкретной логики создания записей.

Тестирование вашей реализации

Давайте проверим, работает ли наш рефакторингнутый код правильно. Создайте файл с именем test_reader.py и добавьте в него следующий код.

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])

Чтобы запустить тест, откройте терминал и выполните следующую команду:

python test_reader.py

Вы должны увидеть вывод, похожий на следующий:

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}

Если вы видите такой вывод, это означает, что ваш рефакторингнутый код работает правильно. И исходные функции, и прямой вызов парсеров дают ожидаемые результаты.

✨ Проверить решение и практиковаться

Итоги

В этом практическом занятии вы изучили несколько важных концепций объектно - ориентированного программирования для улучшения кода на Python. Во - первых, вы реализовали проверку типов в функции print_table(), которая гарантирует, что используются только допустимые форматировщики, тем самым повышая надежность кода. Во - вторых, вы преобразовали класс TableFormatter в абстрактный базовый класс, заставляя подклассы реализовывать определенные методы.

Кроме того, вы применили шаблонный метод (template method pattern), создав абстрактный базовый класс CSVParser и его конкретные реализации. Это уменьшает дублирование кода и сохраняет единообразную структуру алгоритма. Эти техники являются важными для создания более поддерживаемого и надежного кода на Python, особенно в крупномасштабных приложениях. Чтобы продолжить свое обучение, изучите подсказки по типам в Python (PEP 484), протокольные классы и шаблоны проектирования в Python.