Расширяемые программы с использованием наследования

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

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

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

Введение

Наследование - это широко используемый инструмент для написания расширяемых программ. В этом разделе мы исследуем эту идею.

Наследование

Наследование используется для специализации существующих объектов:

class Parent:
  ...

class Child(Parent):
  ...

Новый класс Child называется производным классом или подклассом. Класс Parent известен как базовый класс или суперкласс. Parent указывается в () после имени класса, class Child(Parent):.

Расширение

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

  • Добавляете новые методы
  • Переопределяете некоторые из существующих методов
  • Добавляете новые атрибуты к экземплярам

В итоге вы расширяете существующий код.

Пример

Предположим, что это ваш исходный класс:

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

Вы можете изменить любую часть этого с использованием наследования.

Добавить новый метод

class MyStock(Stock):
    def panic(self):
        self.sell(self.shares)

Пример использования.

>>> s = MyStock('GOOG', 100, 490.1)
>>> s.sell(25)
>>> s.shares
75
>>> s.panic()
>>> s.shares
0
>>>

Переопределение существующего метода

class MyStock(Stock):
    def cost(self):
        return 1.25 * self.shares * self.price

Пример использования.

>>> s = MyStock('GOOG', 100, 490.1)
>>> s.cost()
61262.5
>>>

Новый метод заменяет старый. Другие методы остаются не затронутыми. Это просто замечательно.

Переопределение

Иногда класс расширяет существующий метод, но хочет использовать исходную реализацию внутри переопределения. Для этого используйте super():

class Stock:
 ...
    def cost(self):
        return self.shares * self.price
 ...

class MyStock(Stock):
    def cost(self):
        ## Проверьте вызов `super`
        actual_cost = super().cost()
        return 1.25 * actual_cost

Используйте super() для вызова предыдущей версии.

Внимание: В Python 2 синтаксис был более подробным.

actual_cost = super(MyStock, self).cost()

__init__ и наследование

Если __init__ переопределяется, необходимо инициализировать родителя.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

class MyStock(Stock):
    def __init__(self, name, shares, price, factor):
        ## Проверьте вызов `super` и `__init__`
        super().__init__(name, shares, price)
        self.factor = factor

    def cost(self):
        return self.factor * super().cost()

Вы должны вызвать метод __init__() на super, который представляет собой способ вызова предыдущей версии, как показано ранее.

Использование наследования

Наследование иногда используется для организации связанных объектов.

class Shape:
 ...

class Circle(Shape):
 ...

class Rectangle(Shape):
 ...

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

class CustomHandler(TCPHandler):
    def handle_request(self):
     ...
        ## Настраиваемая обработка

Базовый класс содержит некоторый общий код. Ваш класс наследует и настраивает определенные части.

Отношение "является"

Наследование устанавливает отношение типов.

class Shape:
...

class Circle(Shape):
...

Проверьте объект на принадлежность к классу.

>>> c = Circle(4.0)
>>> isinstance(c, Shape)
True
>>>

Важно: По идее, любой код, работающий с экземплярами родительского класса, также должен работать с экземплярами дочернего класса.

Базовый класс object

Если класс не имеет родителя, вы иногда будете видеть, что в качестве базового используется object.

class Shape(object):
...

object является родителем всех объектов в Python.

*Примечание: Технически это не обязательно, но вы часто будете видеть его указанным в качестве наследия от его обязательного использования в Python 2. Если его опустить, класс по-прежнему неявно наследуется от object.

Множественное наследование

Вы можете наследоваться от нескольких классов, указав их в определении класса.

class Mother:
...

class Father:
...

class Child(Mother, Father):
...

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

Основное применение наследования заключается в написании кода, предназначенного для расширения или настройки по-разному - особенно в библиотеках или фреймворках. Чтобы проиллюстрировать, рассмотрим функцию print_report() в вашей программе report.py. Она должна выглядеть примерно так:

def print_report(reportdata):
    '''
    Выводит красиво отформатированную таблицу из списка кортежей (имя, количество акций, цена, изменение).
    '''
    headers = ('Name','Shares','Price','Change')
    print('%10s %10s %10s %10s' % headers)
    print(('-'*10 +' ')*len(headers))
    for row in reportdata:
        print('%10s %10d %10.2f %10.2f' % row)

Когда вы запускаете свою отчетную программу, вы должны получать такой вывод:

>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
      Name     Shares      Price     Change
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84

Упражнение 4.5: Проблема расширения

