При работе с классами в Python вы наткнетесь на декоратор @classmethod. Он имеет четкую цель, отличающую его от @staticmethod или обычных методов экземпляра (Instance Method).

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


1. Три типа методов: Instance vs. Class vs. Static



В Python классы по умолчанию имеют три типа методов. Главным отличием является то, что метод принимает в качестве первого аргумента.

class MyClass:
    # 1. Метод экземпляра (Instance Method)
    def instance_method(self, arg1):
        # 'self' получает экземпляр класса.
        # Можно обращаться к свойствам экземпляра (self.x) и изменять их.
        print(f"Метод экземпляра вызван с {self} и {arg1}")

    # 2. Метод класса (Class Method)
    @classmethod
    def class_method(cls, arg1):
        # 'cls' получает сам класс (MyClass).
        # Чаще используется для доступа к свойствам класса или
        # выполнения роли 'фабрики', создающей экземпляры.
        print(f"Метод класса вызван с {cls} и {arg1}")

    # 3. Статический метод (Static Method)
    @staticmethod
    def static_method(arg1):
        # Не принимает 'self' или 'cls'.
        # Является утилитной функцией, не зависящей от состояния класса или экземпляра.
        # Эта функция просто 'прикреплена' к классу.
        print(f"Статический метод вызван с {arg1}")

# Пример вызова методов
obj = MyClass()

# Метод экземпляра (вызывается через экземпляр)
obj.instance_method("Hello") 

# Метод класса (можно вызывать как через класс, так и через экземпляр)
MyClass.class_method("World")
obj.class_method("World") # это тоже возможно, но более понятно вызывать через класс.

# Статический метод (можно вызывать как через класс, так и через экземпляр)
MyClass.static_method("Python")
obj.static_method("Python")

Сравнение в таблице

Категория Первый аргумент Объект доступа Основное использование
Метод экземпляра self (экземпляр) Свойства экземпляра (self.name) Манипуляция состоянием объекта
Метод класса cls (класс) Свойства класса (cls.count) Альтернативный конструктор (фабрика)
Статический метод Нет Нет Утилитная функция, логически связанная с классом

2. Ключевое использование @classmethod: Фабричный метод

Наиболее важный и распространенный случай использования @classmethod — это определение "Альтернативного конструктора" или "Фабричного метода".

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

Используя @classmethod, можно создать другую 'точку входа', которая возвращает экземпляр класса, помимо __init__.

Схема concept @classmethod

Пример: Создание объекта Person из строки даты

Предположим, класс Person принимает name и age. Если вы хотите создать объект также из строки формата 'Имя-год рождения', что делать?

import datetime

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def greet(self):
        print(f"Здравствуйте, меня зовут {self.name} и мне {self.age} лет.")

    @classmethod
    def from_birth_year(cls, name, birth_year):
        """
        Принимает год рождения и рассчитывает возраст, создавая экземпляр Person.
        """
        current_year = datetime.datetime.now().year
        age = current_year - birth_year

        # 'cls' указывает на сам класс Person.
        # Это аналогично return Person(name, age), но,
        # использование 'cls' дает преимущества при наследовании. (об этом ниже)
        return cls(name, age)

# Использование обычного конструктора
person1 = Person("Alice", 30)
person1.greet()

# Использование альтернативного конструктора, созданного с помощью @classmethod
person2 = Person.from_birth_year("MadHatter", 1995)
person2.greet()

Результат:

Здравствуйте, меня зовут Alice и мне 30 лет.
Здравствуйте, меня зовут MadHatter и мне 30 лет. (по состоянию на 2025 год)

В примере выше метод from_birth_year обращается к самому классу Person через аргумент cls. Затем он использует рассчитанное значение age, чтобы вызвать cls(name, age), что аналогично вызову Person(name, age) и возвращает новый экземпляр Person.


3. @classmethod и наследование



"Почему бы просто не вызвать Person(...), зачем использовать cls(...)?"

Истинная мощь @classmethod проявляется в наследовании. cls указывает на _тот класс_, в котором был вызван метод.

Предположим, мы создаем класс Student, который наследует от Person.

class Student(Person):
    def __init__(self, name, age, major):
        super().__init__(name, age) # Вызов __init__ родительского класса
        self.major = major

    def greet(self):
        # Переопределение метода
        print(f"Здравствуйте, я {self.major} студент {self.name} и мне {self.age} лет.")

# Вызываем @classmethod родителя из класса Student.
student1 = Student.from_birth_year("Jhon", 2005, "компьютерные науки") 
# Ой, возникает ошибка TypeError!

# При вызове Student.from_birth_year
# запускается Person.from_birth_year, в то время как 'cls' возвращает 'Student'.
# Таким образом, return cls(name, age) превращается в return Student(name, age).
# Однако __init__ класса Student требует дополнительный аргумент 'major'.

Код выше вызывает ошибку, потому что __init__ класса Student требует major. Однако, основной момент в том, что cls указывает на Student, а не на Person.

Если бы from_birth_year был @staticmethod или создан жестко с Person(name, age), вызов Student.from_birth_year вернул бы экземпляр Person, а не Student.

@classmethod гарантирует, что через cls создается правильный экземпляр дочернего класса, даже если метод вызывается из дочернего класса. (Конечно, если сигнатура __init__ дочернего класса изменится, from_birth_year тоже придется переопределить.)


4. Резюме

Итак, подводя итог, можно сказать следующее о @classmethod.

  1. @classmethod принимает cls (сам класс) в качестве первого аргумента.

  2. Основное использование - это 'Альтернативный конструктор (Фабричный метод)'. Он используется для предоставления различных способов создания объекта помимо __init__.

  3. Он мощен в сочетании с наследованием. При вызове из дочернего класса cls указывает на правильный дочерний класс, обеспечивая создание соответствующего экземпляра.

Запомните, что @staticmethod — это утилитная функция, для которой состояние класса/экземпляра вообще не нужно, @classmethod — это фабричный метод, который требует доступа к самому классу, а обычные методы экземпляра используются для работы с состоянием объекта (self).