Skip to content

Instantly share code, notes, and snippets.

@singulared
Last active March 24, 2017 22:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save singulared/e974b7725b14f124316babde5c99a076 to your computer and use it in GitHub Desktop.
Save singulared/e974b7725b14f124316babde5c99a076 to your computer and use it in GitHub Desktop.

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

В одной из начальных лекций мы говорили о том, что в языке 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

Собственно, не будем далеко ходить и рассмотрим пример из стандартной библиотеки 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 не заканчиваются исключительно использованием метаклассов.

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

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

  • Дескрипторы атрибутов классов. В действительности, функция property - это простейший способ задать дескриптор атрибута, который автоматически вызывает функции, управляющие доступом к атрибуту. Они предоставляют способ добавить программный код, вызываемый автоматически при обращении к определенному атрибуту, и вызываются после того, как атрибут будет найден.
  • Декораторы функций и классов. Как вы уже знаете, декораторы, это механизм, позволяющий добавлять логику, которая будет запускаться автоматически при вызове функции или при выполнении операции создания экземпляра.
  • Атрибуты механизма интроспекции - Специальные атрибуты, такие как __class__ и __diсt__, позволяющие нам получать информацию о внутреннем устройстве объектов Python и обрабатывать их обобщенными способами - вывести перечень атрибутов объекта,имя класса и так далее.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment