Практическое применение наследования

Beginner

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

Введение

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

Кроме того, вы реализуете фабричный паттерн (factory pattern) для упрощения выбора классов. Файл, который вы будете изменять, - tableformat.py.

Это Guided Lab, который предоставляет пошаговые инструкции, чтобы помочь вам учиться и практиковаться. Внимательно следуйте инструкциям, чтобы выполнить каждый шаг и получить практический опыт. Исторические данные показывают, что это лабораторная работа уровня начальный с процентом завершения 95%. Он получил 95% положительных отзывов от учащихся.

Понимание проблемы

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

Начнем с рассмотрения существующей функции print_table(). Именно эту функцию мы будем улучшать, чтобы сделать ее более гибкой в плане форматов вывода.

Сначала вам нужно открыть файл tableformat.py в редакторе WebIDE. Путь к этому файлу следующий:

/home/labex/project/tableformat.py

После открытия файла вы увидите текущую реализацию функции print_table(). Эта функция предназначена для форматирования и вывода табличных данных. Она принимает два основных входных параметра: список записей (которые являются объектами) и список имен полей. На основе этих входных данных она выводит красиво отформатированную таблицу.

Теперь давайте протестируем эту функцию, чтобы увидеть, как она работает. Откройте терминал в WebIDE и запустите следующие команды Python. Эти команды импортируют необходимые модули, считывают данные из CSV - файла и затем используют функцию print_table() для отображения данных.

import stock
import reader
import tableformat

portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
tableformat.print_table(portfolio, ['name', 'shares', 'price'])

После выполнения этих команд вы должны увидеть следующий вывод:

      name     shares      price
---------- ---------- ----------
        AA        100       32.2
       IBM         50       91.1
       CAT        150      83.44
      MSFT        200      51.23
        GE         95      40.37
      MSFT         50       65.1
       IBM        100      70.44

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

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

  1. Мы определим базовый класс TableFormatter. Этот класс будет иметь методы, которые используются для форматирования данных. Базовый класс предоставляет общую структуру и функциональность, на основе которой могут строиться все подклассы.
  2. Мы создадим различные подклассы. Каждый подкласс будет предназначен для определенного формата вывода. Например, один подкласс может быть для вывода в формате CSV, другой - для вывода в формате HTML и так далее. Эти подклассы будут наследовать методы от базового класса и также могут добавлять свою собственную специфическую функциональность.
  3. Мы изменим функцию print_table(), чтобы она могла работать с любым форматером. Это означает, что мы можем передавать разные подклассы класса TableFormatter в функцию print_table(), и она сможет использовать соответствующие методы форматирования.

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

Создание базового класса и модификация функции вывода

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

Теперь создадим базовый класс, который определит интерфейс для всех форматеров таблиц. Откройте файл tableformat.py в WebIDE и добавьте следующий код в начало файла:

class TableFormatter:
    """
    Base class for all table formatters.
    This class defines the interface that all formatters must implement.
    """
    def headings(self, headers):
        """
        Generate the table headings.
        """
        raise NotImplementedError()

    def row(self, rowdata):
        """
        Generate a single row of table data.
        """
        raise NotImplementedError()

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

Далее нужно модифицировать функцию print_table() для использования класса TableFormatter. Функция print_table() используется для вывода таблицы данных из списка объектов. Модифицируя ее для использования класса TableFormatter, мы можем сделать функцию более гибкой и способной работать с разными форматами таблиц.

Замените существующую функцию print_table() следующим кодом:

def print_table(records, fields, formatter):
    """
    Print a table of data from a list of objects using the specified formatter.

    Args:
        records: A list of objects
        fields: A list of field names
        formatter: A TableFormatter object
    """
    formatter.headings(fields)
    for r in records:
        rowdata = [getattr(r, fieldname) for fieldname in fields]
        formatter.row(rowdata)

Основное изменение здесь заключается в том, что функция print_table() теперь принимает параметр formatter, который должен быть экземпляром класса TableFormatter или его подкласса. Это означает, что мы можем передавать разные форматеры таблиц в функцию print_table(), и она будет использовать соответствующий форматер для вывода таблицы. Функция делегирует ответственность за форматирование объекту - форматеру, вызывая его методы headings() и row().

Протестируем наши изменения, попробовав использовать базовый класс TableFormatter:

import stock
import reader
import tableformat

portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
formatter = tableformat.TableFormatter()
tableformat.print_table(portfolio, ['name', 'shares', 'price'], formatter)

При запуске этого кода вы должны увидеть ошибку:

Traceback (most recent call last):
...
NotImplementedError

Эта ошибка возникает потому, что мы пытаемся напрямую использовать абстрактный базовый класс, но он не предоставляет реализаций для своих методов. Поскольку методы headings() и row() в классе TableFormatter вызывают исключение NotImplementedError, Python не знает, что делать при вызове этих методов. На следующем этапе мы создадим конкретный подкласс, который предоставит эти реализации.

Реализация конкретного форматера

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

Добавим следующий класс в файл tableformat.py. Этот класс будет наследоваться от абстрактного базового класса TableFormatter и реализовывать методы headings() и row().

class TextTableFormatter(TableFormatter):
    """
    Formatter that generates a plain - text table.
    """
    def headings(self, headers):
        """
        Generate plain - text table headings.
        """
        print(' '.join('%10s' % h for h in headers))
        print(('-'*10 + ' ')*len(headers))

    def row(self, rowdata):
        """
        Generate a plain - text table row.
        """
        print(' '.join('%10s' % d for d in rowdata))

Класс TextTableFormatter наследуется от TableFormatter. Это означает, что он получает все свойства и методы класса TableFormatter, но также предоставляет свои собственные реализации методов headings() и row(). Эти методы отвечают за форматирование заголовков и строк таблицы соответственно. Метод headings() выводит заголовки в красиво отформатированном виде, за которым следует строка из тире, разделяющая заголовки от данных. Метод row() форматирует каждую строку данных аналогичным образом.

Теперь протестируем наш новый форматер. Мы будем использовать модули stock, reader и tableformat для чтения данных из CSV - файла и вывода их с использованием нашего нового форматера.

import stock
import reader
import tableformat

portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
formatter = tableformat.TextTableFormatter()
tableformat.print_table(portfolio, ['name', 'shares', 'price'], formatter)

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

      name     shares      price
---------- ---------- ----------
        AA        100       32.2
       IBM         50       91.1
       CAT        150      83.44
      MSFT        200      51.23
        GE         95      40.37
      MSFT         50       65.1
       IBM        100      70.44

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

Создание дополнительных форматеров

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

Теперь добавим следующие классы в файл tableformat.py. Эти классы определят, как форматировать данные в форматах CSV и HTML соответственно.

class CSVTableFormatter(TableFormatter):
    """
    Formatter that generates CSV formatted data.
    """
    def headings(self, headers):
        """
        Generate CSV headers.
        """
        print(','.join(headers))

    def row(self, rowdata):
        """
        Generate a CSV data row.
        """
        print(','.join(str(d) for d in rowdata))

class HTMLTableFormatter(TableFormatter):
    """
    Formatter that generates HTML table code.
    """
    def headings(self, headers):
        """
        Generate HTML table headers.
        """
        print('<tr>', end=' ')
        for header in headers:
            print(f'<th>{header}</th>', end=' ')
        print('</tr>')

    def row(self, rowdata):
        """
        Generate an HTML table row.
        """
        print('<tr>', end=' ')
        for data in rowdata:
            print(f'<td>{data}</td>', end=' ')
        print('</tr>')

Класс CSVTableFormatter предназначен для форматирования данных в формате CSV (Comma - Separated Values, значения, разделенные запятыми). Метод headings принимает список заголовков и выводит их, разделенных запятыми. Метод row принимает список данных для одной строки и также выводит их, разделенных запятыми.

Класс HTMLTableFormatter, напротив, используется для генерации HTML - кода таблицы. Метод headings создает заголовки таблицы с использованием HTML - тегов <th>, а метод row создает строку таблицы с использованием HTML - тегов <td>.

Протестируем эти новые форматеры, чтобы увидеть, как они работают.

  1. Сначала протестируем CSV - форматер:
import stock
import reader
import tableformat

portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
formatter = tableformat.CSVTableFormatter()
tableformat.print_table(portfolio, ['name', 'shares', 'price'], formatter)

В этом коде мы сначала импортируем необходимые модули. Затем мы читаем данные из CSV - файла с именем portfolio.csv и создаем экземпляры класса Stock. Далее мы создаем экземпляр класса CSVTableFormatter. Наконец, мы используем функцию print_table для вывода данных о портфеле в формате CSV.

Вы должны увидеть следующий вывод в формате CSV:

name,shares,price
AA,100,32.2
IBM,50,91.1
CAT,150,83.44
MSFT,200,51.23
GE,95,40.37
MSFT,50,65.1
IBM,100,70.44
  1. Теперь протестируем HTML - форматер:
formatter = tableformat.HTMLTableFormatter()
tableformat.print_table(portfolio, ['name', 'shares', 'price'], formatter)

Здесь мы создаем экземпляр класса HTMLTableFormatter и снова используем функцию print_table для вывода данных о портфеле в формате HTML.

Вы должны увидеть следующий вывод в формате HTML:

<tr> <th>name</th> <th>shares</th> <th>price</th> </tr>
<tr> <td>AA</td> <td>100</td> <td>32.2</td> </tr>
<tr> <td>IBM</td> <td>50</td> <td>91.1</td> </tr>
<tr> <td>CAT</td> <td>150</td> <td>83.44</td> </tr>
<tr> <td>MSFT</td> <td>200</td> <td>51.23</td> </tr>
<tr> <td>GE</td> <td>95</td> <td>40.37</td> </tr>
<tr> <td>MSFT</td> <td>50</td> <td>65.1</td> </tr>
<tr> <td>IBM</td> <td>100</td> <td>70.44</td> </tr>

Как вы можете видеть, каждый форматер создает вывод в разном формате, но все они используют один и тот же интерфейс, определенный базовым классом TableFormatter. Это отличный пример мощности наследования и полиморфизма. Мы можем писать код, который работает с базовым классом, и он автоматически будет работать с любым подклассом.

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

Создание фабричной функции

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

Добавим следующую функцию в файл tableformat.py. Эта функция будет принимать имя формата в качестве аргумента и возвращать соответствующий объект форматера.

def create_formatter(format_name):
    """
    Create a formatter of the specified type.

    Args:
        format_name: Name of the formatter ('text', 'csv', 'html')

    Returns:
        A TableFormatter object

    Raises:
        ValueError: If format_name is not recognized
    """
    if format_name == 'text':
        return TextTableFormatter()
    elif format_name == 'csv':
        return CSVTableFormatter()
    elif format_name == 'html':
        return HTMLTableFormatter()
    else:
        raise ValueError(f'Unknown format {format_name}')

Функция create_formatter() является фабричной функцией. Она проверяет аргумент format_name, который вы предоставляете. Если он равен 'text', она создает и возвращает объект TextTableFormatter. Если он равен 'csv', она возвращает объект CSVTableFormatter, а если он равен 'html', то возвращает объект HTMLTableFormatter. Если имя формата не распознается, функция вызывает исключение ValueError. Таким образом, пользователи могут легко выбрать форматер, просто указав простое имя, не зная конкретных имен классов.

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

import stock
import reader
from tableformat import create_formatter, print_table

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

## Test with text formatter
formatter = create_formatter('text')
print("\nText Format:")
print_table(portfolio, ['name', 'shares', 'price'], formatter)

## Test with CSV formatter
formatter = create_formatter('csv')
print("\nCSV Format:")
print_table(portfolio, ['name', 'shares', 'price'], formatter)

## Test with HTML formatter
formatter = create_formatter('html')
print("\nHTML Format:")
print_table(portfolio, ['name', 'shares', 'price'], formatter)

В этом коде мы сначала импортируем необходимые модули и функции. Затем мы читаем данные из файла portfolio.csv и создаем объект portfolio. После этого мы тестируем функцию create_formatter() с разными именами форматов: 'text', 'csv' и 'html'. Для каждого формата мы создаем объект форматера, выводим имя формата, а затем используем функцию print_table() для вывода данных из portfolio в указанном формате.

При запуске этого кода вы должны увидеть вывод в трех форматах, разделенных именами форматов:

Text Format:
      name     shares      price
---------- ---------- ----------
        AA        100       32.2
       IBM         50       91.1
       CAT        150      83.44
      MSFT        200      51.23
        GE         95      40.37
      MSFT         50       65.1
       IBM        100      70.44

CSV Format:
name,shares,price
AA,100,32.2
IBM,50,91.1
CAT,150,83.44
MSFT,200,51.23
GE,95,40.37
MSFT,50,65.1
IBM,100,70.44

HTML Format:
<tr> <th>name</th> <th>shares</th> <th>price</th> </tr>
<tr> <td>AA</td> <td>100</td> <td>32.2</td> </tr>
<tr> <td>IBM</td> <td>50</td> <td>91.1</td> </tr>
<tr> <td>CAT</td> <td>150</td> <td>83.44</td> </tr>
<tr> <td>MSFT</td> <td>200</td> <td>51.23</td> </tr>
<tr> <td>GE</td> <td>95</td> <td>40.37</td> </tr>
<tr> <td>MSFT</td> <td>50</td> <td>65.1</td> </tr>
<tr> <td>IBM</td> <td>100</td> <td>70.44</td> </tr>

Фабричная функция делает код более удобным для пользователя, так как скрывает детали создания экземпляров классов. Пользователям не нужно знать, как создавать объекты форматеров; им нужно только указать желаемый формат.

Эта модель использования фабричной функции для создания объектов представляет собой распространенную шаблон проектирования в объектно - ориентированном программировании, известный как Фабричный шаблон (Factory Pattern). Он обеспечивает уровень абстракции между клиентским кодом (кодом, который использует форматер) и фактическими классами реализации (классами форматеров). Это делает код более модульным и легким в использовании.

Обзор ключевых концепций:

  1. Абстрактный базовый класс: Класс TableFormatter служит в качестве интерфейса. Интерфейс определяет набор методов, которые должны иметь все классы, реализующие его. В нашем случае все классы форматеров должны реализовать методы, определенные в классе TableFormatter.

  2. Наследование: Конкретные классы форматеров, такие как TextTableFormatter, CSVTableFormatter и HTMLTableFormatter, наследуются от базового класса TableFormatter. Это означает, что они получают базовую структуру и методы от базового класса и могут предоставлять свои собственные конкретные реализации.

  3. Полиморфизм: Функция print_table() может работать с любым форматером, реализующим необходимый интерфейс. Это означает, что вы можете передать разные объекты форматеров в функцию print_table(), и она будет работать корректно с каждым из них.

  4. Фабричный шаблон: Функция create_formatter() упрощает создание объектов форматеров. Она заботится о деталях создания правильного объекта на основе имени формата, так что пользователям не нужно об этом беспокоиться.

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

Резюме

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

Эти концепции являются мощными инструментами для создания поддерживаемого и расширяемого кода. Система форматирования таблиц, которую вы создали, демонстрирует, как наследование может создать гибкую систему. Вы можете применить эти принципы к другим программным задачам, делая ваш код модульным, повторно используемым и адаптируемым к меняющимся требованиям.