Перейти к содержимому

Как наследовать класс в python

  • автор:

Наследование — Python: Введение в ООП

Все классы, которые мы рассматривали до этого, создавались «с нуля». И до тех пор, пока описываемые классами сущности мало похожи друг на друга, создание абсолютно новых классов работает отлично. Но что делать, если мы хотим, чтобы пара классов содержала один и тот же метод — не одноименный, а именно копию?

Конечно же, мы можем при объявлении класса вместо объявления метода по месту поместить в атрибут ссылку на существующую функцию. И это даже сработает! Но когда таковых методов станет несколько, уследить за тем, что и куда копируется, станет очень сложно. К счастью, есть способ лучше!

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

Когда один класс становится наследником другого, то все атрибуты класса-предка (надкласса, superclass) становятся доступны классу-потомку (подклассу, subclass) — наследуются (достаются в наследство).

Что дает наследование

Наследование позволяет выделить общее для нескольких классов поведение и вынести его в отдельную сущность. То есть наследование является средством переиспользования кода (code reuse) — использования существующего кода для решения новых задач!

Наследование позволяет получить новый класс, немного отличающийся от старого. При этом нам не нужно иметь доступ к коду исходного класса, а значит с помощью наследования мы можем адаптировать (использовать повторно) под наши задачи, в том числе и чужие классы!

Как обычно, рассмотрим пример:

# этот класс у нас уже был class Counter: def __init__(self): self.value = 0 def inc(self): self.value += 1 def dec(self): self.value -= 1 # А этот класс - новый. Наследник Counter class NonDecreasingCounter(Counter): # в скобках указан класс-предок def dec(self): pass 

Если мы выполним эти объявления классов и посмотрим на поведение экземпляра NonDecreasingCounter , то увидим, что он работает как Counter — имеет те же методы и атрибуты (правда, при вызове метода .dec новый счетчик не изменяет текущее значение):

n = NonDecreasingCounter() n.inc() n.inc() n.value # 2 n.dec() n.value # 2 

В объявлении NonDecreasingCounter присутствует метод dec , а вот откуда взялись value и inc ? Они были взяты от предка — класса Counter ! Данный факт даже можно пронаблюдать:

n.dec # > n.inc # > 

Метод dec — метод класса NonDecreasingCounter , связанный с конкретным экземпляром NonDecreasingCounter . А вот inc — метод класса Counter , хоть и связанный с все тем же экземпляром класса-потомка.

Здесь вы можете увидеть сходство с взаимоотношениями между классом и его экземпляром: если экземпляр получает свой собственный атрибут, то этот атрибут заменяет атрибут класса. Точно так же объявления в классе-потомке заменяют собой атрибуты класса-предка, если имя используется то же самое — говорят, переопределяют (override).

И, как и в случае с объектом, который может использовать все содержимое класса и заменять только небольшую часть атрибутов (или добавлять новые!), так и потомок по умолчанию получает все атрибуты предка, часть из которых может изменить.

Все будет super()

Представим, что нас в целом устраивает класс Counter из предыдущего примера, но мы хотим при вызове inc увеличивать значение дважды. Мы могли бы заменить в потомке весь метод и прописать внутри нового метода self.value += 2 . Но если бы позже что-то поменялось в исходном классе Counter , то эти изменения не коснулись бы нашего метода.

Получается, что нам внутри метода потомка нужно получить доступ к методу предка. Методу с тем же именем! Если мы просто обратимся к self.inc , то получим ссылку на новый метод, ведь мы его переопределили.

Тут нам на помощь приходит специальная функция super :

class DoubleCounter(Counter): def inc(self): super().inc() super().inc() 

Вызов super здесь заменяет обращение к self . При этом вы фактически обращаетесь к «памяти предков»: получаете ссылку на атрибут предка. Более того, в данном случае, super().inc — это связанный с текущим экземпляром метод, то есть полноценная «оригинальная версия» из класса-предка. Если бы вы вдруг решили вручную вызвать метод класса предка, то вам бы пришлось использовать его не связанную версию:

class DoubleCounter(Counter): def inc(self): Counter.inc(self) # явно обращаемся к методу класса предка Counter.inc(self) # и передаем ссылку на экземпляр 

Вызов super вместо явного вызова предка хорош не только тем, что автоматически связывает методы. При смене предка (такое бывает) в описании класса super учтет изменения, и вы получите доступ к поведению нового предка. Удобно!

super работает не только с методами, но и с атрибутами классов:

class A: x = 'A' class B(A): x = 'B' def super_x(self): return super().x B().x # 'B' B().super_x() # 'A' 

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

Функция super так названа в честь названия класса-предка: «superclass».

Вызов инициализатора суперкласса с super()

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

Использование super() в __init__ позволяет нам вызвать конструктор суперкласса, что гарантирует, что весь необходимый код инициализации будет выполнен:

class Counter: def __init__(self): self.value = 0 def inc(self): self.value += 1 def dec(self): self.value -= 1 class NonDecreasingCounter(Counter): def __init__(self): super().__init__() # Вызываем конструктор предка self.non_decreasing = True # Дополнительный атрибут для наследника def dec(self): if self.non_decreasing: print("Уменьшение значения запрещено.") else: super().dec() # Вызываем метод dec предка, если уменьшение разрешено n = NonDecreasingCounter() n.inc() print(n.value) # 1 n.dec() # Уменьшение значения запрещено. print(n.value) # 1 n.non_decreasing = False n.dec() print(n.value) # 0 

В этом примере метод __init__ в NonDecreasingCounter вызывает метод __init__ предка Counter с помощью super() . Это гарантирует, что атрибут value инициализируется как в Counter . Класс NonDecreasingCounter добавляет дополнительный атрибут non_decreasing и изменяет поведение метода dec , чтобы контролировать, может ли счетчик уменьшаться. Это демонстрирует, как можно расширить и настроить поведение классов при наследовании.

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

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

В прошлом мы не указывали предка в объявлениях классов, то есть писали так:

class Foo: pass 

В Python3 такая запись равнозначна записи class Foo(object): . То есть, если класс-предок не указан, то таковым считается object — самый базовый класс в Python. Сейчас, в эпоху повсеместного использования Python3, указывать или не указывать наследование от object — дело вкуса.

Открыть доступ

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

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов

Наши выпускники работают в компаниях:

Как работать с наследованием в Python

Изучите основы наследования в Python, создавая гибкие и масштабируемые программы, с примерами и полезными функциями.

Алексей Кодов
Автор статьи
10 июля 2023 в 17:48

Наследование — одна из основных концепций объектно-ориентированного программирования, которая позволяет создавать новые классы на основе существующих, переиспользуя их свойства и методы. В Python наследование реализуется довольно просто и интуитивно. В этой статье мы разберем основы наследования в Python на примерах.

Базовые понятия наследования

Основная идея наследования заключается в том, что один класс (потомок) может наследовать атрибуты и методы другого класса (родителя). Это позволяет избежать дублирования кода и упрощает изменения и обновления.

Создание класса-родителя

Сначала создадим базовый класс Animal , который будет содержать общие атрибуты и методы для всех животных.

class Animal: def __init__(self, name, age): self.name = name self.age = age def speak(self): print(f" makes a sound.")

Создание класса-потомка

Теперь создадим класс Dog , который будет наследовать свойства и методы класса Animal . Для этого в скобках после имени класса указываем имя родительского класса.

class Dog(Animal): def speak(self): print(f" barks.")

Теперь класс Dog имеет доступ ко всем атрибутам и методам класса Animal . Мы также можем переопределить метод speak() для класса Dog , чтобы он выдавал другой результат.

Python-разработчик: новая работа через 9 месяцев
Получится, даже если у вас нет опыта в IT

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

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

class A: def method_a(self): print("Method A") class B: def method_b(self): print("Method B") class C(A, B): pass c = C() c.method_a() c.method_b()

В этом примере класс C наследует методы method_a() и method_b() от классов A и B соответственно.

Важные функции и атрибуты

В контексте наследования в Python есть несколько важных функций и атрибутов, которые могут пригодиться:

  • isinstance(obj, class) — проверяет, является ли объект экземпляром указанного класса или его потомка;
  • issubclass(class1, class2) — проверяет, является ли class1 подклассом class2 ;
  • super() — позволяет вызывать методы родительского класса из класса-потомка.

Заключение

Наследование в Python — мощный инструмент, который позволяет создавать гибкие и масштабируемые программы. Эта статья рассмотрела основы наследования, но возможностей языка гораздо больше. Рекомендуем продолжить изучение Python, чтобы овладеть всеми его возможностями!

Если вы хотите углубить свои знания в Python-разработке, обратите внимание на следующую онлайн-школу:

Введение в множественное наследование и super()

Введение в множественное наследование и super() для Python-разработчиков. Также в этой статье мы рассмотрим, как справляться с проблемой алмаза.

Краткий обзор наследования

По мере расширения ваших проектов и пакетов Python вы неизбежно захотите использовать классы и т.о. применять один из фундаментальных принципов программирования – принцип DRY (Don’t repeat yourself – не повторяйся!). Наследование классов — это фантастический способ создать класс на основе другого класса, следуя принципу DRY.

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

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

В приведенном ниже блоке кода мы продемонстрируем наследование. У нас есть дочерний класс Child , наследуемый от родительского класса Parent .

class Parent: def __init__(self): self.parent_attribute = 'I am a parent' def parent_method(self): print('Back in my day. ') # Create a child class that inherits from Parent class Child(Parent): def __init__(self): Parent.__init__(self) self.child_attribute = 'I am a child' # Create instance of child child = Child() # Show attributes and methods of child class print(child.child_attribute) print(child.parent_attribute) child.parent_method()

Давайте его запустим и получим следующий результат:

I am a child I am a parent Back in my day.

Мы видим, что класс Child «унаследовал» атрибуты и методы от класса Parent . Без какой-либо работы с нашей стороны метод Parent.parent_method является частью класса Child . Чтобы воспользоваться методом Parent.__init__() , нам нужно было явно вызвать метод и передать self . Это потому, что, когда мы добавили метод __init__ в Child , мы перезаписали унаследованный __init__ .

После этого краткого и очень неполного обзора давайте перейдем к сути статьи.

Введение в super()

В простейшем случае функцию super() можно использовать для замены явного вызова Parent.__init__(self) . Наш вводный пример из первого раздела можно переписать с помощью super() , как это показано ниже.

Обратите внимание, что приведенный ниже блок кода написан на Python 3, более ранние версии используют немного другой синтаксис. Кроме того, вывод был опущен, поскольку он идентичен тому, что мы получили в первом примере.

Итак, при использовании super() наш код выглядел бы следующим образом:

class Parent: def __init__(self): self.parent_attribute = 'I am a parent' def parent_method(self): print('Back in my day. ') # Create a child class that inherits from Parent class Child(Parent): def __init__(self): super().__init__() self.child_attribute = 'I am a parent' # Create instance of child child = Child() # Show attributes and methods of child class print(child.child_attribute) print(child.parent_attribute) child.parent_method()

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

Минусы

Можно сказать, что использование super() при одиночном наследовании делает код менее явным и интуитивно понятным. Создание менее явного кода нарушает дзен Python, в котором говорится: «Явное лучше, чем неявное».

Плюсы

С точки зрения поддерживаемости super() может быть полезен даже при одиночном наследовании. Если по какой-либо причине ваш дочерний класс меняет свой шаблон наследования (т.е. изменяется родительский класс или происходит переход к множественному наследованию), то нет необходимости искать и заменять все устаревшие ссылки на ParentClass.method_name() . Таким образом, использование super() позволит всем изменениям пройти через изменение в операторе класса.

Super() и множественное наследование

Прежде чем мы перейдем к множественному наследованию и super() , хотим сразу предупредить, что тема непростая. Однако потратьте немного своего времени, перечитайте статью несколько раз, и всё встанет на свои места!

Во-первых, что такое множественное наследование? Предыдущие примеры кода охватывали один дочерний класс Child , наследуемый от одного родительского класса Parent . При множественном наследовании существует более одного родительского класса. Дочерний класс может наследовать от 2, 3, 10 и т.д. родительских классов.

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

Множественное наследование без super()

Давайте рассмотрим пример множественного наследования без изменения каких-либо родительских методов и, в свою очередь, без super() .

Наш код будет выглядеть следующим образом:

class B: def b(self): print('b') class C: def c(self): print('c') class D(B, C): def d(self): print('d') d = D() d.b() d.c() d.d()

Вот такой результат мы получим:

b c d

Порядок разрешения методов

Полученный вывод не слишком удивителен, учитывая концепцию множественного наследования. D унаследовал методы b и c от своих родительских классов, и все в мире хорошо… пока что.

А что, если и B , и C имеют методы с одинаковыми именами? Именно здесь вступает в действие концепция, называемая «multiple-resolution order» (порядок разрешения методов в Python), или сокращенно MRO.

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

Давайте рассмотрим следующий пример:

class B: def x(self): print('x: B') class C: def x(self): print('x: C') class D(B, C): pass d = D() d.x() print(D.mro()) # Output # x: B # [, , , ]

Когда мы вызываем унаследованный метод x , мы видим только выходные данные, унаследованные от класса B .

Мы можем увидеть MRO нашего класса D , вызвав метод класса mro() . Из вывода D.mro() мы узнаем следующее. По умолчанию наша программа попытается вызвать методы D , затем прибегнет к B , затем к C и, наконец, к классу object . Если метод не будет найден ни в одном из этих мест, то мы получим сообщение об ошибке, говорящее, что в D нет запрошенного нами метода.

Стоит отметить, что по умолчанию каждый класс наследуется от класса object , который и находится в конце каждого MRO.

Множественное наследование, super() и проблема алмаза

Давайте разберем пример использования super() для обработки порядка разрешения методов __init__ более выгодным образом. В примере мы создаем серию классов обработки текста и объединяем их функциональность в другом классе с множественным наследованием. Мы создадим 4 класса, и структура наследования будет соответствовать структуре, показанной на диаграмме ниже.

Примечание. Эта структура предназначена для иллюстративных целей, и чаще всего существуют более удачные способы реализации этой логики.

На самом деле это пример «проблемы алмаза» множественного наследования. Название проблемы связано с формой фигуры, которую образует схема наследования.

Код, представленный ниже, написан с использованием super() :

class Tokenizer: """Tokenize text""" def __init__(self, text): print('Start Tokenizer.__init__()') self.tokens = text.split() print('End Tokenizer.__init__()') class WordCounter(Tokenizer): """Count words in text""" def __init__(self, text): print('Start WordCounter.__init__()') super().__init__(text) self.word_count = len(self.tokens) print('End WordCounter.__init__()') class Vocabulary(Tokenizer): """Find unique words in text""" def __init__(self, text): print('Start init Vocabulary.__init__()') super().__init__(text) self.vocab = set(self.tokens) print('End init Vocabulary.__init__()') class TextDescriber(WordCounter, Vocabulary): """Describe text with multiple metrics""" def __init__(self, text): print('Start init TextDescriber.__init__()') super().__init__(text) print('End init TextDescriber.__init__()') td = TextDescriber('row row row your boat') print('--------') print(td.tokens) print(td.vocab) print(td.word_count)

Запустим наш код и получим следующий результат:

Start init TextDescriber.__init__() Start WordCounter.__init__() Start init Vocabulary.__init__() Start Tokenizer.__init__() End Tokenizer.__init__() End init Vocabulary.__init__() End WordCounter.__init__() End init TextDescriber.__init__() -------- ['row', 'row', 'row', 'your', 'boat'] 5

Прежде всего, мы видим, что класс TextDescriber унаследовал все атрибуты генеалогического дерева классов. Благодаря множественному наследованию мы можем объединять функциональность более чем одного класса.

Давайте теперь обсудим результаты, полученные от методов инициализации класса:

Каждый метод __init__ вызывался один и только один раз.

Класс TextDescriber унаследован от двух классов, унаследованных от Tokenizer . Почему Tokenizer.__init__ не вызывался дважды?

Если бы мы заменили все наши вызовы super() старомодным способом, мы получили бы 2 вызова Tokenizer.__init__ . Использование super() заранее «обдумывает» наш код и пропускает лишнее действие в виде двойного вызова Tokenizer.__init__ .

Каждый метод __init__ был запущен до того, как любой из других был завершен.

На порядок начала и окончания каждого __init__ стоит обращать внимание, если вы пытаетесь установить атрибут, имя которого конфликтует с другим родительским классом. Атрибут будет перезаписан, и это может все запутать.

В нашем случае мы избежали конфликтов имен с унаследованными атрибутами, поэтому все работает как положено.

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

Заключение

Итак, сегодня мы поговорили про множественное наследование и super() . Мы узнали о функции super() и о том, как ее можно использовать для замены ParentName.method в одиночном наследовании. Это может быть более удобной практикой.

Также мы обсудили множественное наследование и о то, как мы можем передать функциональность нескольких родительских классов одному дочернему классу.

Кроме того, мы узнали о порядке разрешения методов MRO и о том, как он решает, что происходит при множественном наследовании, когда возникает конфликт имен между родительскими методами.

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

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

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

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

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

—>

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

В качестве примера рассмотрим два класса столов. Класс 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

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

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