Миксин - классы и кооперативное наследование

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

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

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

Введение

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

Вы также овладеете техниками кооперативного наследования в Python. Файл tableformat.py будет изменен в ходе эксперимента.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/default_arguments("Default Arguments") python/FunctionsGroup -.-> python/keyword_arguments("Keyword Arguments") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/inheritance("Inheritance") subgraph Lab Skills python/function_definition -.-> lab-132498{{"Миксин - классы и кооперативное наследование"}} python/default_arguments -.-> lab-132498{{"Миксин - классы и кооперативное наследование"}} python/keyword_arguments -.-> lab-132498{{"Миксин - классы и кооперативное наследование"}} python/classes_objects -.-> lab-132498{{"Миксин - классы и кооперативное наследование"}} python/inheritance -.-> lab-132498{{"Миксин - классы и кооперативное наследование"}} end

Понимание проблемы с форматированием столбцов

На этом этапе мы рассмотрим ограничение в текущей реализации форматирования таблиц. Также мы изучим возможные решения этой проблемы.

Сначала разберемся, что предстоит сделать. Откроем редактор VSCode и посмотрим на файл tableformat.py в директории проекта. Этот файл важен, так как он содержит код, позволяющий форматировать табличные данные различными способами, например, в текстовом, CSV или HTML форматах.

Для открытия файла используем следующие команды в терминале. Команда cd изменяет текущую директорию на директорию проекта, а команда code открывает файл tableformat.py в VSCode.

cd ~/project
code tableformat.py

При открытии файла вы заметите, что определено несколько классов. Эти классы выполняют различные роли в форматировании табличных данных.

  • TableFormatter: Это абстрактный базовый класс. Он содержит методы, используемые для форматирования заголовков и строк таблицы. Представьте его как чертеж для других классов-форматеров.
  • TextTableFormatter: Этот класс используется для вывода таблицы в простом текстовом формате.
  • CSVTableFormatter: Он отвечает за форматирование табличных данных в формате CSV (Comma-Separated Values, разделенные запятыми значения).
  • HTMLTableFormatter: Этот класс форматирует табличные данные в HTML формате.

В файле также есть функция print_table(). Эта функция использует только что упомянутые классы-форматеры для отображения табличных данных.

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

python3 -c "
from tableformat import print_table, TextTableFormatter, portfolio
formatter = TextTableFormatter()
print_table(portfolio, ['name', 'shares', 'price'], formatter)
"

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

      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

Теперь найдем проблему. Обратите внимание, что значения в столбце price не имеют единообразного формата. Некоторые значения имеют одну десятичную цифру, например 32.2, а другие - две десятичные цифры, например 51.23. В финансовых данных обычно требуется единообразное форматирование.

Вот как должен выглядеть вывод:

      name     shares      price
---------- ---------- ----------
        AA        100      32.20
       IBM         50      91.10
       CAT        150      83.44
      MSFT        200      51.23
        GE         95      40.37
      MSFT         50      65.10
       IBM        100      70.44

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

python3 -c "
from tableformat import TextTableFormatter, portfolio

def print_table(records, fields, formats, formatter):
    formatter.headings(fields)
    for r in records:
        rowdata = [(fmt % getattr(r, fieldname))
             for fieldname, fmt in zip(fields, formats)]
        formatter.row(rowdata)

formatter = TextTableFormatter()
print_table(portfolio,
            ['name','shares','price'],
            ['%s','%d','%0.2f'],
            formatter)
"

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

Другой подход - создать настраиваемый форматер путем наследования. Мы можем создать новый класс, который наследуется от TextTableFormatter и переопределить метод row() для применения нужного форматирования.

python3 -c "
from tableformat import TextTableFormatter, print_table, portfolio

class PortfolioFormatter(TextTableFormatter):
    def row(self, rowdata):
        formats = ['%s','%d','%0.2f']
        rowdata = [(fmt % d) for fmt, d in zip(formats, rowdata)]
        super().row(rowdata)

formatter = PortfolioFormatter()
print_table(portfolio, ['name','shares','price'], formatter)
"

Это решение также работает, но не очень удобно. Каждый раз, когда мы хотим использовать другое форматирование, нам приходится создавать новый класс. Кроме того, мы ограничены конкретным типом форматера, от которого наследуемся, в данном случае TextTableFormatter.

На следующем этапе мы рассмотрим более элегантное решение с использованием миксин-классов (mixin classes).

Реализация миксин-классов для форматирования

На этом этапе мы узнаем о миксин-классах (mixin classes). Миксин-классы - это очень полезная техника в Python. Они позволяют добавлять дополнительную функциональность классам без изменения их исходного кода. Это замечательно, так как помогает сделать код модульным и легко управляемым.

Что такое миксин-классы?

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

Теперь реализуем два миксин-класса в файле tableformat.py. Сначала откройте файл в редакторе. Это можно сделать, запустив следующие команды в терминале:

cd ~/project
code tableformat.py

После открытия файла добавьте следующие определения классов в конец файла, но до любых существующих функций.

class ColumnFormatMixin:
    formats = []
    def row(self, rowdata):
        rowdata = [(fmt % d) for fmt, d in zip(self.formats, rowdata)]
        super().row(rowdata)

Класс ColumnFormatMixin предоставляет функциональность форматирования столбцов. Классовая переменная formats представляет собой список, содержащий коды форматирования. Эти коды используются для форматирования данных в каждом столбце. Метод row() принимает данные строки, применяет коды форматирования к каждому элементу строки, а затем передает отформатированные данные строки родительскому классу с помощью super().row(rowdata).

Далее добавьте еще один миксин-класс, который делает заголовки таблицы заглавными:

class UpperHeadersMixin:
    def headings(self, headers):
        super().headings([h.upper() for h in headers])

Класс UpperHeadersMixin преобразует текст заголовков в верхний регистр. Он принимает список заголовков, преобразует каждый заголовок в верхний регистр, а затем передает измененные заголовки методу headings() родительского класса с помощью super().headings().

Использование миксин-классов

Протестируем наши новые миксин-классы. Запустим некоторый Python-код, чтобы увидеть, как они работают.

python3 -c "
from tableformat import TextTableFormatter, ColumnFormatMixin, portfolio, print_table

class PortfolioFormatter(ColumnFormatMixin, TextTableFormatter):
    formats = ['%s', '%d', '%0.2f']

formatter = PortfolioFormatter()
print_table(portfolio, ['name','shares','price'], formatter)
"

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

      name     shares      price
---------- ---------- ----------
        AA        100      32.20
       IBM         50      91.10
       CAT        150      83.44
      MSFT        200      51.23
        GE         95      40.37
      MSFT         50      65.10
       IBM        100      70.44

Теперь попробуем класс UpperHeadersMixin. Запустите следующий код:

python3 -c "
from tableformat import TextTableFormatter, UpperHeadersMixin, portfolio, print_table

class PortfolioFormatter(UpperHeadersMixin, TextTableFormatter):
    pass

formatter = PortfolioFormatter()
print_table(portfolio, ['name','shares','price'], formatter)
"

Этот код должен отобразить заголовки в верхнем регистре.

      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

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

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

Порядок наследования очень важен. Когда мы определяем class PortfolioFormatter(ColumnFormatMixin, TextTableFormatter), Python ищет методы сначала в ColumnFormatMixin, а затем в TextTableFormatter. Поэтому, когда в ColumnFormatMixin вызывается super().row(), это ссылается на TextTableFormatter.row().

Мы даже можем комбинировать оба миксин-класса. Запустите следующий код:

python3 -c "
from tableformat import TextTableFormatter, ColumnFormatMixin, UpperHeadersMixin, portfolio, print_table

class PortfolioFormatter(ColumnFormatMixin, UpperHeadersMixin, TextTableFormatter):
    formats = ['%s', '%d', '%0.2f']

formatter = PortfolioFormatter()
print_table(portfolio, ['name','shares','price'], formatter)
"

Этот код даст нам и заглавные заголовки, и отформатированные числа.

      NAME     SHARES      PRICE
---------- ---------- ----------
        AA        100      32.20
       IBM         50      91.10
       CAT        150      83.44
      MSFT        200      51.23
        GE         95      40.37
      MSFT         50      65.10
       IBM        100      70.44

На следующем этапе мы сделаем использование этих миксин-классов более удобным, улучшив функцию create_formatter().

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

Создание удобного для пользователя API для миксинов

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

Сначала вам нужно открыть файл tableformat.py. Это можно сделать, запустив следующие команды в терминале. Команда cd изменяет текущую директорию на папку проекта, а команда code открывает файл tableformat.py в редакторе кода.

cd ~/project
code tableformat.py

После открытия файла найдите функцию create_formatter(). В настоящее время она выглядит так:

def create_formatter(name):
    """
    Create an appropriate formatter based on the name.
    """
    if name == 'text':
        return TextTableFormatter()
    elif name == 'csv':
        return CSVTableFormatter()
    elif name == 'html':
        return HTMLTableFormatter()
    else:
        raise RuntimeError(f'Unknown format {name}')

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

Замените существующую функцию create_formatter() на улучшенную версию ниже. Эта новая функция позволяет указать форматы столбцов и преобразовывать заголовки в верхний регистр.

def create_formatter(name, column_formats=None, upper_headers=False):
    """
    Create a formatter with optional enhancements.

    Parameters:
    name : str
        Name of the formatter ('text', 'csv', 'html')
    column_formats : list, optional
        List of format strings for column formatting
    upper_headers : bool, optional
        Whether to convert headers to uppercase
    """
    if name == 'text':
        formatter_cls = TextTableFormatter
    elif name == 'csv':
        formatter_cls = CSVTableFormatter
    elif name == 'html':
        formatter_cls = HTMLTableFormatter
    else:
        raise RuntimeError(f'Unknown format {name}')

    ## Apply mixins if requested
    if column_formats and upper_headers:
        class CustomFormatter(ColumnFormatMixin, UpperHeadersMixin, formatter_cls):
            formats = column_formats
        return CustomFormatter()
    elif column_formats:
        class CustomFormatter(ColumnFormatMixin, formatter_cls):
            formats = column_formats
        return CustomFormatter()
    elif upper_headers:
        class CustomFormatter(UpperHeadersMixin, formatter_cls):
            pass
        return CustomFormatter()
    else:
        return formatter_cls()

Эта улучшенная функция работает следующим образом: сначала она определяет базовый класс форматера на основе аргумента name. Затем, в зависимости от того, были ли предоставлены column_formats и upper_headers, она создает настраиваемый класс форматера, включающий соответствующие миксины. Наконец, она возвращает экземпляр настраиваемого класса форматера.

Теперь протестируем нашу улучшенную функцию с различными комбинациями параметров.

Сначала попробуем использовать форматирование столбцов. Запустите следующую команду в терминале. Эта команда импортирует необходимые функции и данные из файла tableformat.py, создает форматер с форматированием столбцов и затем выводит таблицу с использованием этого форматера.

python3 -c "
from tableformat import create_formatter, portfolio, print_table

formatter = create_formatter('text', column_formats=['%s', '%d', '%0.2f'])
print_table(portfolio, ['name', 'shares', 'price'], formatter)
"

Вы должны увидеть таблицу с отформатированными столбцами. Вывод будет выглядеть так:

      name     shares      price
---------- ---------- ----------
        AA        100      32.20
       IBM         50      91.10
       CAT        150      83.44
      MSFT        200      51.23
        GE         95      40.37
      MSFT         50      65.10
       IBM        100      70.44

Далее попробуем использовать заголовки в верхнем регистре. Запустите следующую команду:

python3 -c "
from tableformat import create_formatter, portfolio, print_table

formatter = create_formatter('text', upper_headers=True)
print_table(portfolio, ['name', 'shares', 'price'], formatter)
"

Вы должны увидеть таблицу с заголовками в верхнем регистре. Вывод будет следующим:

      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

Наконец, объединим оба параметра. Запустите эту команду:

python3 -c "
from tableformat import create_formatter, portfolio, print_table

formatter = create_formatter('text', column_formats=['%s', '%d', '%0.2f'], upper_headers=True)
print_table(portfolio, ['name', 'shares', 'price'], formatter)
"

Это должно отобразить таблицу с отформатированными столбцами и заголовками в верхнем регистре. Вывод будет:

      NAME     SHARES      PRICE
---------- ---------- ----------
        AA        100      32.20
       IBM         50      91.10
       CAT        150      83.44
      MSFT        200      51.23
        GE         95      40.37
      MSFT         50      65.10
       IBM        100      70.44

Улучшенная функция также работает с другими типами форматеров. Например, попробуем ее с форматером CSV. Запустите следующую команду:

python3 -c "
from tableformat import create_formatter, portfolio, print_table

formatter = create_formatter('csv', column_formats=['\\"%s\\"', '%d', '%0.2f'])
print_table(portfolio, ['name', 'shares', 'price'], formatter)
"

Это должно сформировать вывод в формате CSV с отформатированными столбцами. Вывод будет:

name,shares,price
"AA",100,32.20
"IBM",50,91.10
"CAT",150,83.44
"MSFT",200,51.23
"GE",95,40.37
"MSFT",50,65.10
"IBM",100,70.44

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

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

Резюме

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

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