Метаклассы в Python / Хабр

Привет, Хабр! У нас продолжается распродажа в честь черной пятницы. Там вы найдете много занимательных книг.

Возможен вопрос: а что такое метакласс? Если коротко, метакласс относится к  классу точно как класс к объекту.

Метаклассы – не самый популярный аспект языка Python; не сказать, что о них воспоминают в каждой беседе. Тем не менее, они используется в весьма многих статусных проектах: в частности, Django ORM[2], стандартная библиотека абстрактных базовых классов (ABC)[3] и реализации Protocol Buffers [4].

Это сложная фича, позволяющая программисту приспособить под задачу некоторые самые базовые механизмы языка. Именно по причине такой гибкости открываются и возможности для злоупотреблений – но нас это уже не удивляет. С большими возможностями приходит большая ответственность.

Данная тема обычно не затрагивается в различных руководствах и вводных материалах по языку, поскольку считается «продвинутой» — но и с ней надо с чего-то начинать. Я немного поискал в онлайне и в качестве наилучшего введения в тему нашел соответствующий вопрос на StackOverflow и ответы на него [1].

Поехали. Все примеры кода приведены на Python 3.6 – на момент написания статьи это новейшая версия.

Первый контакт

Мы уже кое-что успели обсудить, но пока еще не видели, что представляет собой метакласс. Скоро разберемся с этим, но пока следите за моим рассказом. Начнем с чего-нибудь простого: создадим объект.

>>> o = object()
>>> print(type(o))
<class 'object'>

Мы создали новый object и сохранили ссылку на него в переменной o.
Тип o – это object.

Мы также можем объявить и наш собственный класс:

>>> class A:
...     pass
...
>>> a = A()
>>> print(type(a))
<class '__main__.A'>

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

>>> isinstance(o, object)
True
>>> isinstance(a, A)
True
>>> isinstance(a, object)
True
>>> issubclass(A, object)
True

Выше заметна одна интересная вещь: объект 

a также относится к типу object. Ситуация такова, поскольку класс A является подклассом object (все классы, определяемые пользователем, наследуют от object).

Еще одна интересная вещь – во многих контекстах мы можем взаимозаменяемо применять переменные a и A. Для таких функций как print невелика разница, какую переменную мы ей выдадим, a или A – оба вызова «что-то» выведут на экран.

Давайте поподробнее рассмотрим класс B, который мы только что определили:

>>> class B: ... def __call__(self): ... return 5 ... >>> b = B() >>> print(b) <__main__.B object at 0x1032a5a58> >>> print(B) <class '__main__.B'> >>> b.value = 6 >>> print(b.value) 6 >>> B.value = 7 >>> print(B.value) 7 >>> print(b()) 5 >>> print(B()) <__main__.B object at 0x1032a58d0>

Как видим, b и B во многих отношениях действуют похоже. Можно даже сделать выражение с вызовом функции, в котором использовались бы обе переменные, просто возвращены в данном случае будут разные вещи: 

b возвращает 5, как и указано в определении класса, тогда как B создает новый экземпляр класса.

Это сходство – не случайность, а намеренно спроектированная черта языка. В  Python классы являются сущностями первой категории[5] (ведут себя как все нормальные объекты).

Более того, если классы – как объекты, то у них обязательно должен быть собственный тип:

>>> print(type(object))
<class 'type'>
>>> print(type(A))
<class 'type'>
>>> isinstance(object, type)
True
>>> isinstance(A, type)
True
>>> isinstance(A, object)
True
>>> issubclass(type, object)
True

Оказывается, что и object, и A относятся к классу type – type это «метакласс, задаваемый по умолчанию «. Все остальные метаклассы должны наследовать от него. Возможно, на данном этапе вас уже немного путает, что класс имеет имя type, но в то же время это и функция, возвращающая тип сообщаемого объекта (семантика у type  будет совершенно разной в зависимости от того, сколько аргументов вы ему сообщите – 1 или 3). В таком виде его сохраняют по историческим причинам.

Как object, так и A также являются экземплярами object – в конечном итоге, все они объекты. Каков же в таком случае тип type, могли бы вы спросить?

>>> print(type(type))
<class 'type'>
>>> isinstance(type, type)
True

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

Весь фокус, заключающийся в метаклассах: мы создали A, подкласс object, так, чтобы новый экземпляр a относился к типу

A и, следовательно, object. Таким же образом можно создать подкласс от type под названием Meta. Впоследствии мы можем использовать его как тип для новых классов; они будут экземплярами обоих типов: type и Meta.

Рассмотрим это на практике:

class Meta(type):
    def __init__(cls, name, bases, namespace):
        super(Meta, cls).__init__(name, bases, namespace)
        print("Creating new class: {}".format(cls))
        
    def __call__(cls):
        new_instance = super(Meta, cls).__call__()
        print("Class {} new instance: {}".format(cls, new_instance))
        return new_instance

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

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

  • Он переопределяет магический метод call , чтобы выводилось сообщение ·         всякий раз, когда пользователь применяет синтаксис вызова функций

    к экземпляру – пишет variable().

Оказывается, что в Python создание экземпляра класса имеет ту же форму, что и вызов функции. Если у нас есть функция f, то, чтобы вызвать ее, мы пишем f() . Если у нас есть класс A, то мы пишем A() для создания нового экземпляра. Соответственно, мы используем хук __call__.

Все-таки, метакласс сам по себе не так интересен. Интересное начинается, лишь когда мы создаем экземпляр метакласса. Давайте это и сделаем:

>>> class C(metaclass=Meta):
...     pass
...
Creating new class: <class '__main__.C'>
>>> c = C()
Class <class '__main__.C'> new instance: <__main__.C object at 0x10e99ae48>
>>> print(c)
<__main__.
C object at 0x10e99ae48>

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

Когда мы пишем class C(metaclass=Meta), мы создаем C, представляющий собой экземпляр 

Meta — вызывается Meta.init, и выводится сообщение. На следующем шаге мы вызываем C() для создания нового экземпляра класса C, и на этот раз выполняется Meta.__call__. На последнем шаге мы вывели на экран c, вызывая C.__str__, который, в свою очередь, разрешается в заданную по умолчанию реализацию, определенную в базовом классе object.

Сейчас можем посмотреть все типы наших переменных:

>>> print(type(C))
<class '__main__. Meta'>
>>> isinstance(C, Meta)
True
>>> isinstance(C, type)
True
>>> issubclass(Meta, type)
True
>>> print(type(c))
<class '__main__.C'>
>>> isinstance(c, C)
True
>>> isinstance(c, object)
True
>>> issubclass(C, object)
True

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

Полезный пример: синглтон

В этом разделе мы напишем совсем маленькую библиотеку, в которой будет малость метаклассов. Мы реализуем «эскиз» для паттерна проектирования синглтон [6]  – это класс, который может иметь всего один экземпляр.

Честно говоря, его можно было бы реализовать и без всякого использования метаклассов, просто переопределив метод __new__ в базовом классе, так, чтобы он вернул ранее запомненный экземпляр:

class SingletonBase:
    instance = None
    def __new__(cls, *args, **kwargs):
        if cls. instance is None:
            cls.instance = super().__new__(cls, *args, **kwargs)
        return cls.instance

Вот и все. Любой подкласс, наследующий от SingletonBase, теперь проявляет поведение синглтона.

Рассмотрим, каков он в действии:

>>> class A(SingletonBase): ... pass ... >>> class B(A): ... pass ... >>> print(A()) <__main__.A object at 0x10c8d8710> >>> print(A()) <__main__.A object at 0x10c8d8710> >>> print(B()) <__main__.A object at 0x10c8d8710>

Тот подход, который мы здесь используем, вроде бы работает – при каждой попытке создать экземпляр возвращается тот же самый объект. Но есть и такое поведение, которое может показаться нам неожиданным: при попытке создать экземпляр класса B мы получаем в ответ тот же самый экземпляр A, что и раньше.

Эту проблему можно решить, и не прибегая никоим образом к метаклассам, но решение с ними просто очевидное – так почему бы ими не воспользоваться?

У нас будет такой класс SingletonBaseMeta, чтобы каждый его подкласс при создании инициализировал поле instance со значением None.

Вот что получается:

class SingletonMeta(type):
    def __init__(cls, name, bases, namespace):
        super().__init__(name, bases, namespace)
        cls.instance = None
    def __call__(cls, *args, **kwargs):
        if cls.instance is None:
            cls.instance = super().__call__(*args, **kwargs)
        return cls.instance
class SingletonBaseMeta(metaclass=SingletonMeta):
    pass

Можем попробовать, а работает ли этот подход:

>>> class A(SingletonBaseMeta):
...     pass
...
>>> class B(A):
...     pass
...
>>> print(A())
<__main__.A object at 0x1101f6358>
>>> print(A())
<__main__.A object at 0x1101f6358>
>>> print(B())
<__main__.B object at 0x1101f6eb8>

Поздравляем, по-видимому наша библиотека-синглтон работает именно так, как и планировалось!

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

Полезный пример: упрощенное ORM

Как упоминалось выше, с паттерном синглтон можно красиво разобраться, слегка воспользовавшись метаклассами, но острой необходимости в них нет. Большинство реальных проектов, в которых метаклассы действительно используются – это те или иные вариации на тему ORM[7].

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

Вот как должен выглядеть интерфейс, который мы хотим получить (смоделирован на Django ORM/SQLAlchemy):

class User(ORMBase):
    """ Пользователь в нашей системе """
    id = IntField(initial_value=0, maximum_value=2**32)
    name = StringField(maximum_length=200)
    surname = StringField(maximum_length=200)
    height = IntField(maximum_value=300)
    year_born = IntField(maximum_value=2017)

Мы хотим иметь возможность определять классы и их поля вместе с типами. Для этого нам пригодилась бы возможность сериализовать наш класс в JSON:

>>> u = User()
>>> u.name = "Guido"
>>> u.surname = "van Rossum"
>>> print("User ID={}".format(u.id))
User ID=0
>>> print("User JSON={}". format(u.to_json()))
User JSON={"id": 0, "name": "Guido", "surname": "van Rossum", "height": null, "year_born": null}

И десериализовать его:

>>> w = User('{"id": 5, "name": "John", "surname": "Smith", "height": 185, "year_born": 1989}')
>>> print("User ID={}".format(w.id))
User ID=5
>>> print("User NAME={}".format(w.name))
User NAME=John

Для всего вышеприведенного нам не так уж и нужны метаклассы, так что давайте реализуем одну «изюминку» — добавим  валидацию.

>>> w.name = 5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "simple-orm.py", line 96, in __setattr__
    raise AttributeError('Invalid value "{}" for field "{}"'.format(value, key))
AttributeError: Invalid value "5" for field "name"
>>> w.middle_name = "Stephen"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "simple-orm.py", line 98, in __setattr__
    raise AttributeError('Unknown field "{}"'. format(key))
AttributeError: Unknown field "middle_name"
>>> w.year_born = 3000
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "simple-orm.py", line 96, in __setattr__
    raise AttributeError('Invalid value "{}" for field "{}"'.format(value, key))
AttributeError: Invalid value "3000" for field "year_born"

Напоминание о конструкторе типов

Прежде чем перейти к реализации ORM-библиотеки, я должен напомнить еще об одной вещи, конструкторе типов type. Я упоминал его лишь вскользь, но это важная тема, которую требуется развернуть.

Вспомните эпизод из предыдущего раздела, когда мы определяли метод __init__  для нашего первого метакласса:

class Meta(type):
    def __init__(cls, name, bases, namespace):

Откуда же взялись эти три аргумента namebases и namespace? Это параметры конструктора типов. Три этих значения полностью описывают класс, создаваемый в данный момент.

  • name – просто имя класса в формате строки

  • bases – кортеж базовых классов, может быть пустым

  • namespace – словарь всех полей, определенных внутри класса. Сюда идут все методы и переменные класса.

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

class A:
    X = 5
    def f(self):
        print("Class A {}".format(self))
def f(self):
    print("Class B {}".format(self))
B = type("B", (), {'X': 6, 'f': f})

В этом коде мы определили два почти идентичных класса, A и B.

У них отличаются значения, присвоенные переменной класса X, и выводятся на экран разные значения при вызове метода f. Но на этом все – фундаментальных отличий нет, и оба принципа определения классов эквивалентны. Фактически, интерпретатор Python преобразует первый из описанных здесь механизмов во второй.

>>> print(A)
<class '__main__.A'>
>>> print(B)
<class '__main__.B'>
>>> print(A.X)
5
>>> print(B.X)
6
>>> a = A()
>>> b = B()
>>> a.f()
Class A <__main__.A object at 0x1023432b0>
>>> b.f()
Class B <__main__.B object at 0x1023431d0>

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

Упрощенное ORM – грамотная программа

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

Далее я приведу реализацию в стиле грамотного программирования. Код из этого раздела можно загрузить в интерпретатор Python и запустить.

Мы будем использовать всего один пакет – для синтаксического разбора/сериализации JSON:

import json

Далее определим базовый класс для всех наших полей. Он устроен весьма просто, как и большинство других отдельных частей данной библиотеки. В нем есть реализация-заглушка для валидационной функции и пустое начальное значение.

class Field:
    """ Базовый класс для всех полей. Каждому полю должно быть присвоено начальное значение """
    def __init__(self, initial_value=None):
        self.initial_value = initial_value
    def validate(self, value):
        """ Проверить, является ли это значение допустимым для данного поля """
        return True

Для простоты я реализую всего два подкласса FieldIntField и StringField. При необходимости можно добавить и другие.

class StringField(Field):
    """ Строковое поле. Опционально в нем можно проверять длину строки """
    def __init__(self, initial_value=None, maximum_length=None):
        super().__init__(initial_value)
        self.maximum_length = maximum_length
    def validate(self, value):
        """ Проверить, является ли это значение допустимым для данного поля """
        if super(). validate(value):
            return (value is None) or (isinstance(value, str) and self._validate_length(value))
        else:
            return False
    def _validate_length(self, value):
        """ Проверить, имеет ли строка верную длину """
        return (self.maximum_length is None) or (len(value) <= self.maximum_length)
class IntField(Field):
    """ Целочисленное поле. Опционально можно проверять, является ли записанное в нем число целым"""
    def __init__(self, initial_value=None, maximum_value=None):
        super().__init__(initial_value)
        self.maximum_value = maximum_value
    def validate(self, value):
        """ Проверить, является ли это значение допустимым для данного поля """
        if super().validate(value):
            return (value is None) or (isinstance(value, int) and self._validate_value(value))
        else:
            return False
    def _validate_value(self, value):
        """ Проверить, относится ли целое число к желаемому дмапазону """
        return (self. maximum_value is None) or (value <= self.maximum_value)

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

В StringField мы хотим проверить, относится ли значение к правильному типу – str, и является ли длина строки меньшей или равной максимальному значению (если такое значение определено). В поле IntField мы проверяем, является ли значение целым числом, и является ли оно меньшим или равным, чем сообщенное максимальное значение.

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

Следующий фрагмент кода – это наш метакласс:

class ORMMeta(type):
    """ Метакласс для нашего собственного ORM """
    def __new__(self, name, bases, namespace):
        fields = {
            name: field
            for name, field in namespace. items()
            if isinstance(field, Field)
        }
        new_namespace = namespace.copy()
        # Удалить поля, относящиеся к переменным класса
        for name in fields.keys():
            del new_namespace[name]
        new_namespace['_fields'] = fields
        return super().__new__(self, name, bases, new_namespace)

Наш метакласс совсем не кажется сложным. В нем одна функция, и единственное его назначение – собрать все экземпляры Field в новую переменную класса, которая называется _fields. Все экземпляры полей также удаляются из словаря класса.

Единственная вещь, для которой нам нужен наш метакласс – чтобы он подключался в момент, когда создается наш класс, брал все определения полей и сохранял их все в одном месте.

Собственно, большая часть фактической работы выполняется в базовом классе нашей библиотеки:

class ORMBase(metaclass=ORMMeta):
    """ Пользовательский интерфейс для базового класса """
    def __init__(self, json_input=None):
        for name, field in self. _fields.items():
            setattr(self, name, field.initial_value)
        # Если предоставляется JSON, то мы разберем его
        if json_input is not None:
            json_value = json.loads(json_input)
            if not isinstance(json_value, dict):
                raise RuntimeError("Supplied JSON must be a dictionary")
            for key, value in json_value.items():
                setattr(self, key, value)
    def __setattr__(self, key, value):
        """ Установщик магического метода """
        if key in self._fields:
            if self._fields[key].validate(value):
                super().__setattr__(key, value)
            else:
                raise AttributeError('Invalid value "{}" for field "{}"'.format(value, key))
        else:
            raise AttributeError('Unknown field "{}"'.format(key))
    def to_json(self):
        """ Преобразовать заданный объект в JSON """
        new_dictionary = {}
        for name in self._fields.keys():
            new_dictionary[name] = getattr(self, name)
        return json. dumps(new_dictionary)

