web-dev-qa-db-fra.com

Utilisation de la méthode __call__ d'une métaclasse au lieu de __new__?

Lors de la discussion sur les métaclasses, les documents état:

Vous pouvez bien sûr également remplacer d'autres méthodes de classe (ou ajouter de nouvelles méthodes); Par exemple, définir une méthode __call__() personnalisée dans la métaclasse permet un comportement personnalisé lorsque la classe est appelée, par ex. pas toujours créer une nouvelle instance.

Ma question est la suivante: supposons que je veuille avoir un comportement personnalisé lorsque la classe est appelée, par exemple la mise en cache au lieu de créer de nouveaux objets. Je peux le faire en redéfinissant la méthode __new__ de la classe. Quand voudrais-je définir une métaclasse avec __call__ à la place? Qu'est-ce que cette approche donne qui n'est pas réalisable avec __new__?

30
Eli Bendersky

La réponse directe à votre question est la suivante: lorsque vous souhaitez faire plus que simplement personnaliser la création d’instance, ou lorsque vous souhaitez séparer ce que la classe fait de la manière dont elle est créée.

Voir ma réponse à Créer un singleton en Python et la discussion associée.

Il y a plusieurs avantages.

  1. Cela vous permet de séparer ce que la classe fait des détails de sa création. La métaclasse et la classe sont chacune responsables d'une chose.

  2. Vous pouvez écrire le code une fois dans une métaclasse et l'utiliser pour personnaliser le comportement des appels de plusieurs classes sans se soucier de l'héritage multiple.

  3. Les sous-classes peuvent remplacer le comportement dans leur méthode __new__, mais __call__ sur une métaclasse n'a même pas besoin d'appeler __new__.

  4. S'il y a du travail d'installation, vous pouvez le faire dans la méthode __new__ de la métaclasse, et cela ne se produit qu'une fois, au lieu de chaque fois que la classe est appelée.

Dans de nombreux cas, la personnalisation de __new__ fonctionne également si le principe de la responsabilité unique ne vous inquiète pas.

Mais il y a d'autres cas d'utilisation qui doivent se produire plus tôt, lorsque la classe est créée, plutôt que lorsque l'instance est créée. C'est quand ils entrent en jeu qu'une métaclasse est nécessaire. Voir Quels sont vos cas d'utilisation (concrets) pour les métaclasses en Python? pour beaucoup d’excellents exemples.

18
agf

Une différence est que, en définissant une méthode métaclasse __call__, vous exigez qu’elle soit appelée avant que l’une des méthodes __new__ de la classe ou des sous-classes ne puisse être appelée.

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

Notez que SubFoo.__new__ n'est jamais appelé. En revanche, si vous définissez Foo.__new__ sans métaclasse, vous autorisez les sous-classes à remplacer Foo.__new__.

Bien sûr, vous pouvez définir MetaFoo.__call__ pour appeler cls.__new__, mais cela dépend de vous. En refusant de le faire, vous pouvez empêcher les sous-classes de faire appeler leur méthode __new__.

Je ne vois pas d'avantage convaincant à utiliser une métaclasse ici. Et puisque "Simple est meilleur que complexe", je recommanderais d'utiliser __new__.

14
unutbu

Les différences subtiles deviennent un peu plus visibles lorsque vous observez attentivement l'ordre d'exécution de ces méthodes.

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)

Notez que le code ci-dessus ne fait pas do autre chose que d’enregistrer ce que nous faisons. Chaque méthode renvoie à sa mise en œuvre parente, c'est-à-dire sa valeur par défaut. Donc, à côté de la journalisation, c'est comme si vous aviez simplement déclaré les choses comme suit:

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

Et maintenant, créons une instance de 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__()

Par conséquent, si type est le parent de Meta_1, nous pouvons imaginer une pseudo-implémentation de type.__call__() en tant que telle:

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

Avis de l'ordre d'appel supérieur à Meta_1.__call__() (ou dans ce cas type.__call__()) a la possibilité d'influer sur le fait que des appels à Class_1.__new__() et Class_1.__init__() soient éventuellement effectués. Au cours de son exécution, Meta_1.__call__() pourrait renvoyer un objet qui n’a même pas été touché. Prenons par exemple cette approche du motif singleton:

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)

Observons ce qui se passe lors de tentatives répétées de création d'un objet de type 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

Observez maintenant cette implémentation en utilisant la méthode __new__() de la classe pour essayer de faire la même chose.

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)

Notez que l'implémentation ci-dessus, même si vous enregistrez avec succès un singleton sur la classe, n'empêche pas l'appel de __init__(); cela se produit implicitement dans type.__call__() (type étant la métaclasse par défaut si aucune n'est spécifiée). Cela pourrait entraîner des effets indésirables:

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
10
Michael Ekoka

C'est une question de phases du cycle de vie et de ce à quoi vous avez accès. __call__ est appelé après __new__ et reçoit les paramètres d'initialisation avant ils sont transmis à __init__, afin que vous puissiez les manipuler. Essayez ce code et étudiez sa sortie:

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
pyroscope

Je pensais qu'une version complète de la réponse du pyroscope en Python 3 serait peut-être pratique pour quelqu'un à qui copier, coller et bidouiller (probablement moi, quand je me retrouverai sur cette page à le regarder de nouveau dans 6 mois). Il est tiré de cet article :

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

Les sorties:

** 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 Tutoriel sur la métaprogrammation Python 3 de David Beazley .

0
Chris