Pull to refresh

Использование метаклассов в Python

Reading time 11 min
Views 42K
Некоторые средства метапрограммирования не так часто используются в ежедневной
работе, как обычные в ООП классы или те же декораторы. Для понимания же целей
введения подобных средств в язык требуются конкретные примеры промышленного
применения, некоторые из которых и приведены ниже.



Введение в метаклассы



Итак, классический ООП подразумевает наличие только классов и объектов.
Класс -шаблон для объекта; при объявлении класса указывается вся механика
работы каждого конкретного «воплощения»: задаются данные, инкапсулируемые
в объекте, и методы для работы этими данными.


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

От метакласса можно наследоваться, получая новый метакласс, который, в свою
очередь, можно использовать при определении новых классов. Таким образом,
появляется новое «измерение» наследования, добавляющееся к иерархии наследования
классов: метакласс -> класс -> объект.

Простой пример



Предположим, нас утомило задание атрибутов в контрукторе __init__(self, *args,
**kwargs). Хотелось бы ускорить этот процесс таким образом, чтобы была
возможность задавать атрибуты прямо при создании объекта класса. С обычным
классом такое не пройдет:

  >>>class Man(object):
  >>>    pass
  >>>me = Man(height = 180, weight = 80)
  Traceback (most recent call last):
  File "<stdin>", line 20, in <module>
      TypeError: object.__new__() takes no parameters


Объект конструируется вызовом класса оператором "()". Создадим наследованием от
type метакласс, переопределяющий этот оператор:


  >>>class AttributeInitType(type):
  >>>    def __call__(self, *args, **kwargs):
  >>>        """ Вызов класса создает новый объект. """
  >>>        # Перво-наперво создадим сам объект...
  >>>        obj = type.__call__(self, *args)
  >>>        # ...и добавим ему переданные в вызове аргументы в качестве атрибутов.
  >>>        for name in kwargs:
  >>>            setattr(obj, name, kwargs[name])
  >>>        # вернем готовый объект
  >>>        return obj

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

  >>>class Man(object):
  >>>    __metaclass__ = AttributeInitType

Вуаля:

  >>>me = Man(height = 180, weigth = 80)
  >>>print me.height
  180


Расширение языка (абстрактные классы)


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


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

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

Рассмотрим работу библиотеки abc из предложения по реализации для стандартной библиотеки.

abc


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


>>> from abc import ABCMeta, abstractmethod
>>> class A(object):
>>> 	__metaclass__=ABCMeta
>>> 	@abstractmethod
>>> 	def foo(self): pass
>>> 
>>> A()  
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class A with abstract methods foo

Не вышло. Теперь определим нужный метод:

  
>>>class C(A):
>>>   def foo(self): print(42)
>>>C
<class '__main__.C'>
>>>a=C()
>>>a.foo()
42

Узнаем, как это реализуется в метаклассе (опустив некоторые другие возможности
модуля abc) ABCMeta:

>>>class ABCMeta(type):
>>>    def __new__(mcls, name, bases, namespace):
>>>        bases = _fix_bases(bases)
>>>        cls = super(ABCMeta, mcls).__new__(mcls, name, bases, namespace)
>>>        # Найдем множество(set) имен абстрактных методов среди собственных
>>>        # методов и методов предков
>>>        abstracts = set(name
>>>                     for name, value in namespace.items()
>>>                     if getattr(value, "__isabstractmethod__", False))
>>>        for base in bases:
>>>            for name in getattr(base, "__abstractmethods__", set()):
>>>                value = getattr(cls, name, None)
>>>                if getattr(value, "__isabstractmethod__", False):
>>>                    abstracts.add(name)
>>>        cls.__abstractmethods__ = frozenset(abstracts)
>>>        return cls

Метод _fix_bases добавляет скрытый класс _Abstract в число предков
абстрактного класса. Сам _Abstract проверяет, осталось ли что-нибудь во
множестве(set) __abstractmethods__; если осталось — выкидывает исключение.

