Каковы некоторые (конкретные) варианты использования метаклассов?


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

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

возможность использовать метаклассы заставила многих людей во многих местах использовать классы как какой-то второсортный объект, который просто мне это кажется катастрофой. Должно ли Программирование быть заменено метапрограммированием? Добавление декораторов класса, К сожалению, сделало его еще более приемлемым.

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

Я начну:

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

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

17 85

17 ответов:

у меня есть класс, который обрабатывает неинтерактивное построение, как интерфейс к 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__, но это решение я нашел простой.

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

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

  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']

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)

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)

но это гораздо, гораздо вуду-ER, чем простой метакласс (особенно первый, который действительно плавит ваш мозг). Я хочу сказать, что вы смотрите на метаклассы как на незнакомые и противоинтуитивные, но вы также можете смотреть на них как на следующий шаг эволюции языков программирования: вам просто нужно настроить свое мышление. В конце концов, вы, вероятно, могли бы сделать все в 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

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

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

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

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

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

    для других примеров реального мира, взгляните на различные Ормы, как С SQLAlchemy это!--16--> ОРМ или sqlobject. Опять же, цель состоит в том, чтобы интерпретировать определения (здесь определения столбцов SQL) с определенным значением.

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

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

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

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

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

простой пример из Консервативный Метакласс по Ian Bicking:

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

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).

Edit (by Ali): пример использования коллекций и экземпляров-это то, что я бы предпочел. Важным фактом являются экземпляры, которые дают вам больше энергии и устраняют причину использовать метаклассы. Далее Стоит отметить, что ваш пример использует смесь классов и экземпляров, что, безусловно, указывает на то, что вы не можете просто сделать все это с помощью метаклассов. И создает по-настоящему не универсальный способ делающий это.

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,])
]

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

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

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

class test(baseclass_with_metaclass):
    method_maker_value = "hello"

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

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

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

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

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

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

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

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

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

метаклассы не заменяют Программирование! Они просто трюк, который может автоматизировать или сделать более элегантными некоторые задачи. Хорошим примером этого является одном библиотека подсветки синтаксиса. Он имеет класс под названием RegexLexer что позволяет пользователю определить набор правил лексики как регулярные выражения для класса. Метакласс используется для превращения определений в полезный синтаксический анализатор.

они как соль; это легко использовать слишком много.

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

class NameClass(type):
    def __init__(cls, *args, **kwargs):
       type.__init__(cls, *args, **kwargs)
       cls.name = cls.__name__

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

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

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

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

единственный законный вариант использования метакласса-это не дать другим любопытным разработчикам прикоснуться к вашему коду. Как только любопытный разработчик осваивает метаклассы и начинает ковыряться с вашим, бросьте еще один или два уровня, чтобы не допустить их. Если это не работает, начните использовать type.__new__ или, возможно, какая-то схема с использованием рекурсивного метакласса.

(написано языком в щеку, но я видел, как делается такая путаница. Джанго является прекрасным примером)

Это незначительное использование, но... одна вещь, которую я нашел полезными для метаклассов, - это вызов функции всякий раз, когда создается подкласс. Я кодифицировал это в метакласс, который ищет __initsubclass__ атрибут: всякий раз, когда подкласс создается, все родительские классы, которые определяют этот метод вызываются с __initsubclass__(cls, subcls). Это позволяет создать родительский класс, который затем регистрирует все подклассы с некоторым глобальным реестром, выполняет инвариантные проверки подклассов всякий раз, когда они определены, выполняет поздно-переплетные работы и т. д... все без необходимости вручную вызывать функции или для создания пользовательских метаклассов, которые выполняют каждую из этих отдельных обязанностей.

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

недавно мне пришлось использовать метакласс, чтобы помочь декларативно определить модель SQLAlchemy вокруг таблицы базы данных, заполненной данными переписи населения США из http://census.ire.org/data/bulkdata.html

ИРЛ предоставляет оболочки баз данных для таблиц данных переписи, которые создают целочисленные столбцы в соответствии с Соглашением об именовании от Бюро переписи p012015, p012016, p012017 и т. д.

Я хотел a) иметь доступ к этим столбцам с помощью model_instance.p012017 синтаксис, b) быть довольно явным о том, что я делал, и c) не нужно явно определять десятки полей в модели, поэтому я подкласс Sqlalchemy's DeclarativeMeta для итерации по диапазону столбцов и автоматического создания полей модели, соответствующих столбцам:

from sqlalchemy.ext.declarative.api import DeclarativeMeta

class CensusTableMeta(DeclarativeMeta):
    def __init__(cls, classname, bases, dict_):
        table = 'p012'
        for i in range(1, 49):
            fname = "%s%03d" % (table, i)
            dict_[fname] = Column(Integer)
            setattr(cls, fname, dict_[fname])

        super(CensusTableMeta, cls).__init__(classname, bases, dict_)

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

CensusTableBase = declarative_base(metaclass=CensusTableMeta)

class P12Tract(CensusTableBase):
    __tablename__ = 'ire_p12'

    geoid = Column(String(12), primary_key=True)

    @property
    def male_under_5(self):
        return self.p012003

    ...

там, кажется, законное использование описано здесь - переписывание Python Docstrings с помощью метакласса.

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

https://docs.python.org/3/reference/datamodel.html#metaclass-example

но в общем: очень внимательно оцените, если вам действительно очень нужна дополнительная сложность метаклассов.