У класса ORMBase три метода, и у каждого из них своя конкретная задача:

  • __init__ — первым делом, установить все поля в начальные значения. Затем, если в качестве параметра передается документ в формате JSON, разобрать его и присвоить значения, полученные в процессе считывания, полям нашей модели.

  • __setattr__ — Это магический метод, вызываемый всякий раз, когда кто-нибудь пытается присвоить значение атрибуту класса. Когда кто-нибудь записывает object.attribute = value, вызывается метод object.__setattr__("attribute", value). Переопределив этот метод, мы можем изменить поведение, заданное по умолчанию, в данном случае – при помощи инъекции валидационного кода.

  • to_json – простейший из всех методов в классе. Просто принимает все значения полей и сериализует их в документ JSON.

Вот и вся реализация – наша библиотека готова. Можете сами убедиться, что она работает как положено, и менять ее, если считаете, что она должна работать иначе.

>>> User('{"name": 5}')
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2881, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-1-76a1a93378fc>", line 1, in <module>
    User('{"name": 5}')
  File "/Users/jrx/repos/metaclass-playground/simple-orm.py", line 86, in __init__
    setattr(self, key, value)
  File "/Users/jrx/repos/metaclass-playground/simple-orm.py", line 94, in __setattr__
    raise AttributeError('Invalid value "{}" for field "{}"'.format(value, key))
AttributeError: Invalid value "5" for field "name"

Заключительные замечания

Весь код к этому посту можно скачать в репозитории на GitHub [8].

Надеюсь, эта статья вам понравилась и подсказала вам какие-то идеи. Метаклассы могут казаться немного непонятными и не всегда полезными. Однако, они определенно позволяют собирать элегантные библиотеки и интерфейсы, если уметь метаклассами пользоваться.

Подробнее о том, как метаклассы используются в реальной жизни, можно почитать в статье [9].

Источники

  • [1] http://stackoverflow.com/questions/100003/what-is-a-metaclass-in-python

  • [2] https://docs.djangoproject.com/en/1.10/topics/db/

  • [3] https://docs.python.org/3/library/abc.html#abc.ABCMeta

  • [4] https://developers.google.com/protocol-buffers/docs/pythontutorial

  • [5] https://ru.wikipedia.org/wiki/Объект_первого_класса

  • [6] https://ru.wikipedia.org/wiki/Одиночка_(шаблон_проектирования)

  • [7] https://ru.wikipedia.org/wiki/ORM

  • [8] https://github.com/MillionIntegrals/metaclass-playground

  • [9] http://eli.thegreenplace.net/2011/08/14/python-metaclasses-by-example

Метаклассы в Python

В этом руководстве мы расскажем, что такое метаклассы в Python, зачем они нужны и как их создавать.

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

От редакции Pythonist. О классах можно почитать в статье «Классы в Python».

В Python все является объектом

class TestClass():
    pass
my_test_class = TestClass()
print(my_test_class)
# Oytput:
# <__main__.TestClass object at 0x7f6fcc6bf908>

Классы в Python можно создавать динамически

Функция type() в Python позволяет нам определить тип объекта. Давайте проверим тип объекта, который мы только что создали:

type(TestClass)
# type
type(type)
# type

Подождите, но что это было? Мы ожидали, что тип объекта, который мы создали выше, будет классом, однако это не так. Зафиксируйте, пожалуйста, эту мысль. Мы вернемся к ней чуть позже.

Кроме того, можно заметить, что type имеет тип type. Всё потому, что это экземпляр type.

Функция type, помимо определения типа объектов, имеет еще один волшебный вариант применения. С ее помощью можно динамически создавать классы. Давайте рассмотрим, как это делается.

Показанный ниже класс Pythonist будет создан с использованием type:

class Pythonist():
    pass
PythonistClass = type('Pythonist', (), {})
print(PythonistClass)
print(Pythonist())
# Output:
# <class '__main__.DataCamp'>
# <__main__.DataCamp object at 0x7f6fcc66e358>

Здесь Pythonist — это имя класса, а Pythonist Class — это переменная, содержащая ссылку на класс.

При использовании type мы можем передавать атрибуты класса с помощью словаря:

PythonClass = type('PythonClass', (), {'start_date': 'August 2018', 'instructor': 'John Doe'} )
print(PythonClass.start_date, PythonClass.instructor)
print(PythonClass)
# Output:
# August 2018 John Doe
# <class '__main__.PythonClass'>
[python_ad_block]

Если мы хотим, чтобы наш PythonClass наследовался от класса Pythonist, мы передаем его нашему второму аргументу при определении класса с использованием type:

PythonClass = type('PythonClass', (Pythonist,), {'start_date': 'August 2018', 'instructor': 'John Doe'} )
print(PythonClass)
# Output:
# <class '__main__. PythonClass'>

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

Всякий раз, когда мы вызываем class для создания класса, есть метакласс, который колдует над созданием класса за кулисами. Мы уже видели, как это делает type. Это похоже на str, который создает строки, и int, который создает целые числа. В Python атрибут __class__ позволяет нам проверить тип текущего экземпляра. Давайте создадим строку и проверим ее тип.

article = 'metaclasses'
article.__class__
# str

Мы также можем проверить тип, используя type(article) следующим образом:

type(article)
# str

Проверив тип самого str, мы узнаем, что это тоже type:

type(str)
# type

Если мы проверим тип float, int, list, tuple и dict с помощью type(), мы получим аналогичный результат. Это потому, что все эти объекты имеют тип type:

print(type(list),type(float), type(dict), type(tuple))
# <class 'type'> <class 'type'> <class 'type'> <class 'type'>

Итак, мы уже видели, как type создает классы. Следовательно, когда мы проверяем __class__ из __class__, он должен возвращать type:

article.__class__.__class__
# type

Создание пользовательских метаклассов

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

class MyMeta(type):
    pass
class MyClass(metaclass=MyMeta):
    pass
class MySubclass(MyClass):
    pass

Ниже мы видим, что тип класса MyMetatype, а тип MyClass и MySubClassMyMeta.

print(type(MyMeta))
print(type(MyClass))
print(type(MySubclass))
# Output:
# <class 'type'>
# <class '__main__.MyMeta'>
# <class '__main__.MyMeta'>

При определении класса и отсутствии метакласса по умолчанию будет использоваться метакласс type. Если задан метакласс, не являющийся экземпляром type(), то он используется непосредственно как метакласс.

__new__ и __init__

Метаклассы также могут быть определены одним из двух способов, показанных ниже. Давайте рассмотрим разницу между ними:

class MetaOne(type):
    def __new__(cls, name, bases, dict):
        pass
class MetaTwo(type):
    def __init__(self, name, bases, dict):
        pass

__new__ используется, когда нужно определить кортежи dict или base перед созданием класса. Возвращаемое значение __new__ обычно является экземпляром cls. __new__ позволяет подклассам неизменяемых типов настраивать создание экземпляров. Его можно переопределить в пользовательских метаклассах, чтобы настроить создание класса.

__init__ обычно вызывается после создания объекта для его инициализации.

Метод метакласса __call__

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

Метод метакласса __prepare__

Согласно документам модели данных Python:

«После определения соответствующего метакласса подготавливается пространство имен класса.

Если у метакласса есть атрибут __prepare__, он вызывается как namespace = metaclass.__prepare__(name, bases, **kwds) (где дополнительные аргументы ключевого слова, если они есть, берутся из определения класса).

Если метакласс не имеет __prepare__attribute, то пространство имен класса инициализируется как пустое упорядоченное отображение».

Проектирование Singleton с использованием метакласса

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

class SingletonMeta(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(SingletonMeta,cls).__call__(*args, **kwargs)
        return cls._instances[cls]
class SingletonClass(metaclass=SingletonMeta):
    pass

Заключение

В этой статье мы узнали о том, что такое метаклассы в Python и как мы можем реализовать их в нашем коде. Метаклассы могут применяться, среди прочего, для ведения журнала, регистрации классов во время создания и профилирования. Они кажутся довольно абстрактными понятиями, и у вас может возникнуть сомнение относительно целесообразности их использования. Лучше всего об этом сказал Тим Питерс, питонист с большим опытом:

«Метаклассы — это более глубокая магия, о которой 99% пользователей не должны беспокоиться. Если вы задаетесь вопросом, нужны ли они вам – значит, нет, вы в них не нуждаетесь (люди, которые действительно нуждаются в метаклассах, точно знают, что они им нужны, и не нуждаются в объяснении зачем)».

Надеемся, данная статья была вам полезна! Успехов в написании кода!

Перевод статьи «Introduction to Python Metaclasses».

Метаклассы Python – Real Python