Объектно-ориентированное программирование

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

Два основных аспекта объектно-ориентированного программирования – классы и объекты. Класс создаёт новый тип, а объекты являются экземплярами класса. Аналогично, когда мы говорим о “переменных типа 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).

class Person:
    pass # Пустой блок

p = Person()
print(p)

Вывод:

$ python3 simplestclass.py
<__main__.Person object at 0x019F85F0>

Как это работает:

Мы создаём новый класс при помощи оператора class и имени класса. За этим следует блок выражений, формирующих тело класса. В данном случае блок у нас пуст, на что указывает оператор pass.

Далее мы создаём объект-экземпляр класса, записывая имя класса со скобками. (Мы узнаем больше о реализации в следующем разделе). Для проверки мы выясняем тип переменной, просто выводя её на экран. Так мы видим, что у нас есть экземпляр класса Person в модуле __main__.

Обратите внимание, что выводится также и адрес в памяти компьютера, где хранится ваш объект. На вашем компьютере адрес будет другим, так как Python хранит объекты там, где имеется свободное место.

Методы объектов

Итак, мы выяснили что классы/объекты могут иметь методы, представляющие собой функции, за исключением дополнительной переменной self. А теперь давайте рассмотрим пример (сохраните как method.py).

class Person:
    def sayHi(self):
        print('Привет! Как дела?')

p = Person()
p.sayHi()

# Этот короткий пример можно также записать как Person().sayHi()

Вывод:

$ python3 method.py
Привет! Как дела?

Как это работает:

Здесь мы видим self в действии. Обратите внимание, что метод sayHi не принимает параметров, но тем не менее, имеет self в определении функции.

Метод __init__

Существует много методов, играющих специальную роль в классах Python. Сейчас мы увидим значительность метода __init__.

Метод __init__ запускается, как только объект класса реализуется. Этот метод полезен для осуществления разного рода инициализации, необходимой для данного объекта. Обратите внимание на двойные подчёркивания в начале и в конце имени.

Пример: (сохраните как oop_init.py)

class Person:
    def __init__(self, name):
        self.name = name

    def say_hi(self):
        print('Привет! Меня зовут', self.name)

p = Person('Swaroop')
p.say_hi()

# Предыдущие 2 строки можно
# Person('Swaroop').say_hi()

Вывод:

$ python oop_init.py
Привет! Меня зовут Swaroop

Как это работает:

Здесь мы определяем метод __init__ так, чтобы он принимал параметр name (наряду с обычным self). Далее мы создаём новое поле с именем name. Обратите внимание, что это две разные переменные, даже несмотря на то, что они обе названы name. Это не проблема, так как точка в выражении self.name обозначает, что существует нечто с именем “name”, являющееся частью объекта “self”, и другое name – локальная переменная. Поскольку мы в явном виде указываем, к которому имени мы обращаемся, путаницы не возникнет.

Для создания нового экземпляра p класса Person мы указываем имя класса, после которого – аргументы в скобках: p = Person('Swaroop').

Метод __init__ мы при этом не вызываем явным образом. В этом и заключается специальная роль данного метода.

После этого мы получаем возможность использовать поле self.name в наших методах, что и продемонстрировано в методе say_hi.

Переменные класса и объекта

Функциональную часть классов и объектов (т.е. методы) мы обсудили, теперь давайте ознакомимся с частью данных. Данные, т.е. поля, являются не чем иным, как обычными переменными, заключёнными в пространствах имён классов и объектов. Это означает, что их имена действительны только в контексте этих классов или объектов. Отсюда и название “пространство имён”.

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

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

Переменные объекта принадлежат каждому отдельному экземпляру класса. В этом случае у каждого объекта есть своя собственная копия поля, т.е. не разделяемая и никоим образом не связанная с другими такими же полями в других экземплярах. Это легко понять на примере (сохраните как objvar.py):

class Robot:
    '''Представляет робота с именем.'''
    # Переменная класса, содержащая количество роботов
    population = 0

    def __init__(self, name):
        '''Инициализация данных.'''
        self.name = name
        print('(Инициализация {0})'.format(self.name))

        # При создании этой личности, робот добавляется
        # к переменной 'population'
        Robot.population += 1

    def __del__(self):
        '''Я умираю.'''
        print('{0} уничтожается!'.format(self.name))

        Robot.population -= 1

        if Robot.population == 0:
            print('{0} был последним.'.format(self.name))
        else:
            print('Осталось {0:d} работающих роботов.'.format(Robot.population))

    def sayHi(self):
        '''Приветствие робота.

        Да, они это могут.'''
        print('Приветствую! Мои хозяева называют меня {0}.'.format(self.name))

    def howMany():
        '''Выводит численность роботов.'''
        print('У нас {0:d} роботов.'.format(Robot.population))

    howMany = staticmethod(howMany)

droid1 = Robot('R2-D2')
droid1.sayHi()
Robot.howMany()

droid2 = Robot('C-3PO')
droid2.sayHi()
Robot.howMany()

print("\nЗдесь роботы могут проделать какую-то работу.\n")

print("Роботы закончили свою работу. Давайте уничтожим их.")
del droid1
del droid2

Robot.howMany()

Вывод:

$ python3 objvar.py
(Инициализация R2-D2)
Приветствую! Мои хозяева называют меня R2-D2.
У нас 1 роботов.
(Инициализация C-3PO)
Приветствую! Мои хозяева называют меня C-3PO.
У нас 2 роботов.

Здесь роботы могут проделать какую-то работу.

Роботы закончили свою работу. Давайте уничтожим их.
R2-D2 уничтожается!
Осталось 1 работающих роботов.
C-3PO уничтожается!
C-3PO был последним.
У нас 0 роботов.

Как это работает:

Это длинный пример, но он помогает продемонстрировать природу переменных класса и объекта. Здесь population принадлежит классу Robot, и поэтому является переменной класса. Переменная name принадлежит объекту (ей присваивается значение при помощи self), и поэтому является переменной объекта.

Таким образом, мы обращаемся к переменной класса population как Robot.population, а не self.population. К переменной же объекта name во всех методах этого объекта мы обращаемся при помощи обозначения self.name. Помните об этой простой разнице между переменными класса и объекта. Также имейте в виду, что переменная объекта с тем же именем, что и переменная класса, сделает недоступной (“спрячет”) переменную класса!

Метод howMany принадлежит классу, а не объекту. Это означает, что мы можем определить его как classmethod или staticmethod, в зависимости от того, нужно ли нам знать, в каком классе мы находимся. Поскольку нам не нужна такая информация, мы воспользуемся staticmethod.

Мы могли достичь того же самого, используя декораторы :

@staticmethod
def howMany():
    '''Выводит численность роботов.'''
    print('У нас {0:d} роботов.'.format(Robot.population))

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

Пронаблюдайте, как метод __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).

class SchoolMember:
    '''Представляет любого человека в школе.'''
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print('(Создан SchoolMember: {0})'.format(self.name))
    def tell(self):
        '''Вывести информацию.'''
        print('Имя:"{0}" Возраст:"{1}"'.format(self.name, self.age), end=" ")

class Teacher(SchoolMember):
    '''Представляет преподавателя.'''
    def __init__(self, name, age, salary):
        SchoolMember.__init__(self, name, age)
        self.salary = salary
        print('(Создан Teacher: {0})'.format(self.name))

    def tell(self):
        SchoolMember.tell(self)
        print('Зарплата: "{0:d}"'.format(self.salary))

class Student(SchoolMember):
    '''Представляет студента.'''
    def __init__(self, name, age, marks):
        SchoolMember.__init__(self, name, age)
        self.marks = marks
        print('(Создан Student: {0})'.format(self.name))

    def tell(self):
        SchoolMember.tell(self)
        print('Оценки: "{0:d}"'.format(self.marks))

t = Teacher('Mrs. Shrividya', 40, 30000)
s = Student('Swaroop', 25, 75)

print() # печатает пустую строку

members = [t, s]
for member in members:
    member.tell() # работает как для преподавателя, так и для студента

Вывод:

$ python3 inherit.py
(Создан SchoolMember: Mrs. Shrividya)
(Создан Teacher: Mrs. Shrividya)
(Создан SchoolMember: Swaroop)
(Создан Student: Swaroop)

Имя:"Mrs. Shrividya" Возраст:"40" Зарплата: "30000"
Имя:"Swaroop" Возраст:"25" Оценки: "75"

Как это работает:

Чтобы воспользоваться наследованием, при определении класса мы указываем имена его базовых классов в виде кортежа, следующего сразу за его именем. Далее мы видим, что метод __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.

#!/usr/bin/env python
# Filename: inherit_abc.py

from abc import *

class SchoolMember(metaclass=ABCMeta):
    '''Представляет любого человека в школе.'''
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print('(Создан SchoolMember: {0})'.format(self.name))

    @abstractmethod
    def tell(self):
        '''Вывести информацию.'''
        print('Имя:"{0}" Возраст:"{1}"'.format(self.name, self.age), end=" ")

class Teacher(SchoolMember):
    '''Представляет преподавателя.'''
    def __init__(self, name, age, salary):
        SchoolMember.__init__(self, name, age)
        self.salary = salary
        print('(Создан Teacher: {0})'.format(self.name))

    def tell(self):
        SchoolMember.tell(self)
        print('Зарплата: "{0:d}"'.format(self.salary))

class Student(SchoolMember):
    '''Представляет студента.'''
    def __init__(self, name, age, marks):
        SchoolMember.__init__(self, name, age)
        self.marks = marks
        print('(Создан Student: {0})'.format(self.name))

    def tell(self):
        SchoolMember.tell(self)
        print('Оценки: "{0:d}"'.format(self.marks))

t = Teacher('Mrs. Shrividya', 40, 30000)
s = Student('Swaroop', 25, 75)

#m = SchoolMember('abc', 10)
# Это приведёт к ошибке: "TypeError: Can't instantiate abstract class
# SchoolMember with abstract methods tell"

print() # печатает пустую строку

members = [t, s]
for member in members:
    member.tell() # работает как для преподавателя, так и для студента

Вывод:

$ python3 inherit.py
(Создан SchoolMember: Mrs. Shrividya)
(Создан Teacher: Mrs. Shrividya)
(Создан SchoolMember: Swaroop)
(Создан Student: Swaroop)

Имя:"Mrs. Shrividya" Возраст:"40" Зарплата: "30000"
Имя:"Swaroop" Возраст:"25" Оценки: "75"

Как это работает:

Мы можем объявить метод tell класса SchoolMember абстрактным, и таким образом автоматически запретим создавать экземпляры класса SchoolMember.

Тем не менее, мы можем работать с экземплярами Teacher и Student так, как будто они экземпляры SchoolMember, поскольку они являются подклассами.

Резюме

Мы изучили различные аспекты классов и объектов, равно как и терминологию, связанную с ними. Мы также увидели ряд достоинств и “подводных камней” объектно-ориентированного программирования. Python – в высокой степени объектно-ориентирован, поэтому понимание этих принципов очень поможет вам в дальнейшем.

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

Примечания

[1]boxing and unboxing
[2]self – англ. “сам” (прим.перев.)
[3]также “суперкласс”, “родительский класс” (прим.перев.)
[4]также “субкласс”, “класс-наследник” (прим.перев.)
[5]в оригинальной версии книги этот параграф невидим для читателей, так как находится в комментарии с пометкой автора “It is too sudden to introduce this concept here.”, что означает “Слишком неожиданно представление этой концепции здесь.” (прим.перев.)