Предположим, что вы хотите изменить функцию print_report(), чтобы она поддерживала различные форматы вывода, такие как простой текст, HTML, CSV или XML. Для этого вы могли бы попробовать написать одну огромную функцию, которая делает все. Однако это, скорее всего, приведет к неуправляемой запутанной mess. Вместо этого это идеальный случай для использования наследования.

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

## tableformat.py

class TableFormatter:
    def headings(self, headers):
        '''
        Выводит заголовки таблицы.
        '''
        raise NotImplementedError()

    def row(self, rowdata):
        '''
        Выводит одну строку с данными таблицы.
        '''
        raise NotImplementedError()

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

Измените функцию print_report(), чтобы она принимала объект TableFormatter в качестве входных данных и вызывала методы на нем для получения вывода. Например, так:

## report.py
...

def print_report(reportdata, formatter):
    '''
    Выводит красиво отформатированную таблицу из списка кортежей (имя, количество акций, цена, изменение).
    '''
    formatter.headings(['Name','Shares','Price','Change'])
    for name, shares, price, change in reportdata:
        rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ]
        formatter.row(rowdata)

Поскольку вы добавили аргумент в print_report(), вам также нужно изменить функцию portfolio_report(). Измените ее так, чтобы она создавала TableFormatter так:

## report.py

import tableformat

...
def portfolio_report(portfoliofile, pricefile):
    '''
    Создает отчет о портфеле по данным файлов портфеля и цен.
    '''
    ## Читаем данные из файлов
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Создаем данные для отчета
    report = make_report_data(portfolio, prices)

    ## Выводим их
    formatter = tableformat.TableFormatter()
    print_report(report, formatter)

Запустите этот новый код:

>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
... крашится...

Он должен немедленно завершиться с исключением NotImplementedError. Это не слишком захватывающее, но это именно то, что мы ожидали. Продолжайте дальше.

Упражнение 4.6: Использование наследования для получения различных выводов

Класс TableFormatter, который вы определили в части (a), предназначен для расширения с использованием наследования. На самом деле, это и есть вся идея. Чтобы проиллюстрировать, определите класс TextTableFormatter так:

## tableformat.py
...
class TextTableFormatter(TableFormatter):
    '''
    Выводит таблицу в формате простого текста
    '''
    def headings(self, headers):
        for h in headers:
            print(f'{h:>10s}', end=' ')
        print()
        print(('-'*10 +' ')*len(headers))

    def row(self, rowdata):
        for d in rowdata:
            print(f'{d:>10s}', end=' ')
        print()

Измените функцию portfolio_report() так и попробуйте ее:

## report.py
...
def portfolio_report(portfoliofile, pricefile):
    '''
    Создает отчет о портфеле по данным файлов портфеля и цен.
    '''
    ## Читаем данные из файлов
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Создаем данные для отчета
    report = make_report_data(portfolio, prices)

    ## Выводим их
    formatter = tableformat.TextTableFormatter()
    print_report(report, formatter)

Это должно произвести такой же вывод, как и раньше:

>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
      Name     Shares      Price     Change
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84
>>>

Однако, давайте изменим вывод на что-то другое. Определите новый класс CSVTableFormatter, который выводит данные в формате CSV:

## tableformat.py
...
class CSVTableFormatter(TableFormatter):
    '''
    Выводит данные портфеля в формате CSV.
    '''
    def headings(self, headers):
        print(','.join(headers))

    def row(self, rowdata):
        print(','.join(rowdata))

Измените свою основную программу следующим образом:

def portfolio_report(portfoliofile, pricefile):
    '''
    Создает отчет о портфеле по данным файлов портфеля и цен.
    '''
    ## Читаем данные из файлов
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Создаем данные для отчета
    report = make_report_data(portfolio, prices)

    ## Выводим их
    formatter = tableformat.CSVTableFormatter()
    print_report(report, formatter)

Теперь вы должны увидеть вывод в формате CSV, похожий на этот:

>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
Name,Shares,Price,Change
AA,100,9.22,-22.98
IBM,50,106.28,15.18
CAT,150,35.46,-47.98
MSFT,200,20.89,-30.34
GE,95,13.48,-26.89
MSFT,50,20.89,-44.21
IBM,100,106.28,35.84

С использованием аналогичной идеи определите класс HTMLTableFormatter, который выводит таблицу с таким выводом:

<tr><th>Name</th><th>Shares</th><th>Price</th><th>Change</th></tr>
<tr><td>AA</td><td>100</td><td>9.22</td><td>-22.98</td></tr>
<tr><td>IBM</td><td>50</td><td>106.28</td><td>15.18</td></tr>
<tr><td>CAT</td><td>150</td><td>35.46</td><td>-47.98</td></tr>
<tr><td>MSFT</td><td>200</td><td>20.89</td><td>-30.34</td></tr>
<tr><td>GE</td><td>95</td><td>13.48</td><td>-26.89</td></tr>
<tr><td>MSFT</td><td>50</td><td>20.89</td><td>-44.21</td></tr>
<tr><td>IBM</td><td>100</td><td>106.28</td><td>35.84</td></tr>

