Skip to content

Latest commit

 

History

History
647 lines (423 loc) · 17.6 KB

File metadata and controls

647 lines (423 loc) · 17.6 KB

Урок 6. Методы уровня класса и статические методы: когда, зачем и как применять

До этого момента в курсе мы рассматривали методы как обычные функции, объявленные внутри класса. Такие методы всегда работают с конкретным объектом и получают доступ к его данным через параметр 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 — логически избыточно.


Метод класса (@classmethod)

Метод класса — это метод, который работает с самим классом, а не с его экземплярами.

Определение

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")

Что здесь происходит:

  1. строка разбивается на части;
  2. вызывается cls(...), то есть фактически User(...);
  3. возвращается новый объект.

Статические методы (@staticmethod)

Теперь рассмотрим третий тип методов.

Иногда внутри класса нужна функция, которая:

  • логически относится к классу;
  • но не зависит ни от объекта, ни от класса.

В таком случае используется @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 → доступ только к классу

Статический метод:
    нет привязки → только аргументы

Возможные ошибки

Ошибка 1. Использование staticmethod вместо classmethod

@staticmethod
def create():
    return Vector()  # жесткая привязка к классу

Проблема: если класс унаследуют, метод сломает полиморфизм.

Правильно:

@classmethod
def create(cls):
    return cls()

Ошибка 2. Попытка работать с self в classmethod

@classmethod
def method(cls):
    print(self.x)  # NameError

Ошибка 3. Избыточное использование staticmethod

Если функция:

  • не связана с классом по смыслу,
  • не использует его,

лучше вынести её за пределы класса.


Как выбирать тип метода

Практическое правило:

  1. Если нужен доступ к данным объекта → обычный метод (self)
  2. Если нужен доступ к данным класса → @classmethod
  3. Если это вспомогательная функция → @staticmethod

Но есть более точный критерий:

  • создаешь объект или работаешь с классом → classmethod
  • чистая функция без состояния → staticmethod

Вопросы

  1. Чем отличается self от cls?
  2. Почему метод класса можно вызывать без создания объекта?
  3. В каком случае @staticmethod лучше вынести за пределы класса?
  4. Почему альтернативные конструкторы реализуют через classmethod?
  5. Можно ли вызвать staticmethod через объект?
  6. Почему classmethod важен при наследовании?
  7. Какие ограничения есть у classmethod?
  8. Какие ограничения есть у staticmethod?

Задачи

Задача 1

Создайте класс 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

Задача 2

Создайте класс UserCounter, который считает количество созданных пользователей.

Требования:

  • атрибут класса count = 0
  • при создании каждого объекта счетчик увеличивается
  • метод класса:
get_count()

возвращает количество созданных объектов

Пример использования:

u1 = UserCounter()
u2 = UserCounter()

print(UserCounter.get_count())  # 2

Задача 3

Есть класс 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]

Задача 4

Создайте класс 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

Задача 5

Реализуйте два класса: 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())

Задача 6

Необходимо реализовать класс 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)  # некорректные данные карты

Предыдущий урок | Следующий урок