Чистыйкод дляпродолжающи х
Скачать 7.85 Mb.
|
Множественное наследование Многие языки программирования позволяют классу иметь не более одного (непо- средственного) родительского класса. Python позволяет использовать несколько родительских классов при помощи механизма, называемого множественным на- следованием. Например, можно определить класс Airplane с методом flyInTheAir() и класс Ship с методом floatOnWater() . После этого можно создать класс FlyingBoat , наследующий как классу Airplane , так и Ship ; для этого оба класса следует перечис- лить в команде class с разделением запятыми. Откройте в редакторе окно с новым файлом и сохраните следующий код с именем flyingboat.py : class Airplane: def flyInTheAir(self): print('Flying...') class Ship: def floatOnWater(self): print('Floating...') class FlyingBoat(Airplane, Ship): pass Объекты FlyingBoat , которые мы создаем, наследуют методы flyInTheAir() и floatOnWater() , как показывает следующий сеанс интерактивной оболочки: >>> from flyingboat import * >>> seaDuck = FlyingBoat() >>> seaDuck.flyInTheAir() Flying... >>> seaDuck.floatOnWater() Floating... Множественное наследование работает вполне прямолинейно — при условии, что имена методов родительских классов различны. Такие классы называются примесями (mixins). (Это просто термин для разновидности классов; в Python нет ключевого слова mixin .) Но что произойдет, если ваш класс наследует нескольким сложным классам, содержащим методы с совпадающими именами? Вспомните классы игрового поля для игры «Крестики-нолики» MiniBoard и HintTTTBoard , рассмотренные ранее в этой главе. А если вам нужен класс, который 354 Глава 16.Объектно-ориентированное программирование и наследование выводит компактное игровое поле, а также подсказки? С множественным насле- дованием можно повторно использовать существующие классы. Добавьте следую- щий фрагмент в конец файла tictactoe_oop.py до команды if , в которой вызывается функция main() : class HybridBoard(HintBoard, MiniBoard): pass Этот класс не содержит ничего — он только повторно использует код, унаследован- ный от HintBoard и MiniBoard . Затем измените код в функции main() , чтобы в нем создавался объект HybridBoard : gameBoard = HybridBoard() # Создать объект игрового поля. Оба родительских класса, MiniBoard и HintBoard , содержат метод с именем getBoardStr() . Какой же из методов унаследует HybridBoard ? При выполнении эта программа выводит компактное игровое поле, а также выводит подсказку: --snip-- X.. 123 .O. 456 X.. 789 X can win in one more move. Похоже, Python, как по волшебству, объединил метод getBoardStr() класса MiniBoard с методом getBoardStr() класса HintBoard , чтобы он делал и то и другое! Но это произошло потому, что я написал их так, чтобы они работали друг с другом. Более того, если поменять порядок классов в команде class класса HybridBoard : class HybridBoard(MiniBoard, HintBoard): подсказки полностью пропадут: --snip-- X.. 123 .O. 456 X.. 789 Чтобы понять, почему это происходит, необходимо изучить порядок разрешения методов (MRO, Method Resolution Order) языка Python и то, как работает функ- ция super() Порядок разрешения методов Наша программа «Крестики-нолики» теперь содержит четыре класса для представ- ления игрового поля: три с определенными методами getBoardStr() и четвертый с унаследованным методом getBoardStr() , как показано на рис. 16.2. Порядок разрешения методов 355 TTTBoard getBoardStr() HintBoard getBoardStr() (переопределен, вызывает super()) MiniBoard getBoardStr() (переопределен) HybridBoard (унаследованный метод getBoardStr()) Рис. 16.2. Четыре класса в программе «Крестики-нолики» Когда вы вызываете getBoardStr() для объекта HybridBoard , Python знает, что в классе HybridBoard нет метода с таким именем, и поэтому проверяет родитель- ский класс. Но класс имеет два родительских класса, и в обоих присутствует метод getBoardStr() . Какая версия будет вызвана? С помощью MRO класса HybridBoard вы можете получить упорядоченный список классов, который Python проверяет при наследовании методов либо при вызове функции super() из метода. Для просмотра MRO класса HybridBoard вызовите его метод mro() в интерактивной оболочке: >>> from tictactoe_oop import * >>> HybridBoard.mro() [ Из возвращаемого значения видно, что при вызове метода для HybridBoard Python сначала проверяет его в классе HybridBoard . Если метод не найден, Python про- веряет класс HintBoard , затем класс MiniBoard и, наконец, класс TTTBoard . В конце каждого списка MRO находится встроенный класс object , родительский для всех классов Python. При одиночном наследовании MRO определяется просто: достаточно взять цепоч- ку родительских классов. При множественном наследовании задача усложняется. В Python MRO следует алгоритму C3, подробности которого выходят за рамки книги. Но чтобы определить MRO, достаточно запомнить два правила. 356 Глава 16.Объектно-ориентированное программирование и наследование Python проверяет дочерние классы до родительских классов. Python проверяет классы, задействованные в наследовании, слева направо в порядке их перечисления в команде class Если вызвать getBoardStr() для объекта HybridBoard , Python сначала проверит класс HybridBoard . Затем, поскольку в родителях класса слева направо указаны HintBoard и MiniBoard , Python проверяет HintBoard . Этот родительский класс со- держит метод getBoardStr() , поэтому HybridBoard наследует и вызывает его. Но этим дело не заканчивается: далее метод вызывает super().getBoardStr() Имя функции Python super() выбрано неудачно, потому что возвращает она не родительский класс, а следующий класс в MRO. Это означает, что при вызове getBoardStr() для объекта HybridBoard следующим классом в MRO после HintBoard будет MiniBoard , а не родительский класс TTTBoard . Таким образом, вызов super(). getBoardStr() вызывает метод getBoardStr() класса MiniBoard , который возвращает компактное игровое поле. Оставшийся код getBoardStr() класса HintBoard после этого вызова super() присоединяет текст подсказки к строке. Если изменить команду class класса HybridBoard так, чтобы сначала в ней был указан класс MiniBoard , а потом HintBoard , в списке MRO класс MiniBoard будет предшествовать HintBoard . Это означает, что HybridBoard унаследует версию getBoardStr() от MiniBoard , в которой нет вызова super() . Именно этот порядок становится причиной ошибки с выводом компактного игрового поля без подска- зок: без вызова super() метод getBoardStr() класса MiniBoard не вызовет метод getBoardStr() класса HintBoard Множественное наследование позволяет реализовать значительную функцио- нальность в небольшом объеме кода, но легко приводит к возникновению переус- ложненного, трудного для понимания кода. Отдавайте предпочтение одиночному наследованию, примесям или решениям без наследования. Этих решений часто более чем достаточно для достижения целей вашей программы. Итоги Наследование — механизм повторного использования кода. Оно позволяет соз- давать дочерние классы, наследующие методы родительских классов. Вы можете переопределить методы, чтобы предоставить для них новый код, а также восполь- зоваться функцией super() для вызова исходных методов родительского класса. Дочерний класс связан с родительским классом отношениями типа «является <частным случаем>», потому что дочерний класс может рассматриваться как раз- новидность объекта родительского класса. Итоги 357 В Python использовать классы и наследование не обязательно. Некоторые про- граммисты считают, что преимущества наследования не окупают сложность от его неумеренного использования. Часто решения с композицией (вместо наследования) оказываются более гибкими, потому что они реализуют отношение «содержит» с объектом одного класса и объектами других классов вместо прямого наследова- ния методов этих других классов. Это означает, что объект одного класса может содержать объект другого класса. Например, объект Customer может содержать атрибут birthdate , которому присваивается объект Date , вместо субклассирования Date классом Customer Подобно тому как функция type() возвращает тип переданного ей объекта, функ- ции isinstance() и issubclass() возвращают тип и информацию о наследовании для переданного им объекта. Классы содержат методы и атрибуты объектов, но в них также могут присутствовать методы класса, атрибуты класса и статические методы. И хотя эти возможности используются редко, они позволяют реализовать некоторые объектно-ориенти- рованные приемы, которые не обеспечиваются глобальными переменными или функциями. Python позволяет классу наследовать нескольким родительским классам, хотя иногда это приводит к появлению трудного для понимания кода. Функция super() и методы класса определяют способ наследования методов на основании порядка разрешения методов (MRO). Чтобы просмотреть список MRO в интерактивной оболочке, вызовите метод mro() для класса. В этой и предыдущей главе я рассказал об общих концепциях ООП. В следующей главе мы займемся приемами ООП, специфическими для Python. 17 ООП в Python: свойства и dunder-методы Средства ООП включены во многие языки, но Python пре- доставляет ряд уникальных средств ООП, включая свой- ства и специальные методы. Умение пользоваться этими питоническими средствами помогает написать лаконичный и удобочитаемый код. Свойства позволяют выполнять заданный код при каждом чтении, изменении или удалении атрибута объекта; тем самым гарантируется, что объект не окажется в некорректном состоянии. В других языках такой код часто называется getter- или setter-методами. Dunder-методы (называемые также магическими методами) предо- ставляют возможность использовать ваши объекты со встроенными операторами Python — такими как оператор + . Например, так можно объединить два объекта datetime.timedelta ( datetime.timedelta(days=2) и datetime.timedelta(days=3) ) для создания нового объекта datetime.timedelta(days=5) Кроме других примеров, мы продолжим расширять класс WizCoin , работать с ко- торым начали в главе 15, и добавим в него свойства и перегрузку операторов с ис- пользованием dunder-методов. Эти возможности сделают объекты WizCoin более выразительными и простыми для применения в приложениях, импортирующих модуль wizcoin Свойства В классе BankAccount , использованном в главе 15, атрибут _balance мы пометили как приватный, для чего в начало его имени вставили символ подчеркивания _ Свойства 359 Однако следует помнить, что пометка атрибута как приватного всего лишь является условным соглашением: с технической точки зрения все атрибуты Python являются открытыми, то есть они доступны для кода за пределами класса. Ничто не помешает коду намеренно (в том числе и злонамеренно) изменить атрибут _balance и при- своить ему некорректное значение. Однако вы можете предотвратить случайные изменения приватных атрибутов при помощи свойств. В Python свойства представляют собой атрибуты, которым специально назначаются методы getter, setter и deleter, управляющие их чтением, изменением и удалением. Например, если атрибут должен принимать только целые значения, попытка присвоить ему строку '42' с большой вероятностью создаст ошибку. Свойство вызывает setter-метод для выполнения кода, который исправит (или по крайней мере исправит на ранней стадии) попытку некорректного присва- ивания. Если у вас возникает мысль: «А хорошо бы, чтобы при каждом обращении к атрибуту при его изменении командой присваивания или удалении командой del выполнялся какой-нибудь код», — используйте свойства. Преобразование атрибута в свойство Начнем с создания простого класса, который содержит обычный атрибут вместо свойства. Откройте в редакторе окно с новым файлом, введите следующий код и сохраните его с именем regularAttributeExample.py : class ClassWithRegularAttributes: def __init__(self, someParameter): self.someAttribute = someParameter obj = ClassWithRegularAttributes('some initial value') print(obj.someAttribute) # Выводит 'some initial value' obj.someAttribute = 'changed value' print(obj.someAttribute) # Выводит 'changed value' del obj.someAttribute # Удаляет атрибут someAttribute. Класс ClassWithRegularAttributes содержит обычный атрибут с именем someAttribute . Метод __init__() присваивает someAttribute строку 'some initial value' , но затем значение атрибута напрямую заменяется строкой 'changed value' При выполнении этой программы результат выглядит так: some initial value changed value Вывод показывает, что someAttribute можно легко присвоить любое значение. Недостаток обычных атрибутов заключается в том, что значение, присваиваемое someAttribute , может оказаться некорректным. Гибкость — отличное качество, но некорректное значение someAttribute может стать причиной ошибок в программе. 360 Глава 17.ООП в Python: свойства и dunder-методы Перепишем этот класс с использованием свойств. Выполните следующие действия, чтобы создать свойство для атрибута someAttribute 1. Переименуйте атрибут, чтобы имя начиналось с префикса _ : _someAttribute 2. Создайте метод с именем someAttribute и добавьте декоратор @property Этот getter-метод получает параметр self , который получают все методы. 3. Создайте другой метод с именем someAttribute и декоратором @someAttribute. setter . Этот setter-метод получает параметры self и value 4. Создайте еще один метод с именем someAttribute и добавьте декоратор @someAttribute.deleter . Этот deleter-метод получает параметр self , который получают все методы. Откройте в редакторе окно с новым файлом, введите следующий код и сохраните его с именем propertiesExample.py : class ClassWithProperties: def __init__(self): self.someAttribute = 'some initial value' @property def someAttribute(self): # Get-метод return self._someAttribute @someAttribute.setter def someAttribute(self, value): # Set-метод self._someAttribute = value @someAttribute.deleter def someAttribute(self): # Del-метод del self._someAttribute obj = ClassWithProperties() print(obj.someAttribute) # Выводит 'some initial value' obj.someAttribute = 'changed value' print(obj.someAttribute) # Выводит 'changed value' del obj.someAttribute # Удаляет атрибут _someAttribute. Вывод этой программы совпадает с выводом кода regularAttributeExample.py , потому что фактически они делают одно и то же: выводят исходный атрибут объекта, затем обновляют атрибут и снова выводят его. Но заметим, что код за пределами класса никогда не обращается к атрибуту _someAttribute напрямую (ведь он является приватным). Вместо этого внешний код обращается к свойству someAttribute . Возможно, структура такого свойства кому-то покажется немного абстрактной: оно образуется из getter-, setter- и deleter-метода. Когда мы переименовываем атрибут с именем someAttribute в _someAttribute Свойства 361 и одновременно создаем для него getter-, setter- и deleter-методы, мы получаем результат, который называется свойством someAttribute В этом контексте атрибут _someAttribute называется резервным полем (или резерв- ной переменной); это атрибут, на основе которого создается свойство. Резервная переменная используется многими, но не всеми свойствами. Свойство без резерв- ной переменной мы создадим в разделе «Свойства, доступные только для чтения» позднее в этой главе. Вы никогда не вызываете getter-, setter- и deleter-методы в своем коде, потому что Python делает это за вас в следующих обстоятельствах. Когда Python выполняет код, который обращается к свойству (например, print(obj.someAttribute) ), во внутренней реализации он вызывает getter- метод и использует возвращенное значение. Когда Python выполняет команду присваивания для свойства (например, obj.someAttribute = 'changed value' ), во внутренней реализации он вызывает setter-метод, передавая строку 'changed value' для параметра value Когда Python выполняет команду del для свойства (например, del obj. someAttribute ), во внутренней реализации он вызывает deleter-метод. Код методов getter, setter и deleter работает с резервной переменной напрямую. Ме- тоды getter, setter и deleter не должны работать со свойством, потому что это может привести к ошибкам. Один из возможных примеров: когда метод getter обращается к свойству, это заставляет метод вызвать самого себя, что заставляет его снова об- ратиться к свойству и так далее, пока в программе не произойдет фатальный сбой. Откройте в редакторе окно с новым файлом, введите следующий код и сохраните его с именем badPropertyExample.py : class ClassWithBadProperty: def __init__(self): self.someAttribute = 'some initial value' @property def someAttribute(self): # Get-метод. # Пропущен символ _ в `self._someAttribute`, из-за чего # свойство используется снова, а get-метод вызывается повторно: return self.someAttribute # Снова вызывается get-метод! @someAttribute.setter def someAttribute(self, value): # Set-метод. self._someAttribute = value obj = ClassWithBadProperty() print(obj.someAttribute) # Ошибка, get-метод вызывает get-метод. 362 Глава 17.ООП в Python: свойства и dunder-методы При попытке выполнить этот код метод getter непрерывно вызывает самого себя, пока Python не выдаст исключение RecursionError : Traceback (most recent call last): File "badPropertyExample.py", line 16, in print(obj.someAttribute) # Ошибка, get-метод вызывает getter-метод. File "badPropertyExample.py", line 9, in someAttribute return self.someAttribute # Снова вызывается getter-метод! File "badPropertyExample.py", line 9, in someAttribute return self.someAttribute # Снова вызывается getter-метод! File "badPropertyExample.py", line 9, in someAttribute return self.someAttribute # Снова вызывается getter-метод! [предыдущая строка повторяется еще 996 раз] RecursionError: maximum recursion depth exceeded Для предотвращения рекурсии код методов getter, setter и deleter должен всегда работать с резервной переменной (c префиксом _ в имени), а не со свойством. Код за пределами этих методов должен использовать переменную, хотя, как и с со- глашением о символе _ для обозначения приватного доступа, ничто не помешает написать код для обращения к резервной переменной. |