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

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

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

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

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

NameOfClass()
1

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

variable_name = AnyClass()
1

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

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

class A:
    pass

a = A()
b = A()
1
2
3
4
5

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

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

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

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

print(B.n)              # 5
print(B.adder(4))       # 9
1
2
3
4
5
6
7

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

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

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

l = B()
print(l.n)              #5
print(l.adder(100))
1
2
3

Вывод в консоль:

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

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

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

l.n = 10
print(l.n)              # 10
print(B.n)              # 5
1
2
3

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

Что касается методов, то они также наследуются объектами класса. В данном случае у объекта l нет своего собственного метода adder, значит, он ищется в классе B. Однако от класса B может быть порождено множество объектов. Методы же чаще всего предназначаются для обработки объектов. Таким образом, когда вызывается метод, в него надо передать конкретный объект, который он будет обрабатывать.

Понятно, что передаваемый экземпляр, это объект, к которому применяется метод. Выражение 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
1
2
3
4

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

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

l = B()
m = B()
l.n = 10
print(l.adder(3))       # 13
print(m.adder(4))       # 9
1
2
3
4
5

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

print(m.n is B.n)       # True
print(l.n is B.n)       # False
1
2

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

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

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

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

l.test = "hi"
print(B.test)
1
2

В консоль выведет ошибку: «объект В не имеет атрибута „test“»:

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

Код:

print(l.test)       # hi
1

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

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

class User:
    def setName(self, n):
        self.name = n
    def getName(self):
        try:
            return self.name
        except:
            return "No name"

first = User()
second = User()
first.setName("Bob")
print(first.getName())              # Bob
print(second.getName())             # No name
1
2
3
4
5
6
7
8
9
10
11
12
13
14

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

Упражнения

  1. Программу в которой класс «Животное» с полями тип и кличка:

    class Animal:
        kind = ""
        name = ""
    
    cat = Animal()
    cat.kind = "cat"
    cat.name = "Murzik"
    print("I like my {} {}!".format(cat.kind, cat.name))
    
    1
    2
    3
    4
    5
    6
    7
    8
  2. Создайте ещё 3 экземпляра данного класса присвоив им тип и кличку. Выведите значения полей созданного объекта.

    Добавим в класс «Животное» поля «Очки Жизней» и методы: «кушать», который увеличивает количество очков жизней на 10; «жизнь», который уменьшает количество очков жизней на переданное методу значение. Добавит метод возвращающий текущее значение жизней или над «хочу есть», если значение жизней меньше или равно 0:

    class Animal:
        kind = ""
        name = ""
        hp = 0
    
        def eat(self):
            self.hp +=10
    
        def live(self, n):
            self.hp -= n
    
        def getHP(self):
            if self.hp > 0:
                return self.hp
            else:
                return "I am hungry..."
    
    cat = Animal()
    cat.kind = "cat"
    cat.name = "Murzik"
    print("I like my {} {}!".format(cat.kind, cat.name))    # I like my cat Murzik!
    print(cat.getHP())                                      # I am hungry...
    cat.eat()
    print(cat.getHP())                                      # 10
    cat.live(7)
    print(cat.getHP())                                      # 3
    
    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

    Создайте еще пару объектов это класса и попробуйте применить к ним написанные методы и понять принцип работы кода.

  3. Напишите программу в которой есть класс автомобиль с полями например: марка, модель, цвет. И создайте 3 экземпляра класса. И выведите значения полей в консоль.

  4. Добавьте к классу автомобиль поле «топливо» и методы «заправить» и «ехать», которые будут принимать параметры пополнять или уменьшать объем топлива машины. Метод ехать должен проверять наличие топлива и если его осталось меньше нуля, то выводиться: «бензин кончился».

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