Сообщить об ошибке.

Что такое метод класса в Python и зачем нужен

Альтернативный конструктор (несколько конструкторов) класса в Python

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

class MyClass:
    def method(self):
        return 'instance method called', self

    @classmethod
    def classmethod(cls):
        return 'class method called', cls

Как работают методы класса в Python?

В Python, методы класса отмечаются декоратором @classmethod, следовательно в приведенном выше примере, метод класса будет определен в функции classmethod().

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

Поскольку метод класса имеет доступ только к аргументу cls, он не может изменять состояние экземпляра объекта. Для этого потребуется доступ к аргументу self. НО все же методы класса могут изменять состояние класса, которое применяется ко всем экземплярам класса.

MyClass() настроен таким образом, что реализация каждого метода возвращает кортеж для отслеживания, что происходит, и к каким частям класса или объекта метод может получить доступ.

Вот что происходит, когда мы вызываем метод экземпляра:

>>> obj = MyClass()
>>> obj.method()
# ('instance method called', <__main__.MyClass object at 0x7f9748403f40>)

# Можно передать объект `obj` экземпляра вручную, 
# для получения того же результата
>>> MyClass.method(obj)
# ('instance method called', <__main__.MyClass object at 0x7f9748403f40>)

Этот кусок кода подтвердил, что метод экземпляра класса имеет доступ к экземпляру объекта, напечатанному как <__main__.MyClass object> через аргумент self.

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

Попробуем вызвать метод класса:

>>> obj.classmethod()
# ('class method called', <class '__main__.MyClass'>)

Вызов метода класса obj.classmethod() показал, что он не имеет доступа к объекту <__main__.MyClass object>, а только к объекту <class '__main__.MyClass'>, представляющему сам класс (в Python все является объектом, даже сами классы).

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

Теперь посмотрим, что происходит, когда попытаться вызвать эти методы в самом классе - без предварительного создания экземпляра объекта:

>>> MyClass.classmethod()
# ('class method called', <class '__main__.MyClass'>)
>>> MyClass.method()
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# TypeError: method() missing 1 required positional argument: 'self'

Из примера видно, что можно нормально вызвать метод класса MyClass.classmethod(), но попытка вызвать метод экземпляра MyClass.method() завершилась ошибкой TypeError. Этого следовало ожидать, ведь экземпляр объекта не создан, что означает, что Python не может заполнить аргумент self и следовательно, вызов не выполняется.

Для чего нужны методы класса в Python?

Следующие примеры кода должны сделать понимание метода класса более ясным. Далее рассмотрим пример класса, имеющего дело с информацией о дате (это будет шаблон):

class Date:
    def __init__(self, day=0, month=0, year=0):
        self.day = day
        self.month = month
        self.year = year

    def string_to_db(self):
        return f'{self.year}-{self.month}-{self.day}'

Этот класс, очевидно, можно использовать для хранения информации об определенных датах, без информации о часовом поясе (предположим, что все даты представлены в формате UTC).

Здесь есть конструктор __init__, типичный инициализатор экземпляров классов Python, который получает аргументы как типичный метод экземпляра, имея первый необязательный аргумент self, который содержит ссылку на вновь созданный экземпляр.

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

Предположим, что мы хотим создать много экземпляров класса Date() с информацией о дате, поступающей из внешнего источника, в виде строки с форматом dd.mm.yyyy. Предположим, нужно сделать это в разных местах исходного кода проекта.

Итак, что для этого необходимо сделать:

  • Выполнить синтаксический анализ строки, получить день, месяц и год как три целочисленные переменные или кортеж из трех элементов, включающий эти переменные.
  • Создать экземпляр Date, передав эти значения в конструктор класса при его создании.

Это будет выглядеть так:

>>> string_date = '10.10.2020'
>>> day, month, year = map(int, string_date.split('.'))
>>> date = Date(day, month, year)
>>> date.string_to_db()
# '2020-10-10'

Для выполнения этой задачи, например C++ или Java может реализовать такую ​​возможность с перегрузкой метода __init__, но в Python перегрузка методов отсутствует. Вместо нее необходимо использовать метод класса (декоратор @classmethod).

Создадим еще один "конструктор".

class Date(object):
    def __init__(self, day=0, month=0, year=0):
        self.day = day
        self.month = month
        self.year = year

    @classmethod
    def from_string(cls, date_as_string):
        day, month, year = map(int, date_as_string.split('.'))
        date1 = cls(day, month, year)
        return date1

    def string_to_db(self):
        return f'{self.year}-{self.month}-{self.day}'

Более подробно о перегрузке смотрите в материале "Перегрузка методов в Python".

Обратите внимание, как используется аргумент cls в методе класса from_string() вместо прямого вызова конструктора класса Date().

Теперь создать новый экземпляр Date, так же, можно следующим образом:

>>> date1 = Date.from_string('30.12.2020')
>>> date1.string_to_db()
# '2020-12-30'
>>> date2 = Date.from_string('01.01.2021')
>>> date2.string_to_db()
# '2021-1-1'

Рассмотрим приведенную выше реализацию чтобы понять, какие преимущества здесь есть:

  • Реализован синтаксический анализ строки даты в одном месте, и теперь его можно использовать повторно.
  • Инкапсуляция здесь отлично работает. Если вы думаете, что можете реализовать синтаксический анализ строк как единственную функцию в другом месте, это решение намного лучше соответствует парадигме ООП.
  • cls - это объект, который содержит сам класс, а не его экземпляр. Это довольно круто, потому что, если мы наследуем класс Date, для всех дочерних элементов также будет определен метод класса from_string().

Используя методы класса, можно добавить столько альтернативных конструкторов, сколько необходимо. Такое поведение может сделать интерфейс создаваемых классов самодокументированным (до определенной степени конечно) и упростить их использование.