В этой статье разбираем классы в Python 3 и программирование в стиле ООП. А также узнаем про методы, наследование, полиморфизм и другое.

Классы и объекты в Python 3

На Python 3 можно писать код в стиле ООП. То есть создавать свои классы и объекты (экземпляры классов), а затем использовать эти объекты для решения той или иной задачи.

Новый класс создаётся с помощью ключевого слова class, затем пишется название класса и ставится знак двоеточия/Ниже пишутся переменные и методы класса.

Вот пример класса Human:

class Human:
    #переменная класса
    passport_age = 14

    # конструктор
    def __init__(self, height, weight, sex, age):
        self.height = height
        self.weight = weight
        self.sex = sex
        self.age = age
  • passport_age — это переменная класса. Она относится именно к классу, а не к создаваемым объектам. То есть каждый объект класса будет находить значение этой переменной, но изменить её не сможет.
  • __init__ — это специальный метод, который позволяет инициализировать объект класса при создании. То есть, создавая объект, мы должны в скобках указать его свойства (height, weight, sex, age).
  • self — это ссылка на создаваемый объект класса.

Чтобы стало понятнее, разберу процесс создания экземпляров класса:

alex = Human(height=165, weight=71, sex='man', age=30)
elena = Human(160, 50,'woman', 30)
print(alex.passport_age)
print(elena.passport_age)

### Результат выполнения
14
14

Для создания объекта класса мы должны указать имя объекта (в примере это alex и elena). Затем, после знака равенства, указать класс и в скобках передать параметры, которые мы указали в методе __init__ в классе. Параметры можно указывать либо по порядку (как мы сделали для elena), либо с указанием имени параметра и его значением (как мы сделали для alex).

Переменные класса

С помощью записи имя_объекта.переменная (alex.passport_age), мы можем получить значение переменной объекта. Но если у объекта нет такой переменной, то переменная будет искаться в классе (как в примере выше).

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

alex = Human(height=165, weight=71, sex='man', age=30)
elena = Human(160, 50,'woman', 30)

# изменяем переменную passport_age у alex
# а фактически создаём новую переменную для alex
alex.passport_age = 16

print(alex.passport_age)
print(elena.passport_age)

### Результат выполнения
16
14

Как видим, у elena значение переменной passport_age не изменилось, оно всё также берётся из класса. А у объекта alex значение этой переменной изменилось, оно уже берётся локально из объекта, а не из класса.

Чтобы посмотреть значение переменной класса можно, либо напрямую обратиться к переменной класса (Human.passport_age), либо через метод __class__ (alex.__class__.passport_age):

alex = Human(height=165, weight=71, sex='man', age=30)
alex.passport_age = 16
print(Human.passport_age)
print(alex.__class__.passport_age)

### Результат выполнения
14
14

Метод __init__ и переменные объекта

С переменной класса разобрались, теперь разберёмся с методом __init__ в классе. Этот метод ещё называют конструктором. Как я уже писал, он позволяет в процессе создания объекта задать ему переменные. В нашем случае при создании объекта нужно задать переменные height, weight, sex, age. А переменная self будет ссылаться на созданный объект.

То есть создавая объект таким образом alex = Human(height=165, weight=71, sex=’man’, age=30). У нас получится:

  • alex — это имя объекта;
  • height=165 — попадет в переменную объекта self.height, а так как self ссылается на объект, то в alex.height;
  • weight=71 — попадёт в переменную объекта alex.weight;
  • sex=’man’ — попадёт в переменную объекта alex.sex;
  • age=30 — попадёт в переменную объекта alex.age.

Все эти переменные (alex.height, alex.weight, alex.sex, alex.age) — это переменные объекта, а не класса.

А в процессе работы объекту можно добавлять переменные с помощью конструкции имя_объекта.имя_переменной = значение.

Методы

Обычные методы

Сам класс и объекты класса имеют не только свойства (переменные), но и должны уметь что-то выполнять. Для этого в классе создают методы. Метод — это обычная функция. Но чтобы объекты класса могли ею пользоваться, и чтобы эта функция имела доступ к переменным объекта и класса, функция должна содержать ссылку на объект (self).

Давайте добавим классу Human метод norma_weight(). Этот метод будет рассчитывать, какой вес для данного роста является нормальным и проверять, нормальный ли вес у этого человека. Для высчитывания нормального веса я использую формулу Лоренца.

Вот пример нашего класса Human с добавленным методом расчета нормального веса norm_weight():

class Human:
    #переменная класса
    passport_age = 14

    # конструктор
    def __init__(self, height, weight, sex, age):
        self.height = height
        self.weight = weight
        self.sex = sex
        self.age = age

    # метод
    def norm_weight(self):
        # формула Лоренца (норма веса)
        norm = (self.height - 100) - ((self.height - 150)/2)
        if self.weight > norm:
            print("Вес данного человека превышает норму на", self.weight - norm)
        elif self.weight < norm:
            print("Вес данного человека меньше нормы на", norm - self.weight)

Теперь создадим экземпляр класса и проверим в норме ли у него вес:

alex = Human(height=165, weight=71, sex='man', age=30)
alex.norm_weight()

### результат выполнения
Вес данного человека превышает норму на 13.5

Такой метод с параметром self, принадлежит объекту, поэтому и вызывается через объект (alex.norm_weight()).

Но мы можем создавать методы класса, и вызывать такие методы без создания объектов. Я имею ввиду методы именно класса а не экземпляров класса. Для использования таких методов даже не обязательно создавать объекты класса. Для их создания существуют декораторы:

  • @classmethod — создает метод класса. Такой метод работает исключительно с атрибутами класса (переменными класса).
  • @staticmethod — создаёт статический метод класса. Такой метод не видит атрибуты класса и объекта. Он видит только то, что ему передадут.

Методы класса (@classmethod)

Создавая метод класса с помощью декоратора @classmethod нужно вместо слова self (ссылки на объект) использовать cls (ссылку на класс).

Дополним наш класс методом reach_passport_age(cls, age). Он будет связан с классом (cls) и будет принимать параметр (age). Весть код нового класса:

class Human:
    #переменная класса
    passport_age = 14

    # конструктор
    def __init__(self, height, weight, sex, age):
        self.height = height
        self.weight = weight
        self.sex = sex
        self.age = age

    # метод
    def norm_weight(self):
        # формула Лоренца (норма веса)
        norm = (self.height - 100) - ((self.height - 150)/2)
        if self.weight > norm:
            print("Вес данного человека превышает норму на", self.weight - norm)
        elif self.weight < norm:
            print("Вес данного человека меньше нормы на", norm - self.weight)

    # метод класса
    @classmethod
    def reach_passport_age(cls, age):
        if age >= cls.passport_age:
            print("Человек достиг возраста, когда ему выдают паспорт.")
        else:
            print("Человек ещё не достиг возраста, когда ему выдают паспорт.")

И теперь мы можем работать с методом класса не создавая объекты класса:

Human.reach_passport_age(12)
Human.reach_passport_age(15)

### результат выполнения
Человек ещё не достиг возраста, когда ему выдают паспорт.
Человек достиг возраста, когда ему выдают паспорт.

Статические методы класса (@staticmethod)

Теперь создадим такой же метод, только статический (reach_passport_age2(age)). Этот метод уже не связан с классом, он видит только свои локальные переменные и не видит переменные класса. Поэтому здесь мне пришлось продублировать переменную содержащую возраст выдачи паспортов (pass_age = 14). Вот полный код класса с добавленным методом:

class Human:
    #переменная класса
    passport_age = 14

    # конструктор
    def __init__(self, height, weight, sex, age):
        self.height = height
        self.weight = weight
        self.sex = sex
        self.age = age

    # метод
    def norm_weight(self):
        # формула Лоренца (норма веса)
        norm = (self.height - 100) - ((self.height - 150)/2)
        if self.weight > norm:
            print("Вес данного человека превышает норму на", self.weight - norm)
        elif self.weight < norm:
            print("Вес данного человека меньше нормы на", norm - self.weight)

    # метод класса
    @classmethod
    def reach_passport_age(cls, age):
        if age >= cls.passport_age:
            print("Человек достиг возраста, когда ему выдают паспорт.")
        else:
            print("Человек ещё не достиг возраста, когда ему выдают паспорт.")

    # статический метод класса
    @staticmethod
    def reach_passport_age2(age):
        pass_age = 14
        if age >= pass_age:
            print("Человек достиг возраста, когда ему выдают паспорт.")
        else:
            print("Человек ещё не достиг возраста, когда ему выдают паспорт.")

То есть в классе появляется функция, которая никак не связана ни с экземплярами класса, ни с самим классом. Класс просто содержит эту функцию.

А работает всё точно также:

Human.reach_passport_age2(12)
Human.reach_passport_age2(15)

### результат выполнения
Человек ещё не достиг возраста, когда ему выдают паспорт.
Человек достиг возраста, когда ему выдают паспорт.

Приватные переменные

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

Для примера я напишу новый тестовый класс PrivateTest, в котором размещу приватную переменную ‘__y‘. В этом же классе я создам метод print_y(), который будет выводить значение переменной ‘__y‘:

class PrivateTest:
     def __init__(self):
          self.__y = "Я приватная переменная"

     # метод для обращения к приватной переменной
     def print_y(self):
          print(self.__y)

А вот так можем получить значение этой переменной:

private_test = PrivateTest()
# print(private_test.__y) - так вывести значение переменной не получится
private_test.print_y()

### результат выполнения
Я приватная переменная

Наследование

Если мы создаём новый класс, то он наследуется от object. Но мы можем создать ещё один класс который будет наследоваться от первого класса.

Наследование классов в Python
Наследование классов в Python

При наследовании дочерний класс получает доступ к методам и переменным родительского класса.

Чтобы указать что класс Circle будет наследоваться от класса Shape а не от object, нужно при создании класса Circle в скобках указать Shape.

Вот пример всех трёх классов:

# фигура
class Shape:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, delta_x, delta_y):
        self.x = self.x + delta_x
        self.y = self.y + delta_y

# квадрат
class Square(Shape):
    def __init__(self, side=1, x=0, y=0):
        super().__init__(x, y)
        self.side = side

# круг
class Circle(Shape):
    def __init__(self, radius=1, x=0, y=0):
        super().__init__(x, y)
        self.radius = radius

Любой объект класса Shape будет иметь параметры: x и y (координаты). А также будет иметь метод: move (перемещение объекта, то есть изменение координат).

Так как дочерний класс не инициализирует родительский класс, нужно вручную выполнять функцию super(), для инициализации родительского класса.

Любой объект класса:

  • Square дополнительно имеет атрибут side (размер стороны);
  • Circle имеет дополнительный атрибут radius (радиус).

После создания класса мы можем создать объекты, а затем можем изменить их координаты с помощью метода move():

# создание круга и квадрата
my_circle = Circle(x=10, y=5)
my_square = Square(x=20, y=5)

# перемещение круга и квадрата
my_circle.move(10, 5)
my_square.move(10, 3)

# вывод координат и свойств фигур
print("Координаты нашего круга:", my_circle.x, "x", my_circle.y, "Радиус =", my_circle.radius)
print("Координаты нашего квадрата:", my_square.x, "x", my_square.y, "Сторона = ", my_square.side)

### результат выполнения
Координаты нашего круга: 20 x 10 Радиус = 1
Координаты нашего квадрата: 30 x 8 Сторона =  1

Наследование позволяет уменьшить код. В нашем случае нам не пришлось создавать переменные x и y и метод move для классов Square и Circle, всё это они наследуют от класса Shape.

Множественное наследование

Класс может наследоваться от нескольких классов. Для этого, при создании класса нужно в скобках указать нескольких родителей, например class Person(Doctor, Builder).

Python 3. Множественное наследование
Python 3. Множественное наследование

При этом методы и переменные вначале ищутся в своём классе, затем во всех родительский начиная слева на право. Выполняться будет первый найденный метод. Такой способ поиска методов слева на право называется MRO (Method resolution order).

Python 3. Поиск методов MRO
Python 3. Поиск методов MRO

Итак, реализуем наши классы на Python 3:

class Doctor:
    def can_cure(self):
        print("Я доктор, я умею лечить")
    def can_build(self):
        print("Я доктор, я умею строить, но не очень")

class Builder:
    def can_build(self):
        print("Я строитель, я умею строить")
    def can_cure(self):
        print("Я строитель, я умею лечить, но не очень")

class Person(Doctor, Builder): # MRO - Method resolution order (способ поиска методов слева на право)
    pass

Любой объект класса Doctor, будет иметь два метода can_cure() и can_build(). Любой объект класса Builder будет иметь такие же методы, но они будут выводить немного другую информацию.

При создании класса Person мы в скобках вначале указали Doctor, а затем Builder. Из этого будет следовать что любой объект класса Person будет больше доктором чем строителем. То есть методы can_cure() и can_build() будут браться из класса Builder, так как поиск их там найдёт первее.

alex = Person()
alex.can_build()
print(Person.__mro__) # смотрим порядок поиска методов

### результат выполнения
Я доктор, я умею строить, но не очень
(<class '__main__.Person'>, <class '__main__.Doctor'>, <class '__main__.Builder'>, <class 'object'>)

А с помощью метода __mro__ можем посмотреть порядок поиска методов:

  • вначале они ищутся в классе Person;
  • затем в классе Doctor;
  • после чего в классе Builder;
  • и наконец в object.

Полиморфизм, абстрактные методы и интерфейсы

Полиморфизм — это возможность работы с совершенно разными объектами единым образом.

Например мы можем сделать класс Geom для создания объектов (геометрических фигур). Затем мы можем сделать два класса Rectangle (Прямоугольник) и Square (Квадрат) — наследники от Geom.

В классах Rectangle и Square сделаем одноимённый метод get_pr(), который будет находить периметр данной фигуры.

При этом в родительском классе нужно тоже сделать метод get_pr() — но он будет формальный. Его нужно будет обязательно переопределять в дочерних классах. Этот метод имеет лишь одну цель — дать понять программисту что в дочерним классе этот метод нужно не забыть переопределить.

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

Итак, реализуем наши классы на Python 3:

# интерфейс (класс содержит только абстрактный метод)
class Geom:
    # абстрактный метод
    def get_pr(self):
        print("В дочернем классе должен быть переопределён метод get_pr()")

class Rectangle(Geom):
    def __init__(self, w, h):
        self.w = w
        self.h = h

    def get_pr(self):
        return 2*(self.w + self.h)
    
class Square(Geom):
    def __init__(self, a):
        self.a = a

    def get_pr(self):
        return 4*self.a

Теперь мы можем создать объекты наших классов и вычислить их периметры в цикле, выполняя один и тот же метод — get_pr():

# создадим 4 объекта
my_rectangle1 = Rectangle(1, 2)
my_rectangle2 = Rectangle(3, 4)
my_square1 = Square(10)
my_square2 = Square(20)

# пробежимся по списку наших объектов и определим их периметр
geom = [my_rectangle1, my_rectangle2, my_square1, my_square2]
for g in geom:
    print(g.get_pr()) # метод get_pr вызывается у соответствующего объекта - это и есть пример полиморфизма

### результат выполнения
6
14
40
80

Теперь, если программист напишет новый класс, например Triangle(Geom) (треугольник), и забудет переопределить метод get_pr(), то программа ему об этом скажет.

# интерфейс (класс содержит только абстрактный метод)
class Geom:
    # абстрактный метод
    def get_pr(self):
        print("В дочернем классе должен быть переопределён метод get_pr()")

class Triangle(Geom):
    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c

my_triangle = Triangle(4, 6, 7)
print(my_triangle.get_pr())

### результат выполнения
В дочернем классе должен быть переопределён метод get_pr()
None

Программист поймет свою ошибку и добавит метод:

# интерфейс (класс содержит только абстрактный метод)
class Geom:
     # абстрактный метод
     def get_pr(self):
          print("В дочернем классе должен быть переопределён метод get_pr()")

class Triangle(Geom):
     def __init__(self, a, b, c):
          self.a = a
          self.b = b
          self.c = c
     def get_pr(self):
          return self.a + self.b + self.c

my_triangle = Triangle(4, 6, 7)
print(my_triangle.get_pr())

### результат выполнения
17

Перегрузка операторов

Перегрузка операторов — это придание альтернативного значения какому-то действию. Например, оператор сложения (+) может выполнить сложение двух чисел, объединить две строки, или объединить два списка.

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

В своём классе мы можем использовать эти специальные функции:

  • __add__ (для сложения);
  • __sub__ (для вычитания);
  • __mul__ (для умножения);
  • __truediv__ (для деления);
  • __floordiv__ (целочисленное деление);
  • __mod__ (остаток от деления);
  • __lt__ (меньше);
  • __gt__ (больше);
  • __le__ (меньше или равно);
  • __ge__ (больше или равно);
  • __eq__ (равно).

Для перегрузки оператора в своём классе мы должны написать одну из этих функций и задать ей код. Вот пример:

class Y:

    # конструктор
    def __init__(self, y):
        self.y = y
    
    # этот метод выполнится, если мы будем складывать два объекта
    def __add__(self, other):
        return self.y + other.y
    
    # этот метод выполнится, если мы будем проверять, меньше ли первый объект чем второй
    def __lt__(self, other):
        if self.y < other.y:
            return "Объект 1 меньше объекта 2"
        else:
            return "Объект 1 больше объекта 2"

Мы сделали класс Y, объекты которого будут иметь параметр y. В нём мы определили два метода (помимо конструктора __init__):

  • __add__ — этот метод будет вызываться, когда мы будем складывать два объекта;
  • __lt__ — этот метод будет вызываться, когда мы будет проверять, меньше ли первый объект чем второй.

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

obj1 = Y(3)
obj2 = Y(5)
print(obj1 + obj2)
print(obj1 < obj2)

### результат выполнения
8
Объект 1 меньше объекта 2

Не создав этих методов в классе, объекты класса нельзя было бы складывать и сравнивать.

Итог

Мы узнали как можно использовать классы и объекты на Python 3. Также узнали про параметры классов и объектов (переменные).

Разобрались с методами (функциями в классе). Есть обычные методы, методы класса и статические методы класса.

Узнали что можно использовать приватные переменные в классе, имена которых начинаются на два подчеркивания.

Узнали про наследование классов и про то что первый написанный класс по умолчанию наследуется от object. А также познакомились с множественным наследованием и MRO (поиском методов слева направо).

Узнали про полиморфизм, абстрактные методы, интерфейсы и перегрузку операторов.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *