Как переопределить атрибут базового класса python
В прошлой статье класс Employee полностью перенимал функционал класса Person:
class Person: def __init__(self, name): self.__name = name # имя человека @property def name(self): return self.__name def display_info(self): print(f"Name: ") class Employee(Person): def work(self): print(f" works")
Но что, если мы хотим что-то изменить из этого функционала? Например, добавить работнику через конструктор, новый атрибут, который будет хранить компанию, где он работает или изменить реализацию метода display_info. Python позволяет переопределить функционал базового класса.
Например, изменим классы следующим образом:
class Person: def __init__(self, name): self.__name = name # имя человека @property def name(self): return self.__name def display_info(self): print(f"Name: ") class Employee(Person): def __init__(self, name, company): super().__init__(name) self.company = company def display_info(self): super().display_info() print(f"Company: ") def work(self): print(f" works") tom = Employee("Tom", "Microsoft") tom.display_info() # Name: Tom # Company: Microsoft
Здесь в классе Employee добавляется новый атрибут — self.company , который хранит компания работника. Соответственно метод __init__() принимает три параметра: второй для установки имени и третий для установки компании. Но если в базом классе определен конструктор с помощью метода __init__, и мы хотим в производном классе изменить логику конструктора, то в конструкторе производного класса мы должны вызвать конструктор базового класса. То есть в конструкторе Employee надо вызвать конструктор класса Person.
Для обращения к базовому классу используется выражение super() . Так, в конструкторе Employee выполняется вызов:
super().__init__(name)
Это выражение будет представлять вызов конструктора класса Person, в который передается имя работника. И это логично. Ведь имя работника устанавливается именно в конструкторе класса Person. В самом конструкторе Employee лишь устанавливаем свойство company.
Кроме того, в классе Employee переопределяется метод display_info() — в него добавляется вывод компании работника. Причем мы могли определить этот метод следующим образом:
def display_info(self): print(f"Name: ") print(f"Company: ")
Но тогда строка вывода имени повторяла бы код из класса Person. Если эта часть кода совпадает с методом из класса Person, то нет смысла повторяться, поэтому опять же с помощью выражения super() обращаемся к реализации метода display_info в классе Person:
def display_info(self): super().display_info() # обращение к методу display_info в классе Person print(f"Company: ")
Затем мы можем вызвать вызвать конструктор Employee для создания объекта этого класса и вызвать метод display_info:
tom = Employee("Tom", "Microsoft") tom.display_info()
Консольный вывод программы:
Name: Tom Company: Microsoft
Проверка типа объекта
При работе с объектами бывает необходимо в зависимости от их типа выполнить те или иные операции. И с помощью встроенной функции isinstance() мы можем проверить тип объекта. Эта функция принимает два параметра:
isinstance(object, type)
Первый параметр представляет объект, а второй — тип, на принадлежность к которому выполняется проверка. Если объект представляет указанный тип, то функция возвращает True. Например, возьмем следующую иерархию классов Person-Employee/Student:
class Person: def __init__(self, name): self.__name = name # имя человека @property def name(self): return self.__name def do_nothing(self): print(f" does nothing") # класс работника class Employee(Person): def work(self): print(f" works") # класс студента class Student(Person): def study(self): print(f" studies") def act(person): if isinstance(person, Student): person.study() elif isinstance(person, Employee): person.work() elif isinstance(person, Person): person.do_nothing() tom = Employee("Tom") bob = Student("Bob") sam = Person("Sam") act(tom) # Tom works act(bob) # Bob studies act(sam) # Sam does nothing
Здесь класс Employee определяет метод work(), а класс Student — метод study.
Здесь также определена функция act , которая проверяет с помощью функции isinstance , представляет ли параметр person определнный тип, и зависимости от результатов проверки обращается к определенному методу объекта.
Наследование. Атрибуты private и protected
Мы продолжаем изучение темы «наследование». На этом занятии мы увидим, как влияет режим доступа private и protected атрибутов при наследовании классов.
- _attribute (с одним подчеркиванием) – режим доступа protected (служит для обращения внутри класса и во всех его дочерних классах)
- __attribute (с двумя подчеркиваниями) – режим доступа private (служит для обращения только внутри класса).
class Geom: name = 'Geom' def __init__(self, x1, y1, x2, y2): print(f"инициализатор Geom для ") self.__x1 = x1 self.__y1 = y1 self.__x2 = x2 self.__y2 = y2 class Rect(Geom): def __init__(self, x1, y1, x2, y2, fill='red'): super().__init__(x1, y1, x2, y2) self.__fill = fill
Здесь мы пытаемся в инициализаторе базового класса Geom сформировать приватные локальные свойства с координатами прямоугольника. Дополнительно в инициализаторе самого класса создается приватное свойство __fill. Ниже создадим объект класса Rect:
r = Rect(0, 0, 10, 20)
и выведем все его локальные атрибуты в консоль:
print(r.__dict__)
После запуска программы увидим следующие строчки: инициализатор Geom для
Смотрите, локальные свойства с координатами имеют префикс _Geom, то есть, префикс того класса, в котором они непосредственно были прописаны. Несмотря на то, что параметр self является ссылкой на объект класса Rect. Это особенность поведения (формирования) приватных атрибутов в базовых классах. У них всегда добавляется префикс именно базового класса, а не класса объекта self. А вот последнее свойство __fill имеет ожидаемый префикс _Rect, так как оно было создано в классе Rect. Что из этого следует? Во-первых, мы, конечно же, не можем обратиться в свойствам-координатам в дочернем классе Rect. Если в нем прописать метод get_coords():
def get_coords(self): return (self.__x1, self.__y1, self.__x2, self.__y2)
а, затем, вызвать через объект класса Rect:
r.get_coords()
то увидим ошибку AttributeError. Но если перенести этот метод в базовый класс Geom, то все сработает без ошибок, так как приватным свойствам будет добавлен правильный префикс _Geom. Возможно, вам кажется это немного запутанным? Но давайте вспомним, а для чего вообще нужны и когда используются приватные атрибуты. Мы говорили, что это закрытые от внешнего вмешательства свойства или методы текущего класса, доступные только внутри этого класса и недоступные из других, в том числе и из дочерних классов. Именно поэтому приватные атрибуты жестко привязываются к текущему классу, в котором они создаются, так как по логике предполагается их использовать только внутри этого класса и больше нигде. Если же нам нужно определить закрытые атрибуты, доступные в текущем классе и во всех его дочерних классах, то для этого следует использовать метод определения protected – одно нижнее подчеркивание. Поэтому правильнее было бы создавать свойства-координаты в базовом инициализаторе в режиме protected:
class Geom: name = 'Geom' def __init__(self, x1, y1, x2, y2): print(f"инициализатор Geom для ") self._x1 = x1 self._y1 = y1 self._x2 = x2 self._y2 = y2 class Rect(Geom): def __init__(self, x1, y1, x2, y2, fill='red'): super().__init__(x1, y1, x2, y2) self._fill = fill def get_coords(self): return (self._x1, self._y1, self._x2, self._y2)
Тогда никаких проблем с доступом уже не возникает:
r = Rect(0, 0, 10, 20) print(r.__dict__) r.get_coords()
После запуска программы увидим следующие строчки: инициализатор Geom для
Опять же, как я ранее отмечал, режим доступа protected в реальности никак не ограничивает доступ к атрибутам объектов класса или самого класса. Например, мы можем обратиться к координатам напрямую через экземпляр класса:
print(r._x1)
Никаких ошибок не будет. Нижнее подчеркивание лишь предупреждает (сигнализирует) программиста о защищенном атрибуте, к которому напрямую лучше не обращаться. Этот атрибут был создан для внутренней логики работы алгоритма в классе и не предназначен для обращения извне. В дальнейшем это может привести к проблемам, например, при изменении версии класса, в котором такого атрибута уже не будет, или он будет играть другую роль и т.п.
Атрибуты private и protected на уровне класса
Все также работает и с атрибутами уровня класса. Например, сейчас мы совершенно спокойно можем обратиться к свойству name класса Geom через объект класса Rect:
print(r.name)
Добавив одно нижнее подчеркивание, функционал останется прежним, мы лишь отметим, что к этой переменной извне лучше не обращаться:
print(r._name)
Но, если прописать два подчеркивания, то доступ будет закрыт всюду, кроме самого класса Geom:
print(r.__name)
class Rect(Geom): def __init__(self, x1, y1, x2, y2, fill='red'): super().__init__(x1, y1, x2, y2) self._fill = fill self._name = self.__name
Но в Geom мы можем к ней обращаться:
class Geom: __name = 'Geom' def __init__(self, x1, y1, x2, y2): print(f"инициализатор ") self._x1 = x1 self._y1 = y1 self._x2 = x2 self._y2 = y2
Те же ограничения доступа можно накладывать и на методы. Если в базовом классе Geom определить приватный метод, например, для проверки корректности значений координат:
class Geom: . def __verify_coord(self, coord): return 0 coord 100
то он будет доступен только внутри этого класса и вызвать его, скажем, в дочернем классе Rect уже не получится:
class Rect(Geom): def __init__(self, x1, y1, x2, y2, fill='red'): super().__init__(x1, y1, x2, y2) super().__verify_coord(x1) self._fill = fill
Как сделать переопределение атрибута класса обязательным
У меня есть класс BaseWeapon как сделать так, чтобы после наследования его другим классом обязательно нужно было переопределять атрибут self.stats ? Предполагаю, что нужно использовать модуль abc из стандартной библиотеки. Класс BaseWeapon:
class BaseWeapon: class Stats: def __init__(self, power): self.power = power def __init__(self, owner, name): self._owner = owner self._name = name self._stats = None @property def stats(self): return self._stats @stats.setter def stats(self, another_stats): if isinstance(another_stats, self.Stats): self._stats = another_stats else: raise TypeError(f'Value must be Stats class, not ')
Примеры ожидаемых подклассов:
class NoWeapon(BaseWeapon): def __init__(self, owner): super().__init__(owner, 'Empty') self.stats = self.Stats(power=1) class Claws(BaseWeapon): def __init__(self, owner): super().__init__(owner, 'Claws') self.stats = self.Stats(power=10)
Отслеживать
149k 12 12 золотых знаков 59 59 серебряных знаков 132 132 бронзовых знака
задан 11 мая 2020 в 15:58
Code Is my life Code Is my life
166 8 8 бронзовых знаков
Как вариант решения вашей задачи — не определять атрибут в родительском классе вовсе. В таком случае при любом обращении к методам будет вызываться AttributeError , т.к. атрибута нет, а обращение к нему в методе есть
11 мая 2020 в 16:26
@Евгений Какой именно параметр? _stats или может property stats?
11 мая 2020 в 16:28
не объявлять _stats внутри родительского __init__ . Ведь его вам нужно обязать переопределять?
11 мая 2020 в 16:30
чёт фигня-все мечи 10 повара будут ? пусть лучше клас принимает этотот стат при создании
11 мая 2020 в 16:31
@CodeIsmylife документация/описание класса
11 мая 2020 в 16:37
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
Вы можете вовсе не объявлять _stats в родительском классе, но всё также обращаться к нему в методах этого класса.
Результатом такого решения будет то, что при попытке вызвать любой метод класса будет вызван AttributeError , если _stats не был определён в классе-наследнике.
Фактически это указывает на то, что _stats нужен, но его ещё нет, потому его нужно определить.
Не забывайте указывать это в документации либо описании класса.
class BaseWeapon: """ Дочерний класс обязан определить атрибут _stats """ class Stats: def __init__(self, power): self.power = power def __init__(self, owner, name): self._owner = owner self._name = name # self._stats = None - убрать @property def stats(self): return self._stats @stats.setter def stats(self, another_stats): if isinstance(another_stats, self.Stats): self._stats = another_stats else: raise TypeError(f'Value must be Stats class, not ')
Наследование
Наследование – важная составляющая объектно-ориентированного программирования. Так или иначе мы уже сталкивались с ним, ведь объекты наследуют атрибуты своих классов. Однако обычно под наследованием в ООП понимается наличие классов и подклассов. Также их называют супер- или надклассами и классами, а также родительскими и дочерними классами.
Суть наследования здесь схожа с наследованием объектами от классов. Дочерние классы наследуют атрибуты родительских, а также могут переопределять атрибуты и добавлять свои.
—>
Простое наследование методов родительского класса
В качестве примера рассмотрим два класса столов. Класс Table – родительский по отношению к DeskTable (письменные столы). Независимо от своего типа все столы имеют длину, ширину и высоту. Пусть для письменных столов также важна площадь поверхности. Общее вынесем в класс, частное – в подкласс.
Наследственная связь между классами устанавливается через подкласс. При определении дочернего после его имени в скобках указывается родительский.
class Table: def __init__(self, l, w, h): self.length = l self.width = w self.height = h class DeskTable(Table): def square(self): return self.width * self.length t1 = Table(1.5, 1.8, 0.75) t2 = DeskTable(0.8, 0.6, 0.7) print(t2.square()) # вывод: 0.48
В данном случае у класса DeskTable нет своего конструктора, поэтому он наследует его от родителя. При создании объектов передавать аргументы необходимо в обоих случаях. Попытка вызова DeskTable с пустыми скобками приведет к ошибке.
С другой стороны, экземпляры надкласса Table , согласно неким родственным связям, не наследуют метод square своего подкласса.
В этом смысле терминология «родительский и дочерний класс» не совсем верна. Наследование в ООП – это скорее аналог систематизации и классификации наподобие той, что есть в живой природе. Все млекопитающие имеют четырехкамерное сердце, но только носороги – рог.
Полное переопределение метода надкласса
Рассмотрим вариант программы с «цепочкой наследования». Пусть дочерний по отношению к Table класс DeskTable в свою очередь выступит родительским по отношению к ComputerTable (компьютерные столы):
class Table: def __init__(self, l, w, h): self.length = l self.width = w self.height = h class DeskTable(Table): def square(self): return self.width * self.length class ComputerTable(DeskTable): def square(self, monitor=0.0): return self.width * self.length - monitor t3 = ComputerTable(0.8, 0.6, 0.7) print(t3.square(0.3)) # вывод: 0.18
Допустим, по задумке разработчиков рабочая поверхность компьютерного стола может вычисляться за вычетом площади, которую занимает монитор. В результате метод square в ComputerTable имеет отличия.
Определив в дочернем классе метод, одноименный методу родительского, мы тем самым переопределяем метод родительского класса. При вызове square на экземпляры ComputerTable будет вызываться метод из этого класса, а не из родительского класса DeskTable .
В то же время ComputerTable наследует конструктор класса от своей «бабушки» – класса Table .
Дополнение, оно же расширение, метода
Часто требуется не столько заменить, то есть полностью переопределить, метод родительского класса в дочернем, сколько дополнить, то есть расширить, код метода родительского класса в дочернем. В таких случаях решением является вызов метода надкласса в теле соответствующего метода подкласса. Обычно после этого в теле метода подкласса пишется дополнительный код.
Пусть имеется подкласс кухонных столов, для которых необходимо еще одно поле – количество посадочных мест. Из-за этой детали мы вынуждены в дочернем классе переопределить конструктор родительского.
class Table: def __init__(self, l, w, h): self.length = l self.width = w self.height = h class KitchenTable(Table): def __init__(self, l, w, h, p): self.length = l self.width = w self.height = h self.places = p t4 = KitchenTable(1.5, 2, 0.75, 6)
Поскольку существенная часть кода конструктора подкласса является такой же как в надклассе, правильнее будет вызвать метод другого класса, а не дублировать код:
class Table: def __init__(self, l, w, h): self.length = l self.width = w self.height = h class KitchenTable(Table): def __init__(self, l, w, h, p): Table.__init__(self, l, w, h) self.places = p t4 = KitchenTable(1.5, 2, 0.75, 6)
Здесь в теле конструктора KitchenTable мы вызываем метод __init__ через объект-класс Table , а не через объект-экземпляр. Вспомним, что в таких случаях метод вызывается как обычная функция (объект, к которому применяется метод, не передается в качестве первого аргумента). Поэтому в конструктор надкласса мы «вручную» передаем текущий экземпляр ( self ), записывая его перед остальными аргументами.
У кода выше есть небольшой недостаток. Нам ничего не мешает (при условии совпадения количества параметров) вызвать конструктор другого класса, а не только родительского, указав его имя вместо Table . Кроме того, имя надкласса может измениться, и тогда есть риск неправильных обращений к нему из дочерних классов.
В Python с целью улучшения так называемой обслуживаемости кода можно использовать встроенную в язык функцию super . Наиболее распространенным вариантом ее применения является вызов метода родительского класса из метода подкласса:
class KitchenTable(Table): def __init__(self, l, w, h, p): super().__init__(l, w, h) self.places = p
В данном случае аргумент self в скобках вызываемого родительского метода указывать явно не требуется.
Параметры со значениями по умолчанию у родительского класса
Рассмотрим случай, когда родительский класс имеет параметры со значениями по умолчанию, а дочерний – нет:
class Table: def __init__(self, l=1, w=1, h=1): self.length = l self.width = w self.height = h class KitchenTable(Table): def __init__(self, p, l, w, h): Table.__init__(self, l, w, h) self.places = p
При таком определении классов можно создать экземпляр от Table без передачи аргументов для конструктора:
t = Table()
Можем ли мы создать экземпляр от KitchenTable , передав значение только для параметра p ? Например, вот так:
k = KitchenTable(10)
Возможно ли, что p будет присвоено число 10, а l , w и h получат по единице от родительского класса? Невозможно, будет выброшено исключение по причине несоответствия количества переданных аргументов количеству требуемых конструктором:
. k = KitchenTable(10) TypeError: __init__() missing 3 required positional arguments: 'l', 'w', and 'h'
Когда создается объект от дочернего класса, сначала вызывается его конструктор, если он есть. Интерпретатор еще не знает, что в теле этого конструктора будет вызван конструктор родительского класса. Ведь это не обязательно. Значит, если все параметры дочернего конструктора не имеют значений по умолчанию, при построении объекта все значения должны передаваться.
Поэтому, если требуется допустить создание объектов от дочернего класса без передачи аргументов, придется назначить значения по умолчанию также в конструкторе дочернего класса.
class Table: def __init__(self, l=1, w=1, h=1): self.length = l self.width = w self.height = h class KitchenTable(Table): def __init__(self, l=1, w=1, h=0.7, p=4): Table.__init__(self, l, w, h) self.places = p
Параметр p , которого нет у родительского класса, мы делаем последним не просто так. Бывает, объекты разных родственных классов создаются или обрабатываются в одном цикле, то есть по одному алгоритму. При этом у них должны быть одинаковые «интерфейсы», то есть одинаковое количество передаваемых в конструктор аргументов.
Поэтому лучше, когда методы родственных классов принимают одинаковое число параметров. А если разное, то у «лишних» должны быть значения по-умолчанию, чтобы при вызове конструктора их можно было бы не передавать. Если такие параметры находятся еще и в конце, передачу аргументов для предстоящих параметров можно выполнять без ключей.
Другой вариант – отказаться от конструктора в дочернем классе, а значение для поля places устанавливать отдельным вызовом метода:
class Table: def __init__(self, l=1, w=1, h=1): self.length = l self.width = w self.height = h class KitchenTable(Table): places = 4 def set_places(self, p): self.places = p
Здесь у всех кухонных столов по-умолчанию будет 4 места. Если мы хотим изменить значение поля places , можем вызвать метод set_places . Хотя в случае Python можем сделать это напрямую, присвоив полю. При этом у экземпляра появится собственное поле places .
k = KitchenTable() k.places = 6
Поэтому метод set_places в общем-то не нужен.
В любом случае произвольное количество мест будет устанавливаться не в конструкторе, а отдельно. Если все же требуется указывать места при создании объекта, это можно сделать и в конструкторе родителя:
class Table: def __init__(self, l=1, w=1, h=1): self.length = l self.width = w self.height = h if isinstance(self, KitchenTable): p = int(input("Сколько мест: ")) self.places = p
С помощью функции isinstance проверяется, что создаваемый объект имеет тип KitchenTable . Если это так, то у него появляется поле places .
Мы не используем параметр p со значением по умолчанию в заголовке конструктора потому, что, если объектам других родственных классов он не нужен, не происходило бы путаницы и сложностей с документированием кода.
Практическая работа
Разработайте программу по следующему описанию.
В некой игре-стратегии есть солдаты и герои. У всех есть свойство, содержащее уникальный номер объекта, и свойство, в котором хранится принадлежность команде. У солдат есть метод «иду за героем», который в качестве аргумента принимает объект типа «герой». У героев есть метод увеличения собственного уровня.
В основной ветке программы создается по одному герою для каждой команды. В цикле генерируются объекты-солдаты. Их принадлежность команде определяется случайно. Солдаты разных команд добавляются в разные списки.
Измеряется длина списков солдат противоборствующих команд и выводится на экран. У героя, принадлежащего команде с более длинным списком, увеличивается уровень.
Отправьте одного из солдат первого героя следовать за ним. Выведите на экран идентификационные номера этих двух юнитов.
Курс с примерами решений практических работ:
pdf-версия
X Скрыть Наверх
Объектно-ориентированное программирование на Python
