Как переопределить атрибут базового класса python
Перейти к содержимому

Как переопределить атрибут базового класса python

  • автор:

Наследование#

Наследование является ключевым аспектом ООП и, конечно, доступно в python . Как уже многократно подмечалось ранее, в python всё является объектом. Можно даже сказать больше, любой тип объекта (встроенный или пользовательский) или напрямую или через один из своих базовых классов расширяет тип object .

В частности, следующее объявление класса неявно расширяет object .

Специальный атрибут __bases__ позволяет узнать базовый класс (или классы, python поддерживает множественное наследование). Встроенная функция issubclass возвращает True , отвечает на вопрос, является ли класс указанный в первом аргументе производным от класса указанного во втором аргументе.

Базовый синтаксис#

Допустим, у нас есть базовый класс BaseClass и мы хотим объявить класс DerivedClass , который будет расширять его. Тогда необходимо указать базовый класс в заголовочной строке производного класса в круглых скобках после имени базового класса.

В данном примере заголовок class DerivedClass(BaseClass): сигнализирует, что DerivedClass наследует от BaseClass .

../../_images/single_0.png

При проверке принадлежности экземпляра производного класса к базовому классу метод isinstance вернет True .

Из-за этой особенности, принято проверять принадлежность к классу именно методом isinstance(obj, cls) , а не выражением вида type(obj) == cls . Это позволяет писать код, который не будет замечать разницы между экземплярами базового и производного классов. В ряде ситуаций область применения такого кода можно будет расширять, не редактируя его.

Наследование атрибутов класса#

Кроме того, производный класс наследует атрибуты и методы базового класса (речь пока идет про атрибуты самого класса, а не экземпляра).

Определим базовый класс с атрибутом attr и со статическим (для удобства вызова) методом method .

../../_images/single_0.png

Видим, что через объект объявления производного класса DerivedClass удаётся получить доступ к атрибутам и методам базового класса BaseClass .

Механизм наследования атрибутов не совсем очевиден. У объекта объявления производного класса не появляются атрибуты базового класса, но получить доступ к атрибуту базового класса через объект объявления производного класса получается из-за механизма поиска атрибутов класса.

Пусть мы пытаемся выражением C.x получить доступ к атрибуту x класса C , который расширяет класс B . Тогда выполняется следующая процедура.

Атрибут x ищется у класса C . Если он обнаруживается, то он и возвращается;

Если атрибут x у класса C найти не удаётся, то атрибут x ищется у базового класса B .

Второй шаг выполняется рекурсивно, т.е. если B расширяет класс A и в B тоже не удаётся найти атрибут x , то поиск продолжится в классе A (и далее по цепочке наследования).

Доступ к атрибутам базового класса через экземпляр производного класса тоже возможен, т.к. процедура поиска атрибута у экземпляра делегирует этот поиск классу этого экземпляра (см. процедуру выше), если в самом экземпляре нет такого атрибута.

Перекрытие атрибутов#

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

Расширим определение производного класса DerivedClass из предыдущего примера его собственными атрибутами attr и method .

../../_images/single_0.png

Видим, что теперь через объект объявления производного класса вызываются его же методы.

Множественное наследование#

Можно наследовать сразу от нескольких базовых классов. Для этого необходимо указать их через запятую.

В примере DerivedClass наследует сразу от трех классов.

../../_images/multi.png

Атрибуты в базовых классах пересекаются: атрибут b есть и у LeftBase и у MiddleBase , атрибут c есть и у MiddleBase и у RightBase . Возникает вопрос, если обратиться от производного класса к этим атрибутам, то значение атрибута какого из базовых классов вернется в качестве результата? Распечатаем атрибуты a , b , c и d класса DerivedClass .

Видим, что возвращается атрибут того базового класса, который указан в списке базовых классов первым (самый левый).

Вызов методов базового класса. Функция super #

Иногда все же возникает необходимость вызвать перекрытый метод базового класса в экземпляре производного класса. Яркий пример — инициализация объекта. При создании объекта необходимо убедиться, что будет вызван и метод __init__ базового класса и метод __init__ производного класса. Обычно этого добиваются вызовом инициализирующего метода базового класса в самом начале инициализирующего метода производного класса.

Неопытному программисту на python может показаться, что этого можно добиться следующим образом:

Но это приведет к рекурсии: метод B.__init__ создаётся на этапе объявления класса, а значит при поиске атрибута self.__init__ найдется именно B.__init__ (у экземпляра self такого атрибута нет, а значит поиск идёт в его классе), а не A.__init__ .

Выход из этой ситуации — вызвать метод A.__init__ явно.

При таком подходе произойдет то, чего мы и добивались. Тем не менее принято делать это иначе, а именно использовать встроенную функцию super. В нашем примере, инструкция

Параметр self при вызове через super передавать не надо!

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

../../_images/super.png

Рассмотрим следующую иерархию наследования: South наследует от West и East , каждый из которых в свою очередь расширяют класс North . Хочется, чтобы при инициализации экземпляра класса South вызывались и методы инициализации всех базовых классов. Попробуем реализовать эту схему, указывая все базовые классы явно.

Видим, что метод инициализации класса North вызвался дважды. Первый раз это произошло через класс West , а второй раз через класс East . Теперь заменим все явные упоминания классов через функцию super .

Проблема с тем, что метод инициализации North вызывался дважды, решена! Функция super использует С3-линеаризацию (method resolution order) для определения порядка, в котором вызывать методы классов в иерархии наследования. Но чтобы это работало, необходимо, чтобы везде вызов происходил именно через super .

Абстрактный базовый класс. Абстрактный метод.#

Модуль abc (сокращение от Abstract Base Class ) предоставляет инструменты для реализации абстрактных базовых классов, т.е. классов, которые лишь задают интерфейс и не предназначены для создания экземпляров напрямую. Обычно, абстрактный базовый класс наследует от abc.ABC, а абстрактные методы помечаются декоратором abc.abstractmethod. Производные от такого абстрактного базового класса классы смогут создавать экземпляры, только если они переопределят все абстрактные методы. Если не переопределен хоть один из абстрактных методов, то python возбудит ошибку при попытке создать экземпляр. Так как тело абстрактной функции не играет никакой роли, то в нем часто возбуждают исключение NotImplementedError.

В качестве примера реализуем абстрактный базовый класс Shape для геометрической фигуры. Как и в примере с треугольником, будем считать, что каждая фигура должна уметь считать свой периметр и площадь.

Как переопределить атрибут базового класса python

В прошлой статье класс Employee полностью перенимал функционал класса Person:

Но что, если мы хотим что-то изменить из этого функционала? Например, добавить работнику через конструктор, новый атрибут, который будет хранить компанию, где он работает или изменить реализацию метода display_info. Python позволяет переопределить функционал базового класса.

Например, изменим классы следующим образом:

Здесь в классе Employee добавляется новый атрибут — self.company , который хранит компания работника. Соответственно метод __init__() принимает три параметра: второй для установки имени и третий для установки компании. Но если в базом классе определен конструктор с помощью метода __init__, и мы хотим в производном классе изменить логику конструктора, то в конструкторе производного класса мы должны вызвать конструктор базового класса. То есть в конструкторе Employee надо вызвать конструктор класса Person.

Для обращения к базовому классу используется выражение super() . Так, в конструкторе Employee выполняется вызов:

Это выражение будет представлять вызов конструктора класса Person, в который передается имя работника. И это логично. Ведь имя работника устанавливается именно в конструкторе класса Person. В самом конструкторе Employee лишь устанавливаем свойство company.

Кроме того, в классе Employee переопределяется метод display_info() — в него добавляется вывод компании работника. Причем мы могли определить этот метод следующим образом:

Но тогда строка вывода имени повторяла бы код из класса Person. Если эта часть кода совпадает с методом из класса Person, то нет смысла повторяться, поэтому опять же с помощью выражения super() обращаемся к реализации метода display_info в классе Person:

Затем мы можем вызвать вызвать конструктор Employee для создания объекта этого класса и вызвать метод display_info:

Консольный вывод программы:

Проверка типа объекта

При работе с объектами бывает необходимо в зависимости от их типа выполнить те или иные операции. И с помощью встроенной функции isinstance() мы можем проверить тип объекта. Эта функция принимает два параметра:

Первый параметр представляет объект, а второй — тип, на принадлежность к которому выполняется проверка. Если объект представляет указанный тип, то функция возвращает True. Например, возьмем следующую иерархию классов Person-Employee/Student:

Здесь класс Employee определяет метод work(), а класс Student — метод study.

Здесь также определена функция act , которая проверяет с помощью функции isinstance , представляет ли параметр person определнный тип, и зависимости от результатов проверки обращается к определенному методу объекта.

Python. Metaclass. Переопределение атрибутов

Это можно объяснить, добавив немного лишних print’ов:

Как видно, метакласс — это не родитель класса, а то, что этот класс создает. Одна из целей метаклассов — менять классы на этапе их создания. Что и произошло.

m9_psy's user avatar

Дизайн сайта / логотип © 2023 Stack Exchange Inc; пользовательские материалы лицензированы в соответствии с CC BY-SA . rev 2023.7.27.43547

Нажимая «Принять все файлы cookie» вы соглашаетесь, что Stack Exchange может хранить файлы cookie на вашем устройстве и раскрывать информацию в соответствии с нашей Политикой в отношении файлов cookie.

Наследование

Наследование – важная составляющая объектно-ориентированного программирования. Так или иначе мы уже сталкивались с ним, ведь объекты наследуют атрибуты своих классов. Однако обычно под наследованием в ООП понимается наличие классов и подклассов. Также их называют супер- или надклассами и классами, а также родительскими и дочерними классами.

Суть наследования здесь схожа с наследованием объектами от классов. Дочерние классы наследуют атрибуты родительских, а также могут переопределять атрибуты и добавлять свои.

Простое наследование методов родительского класса

В качестве примера рассмотрим два класса столов. Класс Table – родительский по отношению к DeskTable (письменные столы). Независимо от своего типа все столы имеют длину, ширину и высоту. Пусть для письменных столов также важна площадь поверхности. Общее вынесем в класс, частное – в подкласс.

Наследственная связь между классами устанавливается через подкласс. При определении дочернего после его имени в скобках указывается родительский.

В данном случае у класса DeskTable нет своего конструктора, поэтому он наследует его от родителя. При создании объектов передавать аргументы необходимо в обоих случаях. Попытка вызова DeskTable с пустыми скобками приведет к ошибке.

С другой стороны, экземпляры надкласса Table , согласно неким родственным связям, не наследуют метод square своего подкласса.

В этом смысле терминология «родительский и дочерний класс» не совсем верна. Наследование в ООП – это скорее аналог систематизации и классификации наподобие той, что есть в живой природе. Все млекопитающие имеют четырехкамерное сердце, но только носороги – рог.

Полное переопределение метода надкласса

Рассмотрим вариант программы с «цепочкой наследования». Пусть дочерний по отношению к Table класс DeskTable в свою очередь выступит родительским по отношению к ComputerTable (компьютерные столы):

Допустим, по задумке разработчиков рабочая поверхность компьютерного стола может вычисляться за вычетом площади, которую занимает монитор. В результате метод square в ComputerTable имеет отличия.

Определив в дочернем классе метод, одноименный методу родительского, мы тем самым переопределяем метод родительского класса. При вызове square на экземпляры ComputerTable будет вызываться метод из этого класса, а не из родительского класса DeskTable .

В то же время ComputerTable наследует конструктор класса от своей «бабушки» – класса Table .

Дополнение, оно же расширение, метода

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

Пусть имеется подкласс кухонных столов, для которых необходимо еще одно поле – количество посадочных мест. Из-за этой детали мы вынуждены в дочернем классе переопределить конструктор родительского.

Поскольку существенная часть кода конструктора подкласса является такой же как в надклассе, правильнее будет вызвать метод другого класса, а не дублировать код:

Здесь в теле конструктора KitchenTable мы вызываем метод __init__ через объект-класс Table , а не через объект-экземпляр. Вспомним, что в таких случаях метод вызывается как обычная функция (объект, к которому применяется метод, не передается в качестве первого аргумента). Поэтому в конструктор надкласса мы «вручную» передаем текущий экземпляр ( self ), записывая его перед остальными аргументами.

У кода выше есть небольшой недостаток. Нам ничего не мешает (при условии совпадения количества параметров) вызвать конструктор другого класса, а не только родительского, указав его имя вместо Table . Кроме того, имя надкласса может измениться, и тогда есть риск неправильных обращений к нему из дочерних классов.

В Python с целью улучшения так называемой обслуживаемости кода можно использовать встроенную в язык функцию super . Наиболее распространенным вариантом ее применения является вызов метода родительского класса из метода подкласса:

В данном случае аргумент self в скобках вызываемого родительского метода указывать явно не требуется.

Параметры со значениями по умолчанию у родительского класса

Рассмотрим случай, когда родительский класс имеет параметры со значениями по умолчанию, а дочерний – нет:

При таком определении классов можно создать экземпляр от Table без передачи аргументов для конструктора:

Можем ли мы создать экземпляр от KitchenTable , передав значение только для параметра p ? Например, вот так:

Возможно ли, что p будет присвоено число 10, а l , w и h получат по единице от родительского класса? Невозможно, будет выброшено исключение по причине несоответствия количества переданных аргументов количеству требуемых конструктором:

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

Поэтому, если требуется допустить создание объектов от дочернего класса без передачи аргументов, придется назначить значения по умолчанию также в конструкторе дочернего класса.

Параметр p , которого нет у родительского класса, мы делаем последним не просто так. Бывает, объекты разных родственных классов создаются или обрабатываются в одном цикле, то есть по одному алгоритму. При этом у них должны быть одинаковые «интерфейсы», то есть одинаковое количество передаваемых в конструктор аргументов.

Поэтому лучше, когда методы родственных классов принимают одинаковое число параметров. А если разное, то у «лишних» должны быть значения по-умолчанию, чтобы при вызове конструктора их можно было бы не передавать. Если такие параметры находятся еще и в конце, передачу аргументов для предстоящих параметров можно выполнять без ключей.

Другой вариант – отказаться от конструктора в дочернем классе, а значение для поля places устанавливать отдельным вызовом метода:

Здесь у всех кухонных столов по-умолчанию будет 4 места. Если мы хотим изменить значение поля places , можем вызвать метод set_places . Хотя в случае Python можем сделать это напрямую, присвоив полю. При этом у экземпляра появится собственное поле places .

Поэтому метод set_places в общем-то не нужен.

В любом случае произвольное количество мест будет устанавливаться не в конструкторе, а отдельно. Если все же требуется указывать места при создании объекта, это можно сделать и в конструкторе родителя:

С помощью функции isinstance проверяется, что создаваемый объект имеет тип KitchenTable . Если это так, то у него появляется поле places .

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

Практическая работа

Разработайте программу по следующему описанию.

В некой игре-стратегии есть солдаты и герои. У всех есть свойство, содержащее уникальный номер объекта, и свойство, в котором хранится принадлежность команде. У солдат есть метод «иду за героем», который в качестве аргумента принимает объект типа «герой». У героев есть метод увеличения собственного уровня.

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

Измеряется длина списков солдат противоборствующих команд и выводится на экран. У героя, принадлежащего команде с более длинным списком, увеличивается уровень.

Отправьте одного из солдат первого героя следовать за ним. Выведите на экран идентификационные номера этих двух юнитов.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *