Чистыйкод дляпродолжающи х
Скачать 7.85 Mb.
|
16 Объектно-ориентированное программирование и наследование Определение функции и вызов ее в нескольких местах про- граммы избавляет от копирования исходного кода. Отказ от дублирования кода — полезная практика, потому что, если вам потребуется изменить этот код (чтобы исправить ошиб- ку или добавить новые возможности), изменения достаточно внести только в одном месте. Также без дублирования кода про- грамма становится более короткой и удобочитаемой. Наследование (inheritance) представляет собой метод повторного использования кода, который применяется к классам. Это механизм организации классов в си- стеме отношений «родитель — потомок», в которой дочерний класс наследует копию методов своего родительского класса, что избавляет вас от необходимости дублировать код метода в нескольких классах. Многие программисты считают наследование переоцененным и даже опасным из-за дополнительной сложности, которую добавляют в программу большие иерархии на- следования. Когда вы встречаете в блогах публикации типа «Наследование — зло», не стоит полагать, что они совершенно не обоснованы: да, наследованием можно злоупотреблять. Тем не менее разумное применение наследования может сильно сэкономить время при организации вашего кода. Как работает наследование Чтобы создать новый дочерний класс, укажите имя существующего родительско- го класса в круглых скобках в команде class . Чтобы потренироваться в создании 336 Глава 16.Объектно-ориентированное программирование и наследование дочерних классов, откройте в редакторе окно нового файла, введите следующую программу и сохраните ее с именем inheritanceExample.py : class ParentClass: ❶ def printHello(self): ❷ print('Hello, world!') class ChildClass(ParentClass): ❸ def someNewMethod(self): print('ParentClass objects don't have this method.') class GrandchildClass(ChildClass): ❹ def anotherNewMethod(self): print('Only GrandchildClass objects have this method.') print('Create a ParentClass object and call its methods:') parent = ParentClass() parent.printHello() print('Create a ChildClass object and call its methods:') child = ChildClass() child.printHello() child.someNewMethod() print('Create a GrandchildClass object and call its methods:') grandchild = GrandchildClass() grandchild.printHello() grandchild.someNewMethod() grandchild.anotherNewMethod() print('An error:') parent.someNewMethod() При выполнении этой программы результат выглядит примерно так: Create a ParentClass object and call its methods: Hello, world! Create a ChildClass object and call its methods: Hello, world! ParentClass objects don't have this method. Create a GrandchildClass object and call its methods: Hello, world! ParentClass objects don't have this method. Only GrandchildClass objects have this method. An error: Traceback (most recent call last): File "inheritanceExample.py", line 35, in parent.someNewMethod() # ParentClass objects don't have this method. AttributeError: 'ParentClass' object has no attribute 'someNewMethod' Как работает наследование 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 . Этот механизм обеспечи- вает большую гибкость для будущих архитектурных изменений в обоих классах и упрощает сопровождение кода в будущем. |