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

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

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

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

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

ИмяКласса()

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

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

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

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

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

Класс как пространство имен

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

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

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

Класс как шаблон для создания объектов

На самом деле классы – не модули. Они своего рода шаблоны, от которых создаются объекты-экземпляры. Такие объекты наследуют от класса его атрибуты. Вернемся к нашему классу B и создадим на его основе два объекта:

>>> class B:
...     n = 5
...     def adder(v):
...             return v + B.n
... 
>>> a = B()
>>> b = B()

У объектов, связанных с переменными a и b, нет собственного поля n. Однако они наследуют его от своего класса:

>>> a.n
5
>>> a.n is B.n
True

То есть поля a.n и B.n – это одно и то же поле, к которому можно обращаться и через имя a, и через имя b, и через имя класса. Поле одно, ссылок на него три.

Однако что произойдет в момент присваивания этому полю значения через какой-нибудь объект-экземпляр?

>>> a.n = 10
>>> a.n
10
>>> b.n
5
>>> B.n
5

В этот момент у экземпляра появляется собственный атрибут n, который перекроет (переопределит) родительский, то есть тот, который достался от класса.

>>> a.n is B.n
False
>>> b.n is B.n
True

При этом присвоение через B.n отразится только на b и B, но не на a:

>>> B.n = 100
>>> B.n, b.n, a.n
(100, 100, 10)

Иная ситуация нас ожидает с атрибутом adder. При создании объекта от класса функция adder не наследуется как есть, а как бы превращается для объекта в одноименный метод:

>>> B.adder is b.adder
False
>>> type(B.adder)
<class 'function'>
>>> type(b.adder) 
<class 'method'>

Через имя класса мы вызываем функцию adder:

>>> B.adder(33)
133

Через имя объекта вызываем метод adder:

>>> b.adder(33)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: adder() takes 1 positional 
argument but 2 were given

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

Дело в том, что в отличии от функции в метод первым аргументом всегда передается объект, к которому применяется этот метод. То есть выражение b.adder(33) как бы преобразовывается в adder(b, 33). Сам же b.adder как объект типа method хранит сведения, с каким классом он связан и какому объекту-экземпляру принадлежит:

>>> b.adder
<bound method B.adder of 
<__main__.B object at 0x7fcbf1ab9b80>>

В нашем случае, чтобы вызывать adder через объекты-экземпляры, класс можно переписать так:

>>> class B:
...     n = 5
...     def adder(obj, v):
...             return v + obj.n
... 
>>> b = B()
>>> b.adder(33)
38

В коде выше при вызове метода adder переменной-параметру obj присваивается объект, связанный с переменной, к которой применяется данный метод. В данном случае это объект, связанный с b. Если adder будет вызван на другой объект, то уже он присвоится obj:

>>> a = B()
>>> a.n = 9
>>> a.adder(3)
12

В Python принято переменную-параметр метода, которая связывается с экземпляром своего класса, называть именем self. Таким образом, более корректный код будет таким:

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

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

>>> B.adder(B, 200)
205
>>> B.adder(a, 200)
209

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

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

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

Атрибут __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