Używanie metody wywołania metaklasy zamiast nowej?

Omawiając metaklasy, dokumenty podają:

Możesz oczywiście nadpisać inne metody klasy (lub dodać nowe metody); na przykład definiowanie niestandardowej metody __call__() w metaclass pozwala na niestandardowe zachowanie podczas wywoływania klasy, np. nie zawsze tworzy nową instancję.

Moje pytania brzmią: Załóżmy, że chcę mieć własne zachowanie podczas wywoływania klasy, na przykład buforowanie zamiast tworzenia świeżych obiektów. Mogę to zrobić. poprzez nadpisanie metody __new__ klasy. Kiedy chciałbym zdefiniować metaklasę za pomocą __call__? Co daje takie podejście, którego nie da się osiągnąć z __new__?

Author: kmario23, 2011-08-06

5 answers

Bezpośrednia odpowiedź na twoje pytanie brzmi: kiedy chcesz zrobić Więcej niż tylko dostosować tworzenie instancji, lub kiedy chcesz oddzielić to, co robi Klasa od sposobu jej tworzenia.

Zobacz moją odpowiedź na Tworzenie Singletona w Pythonie i związaną z tym dyskusję.

Istnieje kilka zalet.

  1. Pozwala oddzielić to, co robi Klasa od szczegółów jej tworzenia. Metaklasy i klasy to każdy jest odpowiedzialny za jedną rzecz.

  2. Możesz napisać kod raz w metaklasie i użyć go do dostosowania zachowania wywołania kilku klas bez martwienia się o wielokrotne dziedziczenie.

  3. Podklasy mogą nadpisywać zachowanie w swojej metodzie __new__, ale {[1] } na metaklasie w ogóle nie muszą nawet wywoływać __new__.

  4. Jeśli jest praca konfiguracyjna, możesz to zrobić w metodzie __new__ metaclass i dzieje się to tylko raz, zamiast za każdym razem nazywa się Klasa.

Jest z pewnością wiele przypadków, w których dostosowywanie __new__ działa równie dobrze, jeśli nie martwisz się o zasadę pojedynczej odpowiedzialności.

Ale są inne przypadki użycia, które muszą wystąpić wcześniej, gdy klasa jest tworzona, a nie gdy instancja jest tworzona. To kiedy te przychodzą do gry, metaklasa jest konieczna. Zobacz jakie są Twoje (konkretne) przypadki użycia metaklas w Pythonie? for lots of great przykłady.

 30
Author: agf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:46:37

Subtelne różnice stają się nieco bardziej widoczne, gdy uważnie obserwujesz kolejność wykonywania tych metod.

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)

Zauważ, że powyższy kod nie robi niczego innego niż zapisywanie tego, co robimy. Każda metoda odwołuje się do swojej implementacji macierzystej, tzn. domyślnej. Tak więc oprócz logowania jest to efektywnie tak, jakbyś po prostu zadeklarował rzeczy w następujący sposób: {]}

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

A teraz stwórzmy instancję 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__()

Dlatego jeśli type jest rodzic Meta_1 możemy sobie wyobrazić pseudo implementację type.__call__() jako taką:

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

Informacja z powyższej kolejności połączeń, że Meta_1.__call__() (lub w tym przypadku type.__call__()) ma możliwość wpływania na to, czy połączenia do Class_1.__new__() i Class_1.__init__() zostaną ostatecznie wykonane. W trakcie jego wykonywania {[12] } może zwrócić obiekt, który nie został nawet dotknięty przez. Weźmy na przykład takie podejście do wzoru Singletona:

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)

Obserwujmy, co się dzieje, gdy wielokrotnie próbuje aby utworzyć obiekt typu 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

Teraz obserwuj tę implementację używając metody class' __new__(), aby spróbować osiągnąć to samo.

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)

Zauważ, że powyższa implementacja, mimo że pomyślnie zarejestrowano singleton na klasie, nie uniemożliwia wywołania __init__(), dzieje się to pośrednio w type.__call__() (type jest domyślną metaklasą, jeśli nie podano żadnej). Może to prowadzić do niepożądanych efektów:]}

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

 23
Author: Michael Ekoka,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-13 06:23:49

Jedna różnica polega na tym, że definiując metaklasę __call__ wymagasz, aby została wywołana zanim którakolwiek z metod klasy lub podklasy __new__ otrzyma możliwość wywołania.

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'>, (),{}

Zauważ, że SubFoo.__new__ nigdy nie zostanie wezwany. W przeciwieństwie do tego, jeśli zdefiniujesz Foo.__new__ bez metaklasy, pozwolisz podklasom nadpisać Foo.__new__.

Oczywiście, możesz zdefiniować MetaFoo.__call__ aby zadzwonić cls.__new__, ale to zależy od Ciebie. Odmawiając tego, możesz uniemożliwić podklasom posiadanie ich __new__ metoda wywołana.

Nie widzę przekonującej przewagi w używaniu metaklasy tutaj. A ponieważ "proste jest lepsze niż złożone", polecam użycie __new__.

 16
Author: unutbu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-02-26 20:30:41

Pomyślałem, że udoskonalona wersja Pythona 3 odpowiedzi pyroscope może być przydatna dla kogoś, kto skopiuje, wklej i zhakuje (prawdopodobnie ja, kiedy znajdę się z powrotem na tej stronie patrząc go ponownie w ciągu 6 miesięcy). Pochodzi z tego artykułu :

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')

Wyjścia:

** 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

Innym świetnym źródłem podkreślonym w tym samym artykule jest PyCon 2013 Davida Beazleya Python 3 metaprogramming tutorial .

 7
Author: Chris,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-11-07 15:13:41

To kwestia fazy cyklu życia i tego, do czego masz dostęp. __call__ zostaje wywołana Po __new__ i są przekazywane parametry inicjalizacji przed są przekazywane do __init__, więc można nimi manipulować. Wypróbuj ten kod i zbadaj jego wynik:

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
 1
Author: pyroscope,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-08-06 12:54:37