Как передать аргументы метаклассу из определения класса в 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
