Основы ООП на Python
Внимание! Все тесты в этом разделе разработаны пользователями сайта для собственного использования. Администрация сайта не проверяет возможные ошибки, которые могут встретиться в тестах.
Пройдите внимательно и аккуратно тест на тему Основы ООП на Python
Система оценки: 5* балльная
Список вопросов теста
Вопрос 1
Как создать в переменной obj , объект класса cls , если известно, что конструктор этого класса имеет единственный параметр — self ?
Варианты ответов
- obj = cls.__init__ ()
- obj = cls.__init__ (self)
- obj = cls
- obj = cls ()
Вопрос 2
Выберите истинные утверждения.
Варианты ответов
- Класс — это множество объектов, имеющих общую структуру, состояние и поведение.
- При решении любых задач у объекта будут существенны всегда одни и теже свойства.
- ООП основано на представлении задачи из реального мира в виде множества взаимодействующих объектов.
- Абстракция — это приём, при котором в описании объекта игнорируются свойства несущественные при решении конкретной задачи.
Вопрос 3
Что используется в классах для реализации свойств и поведения их объектов?
Варианты ответов
- Поля
- Схемы
- Методы
- Диаграммы
Вопрос 4
Как в ООП называется сущность, обладающая чёткими границами, состоянием и поведением?
Варианты ответов
- Объект
- Класс
- Абстракция
- Метод
Вопрос 5
С какого служебного слова начинается описание нового класса на языке Python ?
Вопрос 6
Запишите инструкцию, с помощью которой в конструкторе класса можно создать поле fieldName , значение которого в начале будет равно пяти. Ответ запишите без пробелов.
Вопрос 7
Установите соответствия между понятиями и их описаниями.
Варианты ответов
- Экземпляр класса
- Правила взаимодействия объектов в программе
- Функция, реализованная для объектов класса
- Переменная, принадлежащая объекту класса
Вопрос 8
Где в описании класса указываются его предки?
Варианты ответов
- В заголовке
- В любом из методов
- В конструкторе
- В специальном поле
Вопрос 9
Что входит в интерфейс класса?
Варианты ответов
- Внутренние поля
- Внутренние методы
- Общедоступные методы
- Общедоступные поля
Вопрос 10
Установите соответствие между принципами ООП и их описанием.
Позволяет скрывать внутреннее
Позволяет образовывать иерархию классов.
Позволяет учитывать при описании объектов
лишь то, что существенно при решении задачи.
Позволяет свойствам системы использовать объекты с одинаковым интерфейсом без информации о типе и внутренней структуре объекта
Варианты ответов
- Абстракция
- Наследование
- Полиморфизм
- Инкапсуляция
Принципы ООП
Будьте внимательны! У Вас есть 10 минут на прохождение теста. Система оценивания — 5 балльная. Разбалловка теста — 3,4,5 баллов, в зависимости от сложности вопроса. Порядок заданий и вариантов ответов в тесте случайный. С допущенными ошибками и верными ответами можно будет ознакомиться после прохождения теста. Удачи!
Система оценки: 5 балльная
Список вопросов теста
Вопрос 1
Сколько всего принципов объектно-ориентированного программирования?
Вопрос 2
Как в языке Python называется инструмент, позволяющий имитировать доступ к полям класса?
Варианты ответов
- Переменные
- Методы
- Свойства
- Заголовки
Вопрос 3
Что входит в интерфейс класса?
Варианты ответов
- Общедоступные поля
- Внутренние поля
- Общедоступные методы
- Внутренние методы
Вопрос 4
Установите соответствие между принципами ООП и их описанием.
Позволяет скрывать внутреннее устройство объектов.
Позволяет образовывать иерархию классов.
Позволяет учитывать при описании объектов лишь то, что существенно при решении задачи.
Возможность классов-наследников по-разному реализовывать методы своего предка.
Варианты ответов
- Абстракция
- Наследование
- Инкапсуляция
- Полиморфизм
Вопрос 5
Какое условие должно выполняться, чтобы класс Б мог являться наследником класса А.
Варианты ответов
- Объект Б должен быть частью объекта А.
- Оба класса должны описывать одни и те же объекты.
- Б должен быть разновидностью А.
- А и Б должны быть связаны по условию задачи.
Вопрос 6
Где в описании класса указываются его предки?
Варианты ответов
- В конструкторе
- В заголовке
- В любом из методов
- В специальном поле
Вопрос 7
Какой из принципов ООП позволяет изменять внутреннее устройство класса, не изменяя его интерфейс?
Вопрос 8
В алфавитном порядке, через пробел, запишите имена методов, которые должны быть определены в классе, если в нём есть свойство, объявленное с помощью инструкции
objName = property (__getObjName, __setObjName).
Вопрос 9
Укажите отличительные черты абстрактного класса.
Варианты ответов
- Хотя бы один из его методов является абстрактным
- Не применим при решении реальных задач
- Не может иметь наследников
- Объект этого класса нельзя создать
- Обычно используется в иерархии наследования
Вопрос 10
Какой из принципов объектно-ориентированного программирования позволяет сократить код программ?
Наследование классов
Аннотация: Организация иерархий классов. Раннее и позднее связывание. Виртуальные методы. Абстрактные и бесплодные классы. Виды взаимоотношений между классами.
Презентацию к данной лекции Вы можете скачать здесь.
Управлять большим количеством разрозненных классов достаточно сложно. С этой проблемой можно справиться путем упорядочивания и ранжирования классов, то есть объединяя общие для нескольких классов свойства в одном классе и используя его в качестве базового.
Эту возможность предоставляет механизм наследования, который является мощнейшим инструментом ООП . Он позволяет строить иерархии, в которых классы-потомки получают свойства классов-предков и могут дополнять их или изменять. Таким образом, наследование обеспечивает важную возможность многократного использования кода.
Классы, расположенные ближе к началу иерархии, объединяют в себе общие черты для всех нижележащих классов. По мере продвижения вниз по иерархии классы приобретают все больше конкретных особенностей.
Описание класса-потомка
Класс в C# может иметь произвольное количество потомков и только одного предка. При описании класса имя его предка записывается в заголовке класса после двоеточия. Если имя предка не указано, предком считается базовый класс всей иерархии System.Object :
[ атрибуты ] [ спецификаторы ] class имя_класса [ : предки ] тело класса
Обратите внимание, что слово «предки» присутствует в описании класса во множественном числе, хотя класс может иметь только одного предка. Причина в том, что класс наряду с единственным предком может наследовать от интерфейсов — специального вида классов, не имеющих реализации. Интерфейсы рассматриваются на следующей лекции.
Рассмотрим наследование классов на примере. Ранее был описан класс Monster , моделирующий персонаж компьютерной игры. Допустим, нам требуется ввести в игру еще один тип персонажей, который должен обладать свойствами объекта Monster , а кроме того, уметь думать. Будет логично сделать новый объект потомком объекта Monster (листинг 8.11).
using System; namespace ConsoleApplication1 < class Monster < . >class Daemon : Monster < public Daemon() < brain = 1; >public Daemon( string name, int brain ) : base( name ) // 1 < this.brain = brain; >public Daemon( int health, int ammo, string name, int brain ) : base( health, ammo, name ) // 2 < this.brain = brain; >new public void Passport() // 3 < Console.WriteLine( "Daemon \t health = ammo = brain = ", Name, Health, Ammo, brain ); > public void Think() // 4 < Console.Write( Name + " is" ); for ( int i = 0; i < brain; ++i ) Console.Write( " thinking" ); Console.WriteLine( ". " ); >int brain; // закрытое поле > class Class1 < static void Main() < Daemon Dima = new Daemon( "Dima", 3 ); // 5 Dima.Passport(); // 6 Dima.Think(); // 7 Dima.Health -= 10; // 8 Dima.Passport(); >> >
Листинг 8.1. Класс Daemon, потомок класса Monster
В классе Daemon введены закрытое поле brain и метод Think , определены собственные конструкторы, а также переопределен метод Passport . Все поля и свойства класса Monster наследуются в классе Daemon .
Результат работы программы:
Daemon Dima health = 100 ammo = 100 brain = 3 Dima is thinking thinking thinking. Daemon Dima health = 90 ammo = 100 brain = 3
Как видите, экземпляр класса Daemon с одинаковой легкостью использует как собственные ( операторы 5–7), так и унаследованные (оператор 8) элементы класса. Рассмотрим общие правила наследования.
Конструкторы не наследуются, поэтому производный класс должен иметь собственные конструкторы. Порядок вызова конструкторов определяется приведенными ниже правилами.
- Если в конструкторе производного класса явный вызов конструктора базового класса отсутствует, автоматически вызывается конструктор базового класса без параметров.
- Для иерархии, состоящей из нескольких уровней, конструкторы базовых классов вызываются, начиная с самого верхнего уровня. После этого выполняются конструкторы тех элементов класса, которые являются объектами, в порядке их объявления в классе, а затем исполняется конструктор класса. Таким образом, каждый конструктор инициализирует свою часть объекта.
- Если конструктор базового класса требует указания параметров, он должен быть явным образом вызван в конструкторе производного класса в списке инициализации. Вызов выполняется с помощью ключевого слова base . Вызывается та версия конструктора, список параметров которой соответствует списку аргументов, указанных после слова base .
Поля, методы и свойства класса наследуются, поэтому при желании заменить элемент базового класса новым элементом следует явным образом указать компилятору свое намерение с помощью ключевого слова new . В листинге 8.1 таким образом переопределен метод вывода информации об объекте Passport . Метод Passport класса Daemon замещает соответствующий метод базового класса, однако возможность доступа к методу базового класса из метода производного класса сохраняется. Для этого перед вызовом метода указывается все то же волшебное слово base , например:
base.Passport();
Элементы базового класса, определенные как private , в производном классе недоступны. Поэтому в методе Passport для доступа к полям name , health и ammo пришлось использовать соответствующие свойства базового класса. Другое решение заключается в том, чтобы определить эти поля со спецификатором protected , в этом случае они будут доступны методам всех классов, производных от Monster . Оба решения имеют свои достоинства и недостатки.
Во время выполнения программы объекты хранятся в отдельных переменных, массивах или других коллекциях. Во многих случаях удобно оперировать объектами одной иерархии единообразно, то есть использовать один и тот же программный код для работы с экземплярами разных классов. Это возможно благодаря тому, что объекту базового класса можно присвоить объект производного класса.
Попробуем описать массив объектов базового класса и занести туда объекты производного класса. В листинге 8.2 в массиве типа Monster хранятся два объекта типа Monster и один — типа Daemon .
using System; namespace ConsoleApplication1 < class Monster < . >class Daemon : Monster < . // >class Class1 < static void Main() < const int n = 3; Monster[] stado = new Monster[n]; stado[0] = new Monster( "Monia" ); stado[1] = new Monster( "Monk" ); stado[2] = new Daemon ( "Dimon", 3 ); foreach ( Monster elem in stado ) elem.Passport(); // 1 for ( int i = 0; i < n; ++i ) stado[i].Ammo = 0; // 2 Console.WriteLine(); foreach ( Monster elem in stado ) elem.Passport(); // 3 >> >
Листинг 8.2. Массив объектов разных типов
Результат работы программы:
Monster Monia health = 100 ammo = 100 Monster Monk health = 100 ammo = 100 Monster Dimon health = 100 ammo = 100 Monster Monia health = 100 ammo = 0 Monster Monk health = 100 ammo = 0 Monster Dimon health = 100 ammo = 0
Результат радует нас только частично: объект типа Daemon действительно можно поместить в массив , состоящий из элементов типа Monster , но для него вызываются только методы и свойства, унаследованные от предка. Это устраивает нас в операторе 2, а в операторах 1 и 3 хотелось бы, чтобы вызывался метод Passport , переопределенный в потомке.
Итак, присваивать объекту базового класса объект производного класса можно, но вызываются для него только методы и свойства, определенные в базовом классе. Иными словами, возможность доступа к элементам класса определяется типом ссылки, а не типом объекта, на который она указывает.
Это и понятно: ведь компилятор должен еще до выполнения программы решить, какой метод вызывать, и вставить в код фрагмент, передающий управление на этот метод (этот процесс называется ранним связыванием ). При этом компилятор может руководствоваться только типом переменной, для которой вызывается метод или свойство (например, stado[i].Ammo ). То, что в этой переменной в разные моменты времени могут находиться ссылки на объекты разных типов, компилятор учесть не может.
Следовательно, если мы хотим, чтобы вызываемые методы соответствовали типу объекта, необходимо отложить процесс связывания до этапа выполнения программы, а точнее — до момента вызова метода, когда уже точно известно, на объект какого типа указывает ссылка . Такой механизм в C# есть — он называется поздним связыванием и реализуется с помощью так называемых виртуальных методов, которые мы незамедлительно и рассмотрим.
Виртуальные методы
При раннем связывании программа , готовая для выполнения, представляет собой структуру, логика выполнения которой жестко определена. Если же требуется, чтобы решение о том, какой из одноименных методов разных объектов иерархии использовать, принималось в зависимости от конкретного объекта, для которого выполняется вызов, то заранее жестко связывать эти методы с остальной частью кода нельзя.
Следовательно, надо каким-то образом дать знать компилятору, что эти методы будут обрабатываться по -другому. Для этого в C# существует ключевое слово virtual . Оно записывается в заголовке метода базового класса, например:
virtual public void Passport() .
Объявление метода виртуальным означает, что все ссылки на этот метод будут разрешаться в момент его вызова во время выполнения программы. Этот механизм называется поздним связыванием.
Для его реализации необходимо, чтобы адреса виртуальных методов хранились там, где ими можно будет в любой момент воспользоваться, поэтому компилятор формирует для этих методов таблицу виртуальных методов ( Virtual Method Table , VMT ). В нее записываются адреса виртуальных методов (в том числе унаследованных) в порядке описания в классе. Для каждого класса создается одна таблица .
Каждый объект во время выполнения должен иметь доступ к VMT . Связь экземпляра объекта с VMT устанавливается с помощью специального кода, автоматически помещаемого компилятором в конструктор объекта.
Если в производном классе требуется переопределить виртуальный метод , используется ключевое слово override , например:
override public void Passport() .
Переопределенный виртуальный метод должен обладать таким же набором параметров, как и одноименный метод базового класса.
Добавим в листинге 8.2 два волшебных слова — virtual и override — в описания методов Passport соответственно базового и производного классов (листинг 8.3).
using System; namespace ConsoleApplication1 < class Monster < . virtual public void Passport() < Console.WriteLine( "Monster \t health = ammo = ", name, health, ammo ); > . > class Daemon : Monster < . override public void Passport() < Console.WriteLine( "Daemon \t health = ammo = brain = ", Name, Health, Ammo, brain ); > . > class Class1 < static void Main() < const int n = 3; Monster[] stado = new Monster[n]; stado[0] = new Monster( "Monia" ); stado[1] = new Monster( "Monk" ); stado[2] = new Daemon ( "Dimon", 3 ); foreach ( Monster elem in stado ) elem.Passport(); for ( int i = 0; i < n; ++i ) stado[i].Ammo = 0; Console.WriteLine(); foreach ( Monster elem in stado ) elem.Passport(); >> >
Листинг 8.3. Виртуальные методы
Результат работы программы:
Monster Monia health = 100 ammo = 100 Monster Monk health = 100 ammo = 100 Daemon Dimon health = 100 ammo = 100 brain = 3 Monster Monia health = 100 ammo = 0 Monster Monk health = 100 ammo = 0 Daemon Dimon health = 100 ammo = 0 brain = 3
Теперь в циклах 1 и 3 вызывается метод Passport , соответствующий типу объекта, помещенного в массив .
Виртуальные методы базового класса определяют интерфейс всей иерархии. Этот интерфейс может расширяться в потомках за счет добавления новых виртуальных методов. Переопределять виртуальный метод в каждом из потомков не обязательно: если он выполняет устраивающие потомка действия, метод наследуется.
С помощью виртуальных методов реализуется один из основных принципов объектно-ориентированного программирования — полиморфизм. Это слово в переводе с греческого означает «много форм», что в данном случае означает «один вызов — много методов».
Виртуальные методы незаменимы и при передаче объектов в методы в качестве параметров. В параметрах метода описывается объект базового типа, а при вызове в нее передается объект производного класса. Виртуальные методы, вызываемые для объекта из метода, будут соответствовать типу аргумента, а не параметра.
Наследование — 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 студентов
Наши выпускники работают в компаниях: