Как передать аргументы метаклассу из определения класса в Python 3.x?

это Python 3.x версия как передать аргументы метаклассу из определения класса? вопрос, указанный отдельно по запросу, так как ответ значительно отличается от Python 2.x.


В Python 3.x, как передать аргументы метаклассу __prepare__, __new__ и __init__ функции, чтобы автор класса мог дать ввод метаклассу о том, как должен быть создан класс?

как мой случай использования, я использую metaclasses для автоматической регистрации классов и их подклассы в PyYAML для загрузки / сохранения файлов YAML. Это включает в себя некоторую дополнительную логику выполнения, недоступную в запасе PyYAML YAMLObjectMetaClass. Кроме того, я хочу разрешить авторам классов дополнительно указывать тег/тег-формат-шаблон, который PyYAML использует для представления класса и/или объектов функций для построения и представления. Я уже понял, что не могу использовать подкласс PyYAML YAMLObjectMetaClass чтобы выполнить это-- " потому что у нас нет доступа к фактическому объекту класса в __new__ " согласно моему комментарию кода-поэтому я пишу свой собственный метакласс, который обертывает регистрационные функции PyYAML.

в конечном счете, я хочу сделать что-то вдоль линий:

from myutil import MyYAMLObjectMetaClass

class MyClass(metaclass=MyYAMLObjectMetaClass):
    __metaclassArgs__ = ()
    __metaclassKargs__ = {"tag": "!MyClass"}

...где __metaclassArgs__ и __metaclassKargs__ были бы аргументы, идущие к __prepare__, __new__ и __init__ способы MyYAMLObjectMetaClass когда MyClass объект класса становится создан.

Of конечно, я мог бы использовать подход "зарезервированные имена атрибутов", указанный в Python 2.X версия этого вопроса, но я знаю, что есть более элегантный подход.

4 ответов


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

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

class C(metaclass=MyMetaClass, myArg1=1, myArg2=2):
  pass

...и они проходят в ваш метакласс вот так:

class MyMetaClass(type):

  @classmethod
  def __prepare__(metacls, name, bases, **kargs):
    #kargs = {"myArg1": 1, "myArg2": 2}
    return super().__prepare__(name, bases, **kargs)

  def __new__(metacls, name, bases, namespace, **kargs):
    #kargs = {"myArg1": 1, "myArg2": 2}
    return super().__new__(metacls, name, bases, namespace)
    #DO NOT send "**kargs" to "type.__new__".  It won't catch them and
    #you'll get a "TypeError: type() takes 1 or 3 arguments" exception.

  def __init__(cls, name, bases, namespace, myArg1=7, **kargs):
    #myArg1 = 1  #Included as an example of capturing metaclass args as positional args.
    #kargs = {"myArg2": 2}
    super().__init__(name, bases, namespace)
    #DO NOT send "**kargs" to "type.__init__" in Python 3.5 and older.  You'll get a
    #"TypeError: type.__init__() takes no keyword arguments" exception.

ты должен уйти kargs из вызова type.__new__ и type.__init__ (язык Python 3.5 и старше; см. "обновление" ниже) или вам TypeError исключение из-за передачи слишком много аргументов. Это означает,что при передаче аргументов метакласса таким образом мы всегда должны реализовать MyMetaClass.__new__ и MyMetaClass.__init__ чтобы наши пользовательские аргументы ключевых слов не достигали базового класса type.__new__ и type.__init__ методы. type.__prepare__ кажется, обрабатывает дополнительные аргументы ключевого слова изящно (следовательно, почему я передаю их в Примере, на случай, если есть какая-то функциональность, о которой я не знаю, которая зависит от **kargs), поэтому определение type.__prepare__ является необязательным.

обновление

в Python 3.6, кажется type был отрегулирован и type.__init__ теперь можно обрабатывать дополнительные аргументы ключевых слов изящно. Вам все равно нужно определить type.__new__ (бросает TypeError: __init_subclass__() takes no keyword arguments исключения).

распад

в Python 3 вы указываете метакласс через аргумент ключевого слова, а не атрибут класса:

class MyClass(metaclass=MyMetaClass):
  pass

