Как передать аргументы метаклассу из определения класса в Python 3.x?
Это версия Python 3.x Как передать аргументы метаклассу из определения класса? вопрос, указанный отдельно по запросу, так как ответ значительно отличается от Python 2.x.
В Python 3.x как передать аргументы в функции метакласса __prepare__
, __new__
и __init__
чтобы автор класса мог предоставить метаклассу информацию о том, как должен создаваться класс?
В моем случае я использую метаклассы, чтобы включить автоматическую регистрацию классов и их подклассов в PyYAML для загрузки/сохранения YAML файлов. Это включает в себя некоторую дополнительную логику времени выполнения, недоступную в YAMLObjectMetaClass
PyYAML. Кроме того, я хочу разрешить авторам классов по желанию указывать tag/tag-format-template, который PyYAML использует для представления класса и/или объектов функций, используемых для конструирования и представления. Я уже понял, что не могу использовать подкласс PyYAML YAMLObjectMetaClass
для выполнения this-- "потому что у нас нет доступа к реальному объекту класса в __new__
" согласно моему комментарию к коду - поэтому я пишу мой собственный метакласс, который охватывает функции регистрации PyYAML.
В конечном итоге я хочу сделать что-то вроде:
from myutil import MyYAMLObjectMetaClass
class MyClass(metaclass=MyYAMLObjectMetaClass):
__metaclassArgs__ = ()
__metaclassKargs__ = {"tag": "!MyClass"}
... где __metaclassArgs__
и __metaclassKargs__
будут аргументами, идущими к __prepare__
, __new__
и __init__
объекта MyYAMLObjectMetaClass
когда MyYAMLObjectMetaClass
объект класса MyClass
.
Конечно, я мог бы использовать подход "зарезервированные имена атрибутов", приведенный в версии этого вопроса на Python 2.x, но я знаю, что существует более элегантный подход.
Ответы
Ответ 1
Изучив официальную документацию по 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.__init__
методов базового класса type.__new__
и type.__init__
. type.__prepare__
кажется, type.__prepare__
обрабатывает дополнительные аргументы ключевого слова (поэтому я и type.__prepare__
их в примере, на случай, если некоторые функции, о которых я не знаю, опирается на **kargs
), поэтому определение type.__prepare__
является необязательным.
ОБНОВИТЬ
В Python 3.6 кажется, что type
был скорректирован, и type.__init__
теперь может type.__init__
обрабатывать дополнительные аргументы ключевых слов. Вам все еще нужно определить type.__new__
(выдает TypeError: __init_subclass__() takes no keyword arguments
исключение TypeError: __init_subclass__() takes no keyword arguments
).
Сломать
В Python 3 вы определяете метакласс с помощью аргумента ключевого слова, а не атрибута класса:
class MyClass(metaclass=MyMetaClass):
pass
Это утверждение примерно переводится как:
MyClass = metaclass(name, bases, **kargs)
... где metaclass
это значение для "метаклассом" аргумент, который вы прошли в, name
это имя строки вашего класса ('MyClass'
), bases
любые базовые классы, которые вы прошли в (нулевой длины кортежа ()
в этом case), а kargs
- это любые незафиксированные ключевые слова аргументов (в этом случае пустой dict
{}
).
Разбивая это далее, утверждение примерно переводится как:
namespace = metaclass.__prepare__(name, bases, **kargs) #'metaclass' passed implicitly since it 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 "Настройка создания классов".
Ответ 2
Вот версия кода из моего ответа на этот другой вопрос об аргументах метакласса, который был обновлен так, что он будет работать как в 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
Ответ 3
В 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
Ответ 4
Вот самый простой способ передать аргументы метаклассу в 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