Введение

Python, с его изящным и лаконичным синтаксисом, славится своей гибкостью и способностью приспосабливаться к различным задачам программирования. Одним из ключевых элементов его синтаксиса является объектно-ориентированное программирование (ООП), позволяющее структурировать код, делая его более модульным и легко поддерживаемым. В этой статье я постараюсь максимально просто объяснить, что такое метаклассы, и где они используются.

Что такое метаклассы?

Давайте посмотрим на классическую модель объектов в Python.

True, {22: "11"}, [] - это всё объекты и образованны они от соответствующих классов:

True

<class 'bool'>

{22: "11"}

<class 'dict'>

[]

<class 'list'>

Проверить это можно применив функцию type():

print(type(True))

На версии Python 3.12.2 в консоль будет выведен следующий ответ:

<class 'bool'>

Но класс bool, так же как и dict и list, тоже является объектом, и образован от метакласса.

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

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

Самым простым метаклассом можно считать type.

Мы только что использовали его для проверки типов объекта. Когда type получает на вход один параметр, то он возвращает тип объекта, переданного в качестве параметра, в данном случае он не работает как метакласс.

Но, если в type попадает три параметра, то он уже работает как метакласс и создаёт класс на основе переданных параметров.

В качестве параметров должны передаваться имя класса, родители (классы, от которых передаётся наследованное) и словарь атрибутов.

Пример создания классов с помощью type

Давайте попробуем создать простой класс BankAccount с помощью type.

Первым аргументом у нас идёт название класса, вторым - от чего наследуется наш класс и третьим - словарь атрибутов.

B = type("BankAccount", (), {"BALANCE": 0})

У нас получился класс BankAccount с атрибутам balance.

Мы так же можем создать его и стандартным способом.

class BankAccount:
  BALANCE = 0

Думаю, что теперь мы готовы создать свой первый и последний метакласс!

Пример создания своего метакласса

class MyMeta(type):
    def __new__(cls, name, base, attrs):
        attrs.update({"new_attrs": 100})
        return type.__new__(cls, name, base, attrs)
    

class MyClass(metaclass=MyMeta):
    pass 


obj = MyClass()

print(obj.new_attrs)

Для того, чтобы создать создать свой метакласс, нам нужно создать класс, который наследован от класса type, для простоты назовём его MyMeta.

В нём нам нужно в конструкции new прописать следующие параметры (cls - ссылка на созданный класс, name - имя класса, base - список базовых классов, attrs - словарь атрибутов класса). Можно и в init, но для более тонкой настройки лучше использовать new.

В том же методе, мы обновляем словарь атрибутов, добавляя туда новый атрибут - new_attrs и записываем туда значения 100.

После создаём уже наш класс MyClass. В скобках нам нужно указать специальный параметр metaclass и в него должны передать ссылку на наш метакласс. Он ничего не возвращает.

И получается, что у нас будет создаваться класс, по образу и подобию нашего метакласса.

При выводе в консоли у нас будет выводиться:

100

Применение метаклассов

  • ORM (Object-Relational Mapping):

    Метаклассы часто используются в ORM-библиотеках, таких как SQLAlchemy, Django ORM и Peewee, для автоматического создания классов, представляющих таблицы в базе данных. Метаклассы позволяют ORM-библиотекам генерировать классы на основе описания схемы базы данных, что делает взаимодействие с базой данных более удобным и эффективным.

  • Метапрограммирование:

    Метаклассы часто используются для создания собственных DSL (Domain Specific Language) или для расширения языка Python с помощью создания новых конструкций языка. Это может быть полезно для создания более выразительных или декларативных интерфейсов программирования, а также для автоматизации общих шаблонов программирования.

  • Фреймворки и библиотеки:

    Многие фреймворки и библиотеки Python используют метаклассы для реализации различных аспектов своего функционирования. Например, фреймворк для тестирования, такой как unittest, может использовать метаклассы для автоматического обнаружения и запуска тестовых методов. Другие библиотеки могут использовать метаклассы для внедрения механизмов кеширования, логирования или обработки ошибок в классах.

Заключение

Метаклассы в Python — это мощный инструмент, который может быть использован для создания более гибких, масштабируемых и элегантных программных решений.

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

Комментарии (10)


  1. LetoRoma
    09.04.2024 17:17

    Классное объяснение, спасибо!


    1. dyadyaSerezha
      09.04.2024 17:17
      +4

      Ничего классного. Не рассказано главное - зачем?


  1. milssky
    09.04.2024 17:17
    +6

    Это все здорово. Но это можно найти в любой книжке более-менее приличной по питону. А практическая польза? Вы можете привести хоть один кейс практического использования метаклассов?


    1. dyadyaSerezha
      09.04.2024 17:17
      +2

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


    1. m4deme1ns4ne Автор
      09.04.2024 17:17

      добавил


  1. danilovmy
    09.04.2024 17:17
    +7

    "Ничего ты не знаешь, Джон Сноу"

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

    Если рассматривать статью, как попытку разобраться в метаклассах. То мимо. Не разобрался.

    Просто и понятно написано тут https://docs.python.org/3/reference/datamodel.html#metaclasses, но обычно это никто не читает.

    1. Объекты класса как-то себя "проявляют" через методы и содержат атрибуты. Получить класс объекта можно командой type(obj).

    2. Классы задают базовое поведение и задают шаблоны атрибутов для объектов класса. Любой класс это синглтон-объект, который как-то себя "проявляет" через методы и содержит атрибуты. Получить метакласс класса можно командой type(cls). Именно потому, что класс это тоже объект.

    3. Метаклассы задают базовое поведение и задают шаблоны атрибутов для Классов, как объектов. Любой метакласс это синглтон-объект, который как-то себя "проявляет" через методы и содержит атрибуты. Получить Метакласс метакласса можно командой type(metacls). Именно потому, что метакласс - это тоже объект.

    4. Можно продолжить. Создав мета мета класс, который будет задавать базовое поведение и задают шаблоны атрибутов для Мета классов, как объектов.

    Фактически у нас есть только одно взаимоотношение. Класс --> Объект.

    В списке выше это 2 --> 1, 3 --> 2, 4 --> 3, ...

    Метакласс MyMetaв примере статьи, например, управляет поведением инициализации объекта классом MyClass.

    Первый пример применения метакласса: фактически "Мета классом" мы "донастраиваем" работу некоторых методов класса. В Django метакласc модели превращает атрибуты-поля Класса Модели в атрибуты-дескрипторы объекта на моменте инициализации классом объекта. Если бы этого не происходило, то в объекте мы бы имели атрибут-поле (models.RelatedField). Но в объекте мы имеем models.DeferredDescriptor.

    Метакласс имеет не так много методов, поскольку не так уж и много нам надо делать на этом уровне. Базовый метакласс, это который type, например, при создании любого класса добавляет ему набор стандартных дандер-методов, типа __getattr__, и т.п. В твоем примере ты создал class MyClass() ничего ему не объявил, а у класса есть метод __str__. Спасибо метаклассу type который по умолчанию является конструктором класса и делает всю грязную работу за нас.

    Это может пригодиться, если хочется поменять некоторые атодобавляемые методы у ВСЕХ классов этого метакласса. Опять Django. Добавляемый метод __eq__ у класса модели переопределен. Теперь в классе это поведение поменяно. И объекты этого класса в момент сравнения начинают меряться obj.pk ? obj2.pk вместо стандартного поведения id(obj) ? id(obj2)

    Вот второй пример применения метакласса: вместо ручного прописывания методов у каждого дочернего класса, делаем это через метакласс. Кстати, я предпочитаю использовать для этого миксины. В dataclass - это же делают через декоратор.

    Поскольку Метакласс позволяет переопределять дандер и не только методы на момент объявления класса (это же возможно сделать и миксином и наследованием и декоратором) то можно переопределить "__div__" и всю остальную математику и получить класс Path из Pathlib который реализует DSL, в данном случае это привычный file system syntax. Но важно, что этот класс на самом деле - это несколько классов PosixPath, WindowsPath etc. Без метакласса надо было бы переопределять математику в каждом из них ручками. Количество кода увеличилось бы. А через метакласс удалось соблюсти DRY да и KISS тоже где-то рядом.

    По сути метакласс - это всегда синтаксический сахар сильно уменьшающий количество кода, который надо писать. Тут нет магии или мистики. Это просто. Но не для всех.


    1. ValeryIvanov
      09.04.2024 17:17

      Классы задают базовое поведение и задают шаблоны атрибутов для объектов класса. Любой класс это синглтон-объект, который как-то себя "проявляет" через методы и содержит атрибуты. Получить метакласс класса можно командой type(cls). Именно потому, что класс это тоже объект.

      Откуда это взялось? Или я_точно_синглетон = dict() тоже синглетон? Класс это экземпляр класса type или одного из его наследников, не более.

      Метакласс имеет не так много методов, поскольку не так уж и много нам надо делать на этом уровне. Базовый метакласс, это который type, например, при создании любого класса добавляет ему набор стандартных дандер-методов, типа getattr, и т.п. В твоем примере ты создал class MyClass() ничего ему не объявил, а у класса есть метод str. Спасибо метаклассу type который по умолчанию является конструктором класса и делает всю грязную работу за нас.

      type ничего не добавляет своим наследникам. Единственное что type.new делает, так это создаёт новый экземпляр переданного класса и заполняет его словарь, используя переданные параметры. Ну и валидацию выполняет, чтобы от некоторых типов не наследовались, но это не суть важно.

      Вот второй пример применения метакласса: вместо ручного прописывания методов у каждого дочернего класса, делаем это через метакласс. Кстати, я предпочитаю использовать для этого миксины. В dataclass - это же делают через декоратор.

      Это плохой пример использования метаклассов. Как вы уже и написали, миксины для этого подойдут гораздо лучше. Конечно, не стоит специально избегать создание метаклассов, но и городить их почём зря, это тоже не лучшая практика.
      Действительно хорошим примером метакласса может послужить Enum из стандартной библиотеки.

      Поскольку Метакласс позволяет переопределять дандер и не только методы на момент объявления класса (это же возможно сделать и миксином и наследованием и декоратором) то можно переопределить "div" и всю остальную математику и получить класс Path из Pathlib который реализует DSL, в данном случае это привычный file system syntax. Но важно, что этот класс на самом деле - это несколько классов PosixPath, WindowsPath etc. Без метакласса надо было бы переопределять математику в каждом из них ручками. Количество кода увеличилось бы. А через метакласс удалось соблюсти DRY да и KISS тоже где-то рядом.

      Насколько я знаю, pathlib никаким образом не использует метаклассы. Поправьте, если я ошибаюсь. И даже если использует, то зачем в теории может понадобиться переопределять div на каком-нибудь PathType, а не на самом Path?


      1. olsowolso
        09.04.2024 17:17

        [del]

        *ушел курить маны


      1. danilovmy
        09.04.2024 17:17

        Откуда это взялось? Или я_точно_синглетон = dict() тоже синглетон? Класс это экземпляр класса type или одного из его наследников, не более.

        В принципе любой объект питона уникален, так что я_точно_синглетон_dict() можно тоже назвать Cинглетоном. Единый объект класса доступен каждому объекту через type(obj) или через дандер и часто используется как хранитель единого состояния для порожденных объектов, что, собственно, является одной из причин создания Singleton. Потому я субъективно отношу классы к Singleton-объектам.

        Класс это экземпляр класса type или одного из его наследников, не более.

        Нет. Класс не наследует свойства класса type, а является производной работы type. В качестве доказательства: по умолчанию у класса нет методов что есть у type, хотя их можно добавить.

        type ничего не добавляет своим наследникам. Единственное что type.new делает, так это создаёт новый экземпляр переданного класса и заполняет его словарь, используя переданные параметры. Ну и валидацию выполняет, чтобы от некоторых типов не наследовались, но это не суть важно.

        Еще раз. Класс не является наследником type, он является результатом работы функции конструктора класса. На примере:

         MyClass = type('Name', (__bases__,), {'mymethod': lambda *args, **kwargs: 'hi'})

        В примере вызывается функция конструктор, создающая в локальной области видимости класс с именем "Name". Unbounded функции "mymethod" нет в методах базовых классов (если они переданы), и эта функция будет добавлена конструктором type к классу, да, грубо говоря добавлена в MyClass.__dict__, но если попробовать сделать это вручную получим'mappingproxy' object does not support item assignment. Так что это не напрямую "заполнение словаря".

        Насколько я знаю, pathlib никаким образом не использует метаклассы. Поправьте, если я ошибаюсь.

        Поправляю, Path наследован class PurePath(object), который, в свою очередь, ведет себя как metaclass, переопределяя __new__.

        И даже если использует, то зачем в теории может понадобиться переопределять div на каком-нибудь PathType, а не на самом Path?

        Переопределенный __div__ позволяет использовать синтаксис Path('root') / Path('folder'). В этом примере на команду __div__ происходит Path('root').parts + Path('folder').parts. Но Path в реальности это просто маска двух классов:cls = PureWindowsPath if os.name == 'nt' else PurePosixPath. Потому переопределив что-то у класса Path ничего не получим, надо переопределять у PureWindowsPath и PurePosixPath.

        Но спасибо @ValeryIvanov кое что я узнал про конструктор в python3, от отличается от Python2, а я это упустил.


        1. ValeryIvanov
          09.04.2024 17:17
          +1

          Нет. Класс не наследует свойства класса type, а является производной работы type. В качестве доказательства: по умолчанию у класса нет методов что есть у type, хотя их можно добавить.

          Еще раз. Класс не является наследником type, он является результатом работы функции конструктора класса.

          Здесь я запутался в понятиях метакласс и класс. Но я имел ввиду то, что наследники класса type(то есть, метаклассы), это самые обычные классы, type никакой магии не делает.

          Поправляю, Path наследован class PurePath(object), который, в свою очередь, ведет себя как metaclass, переопределяя new.

          Переопределенный div позволяет использовать синтаксис Path('root') / Path('folder'). В этом примере на команду div происходит Path('root').parts + Path('folder').parts. Но Path в реальности это просто маска двух классов:cls = PureWindowsPath if os.name == 'nt' else PurePosixPath. Потому переопределив что-то у класса Path ничего не получим, надо переопределять у PureWindowsPath и PurePosixPath.

          Формально, Path можно назвать метаклассом. Но чаще всего под метаклассами всё же имеют ввиду наследников класса type.
          Но да, если рассматривать Path в таком ключе, то вы правы.