т. (383) 381-86-26

Блог о создании вебсайтов

 

Метаклассы в Python. Разрушение мифов.

 

В качестве вступления

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

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

Мы называем магией явления, с которыми плохо знакомы или не понимаем полностью.

К сожалению, по отношению к метаклассам сложился закостенелый стереотип особенно мутной магии. И это несмотря на то, что они появились еще в Python 1.5 (а это более 10 лет назад). Возможно, что главные виновники мистификации кроются в самих программистах, которые часто элементарно не понимают как именно правильное использование метаклассов может упростить их нелёгкую жизнь и открыть двери в мир продвинутх возможностей Python.

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

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

Классы и объекты. Ликбез.

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

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

Но, как вы знаете, мы можем определять и свои собственные объекты с помощью классов. Каждый объект обладает двумя важными характеристиками: типом и идентичностью. Идентичность можно рассматривать как указатель на область памяти, в которой хранится объект (можно, но нужно понимать, что это всего лишь свойство некоторых реализаций интерпретатора. CPython, например.)


    >>> i = 42
    >>> id(i)
    150849220

А тип (класс) характеризует внутреннее представление, методы и операции, которые можно применять над данным типом. Конкретный объект определенного типа называется экземпляром. Пока что никакой магии, обычные штучки.


    >>> type(i)
    <type 'int'>

Когда вы определяете class, то в Python это определение становится объектом:


    >>> class A(object): pass
    >>> id(A)
    3074358076L
    >>> type(A)
    <type 'type'>    
    >>> isinstance(A, object)
    True

Q: Что нужно знать, чтобы освоить метаклассы в Python?

Проще, чем кажется. Конечно, как любое новое умение, навык применения метаклассов потребует практики и шлифовки, прежде чем он достигнет экспертного уровня. Но разве настоящего питониста такая мелочь как практика остановит? Программировать на Python — это удовольствие!

Итак, метаклассы

Если вы внимательно смотрели предыдущий прмиер, то у вас должен был возникнуть вопрос: "Что создало A?". Если подумать, то можно догадаться, что эту функцию должен выполнять какой-то другой объект. Этим объектом и является метакласс.

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

В нашем примере таким метаклассом является type. Вот как это работает:


    >>> baseclasses, attributes = (object, ), dict()
    >>> A = type('A', baseclasses, attributes)
    >>> id(A)
    3062969308L
    >>> type(A)
    <type 'type'>    

Не стоит смущаться, что type() используется и для определения типа объекта и для создания класса. В документации эта особенность хорошо разъяснена.

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

Так вот на последнем этапе создания класса мы и можем влиять на процесс. Указав интерпретатору, какой следует использовать метакласс либо с помощью атрибута __metaclass__ (Py2), либо с помощью именнованного аргумента class A(metaclass=MyMetaClass)


    >>> class A(object):
        __metaclass__ = YourMetaClass
    ...
    >> class A(metaclass=YourMetaClass):
    ...
    # т.е. вместо
    # >>> type('A', baseclasses, attributes)
    # на последнем шаге будет
    # >>> YourMetaClass('A', baseclasses, attributes) 

Или пример боевого кода из Django Forms:


    >>> class DeclarativeFieldsMetaclass(type):
        """
        Метакласс, конвертирующий атрибуты Field в словарь 'base_fields',
        а также 'base_fields' родительского класса.
        """
        def __new__(cls, name, bases, attrs):
            attrs['base_fields'] = get_declared_fields(bases, attrs)
            new_class = super(DeclarativeFieldsMetaclass,
                         cls).__new__(cls, name, bases, attrs)
            if 'media' not in attrs:
                new_class.media = media_property(new_class)
            return new_class    

    >>> class Form(six.with_metaclass(DeclarativeFieldsMetaclass, BaseForm)):
        "Коллекция Fields + ассоциированные данные"

Класс Form — это отдельный от BaseForm класс, благодаря которому абстрагируется определение атрибутов (полей формы). Данный класс позволяет определять форму в декларативном стиле, тогда как BaseFrom сам по себе таких возможностей не предоставляет:


    >>> from django import forms
    >>> class YourForm(forms.Form):
            additional_info = forms.CharField(...)
            invoice_email = forms.EmailField(...)        

Методы metacls.__new__() и metacls.__init__()

Как видно в отрывке кода из фреймворка Django, для контроля процесса создания и модификации создаваемого класса используется метод __new__(). Поэтому, если вы хотите управлять процессом создания нового объекта класса, то следует переопределять именно этот метод.

Если же требуется всего лишь повлиять на инициализацию нового объекта класса уже после его создания, тогда следует обратить внимание на метод __init__()


    >>> MyKlass = MyMeta.__new__(MyMeta, name, bases, dct)
    >>> MyMeta.__init__(MyKlass, name, bases, dct)
    Этот и следующий пример взяты у Эли Бендерски

Для того, чтобы понять, что же происходит в процессе выполнения этого кода, определим метакласс MyMeta так:


    >>> class MyMeta(type):
    >>>     def __new__(meta, name, bases, dct):
                print '-----------------------------------'
                print "Allocating memory for class", name
                print meta
                print bases
                print dct
                return super(MyMeta, meta).__new__(meta, name, bases, dct)
    >>>     def __init__(cls, name, bases, dct):
                print '-----------------------------------'
                print "Initializing class", name
                print cls
                print bases
                print dct
                super(MyMeta, cls).__init__(name, bases, dct)

    >>> class MyKlass(object):
            __metaclass__ = MyMeta

            def foo(self, param):
                pass

            barattr = 2                

В результате выполнения кода выше получим:


    Allocating memory for class MyKlass
    <class '__main__.MyMeta'>
    (<type 'object'>,)
    {'barattr': 2, '__module__': '__main__',
     'foo': <function foo at 0x00B502F0>,
     '__metaclass__': <class '__main__.MyMeta'>}
    -----------------------------------
    Initializing class MyKlass
    <class '__main__.MyKlass'>
    (<type 'object'>,)
    {'barattr': 2, '__module__': '__main__',
     'foo': <function foo at 0x00B502F0>,
     '__metaclass__': <class '__main__.MyMeta'>}    

Как мы знаем объекты классы создаются в момент импортирования модулей, в так называемое "время создания класса". Т.е. они будут созданы один раз при первоначальном импортировании модуля. Это следует помнить.

Следующий пример взят так же у Эли Бендерски, а он в свою очередь взял его из документации Twisted (ещё один известный Python-фреймворк):


    class AccessorType(type):
        def __init__(self, name, bases, d):
            type.__init__(self, name, bases, d)
            accessors = {}
            prefixs = ["get_", "set_", "del_"]
            for k in d.keys():
                v = getattr(self, k)
                for i in range(3):
                    if k.startswith(prefixs[i]):
                        accessors.setdefault(k[4:], [None, None, None])[i] = v
            for name, (getter, setter, deler) in accessors.items():
                # create default behaviours for the property - if we leave
                # the getter as None we won't be able to getattr, etc..

                # [...] some code that implements the above comment

                setattr(self, name, property(getter, setter, deler, ""))    

Что делает этот метакласс? В его документации сказано: "Метакласс автоматически генерирует свойства. ... Для метода 'set_foo' будет создано свойство 'foo', которое будет использовать set_foo для установки значения foo. Тоже верно для get_foo & del_foo"

Как следует из цитаты документации метакласс AccessorType действует достаточно прямолинейно:

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

Прежде, чем пойти дальше и рассмотреть ещё один приемер применения метаклассов, остановимся и подведём итог тому, что мы уже знаем о метаклассах:

Теперь рассмотрим ещё один пример применения Python-метаклассов в Django. В этом фреймворке определение модели данных (на основе которой создается таблица в БД) выполняется в декларативном стиле:


    # Например так
    class MediaFile(Orderable):
        """Single file to add in a library."""

        library = models.ForeignKey("MediaLibrary",
                                    related_name="files")

Обычному пользователю фреймворка не видна та механика, которая позволяет работать всей этой "магии". А всё потому, что все модели наследуются от класса django.db.models.Model, который в свою очередь использует метакласс ModelBase


    class ModelBase(type):
        """
        Metaclass for all models.
        """
        def __new__(cls, name, bases, attrs):
            super_new = super(ModelBase, cls).__new__
    # ...

Если заглянуть в исходный код ModelBase, то можно увидеть, что этот класс перопределяет метод __new__, а значит влияет на создание нового класса (не экземпляра класса, а именно самого класса): вычисляет необходимые параметры, проводит требующиеся фреймворком проверки — выполняет кучу полезной работы. Обязательно загляните в исходный код. Он лаконичен, хорошо написан, много комментариев. Читать одно удовольствие.

Подводя итоги

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

Следите за нашими публикациями и вы узнаете ещё больше о мистических возможностях Python, его особенностях и интересных трюках, которые сделают из вас могущественного волхва мира программирования на языке Python! =)

Подпишитесь на рассылку, будет интересно!