Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Позднее статическое связывание Python: Погружаясь в классы

В объектно-ориентированном программировании встречаются различные типы методов и атрибутов, включая статические методы, методы класса и классовые атрибуты. Хотя эти элементы являются фундаментальными, они часто могут сбивать с толку, особенно, когда дело доходит до наследования и переопределения.

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

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

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

class A:
    def simple_func():
        print("Simple Function")

    @staticmethod
    def who():
        print('A')

    @staticmethod
    def test():
        A.who()

class B(A):
    @staticmethod
    def who():
        print('B')

obj = B()
obj.simple_func()  # Это вызовет ошибку, потому что Python пытается передать self в simple_func
B.test()  # 'A'

В этом примере статический метод test() вызывает A.who(), который находится внутри класса A. Поэтому A.who() указывает на сам A. Никакая иерархия наследования не может изменить это поведение.

Подобное поведение можно рассматривать как ограничение. Фактически оно игнорирует факт наследования. Но перед тем, как вдаваться в детали, нужно немного разобраться с тем, какую роль играют статические методы в классах.

class A:
    @staticmethod
    def who():
        print('A')

A.who()  # 'A'

В этом примере мы просто объявляем класс A со статическим методом who(), который выводит 'A'. Когда мы вызываем A.who(), он корректно выводит 'A' и показывает, что статические методы можно вызывать напрямую от класса. Но если нам нужно хранить информацию, специфичную для класса, мы можем использовать классовые атрибуты.

Классовые атрибуты и их использование

Данные в классовых атрибутах относятся к классу в целом. Они «описывают» его. В Python самый распространенный пример — это связь сущности с таблицей в базе данных, где она хранится:

class User:
    # Очень важно делать их неизменяемыми!
    _table = 'users'

Почему эту информацию нужно хранить в классовом атрибуте? Потому что она не принадлежит конкретному объекту.

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

Для сохранения сущности в базу данных обычно используются библиотеки ORM (Object-Relationship Mapping), которые знают, как сохранять сущности в базу и как извлечь их. Большинство из них построено на наследовании. Любая сущность должна наследоваться от специального базового класса, который содержит в себе общую логику для работы с базой данных:

class BaseEntity:
    # Код

class User(BaseEntity):
    pass

user = User()
# Сохранение в базу
# user.save()

Метод save() закомментирован, потому что в Python нет встроенной функции сохранения объектов в базу данных. Вместо этого мы используем библиотеки ORM, такие как SQLAlchemy или Django ORM.

Для сохранения пользователя в базу недостаточно знать, что сохранять. Еще нужно понимать, куда сохранять. И эта информация записана в классовом атрибуте:

class User(BaseEntity):
    _table = 'users'

В данном случае класс User, который наследуется от BaseEntity, содержит классовый атрибут _table на 'users'. Это означает, что когда мы сохраняем объект User, мы знаем, что его следует сохранить в таблице 'users'. Но что делать, если мы хотим обратиться к этой информации из базового класса? Рассмотрим этот нюанс подробнее.

Работа с наследованием и методы класса

Возникает вопрос, можно ли добраться до нее из базового класса BaseEntity?

Рассмотрим следующий код:

class BaseEntity:
    def get_table():
        return _table

class User(BaseEntity):
    _table = 'users'

print(User.get_table())  # NameError: name '_table' is not defined

В этом примере мы пытаемся получить доступ к атрибуту _table через метод get_table(). Проблема в том, что такой код не сработает. User.get_table() ссылается на BaseEntity, и вызов get_table() завершится с ошибкой, так как в этом классе нет классового атрибута _table.

Из этой ситуации есть выход: использовать @classmethod:

class BaseEntity:
    @classmethod
    def get_table(cls):
        return cls._table

class User(BaseEntity):
    _table = 'users'

print(User.get_table())  # 'users'

Изменив get_table() на метод класса с помощью декоратора @classmethod, мы можем вызвать метод для экземпляра класса и получить доступ к атрибуту _table текущего класса. Теперь вызов get_table() вернет значение классового атрибута _table, определенного в том классе, с объектом которого идет работа прямо сейчас.

Обратите внимание, что мы используем cls вместо self. Ключевое слово cls используется в Python для обозначения текущего класса в контексте метода класса.

В Python метод класса, определенный с использованием декоратора @classmethod, автоматически получает ссылку на класс, а не на экземпляр. Это позволяет нам обращаться к атрибутам класса внутри метода класса, даже если этот атрибут переопределен в подклассе.

Например, определим метод get_table в базовом классе BaseEntity и переопределим атрибут _table в классе User:

class BaseEntity:
    _table = 'base_entity'

    @classmethod
    def get_table(cls):
        return cls._table

class User(BaseEntity):
    _table = 'users'

print(User.get_table())  # 'users'

Можно заметить, что вызов get_table для класса User возвращает значение атрибута _table, определенное в классе User, а не в BaseEntity. Это показывает, что метод класса get_table работает с текущим классом (User), а не с классом, в котором он был определен (BaseEntity).

Выводы

Использование методов класса с помощью декоратора @classmethod позволяет нам работать с атрибутами текущего класса, а не базового класса. Это особенно полезно при работе с наследованием, когда подклассы могут переопределить некоторые атрибуты класса.

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


Дополнительные материалы

  1. Позднее связывание (Wiki)

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 6 300 ₽ в месяц
Разработка веб-приложений на Django
10 месяцев
с нуля
Старт 2 мая

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»