До этого момента в курсе мы рассматривали методы как обычные функции, объявленные внутри класса. Такие методы всегда работают с конкретным объектом и получают доступ к его данным через параметр self. Это основной и наиболее часто используемый тип методов.
Однако в реальных задачах довольно быстро возникает необходимость в другом поведении:
- иногда логика относится не к конкретному объекту, а ко всему классу в целом;
- иногда нужна вспомогательная функция, связанная с классом по смыслу, но не зависящая ни от объекта, ни от класса.
Для таких случаев в Python предусмотрены два специальных типа методов:
- методы класса (
@classmethod); - статические методы (
@staticmethod).
Задача этого урока — не просто показать синтаксис, а сформировать понимание, в каких ситуациях каждый из этих типов действительно оправдан.
Рассмотрим знакомую конструкцию:
class Vector:
def __init__(self, x, y):
self.x = x # координата x конкретного объекта
self.y = y # координата y конкретного объекта
def get_coord(self):
return self.x, self.y # метод работает с данными объектаИспользование:
v = Vector(10, 20)
print(v.get_coord()) # (10, 20)Здесь важно зафиксировать:
- метод
get_coordпривязан к объекту; - параметр
self— это ссылка на конкретный экземпляр (v); - метод может работать как с атрибутами объекта (
self.x), так и с атрибутами класса.
Технически его можно вызвать и так:
Vector.get_coord(v)Но это неестественный способ — он показывает, что метод ожидает объект.
Представим задачу: нужно контролировать допустимые значения координат.
class Vector:
MIN_COORD = 0
MAX_COORD = 100Вопрос: где должна находиться логика проверки?
Наивное решение — сделать обычный метод:
def validate(self, value):
return self.MIN_COORD <= value <= self.MAX_COORDНо здесь возникает проблема:
- проверка не зависит от конкретного объекта;
- она зависит только от класса.
Значит, использование self — логически избыточно.
Метод класса — это метод, который работает с самим классом, а не с его экземплярами.
class Vector:
MIN_COORD = 0
MAX_COORD = 100
@classmethod
def validate(cls, value):
return cls.MIN_COORD <= value <= cls.MAX_COORDРазберем ключевые моменты:
@classmethod— специальный декоратор;- первый параметр —
cls(ссылка на класс); - метод имеет доступ только к атрибутам класса.
print(Vector.validate(10)) # TrueОбратите внимание:
- объект создавать не нужно;
- метод вызывается напрямую от класса.
Это принципиальное отличие от обычных методов.
class Vector:
MIN_COORD = 0
MAX_COORD = 100
def __init__(self, x, y):
if self.validate(x) and self.validate(y):
self.x = x
self.y = y
else:
self.x = 0
self.y = 0
@classmethod
def validate(cls, value):
return cls.MIN_COORD <= value <= cls.MAX_COORDЗдесь возможны два варианта вызова:
self.validate(x)
Vector.validate(x)Оба работают, потому что:
- объект знает, к какому классу он относится;
- Python автоматически передаст нужный класс в
cls.
Метод класса не имеет доступа к self, а значит:
@classmethod
def some_method(cls):
print(self.x) # Ошибка!Причина: нет ссылки на объект.
Один из самых распространенных сценариев использования @classmethod — создание объекта из альтернативного источника данных.
class User:
def __init__(self, username, email):
self.username = username
self.email = email
@classmethod
def from_string(cls, data_str):
username, email = data_str.split(";")
return cls(username, email) # создание объекта через классИспользование:
user = User.from_string("john;john@mail.com")Что здесь происходит:
- строка разбивается на части;
- вызывается
cls(...), то есть фактическиUser(...); - возвращается новый объект.
Теперь рассмотрим третий тип методов.
Иногда внутри класса нужна функция, которая:
- логически относится к классу;
- но не зависит ни от объекта, ни от класса.
В таком случае используется @staticmethod.
class Vector:
@staticmethod
def norm2(x, y):
return x * x + y * yЭто просто функция, помещенная внутрь класса, для вычисления квадратичной норму вектора.
print(Vector.norm2(3, 4)) # 25Или:
v = Vector()
print(v.norm2(3, 4))Оба варианта работают.
- нет
self; - нет
cls; - никаких скрытых параметров.
Это полностью независимая функция.
Рассмотрим класс для работы с заказами:
class Order:
TAX_RATE = 0.2
def __init__(self, amount):
self.amount = amount @classmethod
def set_tax_rate(cls, new_rate):
cls.TAX_RATE = new_rateЭтот метод меняет состояние класса:
Order.set_tax_rate(0.25) @staticmethod
def format_price(value):
return f"{value:.2f} EUR"Использование:
print(Order.format_price(10)) # 10.00 EURЗдесь нет зависимости ни от объекта, ни от класса.
Обычный метод:
объект → self → доступ к объекту и классу
Метод класса:
класс → cls → доступ только к классу
Статический метод:
нет привязки → только аргументы
@staticmethod
def create():
return Vector() # жесткая привязка к классуПроблема: если класс унаследуют, метод сломает полиморфизм.
Правильно:
@classmethod
def create(cls):
return cls()@classmethod
def method(cls):
print(self.x) # NameErrorЕсли функция:
- не связана с классом по смыслу,
- не использует его,
лучше вынести её за пределы класса.
Практическое правило:
- Если нужен доступ к данным объекта → обычный метод (
self) - Если нужен доступ к данным класса →
@classmethod - Если это вспомогательная функция →
@staticmethod
Но есть более точный критерий:
- создаешь объект или работаешь с классом → classmethod
- чистая функция без состояния → staticmethod
- Чем отличается
selfотcls? - Почему метод класса можно вызывать без создания объекта?
- В каком случае
@staticmethodлучше вынести за пределы класса? - Почему альтернативные конструкторы реализуют через
classmethod? - Можно ли вызвать
staticmethodчерез объект? - Почему
classmethodважен при наследовании? - Какие ограничения есть у
classmethod? - Какие ограничения есть у
staticmethod?
Создайте класс NumberUtils, в котором реализуйте статический метод:
is_positive(number)Метод должен:
- принимать число;
- возвращать
True, если число больше 0; - иначе
False.
Пример использования:
print(NumberUtils.is_positive(10)) # True
print(NumberUtils.is_positive(-5)) # False
print(NumberUtils.is_positive(0)) # FalseСоздайте класс UserCounter, который считает количество созданных пользователей.
Требования:
- атрибут класса
count = 0 - при создании каждого объекта счетчик увеличивается
- метод класса:
get_count()возвращает количество созданных объектов
Пример использования:
u1 = UserCounter()
u2 = UserCounter()
print(UserCounter.get_count()) # 2Есть класс Loader, который умеет разбирать строку с числами:
class Loader:
@staticmethod
def parse_format(string, factory):
seq = factory.build_sequence()
for sub in string.split(","):
item = factory.build_number(sub)
seq.append(item)
return seqВам нужно реализовать класс Factory, чтобы всё работало.
Требования к классу Factory:
- метод
build_sequence()→ возвращает пустой список - метод
build_number(string)→ преобразует строку в целое число
Оба метода должны быть статическими.
Пример использования:
res = Loader.parse_format("4, 5, -6", Factory)
print(res) # [4, 5, -6]Создайте класс Product.
Требования:
- атрибуты:
name,price - атрибут класса:
MIN_PRICE = 0
Метод класса:
check_price(price)- возвращает
True, если цена корректна (>= 0)
В конструкторе:
- если цена некорректна → установить
price = 0
Пример использования:
p1 = Product("Phone", 100)
p2 = Product("TV", -50)
print(p1.price) # 100
print(p2.price) # 0Реализуйте два класса: TextInput и PasswordInput.
Каждый объект создается так:
TextInput(name, size=10)
PasswordInput(name, size=10)Атрибуты:
name— название поляsize— размер поля
Добавить каждому классу метод get_html(), который должен возвращать:
-
Для
TextInput:<p class='login'>Имя: <input type='text' size=10 />
-
Для
PasswordInput:<p class='password'>Имя: <input type='text' size=10 />
Добавьте в каждый класс метод класса check_name(name), который должен:
- проверять длину имени: 3–50 символов
- проверять допустимые символы (буквы, цифры, пробел)
Если данные не корректные, метод должен генерировать ошибку с пояснительным текстом.
Для проверок в классах нужно создать аттрибут:
- CHARS - набор букв, цифр и пробела
Пример использования:
login = TextInput("Логин")
psw = PasswordInput("Пароль")
print(login.get_html())
print(psw.get_html())Необходимо реализовать класс CardCheck, который будет использоваться для создания объектов с данными банковской карты пользователя.
Каждый объект класса создается командой:
card = CardCheck(name, number)где:
name— имя владельца карты (строка);number— номер карты (строка).
В классе должен быть объявлен атрибут:
CHARS_FOR_NAMEОн содержит допустимые символы для имени:
- заглавные латинские буквы (
A-Z)
Инициализатор __init__:
В котором:
-
сохраняются значения
nameиnumberв атрибуты объекта; -
перед сохранением выполняется проверка:
- имени (через метод класса);
- номера карты (через статический метод);
Если хотя бы одна проверка не проходит должно генерироваться исключение.
Метод проверки номера карты:
check_card_number(number)Который должен проверять, что номер карты будет соответствовать формату XXXX-XXXX-XXXX-XXXX, где X — цифра.
Метод должен возвращать True, если формат корректный, и False в противном случае.
Метод проверки имени:
check_name(name)Имя должно:
- состоять из двух слов (имя и фамилия)
- быть записано заглавными символами
- содержать только допустимые символы из
CHARS_FOR_NAME
Метод возвращает:
True, если имя корректноFalse— иначе
Пример использования:
# корректные данные
card1 = CardCheck("IVAN IVANOV", "1234-5678-9012-0000")
print(card1.name) # IVAN IVANOV
print(card1.number) # 1234-5678-9012-0000
# ошибка в имени
try:
card2 = CardCheck("Ivan Ivanov", "1234-5678-9012-0000")
except ValueError as e:
print(e) # некорректные данные карты
# ошибка в номере карты
try:
card3 = CardCheck("PETR PETROV", "1234-5678-9012")
except ValueError as e:
print(e) # некорректные данные карты