это утверждение примерно переводится как:

MyClass = metaclass(name, bases, **kargs)

...где metaclass является ли значение для аргумента "metaclass", который вы передали,name - это имя строки вашего класса ('MyClass'), bases - это любые базовые классы, которые вы передали (кортеж нулевой длины () в данном случае), и kargs любые аргументы сайта неуловленные (пустой dict {} в данном случае).

разбивая это дальше, утверждение примерно переводится на:

namespace = metaclass.__prepare__(name, bases, **kargs)  #`metaclass` passed implicitly since it's a class method.
MyClass = metaclass.__new__(metaclass, name, bases, namespace, **kargs)
metaclass.__init__(MyClass, name, bases, namespace, **kargs)

...где kargs всегда dict аргументов сайта неуловленные мы прошли в к определению класса.

разбивая пример, который я дал выше:

class C(metaclass=MyMetaClass, myArg1=1, myArg2=2):
  pass

...грубо говоря:

namespace = MyMetaClass.__prepare__('C', (), myArg1=1, myArg2=2)
#namespace={'__module__': '__main__', '__qualname__': 'C'}
C = MyMetaClass.__new__(MyMetaClass, 'C', (), namespace, myArg1=1, myArg2=2)
MyMetaClass.__init__(C, 'C', (), namespace, myArg1=1, myArg2=2)

большая часть этой информации пришла из документация Python по "настройке создания класса".


вот версия кода из моего ответа на это другой вопрос о аргументах метакласса, которые были обновлены, чтобы он работал в и Python 2 и 3. По сути, он делает то же самое, что и Бенджамин Петерсон шесть модуль with_metaclass() функция делает-а именно, чтобы явно создать новый базовый класс, используя желаемый метакласс на лету, когда это необходимо и тем самым избежать ошибок из-за синтаксические различия метакласса между двумя версиями Python (потому что способ сделать это не изменился).

from __future__ import print_function
from pprint import pprint

class MyMetaClass(type):
    def __new__(cls, class_name, parents, attrs):
        if 'meta_args' in attrs:
            meta_args = attrs['meta_args']
            attrs['args'] = meta_args[0]
            attrs['to'] = meta_args[1]
            attrs['eggs'] = meta_args[2]
            del attrs['meta_args'] # clean up
        return type.__new__(cls, class_name, parents, attrs)

# Creates base class on-the-fly using syntax which is valid in both
# Python 2 and 3.
class MyClass(MyMetaClass("NewBaseClass", (object,), {})):
    meta_args = ['spam', 'and', 'eggs']

myobject = MyClass()

pprint(vars(MyClass))
print(myobject.args, myobject.to, myobject.eggs)

выход:

dict_proxy({'to': 'and', '__module__': '__main__', 'args': 'spam',
            'eggs': 'eggs', '__doc__': None})
spam and eggs

в Python 3 вы указываете метакласс через аргумент ключевого слова, а не атрибут класса:

стоит сказать, что этот стиль не совместим с Python 2. Если вы хотите поддерживать Python 2 и 3, Вы должны использовать:

from six import with_metaclass
# or
from future.utils import with_metaclass

class Form(with_metaclass(MyMetaClass, object)):
    pass

вот самый простой способ передать аргументы метаклассу в Python 3:

Python 3.x

class MyMetaclass(type):
    def __new__(mcs, name, bases, namespace, **kwargs):
        return super().__new__(mcs, name, bases, namespace)

    def __init__(cls, name, bases, namespace, custom_arg='default'):
        super().__init__(name, bases, namespace)

        print('Argument is:', custom_arg)


class ExampleClass(metaclass=MyMetaclass, custom_arg='something'):
    pass

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

class ArgMetaclass(type):
    def __new__(mcs, name, bases, namespace, **kwargs):
        return super().__new__(mcs, name, bases, namespace)


class MyMetaclass(ArgMetaclass):
    def __init__(cls, name, bases, namespace, custom_arg='default'):
        super().__init__(name, bases, namespace)

        print('Argument:', custom_arg)


class ExampleClass(metaclass=MyMetaclass, custom_arg='something'):
    pass