Создание классов и объектов

В языке программирования Python классы создаются с помощью инструкции class, за которой следует произвольное имя класса, после которого ставится двоеточие, далее с новой строки и с отступом реализуется тело класса:

class ИмяКласса:
    код_тела_класса

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

Объект создается путем вызова класса по его имени. При этом после имени класса обязательно ставятся скобки:

ИмяКласса()

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

имя_переменной = ИмяКласса()

В последствии к объекту обращаются через связанную с ним переменную.

Пример "пустого" класса и двух созданных на его основе объектов:

>>> class A:
...     pass
... 
>>> a = A()
>>> b = A()

Класс как модуль

В языке программирования Python класс можно представить подобным модулю. Также как в модуле в нем могут быть свои переменные со значениями и функции. Также как в модуле у класса есть собственное пространство имен, доступ к которым возможен через имя класса:

>>> class B:
...     n = 5
...     def adder(v):
...             return v + B.n
... 
>>> B.n
5
>>> B.adder(4)
9

Однако в случае классов используется немного иная терминология. Пусть имена, определенные в классе, называются атрибутами этого класса. В примере имена n и adder – это атрибуты класса B. Атрибуты-переменные часто называют полями или свойствами. Свойством является n. Атрибуты-функции называются методами. Методом в классе B является adder. Количество свойств и методов в классе может быть любым.

Класс как создатель объектов

Приведенный выше класс позволяет создавать объекты, но мы не можем применить к объекту метод adder():

>>> l = B()
>>> l.n
5
>>> l.adder(100)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: adder() takes 1 positional 
argument but 2 were given

В сообщении об ошибке говорится, что adder() принимает только один аргумент, а было передано два. Откуда мог взяться второй аргумент, если в скобках было указано только одно число 100?

На самом деле классы – это не модули. Они обладают своими особенностями. Класс создает объекты, которые являются его наследниками. Это значит, что если у объекта нет собственного поля n, то интерпретатор ищет его уровнем выше, то есть в классе. Таким образом, если мы добавляем объекту поле с таким же именем как в классе, то оно перекрывает, то есть переопределяет, поле класса:

>>> l.n = 10
>>> l.n
10
>>> B.n
5

Здесь l.n и B.n – это разные переменные. Первая находится в пространстве имен объекта l. Вторая – в пространстве класса B. Если бы мы не добавили поле n к объекту l, то интерпретатор бы поднялся выше по дереву наследования и пришел бы в класс, где бы и нашел это поле.

Что касается методов, то они также наследуются объектами класса. В данном случае у объекта l нет своего собственного метода adder, значит, он ищется в классе B.

Однако в случае с методами не так все просто, как с полями. В Python по умолчанию один и тот же метод, вызванный от имени класса (например, B.meth()) и от экземпляра этого класса (например, l.meth()), вызывается по-разному. В последнем случае l.meth() невидимо для нас преобразуется в B.meth(l).

Но если метод meth() определен как непринимающий никаких аргументов, а они в него передаются, это приводит к ошибке:

>>> class A:
...     def meth():
...             print(1)
... 
>>> a = A()
>>> a.meth()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: meth() takes 0 positional arguments 
but 1 was given
>>> A.meth()
1

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

Понятно, что передаваемый экземпляр, это объект, к которому применяется метод. Выражение l.adder(100) выполняется интерпретатором следующим образом:

  1. Ищу атрибут adder() у объекта l. Не нахожу.

  2. Тогда иду искать в класс B, так как он создал объект l.

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

Другими словами, выражение l.adder(100) преобразуется в выражение B.adder(l, 100).

Таким образом, интерпретатор попытался передать в метод adder() класса B два параметра – объект l и число 100. Но мы запрограммировали метод adder() так, что он принимает только один параметр. В Python определения методов не предполагают принятие объекта как само собой подразумеваемое. Принимаемый объект надо указывать явно.

По соглашению в Python для ссылки на объект используется имя self. Вот так должен выглядеть метод adder(), если мы планируем вызывать его через объекты:

>>> class B:
...     n = 5
...     def adder(self, v):
...             return v + self.n
... 

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

Протестируем обновленный метод:

>>> l = B()
>>> m = B()
>>> l.n = 10
>>> l.adder(3)
13
>>> m.adder(4)
9

Здесь от класса B создаются два объекта – l и m. Для объекта l заводится собственное поле n. Объект m, за неимением собственного, наследует n от класса B. Можно в этом убедиться, проверив соответствие:

>>> m.n is B.n
True
>>> l.n is B.n
False

В методе adder() выражение self.n – это обращение к свойству n, переданного объекта, и не важно, на каком уровне наследования оно будет найдено.

Если метод не принимает объект, к которому применяется, в качестве первого параметра, то такие методы в других языках программирования называются статическими. Они имеют особый модификатор и могут вызываться как через класс, так и через объект этого класса. В Python для имитации статических методов используется специальный декоратор, после чего метод можно вызывать не только через класс, но и через объект, не передавая сам объект.

Изменение полей объекта

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

>>> l.test = "hi"
>>> B.test
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'B' has no 
attribute 'test'
>>> l.test
'hi'

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

Поэтому принято присваивать полям, а также получать их значения, путем вызова методов:

>>> class User:
...     def setName(self, n):
...             self.name = n
...     def getName(self):
...             try:
...                     return self.name
...             except:
...                     print("No name")
... 
>>> first = User()
>>> second = User()
>>> first.setName("Bob")
>>> first.getName()
'Bob'
>>> second.getName()
No name

Подобные методы называют сеттерами (set – установить) и геттерами (get – получить).

Атрибут __dict__

В Python у объектов есть встроенные специальные атрибуты. Мы их не определяем, но они есть. Одним из таких атрибутов объекта является свойство __dict__. Его значением является словарь, в котором ключи – это имена свойств экземпляра, а значения – текущие значения свойств.

>>> class B:
...     n = 5
...     def adder(self, v):
...             return v + self.n
... 
>>> w = B()
>>> w.__dict__
{}
>>> w.n = 8
>>> w.__dict__
{'n': 8}

В примере у экземпляра класса B сначала нет собственных атрибутов. Свойство n и метод adder – это атрибуты объекта-класса, а не объекта-экземпляра, созданного от этого класса. Лишь когда мы выполняем присваивание новому полю n экземпляра, у него появляется собственное свойство, что мы наблюдаем через словарь __dict__.

В следующем уроке мы увидим, что свойства экземпляра обычно не назначаются за пределами класса. Это происходит в методах классах путем присваивание через self. Например, self.n = 10.

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

>>> w.__dict__['m'] = 100
>>> w.__dict__
{'n': 8, 'm': 100}
>>> w.m
100

Практическая работа

Напишите программу по следующему описанию. Есть класс "Воин". От него создаются два экземпляра-юнита. Каждому устанавливается здоровье в 100 очков. В случайном порядке они бьют друг друга. Тот, кто бьет, здоровья не теряет. У того, кого бьют, оно уменьшается на 20 очков от одного удара. После каждого удара надо выводить сообщение, какой юнит атаковал, и сколько у противника осталось здоровья. Как только у кого-то заканчивается ресурс здоровья, программа завершается сообщением о том, кто одержал победу.

Курс с примерами решений практических работ и всеми уроками:
android-приложение, pdf-версия

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