Объектно-ориентированное программирование¶
До сих пор наши программы состояли из функций, т. е. блоков выражений, которые манипулируют данными. Это называется процедурно-ориентированным стилем программирования. Существует и другой способ организации программ: объединять данные и функционал внутри некоего объекта. Это называется объектно-ориентированной парадигмой программирования. В большинстве случаев можно ограничиться процедурным программированием, а при написании большой программы или если решение конкретной задачи того требует, можно переходить к техникам объектно-ориентированного программирования.
Два основных аспекта объектно-ориентированного программирования — классы и объекты. Класс создаёт новый тип, а объекты являются экземплярами класса. Аналогично, когда мы говорим о "переменных типа int
", это означает, что переменные, которые хранят целочисленные значения, являются экземплярами (объектами) класса int
.
Замечание для программистов на статических языках
Обратите внимание, что даже целые числа рассматриваются как объекты (класса int
), в отличие от C++ и Java (до версии 1.5), где целые числа являются примитивами. См. help(int)
для более детального описания этого класса. Программисты на C# и Java 1.5 могут заметить сходство с концепцией упаковки и распаковки1.
Объекты могут хранить данные в обычных переменных, которые принадлежат объекту. Переменные, принадлежащие объекту или классу, называют полями. Объекты могут также обладать функционалом, т. е. иметь функции, принадлежащие классу. Такие функции принято называть методами класса. Эта терминология важна, так как она помогает нам отличать независимые функции и переменные от тех, что принадлежат классу или объекту. Всё вместе (поля и методы) принято называть атрибутами класса.
Поля бывают двух типов: они могут принадлежать каждому отдельному экземпляру объекта класса или всему классу. Они называются переменными экземпляра и переменными класса соответственно.
Класс создаётся ключевым словом class
. Поля и методы класса записываются в блоке кода с отступом.
self¶
Методы класса имеют одно отличие от обычных функций: они должны иметь дополнительно имя, добавляемое к началу списка параметров. Однако, при вызове метода никакого значения этому параметру присваивать не нужно — его укажет Python. Эта переменная указывает на сам объект экземпляра класса, и по традиции она называется self
2.
Хотя этому параметру можно дать любое имя, настоятельно рекомендуется использовать только имя self
; использование любого другого имени не приветствуется. Есть много достоинств использования стандартного имени: во-первых, любой человек, просматривающий вашу программу, легко узнает его; во-вторых, некоторые специализированные Интегрированные среды разработки (IDE) изначально рассчитаны на использование self
.
Замечание для программистов на C++, Java и C#
self
в Python эквивалентно указателю this
в C++ и ссылке this
в Java и C#.
Вы, должно быть, удивляетесь, как Python присваивает значение self
и почему вам не нужно указывать это значение самостоятельно. Поясним это на примере. Предположим, у нас есть класс с именем MyClass
и экземпляр этого класса с именем myobject
. При вызове метода этого объекта, например, "myobject.method(arg1, arg2)
", Python автоматически превращает это в "MyClass.method(myobject, arg1, arg2)
" — в этом и состоит смысл self
.
Это также означает, что если какой-либо метод не принимает аргументов, у него всё равно будет один аргумент — self
.
Классы¶
Простейший класс показан в следующем примере (сохраните как simplestclass.py
).
1 2 3 4 5 |
|
Вывод:
1 2 |
|
Как это работает:
Мы создаём новый класс при помощи оператора class
и имени класса. За этим следует блок выражений, формирующих тело класса. В данном случае блок у нас пуст, на что указывает оператор pass
.
Далее мы создаём объект-экземпляр класса, записывая имя класса со скобками. (Мы узнаем больше о реализации в следующем разделе). Для проверки мы выясняем тип переменной, просто выводя её на экран. Так мы видим, что у нас есть экземпляр класса Person
в модуле __main__
.
Обратите внимание, что выводится также и адрес в памяти компьютера, где хранится ваш объект. На вашем компьютере адрес будет другим, так как Python хранит объекты там, где имеется свободное место.
Методы объектов¶
Итак, мы выяснили что классы/объекты могут иметь методы, представляющие собой функции, за исключением дополнительной переменной self
. А теперь давайте рассмотрим пример (сохраните как method.py
).
1 2 3 4 5 6 7 8 |
|
Вывод:
1 2 |
|
Как это работает:
Здесь мы видим self
в действии. Обратите внимание, что метод sayHi
не принимает параметров, но тем не менее, имеет self
в определении функции.
Метод __init__
¶
Существует много методов, играющих специальную роль в классах Python. Сейчас мы увидим значительность метода __init__
.
Метод __init__
запускается, как только объект класса реализуется. Этот метод полезен для осуществления разного рода инициализации, необходимой для данного объекта. Обратите внимание на двойные подчёркивания в начале и в конце имени.
Пример: (сохраните как oop_init.py
)
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Вывод:
1 2 |
|
Как это работает:
Здесь мы определяем метод __init__
так, чтобы он принимал параметр name
(наряду с обычным self
). Далее мы создаём новое поле с именем name
. Обратите внимание, что это две разные переменные, даже несмотря на то, что они обе названы name
. Это не проблема, так как точка в выражении self.name
обозначает, что существует нечто с именем "name", являющееся частью объекта "self", и другое name
— локальная переменная. Поскольку мы в явном виде указываем, к которому имени мы обращаемся, путаницы не возникнет.
Для создания нового экземпляра p
класса Person
мы указываем имя класса, после которого — аргументы в скобках: p = Person('Swaroop')
.
Метод __init__
мы при этом не вызываем явным образом. В этом и заключается специальная роль данного метода.
После этого мы получаем возможность использовать поле self.name
в наших методах, что и продемонстрировано в методе say_hi
.
Переменные класса и объекта¶
Функциональную часть классов и объектов (т. е. методы) мы обсудили, теперь давайте ознакомимся с частью данных. Данные, т. е. поля, являются не чем иным, как обычными переменными, заключёнными в пространствах имён классов и объектов. Это означает, что их имена действительны только в контексте этих классов или объектов. Отсюда и название "пространство имён".
Существует два типа полей: переменные класса и переменные объекта, которые различаются в зависимости от того, принадлежит ли переменная классу или объекту соответственно.
Переменные класса разделяемы — доступ к ним могут получать все экземпляры этого класса. Переменная класса существует только одна, поэтому когда любой из объектов изменяет переменную класса, это изменение отразится и во всех остальных экземплярах того же класса.
Переменные объекта принадлежат каждому отдельному экземпляру класса. В этом случае у каждого объекта есть своя собственная копия поля, т.е. не разделяемая и никоим образом не связанная с другими такими же полями в других экземплярах. Это легко понять на примере (сохраните как objvar.py
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
|
Вывод:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Как это работает:
Это длинный пример, но он помогает продемонстрировать природу переменных класса и объекта. Здесь population
принадлежит классу Robot
, и поэтому является переменной класса. Переменная name
принадлежит объекту (ей присваивается значение при помощи self
), и поэтому является переменной объекта.
Таким образом, мы обращаемся к переменной класса population
как Robot.population
, а не self.population
. К переменной же объекта name
во всех методах этого объекта мы обращаемся при помощи обозначения self.name
. Помните об этой простой разнице между переменными класса и объекта. Также имейте в виду, что переменная объекта с тем же именем, что и переменная класса, сделает недоступной ("спрячет") переменную класса!
Метод howMany
принадлежит классу, а не объекту. Это означает, что мы можем определить его как classmethod
или staticmethod
, в зависимости от того, нужно ли нам знать, в каком классе мы находимся. Поскольку нам не нужна такая информация, мы воспользуемся staticmethod
.
Мы могли достичь того же самого, используя декораторы:
1 2 3 4 |
|
Декораторы можно считать неким упрощённым способом вызова явного оператора, как мы видели в этом примере.
Пронаблюдайте, как метод __init__
используется для инициализации экземпляра Robot
с именем. В этом методе мы увеличиваем счётчик population
на 1, так как добавляем ещё одного робота. Также заметьте, что значения self.name
для каждого объекта свои, что указывает на природу переменных объекта.
Помните, что к переменным и методам самого объекта нужно обращаться, пользуясь только self
. Это называется доступом к атрибутам.
В этом примере мы также наблюдали применение строк документации для классов, равно как и для методов. Во время выполнения мы можем обращаться к строке документации класса при помощи "Robot.__doc__
", а к строке документации метода — при помощи "Robot.sayHi.__doc__
".
Наряду с методом __init__
, существует и другой специальный метод __del__
, который вызывается тогда, когда объект собирается умереть, т.е. когда он больше не используется, и занимаемая им память возвращается операционной системе для другого использования. В этом методе мы просто уменьшаем счётчик Robot.population
на 1.
Метод __del__
запускается лишь тогда, когда объект перестаёт использоваться, а поэтому заранее неизвестно, когда именно этот момент наступит. Чтобы увидеть его в действии явно, придётся воспользоваться оператором del
, что мы и сделали выше.
Примечание для программистов на C++/Java/C#
В Python все члены класса (включая данные) являются публичными (public), а все методы — виртуальными (virtual).
Исключение: Если имя переменной начинается с двойного подчёркивания, как, например, __privatevar
, Python делает эту переменную приватной (private).
Поэтому принято имя любой переменной, которая должна использоваться только внутри класса или объекта, начинать с подчёркивания; все же остальные имена являются публичными, и могут использоваться в других классах/объектах. Помните, что это лишь традиция, и Python вовсе не обязывает делать именно так (кроме двойного подчёркивания).
Наследование¶
Одно из главных достоинств объектно-ориентированного программирования заключается в многократном использовании одного и того же кода, и один из способов этого достичь — при помощи механизма наследования. Легче всего представить себе наследование в виде отношения между классами как тип и подтип.
Представим, что нам нужно написать программу, которая отслеживает информацию о преподавателях и студентах в колледже. У них есть некоторые общие характеристики: имя, возраст и адрес. Есть также и специфические характеристики, такие как зарплата, курсы и отпуск для преподавателей, а также оценки и оплата за обучение для студентов.
Можно создать для них независимые классы и работать с ними, но тогда добавление какой-либо новой общей характеристики потребует добавления её к каждому из этих независимых классов в отдельности, что делает программу неповоротливой.
Лучше создать общий класс с именем SchoolMember
, а затем сделать так, чтобы классы преподавателя и студента наследовали этот класс, т.е. чтобы они стали подтипами этого типа (класса), после чего добавить любые специфические характеристики к этим подтипам.
У такого подхода есть множество достоинств. Если мы добавим/изменим какую-либо функциональность в SchoolMember
, это автоматически отобразится и во всех подтипах. Например, мы можем добавить новое поле удостоверения для преподавателей и студентов, просто добавив его к классу SchoolMember
. С другой стороны, изменения в подтипах никак не влияют на другие подтипы. Ещё одно достоинство состоит в том, что обращаться к объекту преподавателя или студента можно как к объекту SchoolMember
, что может быть полезно в ряде случаев, например, для подсчёта количества человек в школе. Когда подтип может быть подставлен в любом месте, где ожидается родительский тип, т.е. объект считается экземпляром родительского класса, это называется полиморфизмом.
Заметьте также, что код родительского класса используется многократно, и нет необходимости копировать его во все классы, как пришлось бы в случае использования независимых классов.
Класс SchoolMember
в этой ситуации называют базовым классом или надклассом3. Классы Teacher
и Student
называют производными классами или подклассами4.
Рассмотрим теперь этот пример в виде программы (сохраните как inherit.py
).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
Вывод:
1 2 3 4 5 6 7 8 |
|
Как это работает:
Чтобы воспользоваться наследованием, при определении класса мы указываем имена его базовых классов в виде кортежа, следующего сразу за его именем. Далее мы видим, что метод __init__
базового класса вызывается явно при помощи переменной self
, чтобы инициализировать часть объекта, относящуюся к базовому классу. Это очень важно запомнить: поскольку мы определяем метод __init__
в подклассах Teacher
и Student
, Python не вызывает конструктор базового класса SchoolMember
автоматически -- его необходимо вызывать самостоятельно в явном виде.
Напротив, если мы не определим метод __init__
в подклассе, Python вызовет конструктор базового класса автоматически.
Здесь же мы видим, как можно вызывать методы базового класса, предваряя запись имени метода именем класса, а затем передавая переменную self
вместе с другими аргументами.
Обратите внимание, что при вызове метода tell
из класса SchoolMember
экземпляры Teacher
или Student
можно использовать как экземпляры SchoolMember
.
Заметьте также, что вызывается метод tell
из подкласса, а не метод tell
из класса SchoolMember
. Это можно понять следующим образом: Python всегда начинает поиск методов в самом классе, что он и делает в данном случае. Если же он не находит метода, он начинает искать методы, принадлежащие базовым классам по очереди, в порядке, в котором они перечислены в кортеже при определении класса.
Замечание по терминологии: если при наследовании перечислено более одного класса, это называется множественным наследованием.
Параметр end
используется в методе tell()
для того, чтобы новая строка начиналась через пробел после вызова print()
.
Метаклассы¶
В обширной теме объектно-ориентированного программирования существует ещё много всего, но мы лишь слегка коснёмся некоторых концепций, чтобы вы просто знали об их существовании.
Точно так же, как классы используются для создания объектов, можно использовать метаклассы5 для создания классов. Метаклассы существуют для изменения или добавления нового поведения в классы.
Давайте рассмотрим пример. Допустим, мы хотим быть уверены, что мы всегда создаём исключительно экземпляры подклассов класса SchoolMember
, и не создаём экземпляры самого класса SchoolMember
.
Для достижения этой цели мы можем использовать концепцию под названием "абстрактные базовые классы". Это означает, что такой класс абстрактен, т. е. является лишь некой концепцией, не предназначенной для использования в качестве реального класса.
Мы можем объявить наш класс как абстрактный базовый класс при помощи встроенного метакласса по имени ABCMeta
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
Вывод:
1 2 3 4 5 6 7 8 |
|
Как это работает:
Мы можем объявить метод tell
класса SchoolMember
абстрактным, и таким образом автоматически запретим создавать экземпляры класса SchoolMember
.
Тем не менее, мы можем работать с экземплярами Teacher
и Student
так, как будто они экземпляры SchoolMember
, поскольку они являются подклассами.
Резюме¶
Мы изучили различные аспекты классов и объектов, равно как и терминологию, связанную с ними. Мы также увидели ряд достоинств и "подводных камней" объектно-ориентированного программирования. Python — в высокой степени объектно-ориентирован, поэтому понимание этих принципов очень поможет вам в дальнейшем.
Далее мы узнаем, как работать с вводом/выводом и получать доступ к файлам в Python.
-
boxing and unboxing ↩
-
self — англ. "сам" (прим.перев.) ↩
-
также "суперкласс", "родительский класс" (прим.перев.) ↩
-
также "субкласс", "класс-наследник" (прим.перев.) ↩
-
в оригинальной версии книги этот параграф невидим для читателей, так как находится в комментарии с пометкой автора "It is too sudden to introduce this concept here.", что означает "Слишком неожиданно представление этой концепции здесь." (прим.перев.) ↩