classmethod, staticmethod и альтернативные конструкторы

⏱ 25 мин · 🧩 заданий: 5

Три вида методов

Вид Декоратор Первый аргумент Доступ
Метод экземпляра — self — объект к данным объекта и класса
Метод класса @classmethod cls — класс к данным класса, может создавать объекты
Статический метод @staticmethod — ни к чему, просто функция в пространстве имён класса
class Pizza:
    sizes = {"S": 25, "M": 30, "L": 35}

    def __init__(self, toppings, size="M"):
        self.toppings = toppings
        self.size = size

    def describe(self):                         # метод экземпляра
        return f"Пицца {self.sizes[self.size]} см: {', '.join(self.toppings)}"

    @classmethod
    def margherita(cls, size="M"):              # метод класса
        return cls(["моцарелла", "томаты"], size)

    @staticmethod
    def area(diameter):                         # статический метод
        return round(3.14159 * (diameter / 2) ** 2)

print(Pizza(["грибы"]).describe())
print(Pizza.margherita("L").describe())
print(Pizza.area(30), Pizza(["сыр"]).area(25))  # статический — и через класс, и через объект

Как Python передаёт первый аргумент

class Demo:
    def instance_m(self):
        return f"self = {type(self).__name__} объект"

    @classmethod
    def class_m(cls):
        return f"cls = {cls.__name__}"

    @staticmethod
    def static_m():
        return "ничего не получил"

d = Demo()
print(d.instance_m())
print(d.class_m(), "|", Demo.class_m())      # через объект и класс — cls одинаковый
print(d.static_m(), "|", Demo.static_m())
try:
    Demo.instance_m()                         # self некому подставить
except TypeError as e:
    print("TypeError:", e)

classmethod и staticmethod — это дескрипторы: при обращении obj.m они решают, что подставить первым аргументом. Механизм разберём в уроке «Дескрипторы и slots».

Альтернативные конструкторы

__init__ в классе один. Если объект нужно создавать из разных источников — строки, словаря, файла, — заводят методы класса from_...:

import json

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

    @classmethod
    def from_dict(cls, data):
        return cls(data["name"], int(data["age"]))

    @classmethod
    def from_json(cls, text):
        return cls.from_dict(json.loads(text))

    @classmethod
    def from_csv_line(cls, line):
        name, age = line.split(",")
        return cls(name.strip(), int(age))

    def __repr__(self):
        return f"{type(self).__name__}({self.name!r}, {self.age})"

print(User.from_dict({"name": "Анна", "age": "28"}))
print(User.from_json('{"name": "Боря", "age": 31}'))
print(User.from_csv_line("Вера, 25"))

Так устроены и встроенные типы:

from datetime import datetime, date

print(dict.fromkeys(["a", "b"], 0))
print(datetime.fromisoformat("2026-10-25T12:30"))
print(date.fromtimestamp(0))
print(int.from_bytes(b"\x01\x00", "big"))

Почему cls, а не имя класса

class Shape:
    def __init__(self, size):
        self.size = size

    @classmethod
    def unit(cls):
        return cls(1)               # правильно: создаст объект того класса, через который вызвали

    @classmethod
    def unit_bad(cls):
        return Shape(1)             # жёстко прописан базовый класс

class Square(Shape):
    def area(self):
        return self.size ** 2

print(type(Square.unit()).__name__, Square.unit().area())
print(type(Square.unit_bad()).__name__)    # Shape — у него нет area()

Метод класса для состояния класса

class Connection:
    _registry = []
    max_connections = 2

    def __init__(self, host):
        self.host = host
        type(self)._registry.append(self)

    @classmethod
    def open(cls, host):
        if len(cls._registry) >= cls.max_connections:
            raise RuntimeError(f"лимит {cls.max_connections} соединений")
        return cls(host)

    @classmethod
    def count(cls):
        return len(cls._registry)

Connection.open("db1")
Connection.open("db2")
print(Connection.count())
try:
    Connection.open("db3")
except RuntimeError as e:
    print("RuntimeError:", e)

staticmethod или функция модуля?

Статический метод — это обычная функция, положенная в класс ради группировки. Ставьте её в класс, если она логически связана с ним (валидатор, конвертер единиц) и её удобно вызывать как Temperature.c_to_f(...). Если функция полезна сама по себе — оставьте её функцией модуля: в Python не нужно всё складывать в классы.

Правило выбора

Нужен объект → обычный метод. Нужен класс (создать объект, прочитать настройки класса) → @classmethod. Не нужно ни то, ни другое → @staticmethod или функция модуля.

Вопрос на собеседовании

«Чем classmethod отличается от staticmethod?» — classmethod получает класс cls (с учётом наследования) и используется для альтернативных конструкторов и работы с состоянием класса. staticmethod ничего не получает — это функция в пространстве имён класса.

Итог

  • Обычный метод получает self, @classmethod — cls, @staticmethod — ничего.
  • Альтернативные конструкторы from_... — классический случай для @classmethod; внутри пишите cls(...).
  • Статический метод — функция, сгруппированная с классом по смыслу.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Добавьте в класс Date альтернативный конструктор from_string(cls, text), который принимает строку "25.10.2026" и возвращает объект класса cls. Используйте именно cls, чтобы наследник MyDate.from_string(...) создавал MyDate.

Какие тесты проверяют код (2)
d = Date.from_string("25.10.2026"); assert (d.day, d.month, d.year) == (25, 10, 2026)
class MyDate(Date):
    pass
assert type(MyDate.from_string("1.1.2000")) is MyDate, "Наследник должен создавать объект своего класса — используйте cls"

Задание 2 Программа на Python

Добавьте в класс Password статический метод is_strong(text): пароль сильный, если в нём не меньше 8 символов, есть хотя бы одна цифра и хотя бы одна заглавная буква. Метод должен вызываться и через класс, и через экземпляр.

Какие тесты проверяют код (3)
assert Password.is_strong("Secret123") is True
assert not Password.is_strong("secret123") and not Password.is_strong("Sec1")
assert Password("Qwerty12345").text == "Qwerty12345"
Задание 3 Заполнить таблицу

Что получает первым аргументом каждый метод?

экземпляр (self)класс (cls)ничего
обычный метод def m(self)
@classmethod
@staticmethod
@classmethod, вызванный через экземпляр obj.m()
Отметьте ячейку — «+», пустая — «−».
Задание 4 Один вариант ответа

Почему в альтернативном конструкторе пишут return cls(...), а не return Date(...)?

Задание 5 Один вариант ответа

Где в стандартной библиотеке используется classmethod как альтернативный конструктор?

Войдите, чтобы отмечать пройденные уроки.