Проверьте свой код, изменив основную программу для создания объекта HTMLTableFormatter вместо объекта CSVTableFormatter.

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

Упражнение 4.7: Полиморфизм на практике

Основной особенностью объектно-ориентированного программирования является то, что вы можете подключить объект к программе, и она будет работать, не требуя изменения существующего кода. Например, если вы написали программу, которая ожидает использовать объект TableFormatter, она будет работать независимо от того, какой именно TableFormatter вы ей на самом деле передадите. Это поведение иногда называется "полиморфизмом".

Одна потенциальная проблема заключается в том, как позволить пользователю выбрать форматтер, который он хочет. Прямое использование имен классов, таких как TextTableFormatter, часто раздражает. Таким образом, вы можете рассмотреть какой-то упрощенный подход. Возможно, вы внедрите if-условие в код, как это:

def portfolio_report(portfoliofile, pricefile, fmt='txt'):
    '''
    Создает отчет о портфеле по данным файлов портфеля и цен.
    '''
    ## Читаем данные из файлов
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Создаем данные для отчета
    report = make_report_data(portfolio, prices)

    ## Выводим их
    if fmt == 'txt':
        formatter = tableformat.TextTableFormatter()
    elif fmt == 'csv':
        formatter = tableformat.CSVTableFormatter()
    elif fmt == 'html':
        formatter = tableformat.HTMLTableFormatter()
    else:
        raise RuntimeError(f'Unknown format {fmt}')
    print_report(report, formatter)

В этом коде пользователь указывает упрощенное имя, такое как 'txt' или 'csv', чтобы выбрать формат. Однако, является ли помещение большого if-условия в функцию portfolio_report() таким хорошим решением? Возможно, лучше перенести этот код в общую функцию в другом месте.

В файле tableformat.py добавьте функцию create_formatter(name), которая позволяет пользователю создать форматтер, указав имя вывода, такое как 'txt', 'csv' или 'html'. Измените portfolio_report(), чтобы она выглядела так:

def portfolio_report(portfoliofile, pricefile, fmt='txt'):
    '''
    Создает отчет о портфеле по данным файлов портфеля и цен.
    '''
    ## Читаем данные из файлов
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Создаем данные для отчета
    report = make_report_data(portfolio, prices)

    ## Выводим их
    formatter = tableformat.create_formatter(fmt)
    print_report(report, formatter)

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

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

Упражнение 4.8: Собираем все вместе

Измените программу report.py так, чтобы функция portfolio_report() принимала необязательный аргумент, определяющий формат вывода. Например:

>>> report.portfolio_report('portfolio.csv', 'prices.csv', 'txt')
      Name     Shares      Price     Change
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84
>>>

Измените основную программу так, чтобы формат можно было указать в командной строке:

$ python3 report.py portfolio.csv prices.csv csv
Name,Shares,Price,Change
AA,100,9.22,-22.98
IBM,50,106.28,15.18
CAT,150,35.46,-47.98
MSFT,200,20.89,-30.34
GE,95,13.48,-26.89
MSFT,50,20.89,-44.21
IBM,100,106.28,35.84
$
✨ Проверить решение и практиковаться

Обсуждение

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

Другой, несколько более глубокий концепцией является идея "соблюдения своих абстракций". В упражнениях мы определили свою собственную класс для форматирования таблицы. Вы можете взглянуть на свой код и сказать себе: "Я должен просто использовать библиотеку форматирования или что-то, что уже сделал другой человек!". Нет, вы должны использовать и свой класс, и библиотеку. Использование собственного класса способствует слабой耦合 и обеспечивает большую гибкость. Пока ваше приложение использует программу интерфейс вашего класса, вы можете изменить внутреннюю реализацию так, как вам удобно. Вы можете написать полностью настраиваемый код. Вы можете использовать стороннюю библиотеку. Вы можете заменить одну стороннюю библиотеку на другую, когда найдете более подходящую. Это не имеет значения - никакой код вашего приложения не сломается, если вы сохраните интерфейс. Это мощная идея, и это одна из причин, по которой вы можете рассмотреть наследование для таких задач.

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

Резюме

Поздравляем! Вы завершили лабораторную работу по наследованию. Вы можете практиковаться в других лабораторных работах в LabEx, чтобы улучшить свои навыки.