Сразу начнем с примера простого класса, который содержит обычный метод и метод класса:
class MyClass:
def method(self):
return 'instance method called', self
@classmethod
def classmethod(cls):
return 'class method called', cls
В 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
и следовательно, вызов не выполняется.
Следующие примеры кода должны сделать понимание метода класса более ясным. Далее рассмотрим пример класса, имеющего дело с информацией о дате (это будет шаблон):
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}'
Обратите внимание, как используется аргумент 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()
.Используя методы класса, можно добавить столько альтернативных конструкторов, сколько необходимо. Такое поведение может сделать интерфейс создаваемых классов самодокументированным (до определенной степени конечно) и упростить их использование.