Аргументы __new__ и __init__ для метаклассов - PullRequest
4 голосов
/ 09 июня 2019

Я немного удивлен порядком вызова метода и различными аргументами при переопределении new и init в метаклассе. Учтите следующее:

class AT(type):
    def __new__(mcs, name, bases, dct):
        print(f"Name as received in new: {name}")
        return super().__new__(mcs, name + 'HELLO', bases + (list,), dct)

    def __init__(cls, name, bases, dct):
        print(f"Name as received in init: {name}")
        pass

class A(metaclass=AT):
    pass

A.__name__

Вывод:

Name as received in new: A
Name as received in init: A
'AHELLO'

Короче говоря, я ожидал, что init получит AHELLO с аргументом name.

Я представлял, что __init__ был вызван super().__new__: если вызов не выполняется в переопределенном __new__, тогда мой __init__ не вызывается.

Может ли кто-нибудь уточнить, как в этом случае вызывается __init__?

Для информации, мой вариант использования для этого состоит в том, что я хотел сделать создание классов, в особом случае, более простым во время выполнения, предоставляя только один «базовый» класс (а не кортеж), затем я добавил этот код в __new__

if not isinstance(bases, tuple):
            bases = (bases, )

однако я обнаружил, что мне также нужно добавить его в __init__.

Ответы [ 2 ]

3 голосов
/ 09 июня 2019

Ваш __init__ метод явно вызывается, и причина этого в том, что ваш __new__ метод возвращает экземпляр вашего класса.

С https://docs.python.org/3/reference/datamodel.html#object.new:

Если __new__() возвращает экземпляр cls, то будет вызываться метод __init__() нового экземпляра, например __init__(self[, ...]), где self - это новый экземпляр, а остальные аргументы такие же, как были переданы в __new__().

Как вы можете видеть, аргументы, передаваемые __init__, - это аргументы, передаваемые в __new__ вызывающему методу, а не при вызове его с помощью super. Это немного расплывчато, но вот что это значит, если вы внимательно прочитаете.

А в остальном все работает так, как и ожидалось:

In [10]: A.__bases__
Out[10]: (list,)

In [11]: a = A()

In [12]: a.__class__.__bases__
Out[12]: (list,)
1 голос
/ 10 июня 2019

Дело в том, что то, что управляет вызовом __new__ и __init__ обычного класса, это метод __call__ в его метаклассе.Код в методе __call__ для type, метатипа по умолчанию, написан на C, но его эквивалент в Python будет следующим:

class type:
    ...
    def __call__(cls, *args, **kw):
         instance = cls.__new__(cls, *args, **kw)  # __new__ is actually a static method - cls has to be passed explicitly
         if isinstance(instance, cls):
               instance.__init__(*args, **kw)
         return instance

Это происходит для большинства экземпляров объектов в Python,в том числе при создании экземпляров самих классов - метакласс неявно вызывается как часть оператора класса.В этом случае __new__ и __init__, вызываемые из type.__call__, являются методами самого метакласса .И в этом случае type действует как «метаметакласс» - понятие редко требуется, но именно оно создает исследуемое вами поведение.

При создании классов type.__new__ будет отвечать за вызовкласс (не метакласс) __init_subclass__ и методы __set_name__ его дескрипторов - так что метод "metametaclass" __call__ не может это контролировать.

Итак, если вы хотите, чтобы аргументы были переданыдля метакласса __init__, который будет программно изменен, «нормальным» способом будет иметь «метаметакласс», наследующий от type и отличающийся от самого метакласса, и переопределяющий его метод __call__:

class MM(type):
    def __call__(metacls, name, bases, namespace, **kw):
        name = modify(name)
        cls = metacls.__new__(metacls, name, bases, namespace, **kw)
        metacls.__init__(cls, name, bases, namespace, **kw)
        return cls
        # or you could delegate to type.__call__, replacing the above with just
        # return super().__call__(modify(name), bases, namespace, **kw)

Конечно, это способ приблизиться к «черепахам до самого дна», чем кто-либо когда-либо хотел бы в производственном коде.

Альтернативой является сохранение измененного имени в качестве атрибута.в метаклассе, чтобы его метод __init__ мог взять оттуда необходимую информацию и игнорировать имя, переданное из его собственного вызова метакласса '__call__.Информационный канал может быть обычным атрибутом в экземпляре метакласса.Что ж, бывает, что «экземпляр метакласса» - это сам создаваемый класс - и, о, видите, - имя, переданное type.__new__, уже записано в нем - в атрибуте __name__.

InДругими словами, все, что вам нужно сделать, чтобы использовать имя класса, измененное в методе метакласса __new__ в его собственном методе __init__, - это игнорировать переданный аргумент name и использовать вместо него cls.__name__:

class Meta(type):
    def __new__(mcls, name, bases, namespace, **kw):
        name = modified(name)
        return super().__new__(mcls, name, bases, namespace, **kw)

    def __init__(cls, name, bases, namespace, **kw):
        name = cls.__name__  # noQA  (otherwise linting tools would warn on the overriden parameter name)
        ...
...