В программировании часто возникают ситуации, когда необходимо изменить поведение существующего класса без изменения его кода. Как это сделать эффективно и без дублирования кода? Для этого существует концепция объектно-ориентированного программирования — переопределение методов.
В этом уроке мы научимся переопределять методы в подклассах и на примерах посмотрим, как переопределение методов может упростить код.
Как работает переопределение
Представим, что у нас есть класс HTMLSelectElement
. Это HTML-элемент со списком элементов и методом для извлечения конкретного элемента:
class HTMLSelectElement:
def __init__(self, options):
self.options = options
def item(self, index):
return self.options[index]
В данном случае класс HTMLSelectElement
является простым и содержит список опций и метод item
, который позволяет извлекать конкретный элемент списка по индексу.
Теперь представим, что нам нужно изменить поведение метода item
так, чтобы он возвращал элемент списка, если индекс находится в пределах допустимого диапазона, и возвращал сообщение об ошибке, если индекс находится вне диапазона. Мы можем сделать это с помощью наследования и переопределения метода item
:
class HTMLCustomSelectElement(HTMLSelectElement):
def item(self, index):
if index < 0 or index >= len(self.options):
return "Ошибка: индекс вне диапазона"
return super().item(index)
Мы создали подкласс HTMLCustomSelectElement
, который переопределяет метод item(self, index)
. Переопределение означает, что в подклассе создается метод с тем же именем, что и в родительском классе.
Наш новый метод выполняет дополнительную работу по проверке индекса, но ему все еще нужен исходный метод item(self, index)
для выборки нужного элемента. Для этого применяется специальный синтаксис, который указывает, что нужно взять метод из родительского класса: super().item(index)
.
Теперь, используя наш новый класс, если пользователь попробует обратиться к элементу списка с несуществующим индексом, он получит сообщение об ошибке:
custom_options = HTMLCustomSelectElement(["Opt 1", "Opt 2", "Opt 3"])
print(custom_options.item(1)) # "Opt 2"
print(custom_options.item(3)) # "Ошибка: индекс вне диапазона"
Таким образом, переопределив метод item
в подклассе, мы расширили его функциональность и добавили обработку ошибок.
В нашем примере мы обращаемся к элементам списка. Но почему понадобился специальный синтаксис? Представим, что вместо него там был бы такой код:
def item(self, possible_index):
self.item(possible_index)
Какой в этом случае метод item
нужно брать — в определении которого мы находимся прямо сейчас или родительский?
Наследование устроено так, что всегда выбирается тот метод, который находится ближе в цепочке наследования. Поэтому вызов через self
породит рекурсию, но родительский метод никогда не будет вызван.
По этой же причине снаружи объекта невозможно вызвать методы, которые были переопределены в наследниках:
select = HTMLCustomSelectElement()
# Этот вызов всегда относится к методу item, переопределенному внутри HTMLCustomSelectElement
# Вызвать item напрямую из HTMLSelectElement невозможно
select.item(3)
Переопределение не ограничивается одним уровнем наследования. Любой переопределенный метод можно снова переопределить в наследниках текущего класса. Главное — соблюдать два условия:
- У обоих методов должны совпадать имена
- Они должны иметь одинаковое количество обязательных аргументов
Теперь рассмотрим, как на практике можно использовать классы-наследники, и какие трудности могут возникнуть.
Как использовать наследников
Создать класс-наследник и начать его использовать — это две большие разницы. В ситуациях, где эти классы создаются вами, все просто. Достаточно заменить вызовы старого класса на новый. Но если объекты этого класса создаются чужим кодом, то задача усложняется. Для подмены такого класса от чужого кода требуется поддержка полиморфного поведения.
Например, при работе с элементами HTML объекты этих классов иногда порождаются самим программистом, а иногда системой. Например:
# Создаем сами
element1 = HTMLSelectElement()
# Где-то внутри создается объект HTMLSelectElement
element2 = html.query_selector('select')
Можно ли в данном случае подменить класс в примере с query_selector()
? Единственный выход — использовать собственный класс. То есть конвертировать вернувшийся объект в объект нужного нам класса. Стоит ли оно того? Почти наверняка, что нет.
Другими словами, наследование для переопределения поведения хоть и кажется логичным шагом, но в действительности имеет серьезные ограничения по использованию.
Выводы
Использование наследования для переопределения поведения может оказаться сложным на практике из-за различных ограничений, связанных с этим подходом. Важно осознавать эти ограничения и учитывать их при принятии решений о проектировании кода. Иногда лучше использовать композиции классов или дизайн паттернов, таких как декораторы.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.