Quelles sont les différences entre une «méthode de classe» et une méthode de métaclasse?

12

En Python, je peux créer une méthode de classe en utilisant le @classmethoddécorateur:

>>> class C:
...     @classmethod
...     def f(cls):
...             print(f'f called with cls={cls}')
...
>>> C.f()
f called with cls=<class '__main__.C'>

Alternativement, je peux utiliser une méthode normale (instance) sur une métaclasse:

>>> class M(type):
...     def f(cls):
...             print(f'f called with cls={cls}')
...
>>> class C(metaclass=M):
...     pass
...
>>> C.f()
f called with cls=<class '__main__.C'>

Comme le montre la sortie de C.f(), ces deux approches offrent des fonctionnalités similaires.

Quelles sont les différences entre l'utilisation @classmethodet l'utilisation d'une méthode normale sur une métaclasse?

user200783
la source

Réponses:

5

Comme les classes sont des instances d'une métaclasse, il n'est pas surprenant qu'une "méthode d'instance" sur la métaclasse se comporte comme une méthode de classe.

Cependant, oui, il existe des différences - et certaines d'entre elles sont plus que sémantiques:

  1. La différence la plus importante est qu'une méthode de la métaclasse n'est pas "visible" à partir d'une instance de classe . Cela se produit parce que la recherche d'attribut en Python (de manière simplifiée - les descripteurs peuvent avoir la priorité) recherche un attribut dans l'instance - s'il n'est pas présent dans l'instance, Python regarde alors dans la classe de cette instance, puis la recherche se poursuit. les superclasses de la classe, mais pas sur les classes de la classe. Le stdlib Python utilise cette fonctionnalité dans la abc.ABCMeta.registerméthode. Cette fonctionnalité peut être utilisée pour de bon, car les méthodes liées à la classe elles-mêmes peuvent être réutilisées en tant qu'attributs d'instance sans aucun conflit (mais une méthode serait toujours en conflit).
  2. Une autre différence, bien qu'évidente, est qu'une méthode déclarée dans la métaclasse peut être disponible dans plusieurs classes, non liées par ailleurs - si vous avez des hiérarchies de classes différentes, pas du tout liées à ce qu'elles traitent, mais que vous voulez une fonctionnalité commune pour toutes les classes , vous devriez trouver une classe mixin, qui devrait être incluse comme base dans les deux hiérarchies (disons pour inclure toutes les classes dans un registre d'application). (NB: le mixin peut parfois être un meilleur appel qu'une métaclasse)
  3. Un classmethod est un objet "classmethod" spécialisé, tandis qu'une méthode dans la métaclasse est une fonction ordinaire.

Ainsi, il arrive que le mécanisme utilisé par les méthodes de classe soit le " protocole descripteur ". Alors que les fonctions normales comportent une __get__méthode qui insérera l' selfargument lors de leur récupération à partir d'une instance, et laissera cet argument vide lors de la récupération à partir d'une classe, un classmethodobjet a un autre __get__, qui insérera la classe elle-même (le "propriétaire") en tant que premier paramètre dans les deux situations.

Cela ne fait aucune différence pratique la plupart du temps, mais si vous souhaitez accéder à la méthode en tant que fonction, dans le but d'y ajouter dynamiquement un décorateur, ou tout autre, pour une méthode dans la métaclasse meta.methodrécupère la fonction, prête à être utilisée , alors que vous devez l'utiliser cls.my_classmethod.__func__ pour le récupérer à partir d'une méthode de classe (et ensuite vous devez créer un autre classmethodobjet et le réattribuer , si vous faites un habillage).

En gros, ce sont les 2 exemples:


class M1(type):
    def clsmethod1(cls):
        pass

class CLS1(metaclass=M1):
    pass

def runtime_wrap(cls, method_name, wrapper):
    mcls = type(cls)
    setattr(mcls, method_name,  wrapper(getatttr(mcls, method_name)))

def wrapper(classmethod):
    def new_method(cls):
        print("wrapper called")
        return classmethod(cls)
    return new_method

runtime_wrap(cls1, "clsmethod1", wrapper)

class CLS2:
    @classmethod
    def classmethod2(cls):
        pass

 def runtime_wrap2(cls, method_name, wrapper):
    setattr(cls, method_name,  classmethod(
                wrapper(getatttr(cls, method_name).__func__)
        )
    )

runtime_wrap2(cls1, "clsmethod1", wrapper)

En d'autres termes: à part la différence importante qu'une méthode définie dans la métaclasse est visible à partir de l'instance et qu'un classmethodobjet ne le fait pas, les autres différences, au moment de l'exécution, sembleront obscures et dénuées de sens - mais cela se produit parce que le langage n'a pas besoin d'aller hors de son chemin avec des règles spéciales pour les méthodes de classe: Les deux façons de déclarer une méthode de classe sont possibles, en conséquence de la conception du langage - une, pour le fait qu'une classe est elle-même un objet, et une autre, comme une possibilité parmi beaucoup d'autres, de l'utilisation du protocole descripteur qui permet de spécialiser l'accès aux attributs dans une instance et dans une classe:

Le classmethodbuiltin est défini en code natif, mais il pourrait simplement être codé en python pur et fonctionnerait exactement de la même manière. Le soufflet de classe 5 lignes peut être utilisé comme classmethoddécorateur sans différences d'exécution par rapport à la @classmethod" at all (though distinguishable through introspection such as calls toreprésentation , and evenintégrée bien sûr):


class myclassmethod:
    def __init__(self, func):
        self.__func__ = func
    def __get__(self, instance, owner):
        return lambda *args, **kw: self.__func__(owner, *args, **kw)

Et, au-delà des méthodes, il est intéressant de garder à l'esprit que les attributs spécialisés tels que a @propertysur la métaclasse fonctionneront comme des attributs de classe spécialisés, tout de même, sans aucun comportement surprenant.

jsbueno
la source
2

Lorsque vous le formulez comme vous l'avez fait dans la question, les @classmethodmétaclasses et peuvent sembler similaires, mais elles ont des objectifs plutôt différents. La classe qui est injectée dans l' @classmethodargument de est généralement utilisée pour construire une instance (c'est-à-dire un constructeur alternatif). D'un autre côté, les métaclasses sont généralement utilisées pour modifier la classe elle-même (par exemple, comme ce que Django fait avec ses modèles DSL).

Cela ne veut pas dire que vous ne pouvez pas modifier la classe à l'intérieur d'une méthode de classe. Mais alors la question devient pourquoi n'avez-vous pas défini la classe de la façon dont vous voulez la modifier en premier lieu? Sinon, il pourrait suggérer à un refactor d'utiliser plusieurs classes.

Développons un peu le premier exemple.

class C:
    @classmethod
    def f(cls):
        print(f'f called with cls={cls}')

Empruntant aux documents Python , ce qui précède se développera comme suit:

class ClassMethod(object):
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, klass=None):
        if klass is None:
            klass = type(obj)
        def newfunc(*args):
            return self.f(klass, *args)
        return newfunc

class C:
    def f(cls):
        print(f'f called with cls={cls}')
    f = ClassMethod(f)

Notez comment __get__peut prendre une instance ou la classe (ou les deux), et donc vous pouvez faire les deux C.fet C().f. C'est différent de l'exemple de métaclasse que vous donnez qui lancera un AttributeErrorfor C().f.

De plus, dans l'exemple de la métaclasse, fn'existe pas dans C.__dict__. Lorsque vous recherchez l'attribut favec C.f, l'interpréteur regarde C.__dict__puis, après avoir échoué à trouver, regarde type(C).__dict__(qui est M.__dict__). Cela peut importe si vous voulez la flexibilité de passer outre fà C, bien que je doute que ce ne sera jamais l' utilisation pratique.

Kent Shikama
la source
0

Dans votre exemple, la différence serait dans certaines autres classes qui auront M défini comme leur métaclasse.

class M(type):
    def f(cls):
        pass

class C(metaclass=M):
    pass

class C2(metaclass=M):
    pass

C.f()
C2.f()
class M(type):
     pass

class C(metaclass=M):
     @classmethod
     def f(cls):
        pass

class C2(metaclass=M):
    pass

C.f()
# C2 does not have 'f'

En savoir plus sur les métaclasses Quels sont les cas d'utilisation (concrets) des métaclasses?

andole
la source
0

@Classmethod et Metaclass sont différents.

Tout en python est un objet. Chaque chose signifie chaque chose.

Qu'est-ce que la métaclasse?

Comme dit, chaque chose est un objet. Les classes sont également des objets, en fait, les classes sont des instances d'autres objets mystérieux formellement appelés méta-classes. La métaclasse par défaut en python est "type" si non spécifié

Par défaut, toutes les classes définies sont des instances de type.

Les classes sont des instances de méta-classes

Peu de points importants sont de comprendre le comportement mentionné

  • Comme les classes sont des instances de méta-classes.
  • Comme tout objet instancié, les objets (instances) obtiennent leurs attributs de la classe. La classe obtiendra ses attributs de Meta-Class

Envisagez de suivre le code

class Meta(type):
    def foo(self):
        print(f'foo is called self={self}')
        print('{} is instance of {}: {}'.format(self, Meta, isinstance(self, Meta)))

class C(metaclass=Meta):
    pass

C.foo()

Où,

  • la classe C est une instance de la classe Meta
  • "classe C" est un objet de classe qui est une instance de "classe Meta"
  • Comme tout autre objet (instance), la "classe C" a accès à ses attributs / méthodes définis dans sa classe "classe Meta"
  • Donc, décodage de "C.foo ()". "C" est une instance de "Meta" et "foo" est une méthode appelant une instance de "Meta" qui est "C".
  • Le premier argument de la méthode "foo" fait référence à l'instance et non à la classe contrairement à "classmethod"

On peut vérifier que si "classe C" est une instance de "Class Meta

  isinstance(C, Meta)

Qu'est-ce que la méthode de classe?

Les méthodes Python seraient liées. Comme python impose la restriction, cette méthode doit être invoquée uniquement avec l'instance. Parfois, nous pouvons souhaiter invoquer des méthodes directement via la classe sans aucune instance (un peu comme les membres statiques en java) sans avoir à créer d'instance. Par exemple, une instance par défaut est requise pour appeler la méthode. Comme solution de contournement, Python fournit une méthode de classe de fonction intégrée pour lier une méthode donnée à une classe au lieu d'une instance.

Comme les méthodes de classe sont liées à la classe. Il prend au moins un argument qui fait référence à la classe elle-même au lieu de l'instance (self)

si la méthode de classe fonction / décorateur intégrée est utilisée. Le premier argument sera la référence à la classe au lieu de l'instance

class ClassMethodDemo:
    @classmethod
    def foo(cls):
        print(f'cls is ClassMethodDemo: {cls is ClassMethodDemo}')

Comme nous avons utilisé "classmethod", nous appelons la méthode "foo" sans créer aucune instance comme suit

ClassMethodDemo.foo()

L'appel de méthode ci-dessus renverra True. Puisque le premier argument cls fait en effet référence à "ClassMethodDemo"

Sommaire:

  • Classmethod reçoit le premier argument qui est "une référence à la classe (traditionnellement appelée cls) elle-même"
  • Les méthodes des méta-classes ne sont pas des méthodes de classe. Les méthodes des méta-classes reçoivent le premier argument qui est "une référence à l'instance (traditionnellement appelée self) et non à la classe"
neotam
la source