Python Metaprogramming - Singulared

Python Metaprogramming

Думаю, многие слышали, что в языке Python всё является объектом. Настало время разобраться в этом чуть глубже.

Для начала давайте посмотрим действительно ли в Python всё является объектом и самое важное, объектами каких классов.

>>> type(1)
<class 'int'>

>>> type('str')
<class 'str'>

>>> class A: pass
>>> a = A()

>>> type(a)
<class '__main__.A'>

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

>>> def f(): pass
>>> f.__class__
<class 'function'>

>>> class A: pass
>>> A.__class__
<class 'type'>

Другими словами, функция f является экземпляром function. А класс А экземпляром type (или проще сказать типом).

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

Однако, это не единственное её применение, type кроме того может создавать объекты классов. Что собственно, и является определением метакласса:

Метакласс — это объект, умеющий управлять созданием классов.

Пока не совсем понятно. Но, давайте попробуем разобраться дальше.

Если класс является экземпляром type, то, соответственно, мы можем вручную повторить процесс создания класса. Допустим у нас есть базовый класс A и унаследованный от него класс B:

class A:
    a = 1

class B(A):
    a = 2

Попробуем проинстранциировать класс C, унаследованный от A "руками".

>>> C = type('C', (A,), {})
>>> c = C()
>>> c.a
1

Работает. Мы видим что метакласс type принимает на вход 3 параметра:

Попробуем создать что-то посложнее:

>>> D = type('D', (A,), {'a':3, 'b': lambda self: print('Я метод b')})
>>> d = D()
>>> d.b()
Я метод b

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

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

Непосредственно последний этап и предоставляет нам эту возможность. Но, как же мы можем заменить метакласс, используемый по умолчанию на свой. А очень просто! Указав интерпретатору, какой конкретно метакласс мы должны использовать с помощью атрибута __metaclass__, либо с помощью именованного аргумента metaclass.

>>> class A(object):
>>>     # Актуально для python2
>>>     __metaclass__ = YourMetaClass
>>>
>>> class A(metaclass=MyMetaClass):
>>>     pass
>>>
>>> a = A()
Я создаю классы!

И собственно сам метакласс

>>> def MyMetaClass(name, bases, dict):
>>>     print('Я создаю классы!')
>>>     return type(name, bases, dict)
>>> 

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

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

class UpscaleMetaclass(type):
    """
    Для того чтобы изменить механизм поведения создания класса, нам нужно переопределить метод __new__.
    Если вам необходимо изменить процесс инициализации уже созданного объекта, переопределяйте метод __init__.
    """
    def __new__(cls, name, bases, attributes_dict):
        attrs = ((name, value) for name, value in attributes_dict.items() if not name.startswith('__'))
        uppercase_attr = dict((name.upper(), value) for name, value in attrs)

        return super().__new__(cls, name, bases, uppercase_attr)

И тут возникает логичный вопрос: Когда и где следует использовать метаклассы. В нём-то и кроется вся магия, окутывающая метапрограммирование. И если вы задаёте себе этот вопрос, то, с 90% вероятностью не стоит. При применении метаклассов разработчик должен чётко осознавать зачем они нужны в данном конкретном случае и какие положительные\отрицательные стороны несёт их применение.

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

Собственно, не будем далеко ходить и рассмотрим пример из стандартной библиотеки python - string.Template:

>>> from string import Template
>>> Template('$value1 + $value2').substitute(value1='Hello', value2='world')
'Hello + world'

Рассмотрим исходный код класса Template

class Template(metaclass=_TemplateMetaclass):
    """A string class for supporting $-substitutions."""

    delimiter = '$'
    idpattern = r'[_a-z][_a-z0-9]*'
    flags = _re.IGNORECASE

    def __init__(self, template):
        self.template = template

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

class _TemplateMetaclass(type):
    pattern = r"""
    %(delim)s(?:
      (?P<escaped>%(delim)s) |   # Escape sequence of two delimiters
      (?P<named>%(id)s)      |   # delimiter and a Python identifier
      {(?P<braced>%(id)s)}   |   # delimiter and a braced identifier
      (?P<invalid>)              # Other ill-formed delimiter exprs
    )
    """

    def __init__(cls, name, bases, dct):
        super(_TemplateMetaclass, cls).__init__(name, bases, dct)
        if 'pattern' in dct:
            pattern = cls.pattern
        else:
            pattern = _TemplateMetaclass.pattern % {
                'delim' : _re.escape(cls.delimiter),
                'id'    : cls.idpattern,
                }
        cls.pattern = _re.compile(pattern, cls.flags | _re.VERBOSE)

Метод __init__ метакласса, берёт значение некоторых атрибутов класса (pattern, delimiter, idpattern) и использует их для построения сложного регулярного выражения, которое, в свою очередь сохраняется в атрибуте класса pattern.

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

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

Однако, возможности метопрограммирования в python не заканчиваются исключительно использованием метаклассов.

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

На самом деле под понятие метапрограммирования можно отнести и другие механизмы, такие как: