Использовать метод __call__ метакласса вместо __new__? - PullRequest
31 голосов
/ 06 августа 2011

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

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

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

Ответы [ 5 ]

19 голосов
/ 06 августа 2011

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

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

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

  1. Этопозволяет отделить то, что класс делает от деталей того, как он создан.Каждый из метаклассов и классов отвечает за одну вещь.

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

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

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

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

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

14 голосов
/ 06 августа 2011

Одно отличие состоит в том, что, определяя метод метакласса __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__ никогда не вызывается.Напротив, если вы определяете Foo.__new__ без метакласса, вы разрешаете подклассам переопределять Foo.__new__.

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

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

12 голосов
/ 07 сентября 2016

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

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
1 голос
/ 07 ноября 2018

Я подумал, что конкретная версия Pyros 3-ответа Pyroscope может быть полезна для кого-то, с кем можно копировать, вставлять и взламывать (возможно, я, когда через 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

Еще один замечательный ресурс, освещенный в той же статье, это PyCon Дэвида Бизли 2013 Python3 Учебник по метапрограммированию .

1 голос
/ 06 августа 2011

Это вопрос фаз жизненного цикла и того, к чему у вас есть доступ.__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
...