Python @staticmethod и @classmethod
Как работают декораторы @staticmethod и @classmethod в Python, когда использовать каждый из них, примеры фабричных методов и вспомогательных функций.
Python предоставляет каждому методу внутри класса один из трёх стилей привязки: он может быть привязан к экземпляру, к самому классу или ни к чему. Декораторы @classmethod и @staticmethod управляют двумя последними стилями.
В этой главе рассматриваются:
- Три типа методов и их различия
@staticmethod— обычная функция, хранящаяся внутри класса@classmethod— метод, получающий класс в качестве первого аргумента- Фабричные методы: наиболее распространённое практическое применение
@classmethod - Альтернативные конструкторы и их взаимодействие с наследованием
- Когда выбирать
@staticmethod,@classmethodили функцию на уровне модуля - Распространённые подводные камни
Перед чтением убедитесь, что вы знакомы с классами и объектами Python и наследованием Python. Для вычисляемого доступа к атрибутам смотрите @property. Для подробного изучения того, как работают декораторы в целом, смотрите Декораторы Python.
Три типа методов
Прежде чем рассмотреть каждый декоратор, вот сравнение бок о бок:
| Метод экземпляра | @classmethod | @staticmethod | |
|---|---|---|---|
| Первый параметр | self (экземпляр) | cls (класс) | нет |
| Получает экземпляр? | Да | Нет | Нет |
| Получает класс? | Через type(self) | Да (напрямую) | Нет |
| Вызывается на экземпляре | Да | Да | Да |
| Вызывается на классе | Да (но self отсутствует) | Да | Да |
| Типичное применение | Работа с данными экземпляра | Фабричные методы, состояние на уровне класса | Утилиты и вспомогательные функции |
class Demo:
def instance_method(self):
return f"instance method — self is {self}"
@classmethod
def class_method(cls):
return f"class method — cls is {cls}"
@staticmethod
def static_method():
return "static method — no self, no cls"
d = Demo()
print(d.instance_method()) # instance method — self is <__main__.Demo object at 0x...>
print(d.class_method()) # class method — cls is <class '__main__.Demo'>
print(d.static_method()) # static method — no self, no cls
# All three can also be called directly on the class:
print(Demo.class_method()) # class method — cls is <class '__main__.Demo'>
print(Demo.static_method()) # static method — no self, no cls@staticmethod
Статический метод — простейший из трёх. Это обычная функция, которая просто находится в пространстве имён класса. Python не передаёт self или cls автоматически.
class MathUtils:
@staticmethod
def add(a, b):
return a + b
@staticmethod
def is_even(n):
return n % 2 == 0
print(MathUtils.add(3, 4)) # 7
print(MathUtils.is_even(10)) # TrueКогда использовать @staticmethod
Используйте @staticmethod, когда вспомогательная функция логически принадлежит классу — для ясности, группировки или разграничения пространства имён — но не нуждается в чтении или изменении состояния экземпляра или класса:
- Вспомогательные функции валидации, вызываемые до создания объекта.
- Чистые функции преобразования или вычисления, значимые только в контексте одного класса.
- Утилиты, используемые несколькими методами одного класса, но нигде больше.
class Temperature:
def __init__(self, celsius):
if not Temperature._is_valid(celsius):
raise ValueError(f"Temperature {celsius} °C is below absolute zero")
self.celsius = celsius
@staticmethod
def _is_valid(celsius):
return celsius >= -273.15
@staticmethod
def celsius_to_fahrenheit(celsius):
return celsius * 9 / 5 + 32
t = Temperature(100)
print(Temperature.celsius_to_fahrenheit(100)) # 212.0
print(Temperature._is_valid(-300)) # FalseОбратите внимание, что _is_valid имеет префикс _, сигнализирующий о том, что метод является внутренним для класса. Вызывающие стороны, которым нужны только объекты Temperature, никогда его не видят — они просто получают ValueError, если передают невозможное значение.
@staticmethod vs функция на уровне модуля
Функция на уровне модуля и @staticmethod почти идентичны по поведению. Разница заключается в том, где находится функция:
- Если функция относится только к
Temperature(или вызывается исключительно изTemperature), поместите её внутрь класса как@staticmethod. - Если это общая утилита, используемая по всему модулю, поместите её на уровне модуля.
Разницы в производительности нет. Это исключительно вопрос организации кода.
@classmethod
Метод класса получает класс в качестве первого аргумента (по соглашению называемого cls — но, как и self, это соглашение, а не ключевое слово). Поскольку он имеет ссылку на класс, он может:
- Читать или изменять атрибуты уровня класса.
- Создавать и возвращать новые экземпляры класса (фабричные методы).
- Корректно работать с подклассами (полиморфные фабрики).
class Counter:
_count = 0 # class-level attribute
def __init__(self):
Counter._count += 1
@classmethod
def get_count(cls):
return cls._count
@classmethod
def reset(cls):
cls._count = 0
Counter()
Counter()
Counter()
print(Counter.get_count()) # 3
Counter.reset()
print(Counter.get_count()) # 0Фабричные методы — наиболее важный случай применения
Наиболее распространённым и ценным применением @classmethod является фабричный метод (также называемый альтернативным конструктором). Фабричный метод создаёт экземпляры из различных видов входных данных, не загромождая __init__ условной логикой.
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
def __repr__(self):
return f"Date({self.year}, {self.month}, {self.day})"
@classmethod
def from_string(cls, date_string):
"""Create a Date from an ISO 8601 string, e.g. '2024-03-15'."""
year, month, day = (int(p) for p in date_string.split("-"))
return cls(year, month, day)
@classmethod
def from_tuple(cls, date_tuple):
"""Create a Date from a (year, month, day) tuple."""
return cls(*date_tuple)
d1 = Date(2024, 3, 15)
d2 = Date.from_string("2024-03-15")
d3 = Date.from_tuple((2024, 3, 15))
print(d1) # Date(2024, 3, 15)
print(d2) # Date(2024, 3, 15)
print(d3) # Date(2024, 3, 15)__init__ остаётся простым — он лишь хранит три целых числа. Методы класса берут на себя логику преобразования. Это чище, чем единственный __init__ с несколькими необязательными параметрами и ветками if/elif.
Почему cls важен для наследования
Когда фабричный метод класса вызывает cls(...) вместо жёсткого кодирования имени класса, он создаёт экземпляр того класса, для которого был вызван метод — даже подкласса. Именно поэтому внутри @classmethod всегда следует предпочитать cls(...) вместо ИмяКласса(...).
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
def __repr__(self):
return f"{type(self).__name__}({self.year}, {self.month}, {self.day})"
@classmethod
def from_string(cls, date_string):
year, month, day = (int(p) for p in date_string.split("-"))
return cls(year, month, day) # uses cls, not Date
class DateTime(Date):
pass # inherits from_string
dt = DateTime.from_string("2024-03-15")
print(dt) # DateTime(2024, 3, 15) — correct subclass
print(type(dt)) # <class '__main__.DateTime'>Если бы from_string жёстко задавал return Date(year, month, day), вызов DateTime.from_string(...) возвращал бы Date, а не DateTime — тихо нарушая контракт наследования.
Изменение состояния на уровне класса
Методы класса также могут выступать как именованные конструкторы с побочными эффектами или манипулировать переменными класса, отслеживающими общее состояние:
class Registry:
_instances = []
def __init__(self, name):
self.name = name
Registry._instances.append(self)
@classmethod
def all(cls):
return list(cls._instances)
@classmethod
def clear(cls):
cls._instances.clear()
Registry("alice")
Registry("bob")
Registry("carol")
print([r.name for r in Registry.all()]) # ['alice', 'bob', 'carol']
Registry.clear()
print(Registry.all()) # []Вызов на экземпляре и на классе
Как @staticmethod, так и @classmethod можно вызывать и на экземпляре, и на классе. Python поддерживает обе формы:
class Circle:
PI = 3.14159265
def __init__(self, radius):
self.radius = radius
def area(self):
return Circle.PI * self.radius ** 2
@classmethod
def unit_circle(cls):
"""Return a circle with radius 1."""
return cls(1)
@staticmethod
def describe():
return "A circle is a round plane figure."
c = Circle(5)
# staticmethod — callable on instance or class
print(c.describe()) # A circle is a round plane figure.
print(Circle.describe()) # A circle is a round plane figure.
# classmethod — callable on instance or class
unit = c.unit_circle()
print(unit.radius) # 1
print(Circle.unit_circle().radius) # 1Вызов на классе обычно нагляднее — он сигнализирует читателю, что данные экземпляра не задействованы.
Совместное использование @classmethod и @staticmethod
Метод класса может делегировать работу по валидации статическому методу, поскольку у метода класса есть доступ к cls для его вызова:
class PositiveNumber:
def __init__(self, value):
self.value = value
def __repr__(self):
return f"PositiveNumber({self.value})"
@staticmethod
def _validate(value):
if value <= 0:
raise ValueError(f"Expected a positive number, got {value!r}")
@classmethod
def create(cls, value):
cls._validate(value)
return cls(value)
n = PositiveNumber.create(42)
print(n) # PositiveNumber(42)
try:
PositiveNumber.create(-5)
except ValueError as e:
print(e) # Expected a positive number, got -5Быстрый справочник: какой декоратор выбрать?
| Ситуация | Рекомендация |
|---|---|
Метод читает или записывает self | Обычный метод экземпляра |
| Метод создаёт новый экземпляр | @classmethod (фабричный / альтернативный конструктор) |
| Метод читает или записывает атрибут класса | @classmethod |
| Метод является чистой вспомогательной функцией без данных класса или экземпляра | @staticmethod (или функция на уровне модуля) |
| Метод валидирует входные данные перед созданием объекта | @staticmethod |
| Метод должен корректно работать в подклассах | @classmethod (используйте cls, а не жёстко заданное имя класса) |
Распространённые подводные камни
Забытый cls в @classmethod
Если вы жёстко кодируете имя класса вместо использования cls, наследование тихо ломается:
class Animal:
@classmethod
def create(cls):
return cls() # correct — returns an instance of the actual class
class Dog(Animal):
pass
print(type(Dog.create())) # <class '__main__.Dog'> — correctВнутри метода класса всегда используйте cls(...), никогда Animal(...).
Обращение к self или cls в @staticmethod
@staticmethod не получает неявного первого аргумента. Попытка обратиться к self или cls внутри него является ошибкой:
class Bad:
label = "bad"
@staticmethod
def show():
# print(cls.label) # NameError: name 'cls' is not defined
print("use @classmethod if you need cls")
Bad.show() # use @classmethod if you need clsЕсли вам нужен cls в том, что вы считали статическим методом, переключите его на @classmethod.
Путаница с декораторами
Методы @classmethod должны иметь cls в качестве первого явного параметра, а методы @staticmethod — ни одного. Их перепутывание вызывает TypeError во время вызова, а не при определении — что может удивить:
class Broken:
@staticmethod
def forgot_cls(cls): # cls is just a regular positional argument here
return cls
# Broken.forgot_cls() # TypeError: forgot_cls() missing 1 required positional argument: 'cls'Переопределение в подклассах
Оба декоратора работают с super() и могут быть переопределены:
class Base:
@classmethod
def who(cls):
return f"Base.who called with cls={cls.__name__}"
class Child(Base):
@classmethod
def who(cls):
parent = super().who()
return f"Child.who — parent said: {parent}"
print(Child.who())
# Child.who — parent said: Base.who called with cls=ChildОбратите внимание, что cls в Base.who всё равно равен Child — потому что метод был вызван через Child.
Практический пример: класс User
Вот полный пример, объединяющий методы экземпляра, фабричный метод класса и статический метод-валидатор:
import re
class User:
_all_users = []
def __init__(self, name, email):
User._validate_email(email)
self.name = name
self.email = email
User._all_users.append(self)
def __repr__(self):
return f"User(name={self.name!r}, email={self.email!r})"
# --- instance method ---
def greet(self):
return f"Hello, my name is {self.name}."
# --- factory / alternative constructor ---
@classmethod
def from_dict(cls, data):
"""Create a User from a dict like {'name': 'Alice', 'email': '[email protected]'}."""
return cls(data["name"], data["email"])
# --- class-level query ---
@classmethod
def count(cls):
return len(cls._all_users)
# --- pure helper, no instance or class data needed ---
@staticmethod
def _validate_email(email):
pattern = r"^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$"
if not re.match(pattern, email):
raise ValueError(f"Invalid email address: {email!r}")
# Create via normal constructor
u1 = User("Alice", "[email protected]")
# Create via factory
u2 = User.from_dict({"name": "Bob", "email": "[email protected]"})
print(u1.greet()) # Hello, my name is Alice.
print(u2.greet()) # Hello, my name is Bob.
print(User.count()) # 2
try:
User("Carol", "not-an-email")
except ValueError as e:
print(e) # Invalid email address: 'not-an-email'Этот шаблон — __init__ для обычного создания объектов, @classmethod для альтернативных конструкторов, @staticmethod для вспомогательных функций — встречается по всей стандартной библиотеке Python (см. datetime.date.today(), datetime.date.fromisoformat(), int.from_bytes()).
Резюме
- Метод экземпляра получает
selfи имеет полный доступ к состоянию объекта. @classmethodполучаетcls— сам класс — вместо экземпляра. Используйте его для фабричных методов и всего, что работает с состоянием на уровне класса. Всегда используйтеcls(...)внутри него, чтобы подклассы работали корректно.@staticmethodне получает ниself, ниcls. Используйте его для чистой утилитарной логики, которая принадлежит пространству имён класса, но не нуждается в данных объекта или класса.
Для вычисляемых атрибутов, выглядящих как обычный доступ к атрибутам, смотрите @property. Для полного механизма декораторов, лежащего в основе всех трёх, смотрите Декораторы Python.