Есть ли причина выбирать __new__ вместо __init__ при определении метакласса?

Я всегда устанавливал метаклассы примерно так:

class SomeMetaClass(type):
    def __new__(cls, name, bases, dict):
        #do stuff here

Но я только что наткнулся на метакласс, который был определен следующим образом:

class SomeMetaClass(type):
    def __init__(self, name, bases, dict):
        #do stuff here

Есть ли причина предпочесть одно другому?

Обновление. Имейте в виду, что я спрашиваю об использовании __new__ и __init__ в метаклассе. Я уже понимаю разницу между ними в другом классе. Но в метаклассе я не могу использовать __new__ для реализации кэширования, потому что __new__ вызывается только при создании класса в метаклассе.


person Jason Baker    schedule 03.12.2009    source источник


Ответы (5)


Если вы хотите изменить атрибуты dict перед созданием класса или изменить базовый кортеж, вы должны использовать __new__. К тому времени, когда __init__ увидит аргументы, объект класса уже существует. Кроме того, вы должны использовать __new__, если хотите вернуть что-то отличное от вновь созданного класса рассматриваемого типа.

С другой стороны, к моменту запуска __init__ класс уже существует. Таким образом, вы можете делать такие вещи, как дать ссылку на только что созданный класс одному из его объектов-членов.

Изменить: изменена формулировка, чтобы было более понятно, что под "объектом" я подразумеваю класс-объект.

person Matt Anderson    schedule 03.12.2009
comment
Гораздо яснее. И это имеет смысл. Есть ли какая-то причина использовать __init__? - person Jason Baker; 03.12.2009
comment
__new__ может делать все, что может __init__, но не наоборот. Однако __init__ может быть более удобным в использовании, так же, как __new__ и __init__ связаны в обычном экземпляре класса. Вы должны не забыть вернуть что-то, например, из __new__, но с __init__ вы этого не сделаете. - person Matt Anderson; 03.12.2009
comment
@JasonBaker Я понимаю, что это 5-летняя тема, и я не ожидаю получить какой-либо ответ. Разве вы не хотели бы использовать init всякий раз, когда вам нужно создать экземпляр рассматриваемого класса. Как и в случае с шаблоном легковеса. - person Mike S; 10.10.2014

Вы можете увидеть полное описание в официальной документации, но в основном __new__ вызывается перед созданием нового объекта (с целью его создания), а __init__ вызывается после создания нового объекта (с целью его инициализации).

Использование __new__ позволяет использовать такие трюки, как кэширование объектов (всегда возвращая один и тот же объект для одних и тех же аргументов, а не создавая новые) или создание объектов класса, отличного от запрошенного (иногда используется для возврата более конкретных подклассов запрошенного класса). Как правило, если вы не делаете что-то довольно странное, __new__ имеет ограниченную полезность. Если вам не нужно использовать такие трюки, придерживайтесь __init__.

person Ben Blank    schedule 03.12.2009
comment
Я понимаю разницу между __new__ и __init__ для обычного класса. Я спрашивал, почему я хочу использовать их для метакласса. - person Jason Baker; 03.12.2009
comment
@JasonBaker Механизм точно такой же. Классы являются объектами своих метаклассов, поэтому их создание работает одинаково. - person glglgl; 21.10.2012

На самом деле несколько отличий.

Во-первых, первый аргумент в __new__ и __init__ не один и тот же, что не помогает всем, кто просто использует cls. Кто-то указал на это, и это важно для понимания разницы:

  • __new__ получает метаклассMyType в моем примере (помните, что класс уровня приложения еще не создан). Здесь вы можете изменить bases (что может привести к ошибкам разрешения MRO, если вы не будете осторожны).

  • __init__ получает недавно созданный класс уровня приложения, Bar и Foo, и к тому времени пространство имен этого класса уже заполнено, см. cls_attrib в примере ниже.

Образец кода:

class Mixin:
    pass

class MyType(type):


    def __new__(mcls, name, bases, attrs, **kwargs):
        print("  MyType.__new__.mcls:%s" % (mcls))

        if not Mixin in bases:
            #could cause MRO resolution issues, but if you want to alter the bases
            #do it here
            bases += (Mixin,)

        #The call to super.__new__ can also modify behavior:
        #                                   ???? classes Foo and Bar are instances of MyType
        return super(MyType, mcls).__new__(mcls, name, bases, attrs)

        #now we're back to the standard `type` 
        #doing this will neuter most of the metaclass behavior, __init__ wont
        #be called.                         ????
        #return super(MyType, mcls).__new__(type, name, bases, attrs)

    def __init__(cls, name, bases, attrs):
        print("  MyType.__init__.cls:%s." % (cls))

        #I can see attributes on Foo and Bar's namespaces
        print("    %s.cls_attrib:%s" % (cls.__name__, getattr(cls, "cls_attrib", None)))
        return super().__init__(name, bases, attrs)


print("\n Foo class creation:")
class Foo(metaclass=MyType):
    pass


print("\n bar class creation:")
class Bar(Foo):
    #MyType.__init__ will see this on Bar's namespace
    cls_attrib = "some class attribute"

выход:

 Foo class creation:
  MyType.__new__.mcls:<class '__main__.test.<locals>.MyType'>
  MyType.__init__.cls:<class '__main__.test.<locals>.Foo'>.
    Foo.cls_attrib:None

 Bar class creation:
  MyType.__new__.mcls:<class '__main__.test.<locals>.MyType'>
  MyType.__init__.cls:<class '__main__.test.<locals>.Bar'>.
    Bar.cls_attrib:some class attribute
person JL Peyret    schedule 03.02.2020

Как уже было сказано, если вы собираетесь изменить что-то вроде базовых классов или атрибутов, вам придется сделать это в __new__. То же самое верно и для name класса, но здесь, похоже, есть одна особенность. Когда вы меняете name, оно не распространяется на __init__, хотя, например, attr есть.

Итак, у вас будет:

class Meta(type):
    def __new__(cls, name, bases, attr):
        name = "A_class_named_" + name
        return type.__new__(cls, name, bases, attr)

    def __init__(cls, name, bases, attr):
        print "I am still called '" + name + "' in init"
        return super(Meta, cls).__init__(name, bases, attr)

class A(object):
    __metaclass__ = Meta

print "Now I'm", A.__name__

отпечатки

I am still called 'A' in init
Now I'm A_class_named_A

Это важно знать, если __init__ вызывает суперметакласс, который выполняет дополнительную магию. В этом случае перед вызовом super.__init__ необходимо снова изменить имя.

person Debilski    schedule 11.03.2011
comment
__init__ вызывается с параметром name, установленным на исходное имя, то есть с теми же аргументами, что и __new__. Но cls.__name__ должен быть новым на тот момент. - person glglgl; 21.10.2012
comment
То есть с print "I am still called '" + name + "' in init but have '" + cls.__name__ + "'" вы получаете I am still called 'A' in init but have 'A_class_named_A'. - person glglgl; 21.10.2012

Вы можете реализовать кэширование. Person("Jack") всегда возвращает новый объект во втором примере, в то время как вы можете найти существующий экземпляр в первом примере с помощью __new__ (или ничего не возвращать, если хотите).

person Otto Allmendinger    schedule 03.12.2009