ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 03.12.2023
Просмотров: 383
Скачиваний: 1
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Как работает наследование
337
Мы создали три класса с именами
ParentClass
❶
,
ChildClass
❸
и
GrandchildClass
❹
ChildClass
субклассирует
ParentClass
; это означает, что
ChildClass содержит те же методы, что и
ParentClass
. Мы говорим, что
ChildClass наследует методы от
ParentClass
. Кроме того, класс
GrandchildClass субклассирует
ChildClass
, поэтому он содержит все методы
ChildClass и его родителя
ParentClass
Используя механизм наследования, мы фактически скопировали код метода printHello()
❷
в классы
ChildClass и
GrandchildClass
. Любые изменения, вноси- мые в код printHello()
, воздействуют не только на
ParentClass
, но и на
ChildClass и
GrandchildClass
. Происходящее можно сравнить с изменением кода функции, который обновляет все ее вызовы. Эти отношения показаны на рис. 16.1. Обрати- те внимание: на диаграмме классов стрелка ведет от субкласса к базовому классу.
Такое обозначение отражает тот факт, что класс всегда знает свой базовый класс, но не знает свои субклассы.
ParentClass printHello()
ChildClass someNewMethod()
GrandchildClass anotherNewMethod()
ParentClass printHello()
ChildClass printHello()
(наследуется)
someNewMethod()
GrandchildClass printHello()
(наследуется)
someNewMethod()
(наследуется)
anotherNewMethod()
Рис. 16.1. Иерархическая диаграмма (слева) и диаграмма Венна (справа), изображающие отношения трех классов и их методов
Обычно говорят, что родительский и дочерний классы образуют отношения «яв- ляется <частным случаем>». Объект
ChildClass является объектом
ParentClass
, потому что он содержит все те же методы, которые содержит объект
ParentClass
, а также некоторые дополнительные методы. Отношения являются односторонни- ми: объект
ParentClass не является объектом
ChildClass
. Если объект
ParentClass попытается вызвать метод someNewMethod()
, существующий только для объектов
ChildClass
(и субклассов
ChildClass
), Python выдает ошибку
AttributeError
338
Глава 16.Объектно-ориентированное программирование и наследование
Программисты часто считают, что взаимосвязанные классы должны образовывать некоторую иерархию из реального мира. В учебниках ООП связи между роди- тельскими, дочерними и «внучатыми» классами часто объясняются на примере иерархий ЖивотноеПтицаЛасточка, ФигураПрямоугольникКвадрат и т. д.
Но я напомню, что главной целью наследования является повторное использование кода. Если вашей программе нужен класс с набором методов, который является полным надмножеством методов другого класса, наследование позволит избежать копирования кода.
Дочерние классы также иногда называются производными классами, или субклас-
сами, а родительские классы — базовыми классами, или суперклассами.
Переопределение методов
Дочерние классы наследуют все методы своих родительских классов. Но дочерний класс может переопределить унаследованный метод, предоставляя собственный метод с собственным кодом. Имя переопределяющего метода дочернего класса совпадает с именем метода родительского класса.
Для демонстрации этой концепции вернемся к игре «Крестики-нолики», созданной в предыдущей главе. На этот раз мы создадим новый класс
MiniBoard
, который субклассирует
TTTBoard и переопределяет getBoardStr()
для вывода уменьшенного изображения игрового поля. Программа предлагает игроку выбрать стиль игрового поля. Копировать остальные методы
TTTBoard не нужно, потому что
MiniBoard на- следует их.
Добавьте следующий фрагмент в конец файла tictactoe_oop.py
, чтобы создать дочер- ний класс, производный от
TTTBoard
, а затем переопределить метод getBoardStr()
:
class MiniBoard(TTTBoard):
def getBoardStr(self):
"""Возвращает уменьшенное текстовое представление игрового поля."""
# Пробелы заменяются символами '.'
for space in ALL_SPACES:
if self._spaces[space] == BLANK:
self._spaces[space] = '.'
boardStr = f'''
{self._spaces['1']}{self._spaces['2']}{self._spaces['3']} 123
{self._spaces['4']}{self._spaces['5']}{self._spaces['6']} 456
{self._spaces['7']}{self._spaces['8']}{self._spaces['9']} 789'''
# Символы '.' снова заменяются пробелами.
for space in ALL_SPACES:
if self._spaces[space] == '.':
self._spaces[space] = BLANK
return boardStr
Как работает наследование
339
Как и метод getBoardStr()
класса
TTTBoard
, метод getBoardStr()
класса
MiniBoard создает многострочное представление игрового поля, которое выводится при пере- даче функции print()
. Но эта строка намного компактнее, в ней отсутствуют линии между знаками X и O, а пустые клетки обозначаются точками.
Измените строку main()
так, чтобы она создавала экземпляр объекта
MiniBoard вместо объекта
TTTBoard
:
if input('Use mini board? Y/N: ').lower().startswith('y'):
gameBoard = MiniBoard() # Создать объект MiniBoard.
else:
gameBoard = TTTBoard() # Создать объект TTTBoard.
Не считая изменения одной строки в main()
, остальной код программы работа- ет так же, как прежде. Теперь при запуске программы вывод выглядит примерно так:
Welcome to Tic-Tac-Toe!
Use mini board? Y/N: y
... 123
... 456
... 789
What is X's move? (1-9)
1
X.. 123
... 456
... 789
What is O's move? (1-9)
--snip--
XXX 123
.OO 456
O.X 789
X has won the game!
Thanks for playing!
Программа легко адаптируется для включения обеих реализаций классов игрового поля. Конечно, если вам нужна только мини-версия игрового поля, вы могли легко заменить код метода getBoardStr()
для
TTTBoard
. Но если вам нужны обе версии, наследование позволяет легко создать два класса за счет повторного использования общего кода.
Если бы мы не использовали наследование, можно было бы, скажем, добавить в
TTTBoard новый атрибут с именем useMiniBoard и включить в getBoardStr()
коман ду if
- else для выбора одного из двух вариантов игрового поля (обычного или компактного). Для простого изменения такое решение могло бы сработать. Но что, если субкласс
MiniBoard должен переопределить 2, 3 или даже 100 методов?
Как быть, если вы захотите создать несколько разных субклассов
TTTBoard
? Отказ
340
Глава 16.Объектно-ориентированное программирование и наследование от наследования вызовет стремительное размножение команд if
- else внутри методов и заметно усложнит код. Использование субклассов и переопределения методов позволяет лучше разбить код на субклассы, обрабатывающие эти разные сценарии использования.
Функция super()
Переопределенный метод дочернего класса часто бывает похож на метод родитель- ского класса. И хотя наследование является средством повторного использования кода, переопределение метода может заставить вас переписать код метода роди- тельского класса как часть кода метода дочернего класса. Чтобы предотвратить дублирование кода, встроенная функция super()
позволяет переопределяющему методу вызвать исходный метод родительского класса.
Например, создадим новый класс с именем
HintBoard
, который субклассирует
TTTBoard
. Новый класс переопределяет getBoardStr()
, чтобы после вывода игрового поля также добавлялась подсказка о том, может ли X или O выиграть при своем следующем коде. Это означает, что метод getBoardStr()
класса
HintBoard должен сделать все, что делает метод getBoardStr()
класса
TTTBoard для вывода игрового поля. Вместо повторения кода можно воспользоваться вызовом super()
, чтобы вызвать метод getBoardStr()
класса
TTTBoard из метода getBoardStr()
класса
HintBoard
. Добавьте следующий фрагмент в конец файла tictactoe_oop.py
:
class HintBoard(TTTBoard):
def getBoardStr(self):
"""Возвращает текстовое представление игрового поля с подсказкой."""
boardStr = super().getBoardStr() # Вызвать getBoardStr() в TTTBoard.
❶
xCanWin = False oCanWin = False originalSpaces = self._spaces # Сохранить _spaces.
❷
for space in ALL_SPACES: # Проверить каждую клетку:
# Смоделировать ход X в эту клетку:
self._spaces = copy.copy(originalSpaces)
if self._spaces[space] == BLANK:
self._spaces[space] = X
if self.isWinner(X):
xCanWin = True
# Смоделировать ход O в эту клетку:
self._spaces = copy.copy(originalSpaces)
❸
if self._spaces[space] == BLANK:
self._spaces[space] = O
if self.isWinner(O):
oCanWin = True if xCanWin:
boardStr += '\nX can win in one more move.'
if oCanWin:
Как работает наследование
341
boardStr += '\nO can win in one more move.'
self._spaces = originalSpaces return boardStr
Сначала super().getBoardStr()
❶
выполняет код метода getBoardStr()
родитель- ского класса
TTTBoard
, который возвращает строку с игровым полем. Строка времен- но сохраняется в переменной с именем boardStr
. Так как представление игрового поля было сгенерировано повторным использованием метода getBoardStr()
класса
TTTBoard
, оставшийся код этого метода занимается генерированием подсказки. За- тем метод getBoardStr()
присваивает переменным xCanWin и oCanWin значение
False и сохраняет словарь self._spaces в переменной originalSpaces
❷
. Далее цикл for перебирает все клетки поля от 1 до 9. Внутри цикла атрибуту self._spaces при- сваивается копия словаря originalSpaces
, и если текущая клетка перебора пуста, в нее помещается знак X. Таким образом моделируется ход X в эту пустую клетку следующим ходом. Вызов self.isWinner()
определит, принесет ли этот ход вы- игрыш, и если принесет — xCanWin присваивается
True
. Затем те же шаги повторя- ются для O, чтобы определить, сможет ли O выиграть ходом в эту клетку
❸
. Этот метод использует модуль copy для создания копии словаря в self._spaces
, поэтому в начало tictactoe.py необходимо добавить следующую строку:
import copy
Затем измените строку main()
, чтобы она создавала экземпляр
HintBoard вместо объекта
TTTBoard
:
gameBoard = HintBoard() # Создать объект игрового поля.
Кроме одной измененной строки в main()
, оставшаяся часть программы работает точно так же, как прежде. Если запустить программу, результат будет выглядеть так:
Welcome to Tic-Tac-Toe!
--snip--
X| | 1 2 3
-+-+-
| |O 4 5 6
-+-+-
| |X 7 8 9
X can win in one more move.
What is O's move? (1-9)
5
X| | 1 2 3
-+-+-
|O|O 4 5 6
-+-+-
| |X 7 8 9
O can win in one more move.
--snip--
The game is a tie!
Thanks for playing!
342
Глава 16.Объектно-ориентированное программирование и наследование
В конце метода, если xCanWin или oCanWin содержит
True
, в строку boardStr вклю- чается дополнительное сообщение. Наконец, функция возвращает boardStr
Не в каждом переопределенном методе необходимо использовать super()
! Если переопределенный метод класса делает что-то совершенно отличное от пере- определенного метода родительского класса, вызывать переопределенный метод с использованием super()
необязательно. Функция super()
особенно полезна, когда класс содержит несколько родительских методов, как объясняется в разделе
«Множественное наследование» этой главы.
Предпочитайте композицию наследованию
Наследование — эффективный механизм повторного использования кода. Воз- можно, вам захочется немедленно начать применять его во всех ваших классах.
Тем не менее базовые классы не всегда настолько тесно связаны с субклассами.
С созданием нескольких уровней наследования в код добавляется не столько порядок, сколько рутина. И хотя наследование может использоваться для клас- сов, связанных отношениями «является <частным случаем>» (иначе говоря, когда дочерний класс является разновидностью родительского класса), часто для классов с отношениями «является» предпочтительнее использовать меха- низм, называемый композицией. Композиция — прием проектирования классов, основанный на включении объектов в класс (вместо наследования классов этих объектов). Именно это происходит при добавлении атрибутов в классы. При проектировании классов с возможностью применения наследования следует предпочесть композицию наследованию. Собственно, именно это происходило во всех примерах этой и предыдущей главы.
Объект
WizCoin
«содержит» количества монет разного номинала.
Объект
TTTBoard
«содержит» набор из девяти клеток.
Объект
MiniBoard
«содержит» объект
TTTBoard
, так что он тоже «содержит» набор из девяти клеток.
Объект
HintBoard
«содержит» объект
TTTBoard
, так что он тоже «содержит» набор из девяти клеток.
Вернемся к классу
WizCoin из предыдущей главы. Если мы создали класс
WizardCustomer для представления клиентов волшебной лавки, эти клиенты бу- дут носить с собой некую сумму денег, которая представляется классом
WizCoin
Однако эти классы не связаны отношениями «является» — объект
WizardCustomer не может рассматриваться как разновидность объекта
WizCoin
. При использовании наследования получится довольно неуклюжий код:
Как работает наследование
343
import wizcoin class WizardCustomer(wizcoin.WizCoin):
❶
def __init__(self, name):
self.name = name super().__init__(0, 0, 0)
wizard = WizardCustomer('Alice')
print(f'{wizard.name} has {wizard.value()} knuts worth of money.')
print(f'{wizard.name}\'s coins weigh {wizard.weightInGrams()} grams.')
В этом примере
WizardCustomer наследует методы объекта
WizCoin
❶
— такие как value()
и weightInGrams()
. Формально
WizardCustomer
, наследующий объекту
WizCoin
, может делать то же самое, что и объект
WizardCustomer
, содержащий объ- ект
WizCoint в атрибуте. Однако имена wizard.value()
и wizard.weightInGrams()
выглядят странно; создается впечатление, что они возвращают сумму и вес волшеб- ника, а не сумму и вес его монет. Кроме того, если позднее потребуется добавить метод weightInGrams()
для веса волшебника, имя уже будет занято. Гораздо лучше включить объект
WizCoin в атрибут, потому что волшебник-клиент «содержит» некоторое количество монет:
import wizcoin class WizardCustomer:
def __init__(self, name):
self.name = name self.purse = wizcoin.WizCoin(0, 0, 0)
❶
wizard = WizardCustomer('Alice')
print(f'{wizard.name} has {wizard.purse.value()} knuts worth of money.')
print(f'{wizard.name}\'s coins weigh {wizard.purse.weightInGrams()} grams.')
Вместо того чтобы наследовать методы от
WizCoin в классе
WizardCustomer
, мы вклю- чаем в класс
WizardCustomer атрибут purse
❶
, который содержит объект
WizCoin
При использовании композиции любые изменения в методах класса
WizCoin не приведут к изменению методов класса
WizardCustomer
. Этот механизм обеспечи- вает большую гибкость для будущих архитектурных изменений в обоих классах и упрощает сопровождение кода в будущем.
1 ... 32 33 34 35 36 37 38 39 40
Обратная сторона наследования
Главный недостаток наследования связан с тем, что любые будущие изменения в родительском классе обязательно наследуются всеми его дочерними классами.
В большинстве случаев такое жесткое связывание — именно то, что требуется.
344
Глава 16.Объектно-ориентированное программирование и наследование
Но в некоторых ситуациях требования к коду плохо вписываются в модель на- следования.
Представьте, что в программе моделирования дорожного движения используются классы
Car
,
Motorcycle и
LunarRover
. Они содержат похожие методы — такие как startIgnition()
и changeTire()
. Вместо того чтобы копировать этот код в каждый класс, можно создать родительский класс
Vehicle
, которому будут наследовать классы
Car
,
Motorcycle и
LunarRover
. Если теперь вам потребуется исправить ошибку, скажем, в методе changeTire()
, изменения придется вносить только в од- ном месте. Это особенно полезно, если классу
Vehicle наследуют десятки разных классов, связанных с транспортными средствами. Код этих классов будет выглядеть примерно так:
class Vehicle:
def __init__(self):
print('Vehicle created.')
def startIgnition(self):
pass # Здесь размещается код зажигания.
def changeTire(self):
pass # Здесь размещается код замены шин.
class Car(Vehicle):
def __init__(self):
print('Car created.')
class Motorcycle(Vehicle):
def __init__(self):
print('Motorcycle created.')
class LunarRover(Vehicle):
def __init__(self):
print('LunarRover created.')
Но все будущие изменения в
Vehicle также будут распространяться и на эти классы.
Что произойдет, если понадобится добавить метод changeSparkPlug()
? У машин и мотоциклов есть свечи зажигания, но у луноходов (
LunarRover
) их нет. Предпочи- тая композицию наследованию, можно создать раздельные классы
CombustionEngine
(двигатель внутреннего сгорания) и
ElectricEngine
(электрический двигатель).
Затем мы проектируем класс
Vehicle
, так чтобы он содержал атрибут engine
— либо
CombustionEngine
, либо
ElectricEngine
— с соответствующими методами:
class CombustionEngine:
def __init__(self):
print('Combustion engine created.')
def changeSparkPlug(self):
pass # Здесь размещается код замены свечи зажигания.
class ElectricEngine:
Как работает наследование
345
def __init__(self):
print('Electric engine created.')
class Vehicle:
def __init__(self):
print('Vehicle created.')
self.engine = CombustionEngine() # Используется по умолчанию.
--snip-- class LunarRover(Vehicle):
def __init__(self):
print('LunarRover created.')
self.engine = ElectricEngine()
Возможно, вам придется переписать большие объемы кода, особенно если програм- ма содержит несколько классов, наследующих существующему классу
Vehicle
: все вызовы vehicleObj.changeSparkPlug()
должны быть преобразованы в vehicleObj.
engine.changeSparkPlug()
для каждого объекта класса
Vehicle или его субклассов.
Так как столь значительные изменения могут привести к появлению ошибок, воз- можно, вы предпочтете, чтобы метод changeSparkPlug()
для
LunarVehicle не делал ничего. В этом случае в питоническом стиле следует присвоить changeSparkPlug значение
None внутри класса
LunarVehicle
:
class LunarRover(Vehicle):
changeSparkPlug = None def __init__(self):
print('LunarRover created.')
В строке changeSparkPlug
=
None используется синтаксис, описанный в разделе
«Атрибуты классов» этой главы. В следующем фрагменте переопределяется ме- тод changeSparkPlug()
, унаследованный от
Vehicle
, и при вызове его с объектом
LunarRover происходит ошибка:
>>> myVehicle = LunarRover()
LunarRover created.
>>> myVehicle.changeSparkPlug()
Traceback (most recent call last):
File "
TypeError: 'NoneType' object is not callable
Эта ошибка позволит быстро и непосредственно узнать о проблеме, если вы по- пытаетесь вызвать этот неподходящий метод с объектом
LunarRover
. Любые до- черние классы
LunarRover также унаследуют значение
None для changeSparkPlug()
Сообщение об ошибке
TypeError:
'NoneType'
object is not callable
(TypeError: объект 'NoneType'
не может вызываться) информирует, что программист класса
LunarRover намеренно задал для метода changeSparkPlug()
значение
None
. Если бы такой метод не существовал изначально, то вы бы получили сообщение об ошибке