>>>class _Abstract(object):
>>>    def __new__(cls, *args, **kwds):
>>>        am = cls.__dict__.get("__abstractmethods__")
>>>        if am:
>>>            raise TypeError("can't instantiate abstract class %s "
>>>                            "with abstract methods %s" %
>>>                            (cls.__name__, ", ".join(sorted(am))))
>>>        return super(_Abstract, cls).__new__(cls, *args, **kwds)
>>>
>>>def _fix_bases(bases):
>>>    for base in bases:
>>>        if issubclass(base, _Abstract):
>>>            # _Abstract уже среди предков
>>>            return bases
>>>    if object in bases:
>>>        # Заменяем object на _Abstract, если класс прямо наследуется от object
>>>        # и не перечислен среди прочих предков
>>>        return tuple([_Abstract if base is object else base
>>>                      for base in bases])
>>>    # Добавляем _Abstract в конец в противном случае
>>>    return bases + (_Abstract,)

В каждом абстрактном классе хранится по «замороженному» множеству(frozenset)
абстрактных методов; то есть тех методов (функций-объектов), у которых есть
атрибут __isabstractmethod__, выставляемый соответствующим декоратором:

>>>def abstractmethod(funcobj):
>>>    funcobj.__isabstractmethod__ = True
>>>    return funcobj

Итак, абстрактный метод получает атрибут __isabstractmethod__ при назначении ему
декоратора. Атрибуты после наследования от абстрактного класса собираются во
множестве "__abstractmethods__" класса-наследника. Если множество не пустое, и
программист пытается создать объект класса, то будет вызвано исключение
TypeError со списком неопределенных методов.

Вывод

Просто? Просто. Язык расширен? Расширен. Комментарии, как говорится, излишни.

DSL в Django


Один из продвинутых примеров DSL — механизм ORM Django на примере класса Model и
метакласса ModelBase. Конкретно связь с базой данный здесь не интересны, имеет
смысл сконцентрироваться на создании экземпляра класса-наследника класса Model.

Большая часть следующего подраздела — подробный разбор кода
ModelBase. Читателям, не нуждающимся в подробностях, достаточно прочитать вывод
в конце раздела «Django».

Разбор метакласса ModelBase

Вся механика работы метакласса ModelBase сконцентрирована в месте
переопределения метода __new__, вызываемого непосредственно перед созданием
экземпляра класса модели:

  >>>class ModelBase(type):
  >>>    """
  >>>    Metaclass for all models.
  >>>    """
  >>>    def __new__(cls, name, bases, attrs):
  >>>        super_new = super(ModelBase, cls).__new__
  >>>        parents = [b for b in bases if isinstance(b, ModelBase)]
  >>>        if not parents:
  >>>            # If this isn't a subclass of Model, don't do anything special.
  >>>            return super_new(cls, name, bases, attrs)

В самом начале метода просто создается экземпляр класса и, если этот класс не
наследует от Model, просто возращается.

Все конкретные опции класса модели собираются в атрибуте класса _meta, который
может быть создан с нуля, унаследоваться от предка или быть подкорректирован в
локальном классе Meta:

  >>>        # Создание клаcса
  >>>        module = attrs.pop('__module__')
  >>>        new_class = super_new(cls, name, bases, {'__module__': module})
  >>>        attr_meta = attrs.pop('Meta', None)
  >>>        abstract = getattr(attr_meta, 'abstract', False)
  >>>        if not attr_meta:
  >>>            meta = getattr(new_class, 'Meta', None)
  >>>        else:
  >>>            meta = attr_meta
  >>>        base_meta = getattr(new_class, '_meta', None)

Кроме того, видим, что класс может быть абстрактным, не соответствующим
какой-либо таблице в базе данных.

Момент истины в процессе создания класса модели наступает при внесении в него
параметров по умолчанию:

  >>>        new_class.add_to_class('_meta', Options(meta, **kwargs))

add_to_class либо вызывает метод contribute_to_class аргумента, либо, если
такового нет, просто добавляет именованный атрибут классу.

Класс же Options в своем contribute_to_class делает атрибут _meta ссылкой на
самого себя и собирает в нем различные параметры, вроде названия таблицы базы
данных, списка полей модели, списка виртуальных полей модели, прав доступа и
других. Он также проводит проверки связей с другими моделями на уникальность
названий полей в БД.

Далее в методе __new__ неабстрактному классу добавляются именованные
исключения:

  >>>        if not abstract:
  >>>            new_class.add_to_class('DoesNotExist',
  >>>                subclass_exception('DoesNotExist', ObjectDoesNotExist, module))
  >>>            new_class.add_to_class('MultipleObjectsReturned',
  >>>                subclass_exception('MultipleObjectsReturned',
  >>>                    MultipleObjectsReturned, module))

Если класс-родитель — не абстрактный, и параметры не установлены явно в локальном
классе Meta, то наследуем параметры ordering и get_latest_by:

  >>>        if base_meta and not base_meta.abstract:
  >>>            if not hasattr(meta, 'ordering'):
  >>>                new_class._meta.ordering = base_meta.ordering
  >>>            if not hasattr(meta, 'get_latest_by'):
  >>>                new_class._meta.get_latest_by = base_meta.get_latest_by

Менеджер по умолчанию должен быть нулевым. Если такая модель уже существует — завершаем обработку, возвращая эту модель:

  >>>        if getattr(new_class, '_default_manager', None):
  >>>            new_class._default_manager = None
  >>>        
  >>>        m = get_model(new_class._meta.app_label, name, False)
  >>>        if m is not None:
  >>>            return m


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

  >>>        for obj_name, obj in attrs.items():
  >>>            new_class.add_to_class(obj_name, obj)

Теперь требуется пройтись по полям модели и найти связи типа «один к одному»,
которые будут использовать чуть ниже:

  >>>        # Do the appropriate setup for any model parents.
  >>>        o2o_map = dict([(f.rel.to, f) for f in new_class._meta.local_fields
  >>>                if isinstance(f, OneToOneField)])

Проход по предкам модели для наследования различных полей, с отбрасыванием тех,
что не являются наследниками Model. Далее переведены комментарии, которых
достаточно для понимания происходящего:

  >>>        for base in parents:
  >>>            if not hasattr(base, '_meta'):
  >>>                # Модели без _meta не являются действующими и интереса не представляют
  >>>                continue
  >>>
  >>>            # Все поля произвольного типа для данной модели
  >>>            new_fields = new_class._meta.local_fields + \
  >>>                         new_class._meta.local_many_to_many + \
  >>>                         new_class._meta.virtual_fields
  >>>            field_names = set([f.name for f in new_fields])
  >>>
  >>>            if not base._meta.abstract:
  >>>                # Обрабатываем "конкретные" классы...
  >>>                if base in o2o_map:
  >>>                    field = o2o_map[base]
  >>>                    field.primary_key = True
  >>>                    new_class._meta.setup_pk(field)
  >>>                else:
  >>>                    attr_name = '%s_ptr' % base._meta.module_name
  >>>                    field = OneToOneField(base, name=attr_name,
  >>>                            auto_created=True, parent_link=True)
  >>>                    new_class.add_to_class(attr_name, field)
  >>>                new_class._meta.parents[base] = field
  >>>
  >>>            else:
  >>>                # .. и абстрактные.
  >>>
  >>>                # Проверка на столкновения имен между классами,
  >>>                # объявленными в данном классе и в абстрактном предке
  >>>                parent_fields = base._meta.local_fields + base._meta.local_many_to_many
  >>>                for field in parent_fields:
  >>>                    if field.name in field_names:
  >>>                        raise FieldError('Local field %r in class %r clashes '\
  >>>                                         'with field of similar name from '\
  >>>                                         'abstract base class %r' % \
  >>>                                            (field.name, name, base.__name__))
  >>>                    new_class.add_to_class(field.name, copy.deepcopy(field))
  >>>
  >>>                # Все неабстрактные родители передаются наследнику
  >>>                new_class._meta.parents.update(base._meta.parents)
  >>>
  >>>            # Базовые Менеджеры наследуются от абстрактных классов
  >>>            base_managers = base._meta.abstract_managers
  >>>            base_managers.sort()
  >>>            for _, mgr_name, manager in base_managers:
  >>>                val = getattr(new_class, mgr_name, None)
  >>>                if not val or val is manager:
  >>>                    new_manager = manager._copy_to_model(new_class)
  >>>                    new_class.add_to_class(mgr_name, new_manager)
  >>>
  >>>            # Виртуальные поля (вроде GenericForeignKey) берем от родителя
  >>>            for field in base._meta.virtual_fields:
  >>>                if base._meta.abstract and field.name in field_names:
  >>>                    raise FieldError('Local field %r in class %r clashes '\
  >>>                                     'with field of similar name from '\
  >>>                                     'abstract base class %r' % \
  >>>                                        (field.name, name, base.__name__))
  >>>                new_class.add_to_class(field.name, copy.deepcopy(field))
  >>>

Абстрактные классы моделей нигде не регистрируются:

  >>>        if abstract:
  >>>            # Абстрактные модели не могут инстанцироваться и не появляются
  >>>            #  в списке моделей для приложения, поэтому обратываются немного иначе, нежели
  >>>            #  нормальные модели
  >>>            attr_meta.abstract = False
  >>>            new_class.Meta = attr_meta
  >>>            return new_class

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

  >>>        new_class._prepare()
  >>>        register_models(new_class._meta.app_label, new_class)
  >>>        return get_model(new_class._meta.app_label, name, False)


Вывод

Итак, подведем итоги. Зачем понадобились метаклассы?

1) Класс-модель должен иметь набор обязательных параметров (имя таблицы, имя
джанго-приложения, список полей, связи с другими моделями и многие другие) в
атрибуте _meta, которые и определяются при создании каждого класса, наследующего
от Model.

2) Эти параметры сложным образом наследуются от обычных и абстрактных
классов-предков, что некрасиво закладывать в сам класс.

3) Появляется возможность спрятать происходящее от программиста, использующего
фреймворк.

Замечаньица


1) Если явно не указывать наследование класса от object, то класс использует
метакласс, указанный в глобальной переменной __metaclass__, что иногда может
быть удобно при многократном использовании собственного метакласса в пределах
одного модуля. Простой пример, приведенный в начале заметки, можно переделать
следующим образом:

  class AttributeInitType(type):
      def __call__(self, *args, **kwargs):
      obj = type.__call__(self, *args)
      for name in kwargs:
      setattr(obj, name, kwargs[name])
      return obj

  __metaclass__ = AttributeInitType

  class Man:
      pass

  me = Man(height = 180, weigth = 80)
  print me.height

  В стандартный поток выведется:
  180

2) Есть такой супергуру питоновский, Тим Питерс. Он очень удачно сказал про
применение метаклассов и аналогичных средств из разряда черной магии Питона:

    Metaclasses are deeper magic than 99% of users should ever worry
    about. If you wonder whether you need them, you don't (the
    people who actually need them know with certainty that they need
    them, and don't need an explanation about why).

На русском это примерно так звучит:

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

Мораль тут простая: не мудрите. Метаклассы в большинстве случаев — лишнее. Питонист должен руководствоваться принципом наименьшего удивления;
менять классическую схему работы ООП не стоит просто ради самолюбования.

Ссылочки по мотивам



Английская Википедия — отсюда позаимствован простой примерчик
PEP-3119 — здесь
описываются абстрактные классы в полном своем варианте.
Ролик
на английском
, подробный разговор про метаклассы в Питоне с примерами
использования. Там по ссылкам можно найти и саму статью с примерами, очень
поучительно.
Tags:
Hubs:
+66
Comments 29
Comments Comments 29

Articles