Книга: Как устроен Python. Гид для разработчиков, программистов и интересующихся
Назад: 21. Классы
Дальше: 23. Исключения

22. Субклассирование

Помимо группировки состояния и операций, классы также обеспечивают повторное использование кода. Если у вас уже имеется класс, а вам нужен другой класс, слегка отличающийся от него поведением, один из способов повторного использования заключается в субклассировании. Класс, от которого производится субклассирование, называется суперклассом (другое распространенное название суперкласса — родительский класс).

Предположим, вы хотите создать кресло, способное вмещать шесть лыжников. Чтобы создать класс Chair6, моделирующий кресло для шести человек, — более специализированную версию Chair, можно воспользоваться субклассированием. Субклассы позволяют программисту наследовать методы родительских классов и переопределять методы, которые нужно изменить.

Ниже приведен класс Chair6, который является субклассом CorrectChair:

>>> class Chair6(CorrectChair):

... max_occupants = 6

Обратите внимание: родительский класс CorrectChair заключается в круглые скобки после имени класса. Заметим, что Chair6 не определяет конструктор в своем теле, однако вы можете создавать экземпляры класса:

>>> sixer = Chair6(76)

Как Python создает объект, если в классе не определен конструктор? Вот что происходит: когда Python ищет метод .__init__, поиск начинается

515169.png 

Рис. 22.1. Атрибут __bases__ в субклассе. Связь между субклассом и его родительскими классами позволяет искать атрибуты в четко определенном порядке. Если атрибут определен в экземпляре субкласса, то используется этот атрибут. Если нет, то после экземпляра поиск продолжается в классе (__class__) экземпляра. Если и эта попытка оказывается неудачной, поиск осуществляется в родительских классах (__bases__)

515147.png 

Рис. 22.2. Создание экземпляра субкласса. Обратите внимание: экземпляр содержит указатель на свой класс, а класс — указатели на все родительские классы (атрибут __bases__)

с Chair6. Так как класс Chair6 содержит только атрибут max_occupants, Python не найдет здесь метод .__init__. Но поскольку Chair6 является субклассом CorrectChair, он обладает атрибутом __bases__ с перечислением базовых классов, сведенных в кортеж:

>>> Chair6.__bases__

(__main__.CorrectChair,)

Затем Python ищет конструктор в базовых классах. Он находит конструктор в CorrectChair и использует его для создания нового класса.

Такой же поиск происходит при вызове .load для экземпляра. У экземпляра нет атрибута, соответствующего имени метода, поэтому Python проверяет класс экземпляра. В Chair6 тоже нет метода .load, поэтому Python ищет атрибут в базовом классе CorrectChair. Здесь метод .load вызывается со слишком большим значением, что приводит к ошибке ValueError:

>>> sixer.load(7)

Traceback (most recent call last):

File "/tmp/chair.py", line 30, in <module>

sixer.load(7)

File "/tmp/chair.py", line 13, in load

new_val = self._check(self.count + number)

File "/tmp/chair.py", line 23, in _check

number))

ValueError: Invalid count:7

Python находит метод в базовом классе, но вызов метода ._check приводит к ошибке ValueError.

22.1. Подсчет остановок

Иногда лыжнику не удается нормально сесть на подъемник. В таких случаях оператор замедляет движение или останавливает подъемник, чтобы помочь лыжнику. Мы можем воспользоваться Python для создания нового класса, который будет подсчитывать количество таких остановок.

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

Ниже приведен класс, который получает функцию is_stalled в конструкторе. Эта функция будет вызываться при каждом вызове .load:

>>> class StallChair(CorrectChair):

... def __init__(self, id, is_stalled):

... super().__init__(id)

... self.is_stalled = is_stalled

... self.stalls = 0

...

... def load(self, number):

... if self.is_stalled(number, self):

... self.stalls += 1

... super().load(number)

Чтобы создать экземпляр этого класса, необходимо предоставить функцию is_stalled. Следующая простая функция генерирует остановки в 10 % случаев:

>>> import random

>>> def ten_percent(number, chair):

... """Return True 10% of time"""

... return random.random() < .1

Теперь можно создать экземпляр, указав функцию ten_percent в качестве параметра is_stalled:

>>> stall42 = StallChair(42, ten_percent)

22.2. super

Напомним, что StallChair определяет свой собственный метод .__init__, который вызывается при создании экземпляра. Обратите внимание: первая строка конструктора выглядит так:

super().__init__(id)

При вызове super внутри метода вы получаете доступ к правильному родительскому классу. Строка в конструкторе позволяет вызвать конструктор CorrectChair. Вместо того чтобы повторять логику назначения

515186.png 

Рис. 22.3. Код создания субкласса с измененными методами. Обратите внимание на использование super() для вызова метода родительского класса. Диаграмма показывает, какие объекты создаются при создании класса, который является субклассом

атрибутов id и count, вы можете использовать логику из родительского класса. Так как StallChair имеет дополнительные атрибуты, которые нужно задать для экземпляра, это можно сделать после вызова родительского конструктора.

Метод .load тоже содержит вызов super:

def load(self, number):

if self.is_stalled(number, self):

self.stalls += 1

super().load(number)

В методе .load вы вызываете функцию is_stalled, чтобы определить, останавливался ли подъемник, после чего передаете управление исходной функциональности .load из CorrectChair при помощи super.

Размещение общего кода в одном месте (в базовом классе) сокращает количество ошибок и дублирований кода.

ПРИМЕЧАНИЕ

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

22.3. Итоги

В этой главе рассматриваются субклассы — новые специализированные классы, использующие код из своих базовых классов (также называемых суперклассами или родительскими классами). Для любого метода, не реализованного в субклассе, Python использует функциональность родительского класса. В реализации метода вы можете либо переопределить его полностью, либо включить вызов super. При вызове super вы получаете доступ к родительскому классу, чтобы использовать содержащуюся в нем функциональность.

22.4. Упражнения

1. Создайте класс, представляющий кошку. Что может делать кошка? Какими свойствами она обладает? Создайте субкласс кошки, представляющий тигра. Как изменится поведение субкласса?

2. В предыдущей главе вы создали класс, представляющий Марио из видеоигры Super Mario Brothers. В последних изданиях игры можно было играть за других персонажей. Все они обладали сходной базовой функциональностью, но различались способностями. Создайте базовый класс, представляющий персонажа, после чего реализуйте четыре субкласса — для Марио, Луиджи, Тода и Принцессы.

Имя

Марио

Луиджи

Тод

Принцесса

Скорость

4

3

5

2

Прыжок

4

5

2

3

Сила

4

3

5

2

Специальное умение

     

Парение в воздухе

 

Назад: 21. Классы
Дальше: 23. Исключения