Подтвердить что ты не робот

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

4b9b3361

Ответ 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