Какие (конкретные) варианты использования для метаклассов? - PullRequest
96 голосов
/ 24 декабря 2008

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

Я считаю, что вам почти никогда не нужно использовать метаклассы. Зачем? потому что я думаю, что если вы делаете что-то подобное с классом, вам, вероятно, следует делать это с объектом. И небольшой редизайн / рефакторинг в порядке.

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

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

Я начну:

Иногда при использовании стороннего библиотека полезно иметь возможность мутировать класс определенным образом.

(Это единственный случай, о котором я могу подумать, и он не конкретный)

Ответы [ 17 ]

75 голосов
/ 26 июня 2015

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

Большинство метаклассов, которые я видел, делают одну из двух вещей:

  1. Регистрация (добавление класса в структуру данных):

    models = {}
    
    class ModelMetaclass(type):
        def __new__(meta, name, bases, attrs):
            models[name] = cls = type.__new__(meta, name, bases, attrs)
            return cls
    
    class Model(object):
        __metaclass__ = ModelMetaclass
    

    Всякий раз, когда вы подкласс Model, ваш класс регистрируется в словаре models:

    >>> class A(Model):
    ...     pass
    ...
    >>> class B(A):
    ...     pass
    ...
    >>> models
    {'A': <__main__.A class at 0x...>,
     'B': <__main__.B class at 0x...>}
    

    Это также можно сделать с помощью декораторов классов:

    models = {}
    
    def model(cls):
        models[cls.__name__] = cls
        return cls
    
    @model
    class A(object):
        pass
    

    Или с явной функцией регистрации:

    models = {}
    
    def register_model(cls):
        models[cls.__name__] = cls
    
    class A(object):
        pass
    
    register_model(A)
    

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

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

    >>> class B(A):
    ...     pass
    ...
    >>> models
    {'A': <__main__.A class at 0x...> # No B :(
    
  2. Рефакторинг (изменение атрибутов класса или добавление новых):

    class ModelMetaclass(type):
        def __new__(meta, name, bases, attrs):
            fields = {}
            for key, value in attrs.items():
                if isinstance(value, Field):
                    value.name = '%s.%s' % (name, key)
                    fields[key] = value
            for base in bases:
                if hasattr(base, '_fields'):
                    fields.update(base._fields)
            attrs['_fields'] = fields
            return type.__new__(meta, name, bases, attrs)
    
    class Model(object):
        __metaclass__ = ModelMetaclass
    

    Всякий раз, когда вы создаете подкласс Model и определяете некоторые атрибуты Field, они вводятся со своими именами (например, для более информативных сообщений об ошибках) и группируются в словарь _fields (для простой итерации, без необходимости просматривать все атрибуты класса и все атрибуты его базовых классов каждый раз):

    >>> class A(Model):
    ...     foo = Integer()
    ...
    >>> class B(A):
    ...     bar = String()
    ...
    >>> B._fields
    {'foo': Integer('A.foo'), 'bar': String('B.bar')}
    

    Опять же, это можно сделать (без наследования) с помощью декоратора классов:

    def model(cls):
        fields = {}
        for key, value in vars(cls).items():
            if isinstance(value, Field):
                value.name = '%s.%s' % (cls.__name__, key)
                fields[key] = value
        for base in cls.__bases__:
            if hasattr(base, '_fields'):
                fields.update(base._fields)
        cls._fields = fields
        return cls
    
    @model
    class A(object):
        foo = Integer()
    
    class B(A):
        bar = String()
    
    # B.bar has no name :(
    # B._fields is {'foo': Integer('A.foo')} :(
    

    Или явно:

    class A(object):
        foo = Integer('A.foo')
        _fields = {'foo': foo} # Don't forget all the base classes' fields, too!
    

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

    class B(A):
        bar = String()
    
    # vs.
    
    class B(A):
        bar = String('bar')
        _fields = {'B.bar': bar, 'A.foo': A.foo}
    

Рассмотрев наиболее распространенные и конкретные случаи использования, единственные случаи, когда вам абсолютно НЕОБХОДИМО использовать метаклассы, это когда вы хотите изменить имя класса или список базовых классов, поскольку после определения эти параметры запекаются в классе, и никакой декоратор или функция не могут их испечь.

class Metaclass(type):
    def __new__(meta, name, bases, attrs):
        return type.__new__(meta, 'foo', (int,), attrs)

class Baseclass(object):
    __metaclass__ = Metaclass

class A(Baseclass):
    pass

class B(A):
    pass

print A.__name__ # foo
print B.__name__ # foo
print issubclass(B, A)   # False
print issubclass(B, int) # True

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

В любом случае, в Python 3 метаклассы также имеют метод __prepare__, который позволяет вам преобразовывать тело класса в отображение, отличное от dict, поддерживая, таким образом, упорядоченные атрибуты, перегруженные атрибуты и другие забавные интересные вещи:

import collections

class Metaclass(type):

    @classmethod
    def __prepare__(meta, name, bases, **kwds):
        return collections.OrderedDict()

    def __new__(meta, name, bases, attrs, **kwds):
        print(list(attrs))
        # Do more stuff...

class A(metaclass=Metaclass):
    x = 1
    y = 2

# prints ['x', 'y'] rather than ['y', 'x']

1058 *

class ListDict(dict):
    def __setitem__(self, key, value):
        self.setdefault(key, []).append(value)

class Metaclass(type):

    @classmethod
    def __prepare__(meta, name, bases, **kwds):
        return ListDict()

    def __new__(meta, name, bases, attrs, **kwds):
        print(attrs['foo'])
        # Do more stuff...

class A(metaclass=Metaclass):

    def foo(self):
        pass

    def foo(self, x):
        pass

# prints [<function foo at 0x...>, <function foo at 0x...>] rather than <function foo at 0x...>

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

import itertools

class Attribute(object):
    _counter = itertools.count()
    def __init__(self):
        self._count = Attribute._counter.next()

class A(object):
    x = Attribute()
    y = Attribute()

A._order = sorted([(k, v) for k, v in vars(A).items() if isinstance(v, Attribute)],
                  key = lambda (k, v): v._count)

1064 *

class A(object):

    def _foo0(self):
        pass

    def _foo1(self, x):
        pass

    def foo(self, x=None):
        if x is None:
            return self._foo0()
        else:
            return self._foo1(x)

Помимо того, что он намного более уродлив, он также менее гибок: что, если вам нужны упорядоченные литеральные атрибуты, такие как целые числа и строки? Что если None является допустимым значением для x?

Вот креативный способ решить первую проблему:

import sys

class Builder(object):
    def __call__(self, cls):
        cls._order = self.frame.f_code.co_names
        return cls

def ordered():
    builder = Builder()
    def trace(frame, event, arg):
        builder.frame = frame
        sys.settrace(None)
    sys.settrace(trace)
    return builder

@ordered()
class A(object):
    x = 1
    y = 'foo'

print A._order # ['x', 'y']

А вот творческий способ решить второй:

_undefined = object()

class A(object):

    def _foo0(self):
        pass

    def _foo1(self, x):
        pass

    def foo(self, x=_undefined):
        if x is _undefined:
            return self._foo0()
        else:
            return self._foo1(x)

Но это намного, намного вуду, чем простой метакласс (особенно первый, который действительно тает ваш мозг). Я хочу сказать, что метаклассы выглядят как незнакомые и не интуитивно понятные, но вы также можете рассматривать их как следующий шаг эволюции в языках программирования: вам просто нужно изменить свое мышление. В конце концов, вы, вероятно, могли бы делать все в C, включая определение структуры с помощью указателей на функции и передачу ее в качестве первого аргумента ее функциям. Человек, впервые увидевший C ++, может сказать: «Что это за магия? Почему компилятор неявно передает this в методы, а не в обычные и статические функции? Лучше быть явным и подробным о своих аргументах». Но тогда объектно-ориентированное программирование становится гораздо более мощным, когда вы его получаете; и это тоже квази-аспектно-ориентированное программирование, я полагаю. И когда вы понимаете метаклассы, они на самом деле очень просты, так почему бы не использовать их, когда это удобно?

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

class MetaMetaclass(type):
    def __new__(meta, name, bases, attrs):
        def __new__(meta, name, bases, attrs):
            cls = type.__new__(meta, name, bases, attrs)
            cls._label = 'Made in %s' % meta.__name__
            return cls 
        attrs['__new__'] = __new__
        return type.__new__(meta, name, bases, attrs)

class China(type):
    __metaclass__ = MetaMetaclass

class Taiwan(type):
    __metaclass__ = MetaMetaclass

class A(object):
    __metaclass__ = China

class B(object):
    __metaclass__ = Taiwan

print A._label # Made in China
print B._label # Made in Taiwan
35 голосов
/ 25 декабря 2008

Цель метаклассов не состоит в том, чтобы заменить различие класса / объекта метаклассом / классом - это изменить поведение определений классов (и, следовательно, их экземпляров) каким-либо образом. По сути, это изменение поведения оператора класса способами, которые могут быть более полезными для вашего конкретного домена, чем по умолчанию. Вещи, для которых я их использовал:

  • Отслеживание подклассов, обычно для регистрации обработчиков. Это удобно при использовании настройки стиля плагина, где вы хотите зарегистрировать обработчик для определенной вещи, просто создав подклассы и настроив несколько атрибутов класса. например. Предположим, вы пишете обработчик для различных музыкальных форматов, где каждый класс реализует соответствующие методы (тэги play / get и т. д.) для своего типа. Добавление обработчика для нового типа становится:

    class Mp3File(MusicFile):
        extensions = ['.mp3']  # Register this type as a handler for mp3 files
        ...
        # Implementation of mp3 methods go here
    

    Затем метакласс поддерживает словарь {'.mp3' : MP3File, ... } и т. Д. И создает объект соответствующего типа, когда вы запрашиваете обработчик через фабричную функцию.

  • Изменение поведения. Возможно, вы захотите придать особое значение определенным атрибутам, что приведет к изменению поведения, когда они присутствуют. Например, вы можете искать методы с именами _get_foo и _set_foo и прозрачно преобразовывать их в свойства. В качестве примера из реальной жизни, вот рецепт, который я написал, чтобы дать больше C-подобных структурных определений. Метакласс используется для преобразования объявленных элементов в строку формата структуры, обработки наследования и т. Д. И создания класса, способного с ним справляться.

    Для других реальных примеров взгляните на различные ORM, такие как sqlalchemy's ORM или sqlobject . Опять же, цель состоит в том, чтобы интерпретировать определения (здесь определения столбцов SQL) с определенным значением.

23 голосов
/ 26 декабря 2008

У меня есть класс, который обрабатывает неинтерактивные черчения, как интерфейс для Matplotlib. Тем не менее, иногда хочется делать интерактивные черчения. Имея всего пару функций, я обнаружил, что могу увеличивать количество фигур, вручную рисовать и т. Д., Но мне нужно было делать это до и после каждого вызова печати. Поэтому, чтобы создать как интерактивную графическую оболочку, так и внеэкранную графическую оболочку, я обнаружил, что более эффективно делать это с помощью метаклассов, оборачивая соответствующие методы, чем делать что-то вроде:

class PlottingInteractive:
    add_slice = wrap_pylab_newplot(add_slice)

Этот метод не поспевает за изменениями API и т. Д., Но он перебирает атрибуты класса в __init__ перед переустановкой атрибутов класса, более эффективен и поддерживает актуальность:

class _Interactify(type):
    def __init__(cls, name, bases, d):
        super(_Interactify, cls).__init__(name, bases, d)
        for base in bases:
            for attrname in dir(base):
                if attrname in d: continue # If overridden, don't reset
                attr = getattr(cls, attrname)
                if type(attr) == types.MethodType:
                    if attrname.startswith("add_"):
                        setattr(cls, attrname, wrap_pylab_newplot(attr))
                    elif attrname.startswith("set_"):
                        setattr(cls, attrname, wrap_pylab_show(attr))

Конечно, могли бы быть лучшие способы сделать это, но я нашел это эффективным. Конечно, это также можно сделать в __new__ или __init__, но это было решение, которое я нашел наиболее простым.

17 голосов
/ 25 декабря 2008

Давайте начнем с классической цитаты Тима Питера:

Метаклассы - более глубокая магия, чем 99% пользователей должны когда-либо беспокоиться. Если вам интересно, нужны ли они вам, вы не (люди, которые на самом деле нуждаются они с уверенностью знают, что они нужны они и не нужны объяснение почему). Тим Питерс (c.l.p post 2002-12-22)

Сказав это, я (периодически) сталкивался с истинным использованием метаклассов. Тот, который приходит на ум, находится в Django, где все ваши модели наследуются от моделей. Модель. models.Model, в свою очередь, делает какую-то серьезную магию, чтобы обернуть ваши модели БД в совершенство ORM от Django. Это волшебство происходит посредством метаклассов. Он создает всевозможные классы исключений, классы менеджеров и т. Д. И т. Д.

См. Django / db / models / base.py, класс ModelBase () для начала истории.

6 голосов
/ 14 августа 2011

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

Когда несколько классов имеют одинаковое специальное поведение, повторение __metaclass__=X, очевидно, лучше, чем повторение кода специального назначения и / или представление специальных суперклассов общего назначения.

Но даже с одним специальным классом и без предсказуемого расширения __new__ и __init__ метакласса являются более чистым способом инициализации переменных класса или других глобальных данных, чем смешивание специального кода и нормальных def и class операторов в теле определения класса.

6 голосов
/ 25 декабря 2008

Метаклассы могут быть полезны для построения предметно-ориентированных языков в Python. Конкретными примерами являются Django, декларативный синтаксис схем базы данных SQLObject.

Базовый пример из Консервативный метакласс , написанный Яном Бикингом:

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

class Registration(schema.Schema):
    first_name = validators.String(notEmpty=True)
    last_name = validators.String(notEmpty=True)
    mi = validators.MaxLength(1)
    class Numbers(foreach.ForEach):
        class Number(schema.Schema):
            type = validators.OneOf(['home', 'work'])
            phone_number = validators.PhoneNumber()

Некоторые другие методы: Ингредиенты для создания DSL в Python (pdf).

Редактировать (Али): Пример того, как сделать это с использованием коллекций и экземпляров, я бы предпочел. Важным фактом являются случаи, которые дают вам больше возможностей и устраняют причину использования метаклассов. Также стоит отметить, что в вашем примере используется смесь классов и экземпляров, что, безусловно, свидетельствует о том, что вы не можете просто делать все это с метаклассами. И создает действительно неоднородный способ сделать это.

number_validator = [
    v.OneOf('type', ['home', 'work']),
    v.PhoneNumber('phone_number'),
]

validators = [
    v.String('first_name', notEmpty=True),
    v.String('last_name', notEmpty=True),
    v.MaxLength('mi', 1),
    v.ForEach([number_validator,])
]

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

5 голосов
/ 25 декабря 2008

Единственный раз, когда я использовал метаклассы в Python, был при написании оболочки для API Flickr.

Моей целью было очистить api-сайт flickr и динамически сгенерировать полную иерархию классов, чтобы разрешить доступ API с использованием объектов Python:

# Both the photo type and the flickr.photos.search API method 
# are generated at "run-time"
for photo in flickr.photos.search(text=balloons):
    print photo.description

Так что в этом примере, поскольку я сгенерировал весь API-интерфейс Python Flickr с веб-сайта, я действительно не знаю определения классов во время выполнения. Возможность динамически генерировать типы была очень полезна.

5 голосов
/ 28 декабря 2008

Я думал о том же самом только вчера и полностью согласен. Сложности в коде, вызванные попытками сделать его более декларативным, как правило, усложняют поддержку кодовой базы, затрудняют чтение и, на мой взгляд, менее питонны. Обычно он также требует большого количества copy.copy () (для сохранения наследования и копирования из класса в экземпляр) и означает, что вам нужно искать во многих местах, чтобы увидеть, что происходит (всегда с метаклассом), что противоречит зерно питона также. Я просматривал formencode и sqlalchemy code, чтобы посмотреть, стоил ли такой декларативный стиль, а его явно нет. Такой стиль следует оставить дескрипторам (таким как свойство и методы) и неизменным данным. В Ruby улучшена поддержка таких декларативных стилей, и я рад, что основной язык Python не идет по этому пути.

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

class test(baseclass_with_metaclass):
    method_maker_value = "hello"

может иметь метакласс, который генерирует метод в этом классе со специальными свойствами, основанными на "привет" (скажем, метод, который добавляет "привет" в конец строки). Для удобства сопровождения может быть полезно убедиться, что вам не нужно писать метод в каждом подклассе, который вы создаете, вместо этого все, что вам нужно определить, это method_maker_value.

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

4 голосов
/ 25 декабря 2008

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

Тем не менее, в Smalltalk и Ruby может быть очень удобно изменять существующий класс, но Python не любит делать это напрямую.

Есть отличная статья developerWorks о метаклассировании в Python, которая может помочь. Статья Википедии тоже довольно хороша.

3 голосов
/ 04 июня 2013

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

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

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

...