Использование метода __call__ для метакласса вместо __new__?

При обсуждении метаклассов the docs:

Вы можете, конечно, переопределить другие методы класса (или добавить новые методы); например, определение пользовательского метода __call__() в metaclass позволяет настраивать поведение при вызове класса, например. не всегда создавая новый экземпляр.

Мои вопросы: предположим, что я хочу иметь пользовательское поведение при вызове класса, например, кэширование вместо создания свежих объектов. Я могу сделать это, переопределив метод __new__ класса. Когда я хочу вместо этого определить метакласс с __call__? Что этот подход дает, что не достижимо с помощью __new__?

Ответы

Ответ 1

Прямой ответ на ваш вопрос: когда вы хотите сделать больше, чем просто настроить создание экземпляра, или если вы хотите отделить класс от от того, как это сделать создан.

См. мой ответ на Создание синглета в Python и связанное с ним обсуждение.

Есть несколько преимуществ.

  • Это позволяет вам отделить то, что делает класс, от деталей того, как он создан. Метаклас и класс отвечают за одну вещь.

  • Вы можете написать код один раз в метаклассе и использовать его для настройки поведения вызовов нескольких классов, не беспокоясь о множественном наследовании.

  • Подклассы могут переопределять поведение в своем методе __new__, но __call__ в метаклассе вообще не нужно даже вызывать __new__.

  • Если есть работа по настройке, вы можете сделать это в методе __new__ метакласса, и это происходит только один раз, а не каждый раз, когда вызывается класс.

Конечно, есть много случаев, когда настройка __new__ работает так же хорошо, если вас не беспокоит принцип единой ответственности.

Но есть и другие варианты использования, которые должны выполняться раньше, когда класс создается, а не при создании экземпляра. Это когда они входят, чтобы играть, что метакласс необходим. См. Каковы ваши (конкретные) прецеденты для метаклассов в Python? для множества замечательных примеров.

Ответ 2

Одно из отличий состоит в том, что, определяя метод метакласса __call__ вы __call__, чтобы он вызывался до того, как любой из методов класса или подкласса __new__ получит возможность быть вызванным.

class MetaFoo(type):
    def __call__(cls,*args,**kwargs):
        print('MetaFoo: {c},{a},{k}'.format(c=cls,a=args,k=kwargs))

class Foo(object):
    __metaclass__=MetaFoo

class SubFoo(Foo):
    def __new__(self,*args,**kwargs):
        # This never gets called
        print('Foo.__new__: {a},{k}'.format(a=args,k=kwargs))

 sub=SubFoo()
 foo=Foo()

 # MetaFoo: <class '__main__.SubFoo'>, (),{}
 # MetaFoo: <class '__main__.Foo'>, (),{}

Обратите внимание, что SubFoo.__new__ никогда не SubFoo.__new__. Напротив, если вы определяете Foo.__new__ без метакласса, вы разрешаете подклассам переопределять Foo.__new__.

Конечно, вы можете определить MetaFoo.__call__ для вызова cls.__new__, но это до вас. Отказавшись от этого, вы можете запретить подклассам __new__ их метод __new__.

Я не вижу здесь убедительного преимущества использования метакласса. А поскольку "Простое лучше, чем сложное", я бы рекомендовал использовать __new__.

Ответ 3

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

class Meta_1(type):
    def __call__(cls, *a, **kw):
        print "entering Meta_1.__call__()"
        rv = super(Meta_1, cls).__call__(*a, **kw)
        print "exiting Meta_1.__call__()"
        return rv

class Class_1(object):
    __metaclass__ = Meta_1
    def __new__(cls, *a, **kw):
        print "entering Class_1.__new__()"
        rv = super(Class_1, cls).__new__(cls, *a, **kw)
        print "exiting Class_1.__new__()"
        return rv

    def __init__(self, *a, **kw):
        print "executing Class_1.__init__()"
        super(Class_1,self).__init__(*a, **kw)

Обратите внимание, что приведенный выше код фактически не делает ничего, кроме журнала, что мы делаем. Каждый метод отказывается от своей родительской реализации, то есть по умолчанию. Таким образом, помимо ведения журнала эффективно, как будто вы просто заявили следующее:

class Meta_1(type): pass
class Class_1(object):
    __metaclass__ = Meta_1

И теперь создайте экземпляр Class_1

c = Class_1()
# entering Meta_1.__call__()
# entering Class_1.__new__()
# exiting Class_1.__new__()
# executing Class_1.__init__()
# exiting Meta_1.__call__()

Поэтому, если type является родительским элементом Meta_1, мы можем представить себе псевдо-реализацию type.__call__() как такового:

class type:
    def __call__(cls, *args, **kwarg):

        # ... a few things could possibly be done to cls here... maybe... or maybe not...

        # then we call cls.__new__() to get a new object
        obj = cls.__new__(cls, *args, **kwargs)

        # ... a few things done to obj here... maybe... or not...

        # then we call obj.__init__()
        obj.__init__(*args, **kwargs)

        # ... maybe a few more things done to obj here

        # then we return obj
        return obj

Обратите внимание, что из приведенного выше порядка вызовов Meta_1.__call__() (или в этом случае type.__call__()) предоставляется возможность влиять на то, будут ли в конечном итоге совершены вызовы Class_1.__new__() и Class_1.__init__(). В ходе выполнения Meta_1.__call__() мог возвращать объект, который даже не был затронут. Возьмем, к примеру, такой подход к одноэлементному шаблону:

class Meta_2(type):
    __Class_2_singleton__ = None
    def __call__(cls, *a, **kw):
        # if the singleton isn't present, create and register it
        if not Meta_2.__Class_2_singleton__:
            print "entering Meta_2.__call__()"
            Meta_2.__Class_2_singleton__ = super(Meta_2, cls).__call__(*a, **kw)
            print "exiting Meta_2.__call__()"
        else:
            print ("Class_2 singleton returning from Meta_2.__call__(), "
                    "super(Meta_2, cls).__call__() skipped")
        # return singleton instance
        return Meta_2.__Class_2_singleton__

class Class_2(object):
    __metaclass__ = Meta_2
    def __new__(cls, *a, **kw):
        print "entering Class_2.__new__()"
        rv = super(Class_2, cls).__new__(cls, *a, **kw)
        print "exiting Class_2.__new__()"
        return rv

    def __init__(self, *a, **kw):
        print "executing Class_2.__init__()"
        super(Class_2, self).__init__(*a, **kw)

Посмотрим, что происходит, когда многократно пытаешься создать объект типа Class_2

a = Class_2()
# entering Meta_2.__call__()
# entering Class_2.__new__()
# exiting Class_2.__new__()
# executing Class_2.__init__()
# exiting Meta_2.__call__()

b = Class_2()
# Class_2 singleton returning from Meta_2.__call__(), super(Meta_2, cls).__call__() skipped

c = Class_2()
# Class_2 singleton returning from Meta_2.__call__(), super(Meta_2, cls).__call__() skipped

print a is b is c
True

Теперь наблюдайте эту реализацию, используя метод класса __new__(), чтобы попытаться выполнить одно и то же.

import random
class Class_3(object):

    __Class_3_singleton__ = None

    def __new__(cls, *a, **kw):
        # if singleton not present create and save it
        if not Class_3.__Class_3_singleton__:
            print "entering Class_3.__new__()"
            Class_3.__Class_3_singleton__ = rv = super(Class_3, cls).__new__(cls, *a, **kw)
            rv.random1 = random.random()
            rv.random2 = random.random()
            print "exiting Class_3.__new__()"
        else:
            print ("Class_3 singleton returning from Class_3.__new__(), "
                   "super(Class_3, cls).__new__() skipped")

        return Class_3.__Class_3_singleton__ 

    def __init__(self, *a, **kw):
        print "executing Class_3.__init__()"
        print "random1 is still {random1}".format(random1=self.random1)
        # unfortunately if self.__init__() has some property altering actions
        # they will affect our singleton each time we try to create an instance 
        self.random2 = random.random()
        print "random2 is now {random2}".format(random2=self.random2)
        super(Class_3, self).__init__(*a, **kw)

Обратите внимание, что приведенная выше реализация, хотя и успешно регистрирует одноэлемент в классе, не предотвращает вызов __init__(), это происходит неявно в type.__call__() (type, являющемся метаклассом по умолчанию, если ни один не указан). Это может привести к некоторым нежелательным эффектам:

a = Class_3()
# entering Class_3.__new__()
# exiting Class_3.__new__()
# executing Class_3.__init__()
# random1 is still 0.282724600824
# random2 is now 0.739298365475

b = Class_3()
# Class_3 singleton returning from Class_3.__new__(), super(Class_3, cls).__new__() skipped
# executing Class_3.__init__()
# random1 is still 0.282724600824
# random2 is now 0.247361634396

c = Class_3()
# Class_3 singleton returning from Class_3.__new__(), super(Class_3, cls).__new__() skipped
# executing Class_3.__init__()
# random1 is still 0.282724600824
# random2 is now 0.436144427555

d = Class_3()
# Class_3 singleton returning from Class_3.__new__(), super(Class_3, cls).__new__() skipped
# executing Class_3.__init__()
# random1 is still 0.282724600824
# random2 is now 0.167298405242

print a is b is c is d
# True

Ответ 4

Это вопрос фаз жизненного цикла и того, к чему у вас есть доступ. __call__ вызывается после __new__ и передается параметрами инициализации до того, как они будут переданы в __init__, поэтому вы можете ими управлять. Попробуйте этот код и изучите его вывод:

class Meta(type):
    def __new__(cls, name, bases, newattrs):
        print "new: %r %r %r %r" % (cls, name, bases, newattrs,)
        return super(Meta, cls).__new__(cls, name, bases, newattrs)

    def __call__(self, *args, **kw):
        print "call: %r %r %r" % (self, args, kw)
        return super(Meta, self).__call__(*args, **kw)

class Foo:
    __metaclass__ = Meta

    def __init__(self, *args, **kw):
        print "init: %r %r %r" % (self, args, kw)

f = Foo('bar')
print "main: %r" % f

Ответ 5

Я подумал, что конкретная версия Pyros 3-ответа на пироскоп может быть полезной для кого-то, кто сможет скопировать, вставить и взломать (вероятно, я, когда я снова окажусь на этой странице, разыскивая его через 6 месяцев). Это взято из этой статьи:

class Meta(type):

     @classmethod
     def __prepare__(mcs, name, bases, **kwargs):
         print('  Meta.__prepare__(mcs=%s, name=%r, bases=%s, **%s)' % (
             mcs, name, bases, kwargs
         ))
         return {}

     def __new__(mcs, name, bases, attrs, **kwargs):
         print('  Meta.__new__(mcs=%s, name=%r, bases=%s, attrs=[%s], **%s)' % (
             mcs, name, bases, ', '.join(attrs), kwargs
         ))
         return super().__new__(mcs, name, bases, attrs)

     def __init__(cls, name, bases, attrs, **kwargs):
         print('  Meta.__init__(cls=%s, name=%r, bases=%s, attrs=[%s], **%s)' % (
             cls, name, bases, ', '.join(attrs), kwargs
         ))
         super().__init__(name, bases, attrs)

     def __call__(cls, *args, **kwargs):
         print('  Meta.__call__(cls=%s, args=%s, kwargs=%s)' % (
             cls, args, kwargs
         ))
         return super().__call__(*args, **kwargs)

print('** Meta class declared')

class Class(metaclass=Meta, extra=1):

     def __new__(cls, myarg):
         print('  Class.__new__(cls=%s, myarg=%s)' % (
             cls, myarg
         ))
         return super().__new__(cls)

     def __init__(self, myarg):
         print('  Class.__init__(self=%s, myarg=%s)' % (
             self, myarg
         ))
         self.myarg = myarg
         super().__init__()

     def __str__(self):
         return "<instance of Class; myargs=%s>" % (
             getattr(self, 'myarg', 'MISSING'),
         )

print('** Class declared')

Class(1)
print('** Class instantiated')

Выходы:

** Meta class declared
  Meta.__prepare__(mcs=<class '__main__.Meta'>, name='Class', bases=(), **{'extra': 1})
  Meta.__new__(mcs=<class '__main__.Meta'>, name='Class', bases=(), attrs=[__module__, __qualname__, __new__, __init__, __str__, __classcell__], **{'extra': 1})
  Meta.__init__(cls=<class '__main__.Class'>, name='Class', bases=(), attrs=[__module__, __qualname__, __new__, __init__, __str__, __classcell__], **{'extra': 1})
** Class declared
  Meta.__call__(cls=<class '__main__.Class'>, args=(1,), kwargs={})
  Class.__new__(cls=<class '__main__.Class'>, myarg=1)
  Class.__init__(self=<instance of Class; myargs=MISSING>, myarg=1)
** Class instantiated

Другим замечательным ресурсом, освещенным в этой же статье, является учебник по метапрограммированию Python 3 Дэвида Бизли (Python 3).