До сих пор мы говорили об атрибутах экземпляра класса, которые принадлежат конкретному объекту. Однако в Python представлены, в том числе, и атрибуты класса, которые принадлежат самому классу и являются общими для всех его экземпляров.

Чтобы понять разницу, давайте рассмотрим пример с классом Parcel, описывающим посылку в курьерской службе. У такого класса могут быть следующие атрибуты:

  • Атрибуты экземпляра класса – это уникальные характеристики конкретной посылки. Например, weight для веса или address для адреса доставки. У каждой посылки свой вес и свой адрес.
  • Атрибуты класса – это характеристики, которые одинаковы для всех посылок, обрабатываемых этой службой. Например, carrier для названия курьерской компании. Это свойство не меняется от посылки к посылке.

Атрибуты класса определяются внутри тела класса, но вне любого метода. К ним можно получить доступ как через сам класс, так и через любой его объект.

class Parcel:
    # Атрибут класса
    carrier = "МегаБыстраяДоставка"

    def __init__(self, weight: int | float, address: str):
        # Атрибуты экземпляра
        self.weight = weight
        self.address = address

Теперь давайте создадим два объекта этого класса и получим значение атрибута carrier с названием курьерской службы для обоих:

parcel1 = Parcel(5, "ул. Ленина, д. 10")
parcel2 = Parcel(2, "пр. Мира, д. 25")

print(f"Посылку доставит: {parcel1.carrier}")
# Вывод: Посылку доставит: МегаБыстраяДоставка
print(f"Посылку доставит: {parcel2.carrier}")
# Вывод: Посылку доставит: МегаБыстраяДоставка

Атрибут carrier является атрибутом класса, поэтому он общий для всех созданных объектов, и мы можем получить его через имя класса:

print(f"Курьерская служба: {Parcel.carrier}")
# Вывод: Курьерская служба: МегаБыстраяДоставка

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

Атрибуты класса могут быть полезны в следующих случаях:

  • Хранение констант – если у вас есть какое-то значение, которое никогда не меняется (например, максимальный вес посылки), его можно хранить как атрибут класса. Это делает код более читабельным, так как сразу видно, к какой логической группе относится константа.
  • Счётчики – атрибуты класса могут использоваться для подсчета количества созданных экземпляров. Например, каждый раз, когда создается новый объект-посылка, мы можем увеличивать значение атрибута класса, чтобы знать общее количество отправлений.
  • Значения по умолчанию – если атрибут должен иметь одинаковое значение для всех объектов, но может быть изменен в будущем, его также удобно хранить как атрибут класса.

Изменение атрибута класса

При работе с атрибутами класса следует понимать, как происходит их изменение. Если вы изменяете атрибут класса через сам класс, это изменение отразится на всех экземплярах:

Parcel.carrier = "Скорость Без Границ"
print(f"Теперь посылку доставит: {parcel1.carrier}")
# Вывод: Теперь посылку доставит: Скорость Без Границ

Однако, если вы изменяете атрибут класса через объект, вы не измените сам атрибут класса. Вместо этого вы создадите новый атрибут экземпляра с тем же именем, который скроет атрибут класса:

parcel1.carrier = "Блиц"
print(f"Посылку доставит: {parcel1.carrier}")
# Вывод: Посылку доставит: Блиц

При этом у других объектов и у самого класса Parcel значение атрибута класса carrier осталось без изменений:

print(f"Посылку доставит: {parcel2.carrier}")
# Вывод: Посылку доставит: Скорость Без Границ

Поэтому, если вы хотите изменить атрибут класса для всех экземпляров, делайте это через имя класса, а не через его объекты.

Методы класса

Как и атрибуты, которые могут быть как у объекта, так и класса в целом, так и методы могут относиться к классу в целом, а не к его экземплярам. Такие методы называются методами класса и предназначены для решения задач, которые логически связаны с самим классом, а не с конкретными объектами, поэтому они могут работать только с атрибутами класса.

Метод класса объявляется с помощью декоратора @classmethod, а в качестве первого аргумента принимают ссылку на сам класс, который принято обозначать cls, а не ссылку на объект self.

Давайте рассмотрим пример с классом Item для товаров, добавляемых в корзину пользователя в интернет-магазине. У этого класса есть атрибуты класса: счётчик total_items для подсчёта общего количества товаров в корзине и cart_size для ограничения размера корзины (максимального количества товаров в ней).

Для изменения значения cart_size удобно написать метод класса set_cart_size(), так как он позволит не просто изменить значение атрибута, а прописать дополнительную логику: максимальный размер корзины должен быть больше или равен текущему количеству товаров в ней:
 

class Item:
    total_items = 0  # Начальное количество товаров корзине
    cart_size = 100  # Максимальное количество товаров в корзине

    def __init__(self, name: str, price: int):
        # Проверяем, не превышен ли размер корзины
        if Item.total_items >= Item.cart_size:
            print("Достигнут лимит товаров в корзине")
            return

        self.name = name  # Название товара
        self.price = price  # Цена товара
        Item.total_items += 1
        
    @classmethod
    def set_cart_size(cls, new_cart_size: int) -> None:
        # Проверяем, что товаров в корзине меньше, чем новый размер корзи-ны
        if new_cart_size >= cls.total_items:
            cls.cart_size = new_cart_size
            print(f"Размер корзины изменён на {cls.cart_size}")
        else:
            print(f"Размер корзины должен быть больше {cls.total_items}")

Теперь мы можем изменить размер корзины через метод класса, который осуществляет дополнительную проверку:

item1 = Item("Веер", 100)
item2 = Item("Вентилятор", 1250)
print(f"Всего товаров в корзине: {Item.total_items}")

Item.set_cart_size(1)
# Вывод: Размер корзины должен быть больше 2
Item.set_cart_size(20)
# Вывод: Размер корзины изменён на 20

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

Если другой класс, например DigitalItem для электронных товаров, наследуется от Item, то атрибуты cls.total_items и cls.cart_size будут ссылаться на атрибуты дочернего, а не родительского класса. Это позволит каждому дочернему классу вести свой собственный, независимый счетчик, если это необходимо.

Статические методы

Помимо методов класса и его экземпляра, в Python представлен ещё один тип методов, называемых статическими методами. Они отличаются от двух предыдущих тем, что не имеют доступа ни к экземпляру класса (self), ни к самому классу (cls).

Статические методы объявляются с помощью декоратора @staticmethod и представляют собой обычные функции, которые помещены внутрь класса для организации кода. Они не зависят от состояния объекта или атрибутов класса, а их основным назначением является логическая группировка связанных функций.

Например, создадим класс Math для математических вычислений. В нём может быть функция factorial() для вычисления факториала числа и функция add() для сложения двух чисел. Эти функции не используют ни атрибуты класса, ни атрибуты экземпляра, но логически принадлежат к одной группе, поэтому мы помещаем их в этот класс как статические методы:

class Math:
    @staticmethod
    def factorial(n):
        if n == 0 or n == 1:
            return 1
        return n * Math.factorial(n - 1)

    @staticmethod
    def add(x, y):
        return x + y

Такие методы вызываются напрямую через имя класса как в самом классе, так и вне его:

print(f"5! = {Math.factorial(5)}")
# Вывод: 5! = 120
print(f"5 + 3 = {Math.add(5, 3)}")
# Вывод: 5 + 3 = 8

Статические методы полезны, когда функция логически принадлежит классу, но не зависит от его состояния, например, является вспомогательной функцией.

Примеры

Пример 1. Продажа билетов в кинотеатре

Класс TicketSale описывает продажу билетов кинотеатре. Базовая цена билета base_price и скидка discount являются общими для всех экземпляров класса, поэтому они объявлены как атрибуты класса. Метод класса apply_discount() изменяет скидку, которая используется при расчёте итоговой стоимости билета в методе экземпляра класса calculate_final_price():

class TicketSale:
    """Описывает продажу билетов, учитывая цену, скидки и ограничения."""
    
    # Атрибуты класса: общие настройки
    base_price = 100  # Цена билета по умолчанию
    discount = 0  # Скидка по умолчанию

    def __init__(self, movie_title: str):
        """Конструктор класса TickerSale.
        
        Параметры:
            movie_title: Название фильма.
        """
        self.movie_title = movie_title
        
    @classmethod
    def apply_discount(cls, discount: float) -> None:
        """Метод класса. Изменяет значение скидки, если она больше нуля.
        
        Параметры:
            discount: Новая скидка в процентах.
        """
        if discount > 0:
            cls.discount = discount
            print(f"Добавлена скидка {discount} %")
        else:
            print("Скидка должна быть больше нуля")

    def calculate_final_price(self) -> float:
        """Рассчитывает и возвращает итоговую цену."""
        return self.base_price * (100 - self.discount) / 100


# Используем метода класса для изменения общей скидки
TicketSale.apply_discount(5)  # Добавляем скидку 5%

# Создадим билет
ticket = TicketSale("Дракула (2025)")
final_price = ticket.calculate_final_price()
print(f"Итоговая цена билета на фильм {ticket.movie_title}: {final_price} руб.")

Вывод:

Добавлена скидка 5 %
Итоговая цена билета на фильм Дракула (2025): 95.0 руб.

Пример 2. Работа с оценками студентов

Класс StudentScore предоставляет функции для работы с оценками студентов. Статический метод calculate_average() рассчитывает средний балл, а метод core_to_grade() преобразует баллы в оценку. Эти методы можно использовать без создания экземпляра класса StudentScore:

class StudentScore:
    """Предоставляет функции для работы с оценками студентов."""

    @staticmethod
    def calculate_average(scores: list[int]) -> float:
        """Статический метод. Рассчитывает среднее арифметическое.
        
        Параметры:
            scores: Список с баллами или оценками студента.
            
        Возвращает:
            Среднее арифметическое.
        """
        if not scores:
            return 0.0
        return sum(scores) / len(scores)

    @staticmethod
    def score_to_grade(score: int) -> int:
        """Статический метод. Преобразует баллы в оценку.
        
        Параметры:
            score: Набранные баллы.
            
        Возвращает:
            Оценка по пятибалльной шкале.
        """
        if score >= 85: return 5
        if score >= 75: return 4
        if score >= 60: return 3
        return 2


# Перевод баллов в оценку
score = 85
grade = StudentScore.score_to_grade(score)
print(f"Балл {score} соответствует оценке {grade}")

# Расчёт среднего арифметического
student_grades = [5, 4, 4, 3, 5]
avg = StudentScore.calculate_average(student_grades)
print(f"Среднее арифметическое оценок {student_grades}: {avg}")

Вывод:

Балл 85 соответствует оценке 5
Среднее арифметическое оценок [5, 4, 4, 3, 5]: 4.2

Пример 3. Разработка игры

Класс GameSession описывает запуск игры с настраиваемой сложностью и количеством жизней. Допустимые уровни сложности хранятся в атрибуте класса difficulty_levels, а количество жизней по умолчанию в атрибуте класса default_lives.

Метод класса create_default_game() создаёт игру с настройками по умолчанию, в котором уровнем сложности является первый элемент списка difficulty_levels.

Метод экземпляра класса start() запускает игру с переданными настройками. При этом статический метод _validate_level() является вспомогательной функцией для создания экземпляра класса GameSession и проверяет, что переданный конструктору класса уровень совпадает с одним из значений в списке difficulty_levels.

class GameSession:
    """Описывает запуск игры с настройками сложности."""
    
    # Атрибуты класса: константы сложности и количества жизней
    difficulty_levels = ["Легко", "Тяжело", "Экстримально"]
    default_lives = 3

    def __init__(self, difficulty: str, lives: int=default_lives):
        """Конструктор класса GameSession.
      
        Параметры:
            difficulty: Сложность.
            lives: Количество жизней.
        """
        # Проверка через статический метод
        if not self._validate_level(difficulty): 
            raise ValueError(f"Неизвестный уровень сложности")
            
        self.difficulty = difficulty
        self.lives = lives
        
    @staticmethod
    def _validate_level(level: str) -> bool:
        """Статический метод. Проверяет, что уровень сложности допустим.
        
        Возвращает:
            True, если сложность является допустимой, иначе - False.
        """
        return level in GameSession.difficulty_levels

    @classmethod
    def create_default_game(cls) -> None:
        """Метод класса. Запускает игру с настройками по умолчанию."""
        print(f"Запуск игры с настройками по умолчанию... ")
        print(f"Сложность: {cls.difficulty_levels[0]}. Жизни: {cls.default_lives}")

    def start(self) -> None:
        """Запускает игру с переданными настройками."""
        print(f"Запуск игры с пользовательскими настройками... ")
        print(f"Сложность: {self.difficulty}. Жизни: {self.lives}")


# Создание игры по умолчанию
GameSession.create_default_game()

# Создание игры с собственными настройками
hard_game = GameSession("Тяжело", 2)
hard_game.start()

Вывод:

Запуск игры с настройками по умолчанию... 
Сложность: Легко, жизни: 3
Запуск игры с пользовательскими настройками... 
Сложность: Тяжело, жизни: 2

Итоги

  • Атрибуты класса – это атрибуты, которые принадлежат самому классу и являются общими для всех его экземпляров. Они определяются внутри тела класса, но вне любого метода.
  • Методы класса – это методы, которые относятся к классу в целом, а не к его экземплярам.
  • Методы класса создаются с помощью декоратора @classmethod. Первым аргументом они принимают ссылку не на объект, а на сам класс, который принято обозначать cls.
  • Статические методы – это методы, которые не имеют доступа ни к экземпляру класса, ни к самому классу. Они представляют собой обычные функции, помещённые внутри класса для организации кода.
  • Статические методы создаются с помощью декоратора @staticmethod.

Задания для самопроверки

1. Можете ли вы изменить атрибут класса через экземпляр этого класса, а не через сам класс?

Нет, так как это не изменяет значение атрибута класса, а создаёт новый атрибут экземпляра класса с этим же именем.

2. Почему статические методы не принимают ни ссылку на экземпляр класса self, ни ссылку на сам класс cls?

Статические методы представляют собой обычные функции, помещённые внутри класса.

3. Создайте класс для планеты Planet с атрибутом класса planet_count = 0 для счётчика созданных планет. В конструкторе __init__(), при каждом создании нового объекта, увеличивайте значение этого атрибута на 1. Создайте минимум 3 экземпляра класса Planet и выведите на экран итоговое значение атрибута planet_count.

class Planet:
    planet_count: int = 0  # Атрибут класса

    def __init__(self, name: str):
        self.name = name
        # При создании нового объекта увеличиваем общий счетчик класса
        Planet.planet_count += 1 


p1 = Planet("Земля")
p2 = Planet("Марс")
p3 = Planet("Юпитер")
print(f"Создано планет: {Planet.planet_count}")
# Вывод: 3

4. Создайте класс для команды Team с атрибутом класса sport = "Футбол". Создайте метод класса change_sport(cls, new_sport), который изменяет значение атрибута sport. Измените вид спорта на "Хоккей", вызвав Team.change_sport("Хоккей"), и выведите на экран значение атрибута sport.

class Team:
    sport = "Футбол"  # Атрибут класса

    # Метод класса: принимает cls (ссылку на класс Team)
    @classmethod
    def change_sport(cls: "Team", new_sport: str) -> None:
        # Используем cls для изменения атрибута класса
        cls.sport = new_sport 


Team.change_sport("Хоккей")
print(Team.sport)
# Вывод: Хоккей

5. Создайте класс для вспомогательных функций Utility. Создайте статический метод is_valid_email(email_address), который принимает строку email_address и возвращает True, если в ней есть символы @ и . (точка), иначе – False. Вызовите этот метод для строк "legolas777@example.ru" и "big_thranduil".

class Utility:
    # Статический метод не принимает ни self, ни cls
    @staticmethod
    def is_valid_email(email_address: str) -> bool:
        return "@" in email_address and "." in email_address


email1 = "legolas777@example.ru"
print(Utility.is_valid_email(email1))
# Вывод: True
email2 = "big_thranduil"
print(Utility.is_valid_email(email2))
# Вывод: False