Введение
Наследование - это широко используемый инструмент для написания расширяемых программ. В этом разделе мы исследуем эту идею.
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
Предположим, что вы хотите изменить функцию 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
. Это не слишком захватывающее, но это именно то, что мы ожидали. Продолжайте дальше.
Класс 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
.
Основной особенностью объектно-ориентированного программирования является то, что вы можете подключить объект к программе, и она будет работать, не требуя изменения существующего кода. Например, если вы написали программу, которая ожидает использовать объект 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)
Попробуйте вызвать функцию с разными форматами, чтобы убедиться, что все работает.
Измените программу 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, чтобы улучшить свои навыки.