web-dev-qa-db-fra.com

Comment s'inscrire automatiquement

Je veux avoir une instance de classe enregistrée lorsque la classe est définie. Idéalement, le code ci-dessous ferait l'affaire.

registry = {}

def register( cls ):
   registry[cls.__name__] = cls() #problem here
   return cls

@register
class MyClass( Base ):
   def __init__(self):
      super( MyClass, self ).__init__() 

Malheureusement, ce code génère l'erreur NameError: global name 'MyClass' is not defined.

Ce qui se passe, c'est à la ligne #problem here que j'essaie d'instancier une MyClass mais le décorateur n'est pas encore revenu et il n'existe donc pas.

Est-ce que l’autre côté utilise des métaclasses ou quelque chose comme ça?

33
deft_code

Oui, les méta-classes peuvent le faire. La méthode __new__ d'une méta-classe retourne la classe, donc inscrivez-la avant de la retourner.

class MetaClass(type):
    def __new__(cls, clsname, bases, attrs):
        newclass = super(MetaClass, cls).__new__(cls, clsname, bases, attrs)
        register(newclass)  # here is your register function
        return newclass

class MyClass(object):
    __metaclass__ = MetaClass

L'exemple précédent fonctionne avec Python 2.x. Dans Python 3.x, la définition de MyClass est légèrement différente (alors que MetaClass n'est pas affiché car il est inchangé - sauf que super(MetaClass, cls) peut devenir super() si vous voulez):

#Python 3.x

class MyClass(metaclass=MetaClass):
    pass

Depuis Python 3.6, il existe également une nouvelle méthode __init_subclass__ (voir PEP 487 ) qui peut être utilisée à la place d'une méta classe (merci à @matusko pour sa réponse ci-dessous):

class ParentClass:
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        register(cls)

class MyClass(ParentClass):
    pass

[edit: correction de l'argument cls manquant à super().__new__()]

[edit: exemple ajouté Python 3.x]

[edit: ordre corrigé des arguments en super (), et description améliorée des différences 3.x]

[edit: exemple Python 3.6 __init_subclass__]

38
dappawit

Le problème ne provient pas de la ligne que vous avez indiquée, mais de l'appel super dans la méthode __init__. Le problème persiste si vous utilisez une métaclasse comme suggéré par dappawit; la raison pour laquelle l'exemple de cette réponse fonctionne est simplement que dappawit a simplifié votre exemple en omettant la classe Base et donc l'appel super. Dans l'exemple suivant, ni ClassWithMeta ni DecoratedClass ne fonctionnent:

registry = {}
def register(cls):
    registry[cls.__name__] = cls()
    return cls

class MetaClass(type):
    def __new__(cls, clsname, bases, attrs):
        newclass = super(cls, MetaClass).__new__(cls, clsname, bases, attrs)
        register(newclass)  # here is your register function
        return newclass

class Base(object):
    pass


class ClassWithMeta(Base):
    __metaclass__ = MetaClass

    def __init__(self):
        super(ClassWithMeta, self).__init__()


@register
class DecoratedClass(Base):
    def __init__(self):
        super(DecoratedClass, self).__init__()

Le problème est le même dans les deux cas; la fonction register est appelée (soit par la métaclasse, soit directement en tant que décorateur) après la création de objet class, mais avant qu’il ait été lié à un nom. C'est là que super devient gnarly (dans Python 2.x), car il vous est demandé de faire référence à la classe dans l'appel super, ce que vous ne pouvez raisonnablement faire qu'en utilisant le nom global et en vous assurant qu'elle aura été liée à ce nom. au moment où l'appel super est appelé. Dans ce cas, cette confiance est mal placée.

Je pense qu'une métaclasse est la mauvaise solution ici. Les métaclasses servent à créer une famille de classes ayant un comportement personnalisé commun, tout comme les classes servent à créer une famille d'instances ayant un comportement personnalisé commun. Tout ce que vous faites est d'appeler une fonction sur une classe. Vous ne définiriez pas une classe pour appeler une fonction sur une chaîne, vous ne devriez pas non plus définir une métaclasse pour appeler une fonction sur une classe.

Le problème est donc une incompatibilité fondamentale entre: (1) l’utilisation de hooks dans le processus de création de classe pour créer des instances de la classe et (2) l’utilisation de super.

Une façon de résoudre ce problème consiste à ne pas utiliser super. super résout un problème difficile, mais en présente d’autres (c’est l’un d’eux). Si vous utilisez un schéma d'héritage multiple complexe, les problèmes de super sont meilleurs que ceux de ne pas utiliser super et si vous héritez de classes tierces utilisant super, vous devez utiliser super. Si aucune de ces conditions n'est vraie, le simple fait de remplacer vos appels super par des appels de classe de base directs peut s'avérer une solution raisonnable.

Une autre solution consiste à ne pas associer register à la création de classe. Ajouter register(MyClass) après chacune de vos définitions de classe revient à ajouter @register avant elles ou __metaclass__ = Registered (ou ce que vous appelez la métaclasse). Une ligne en bas est beaucoup moins auto-documentée qu'une déclaration de Nice en haut de la classe cependant, donc ça ne semble pas génial, mais encore une fois, cela peut en fait être une solution raisonnable.

Enfin, vous pouvez vous tourner vers des hacks qui sont désagréables, mais qui fonctionneront probablement. Le problème est qu'un nom est recherché dans la portée globale d'un module juste avant il a été lié ici. Donc, vous pouvez tricher, comme suit:

def register(cls):
    name = cls.__name__
    force_bound = False
    if '__init__' in cls.__dict__:
        cls.__init__.func_globals[name] = cls
        force_bound = True
    try:
        registry[name] = cls()
    finally:
        if force_bound:
            del cls.__init__.func_globals[name]
    return cls

Voici comment cela fonctionne:

  1. Nous vérifions d’abord si __init__ est dans cls.__dict__ (et non si elle possède un attribut __init__, qui sera toujours vrai). Si elle hérite d'une méthode __init__ d'une autre classe, nous allons probablement bien (car la superclasse will est déjà liée à son nom de la manière habituelle), et la magie que nous sommes sur le point de faire ne fonctionne pas. sur object.__init__ afin d'éviter d'essayer cela si la classe utilise un __init__ par défaut.
  2. Nous examinons la méthode __init__ et prenons son dictionnaire func_globals, qui est l'endroit où les recherches globales (telles que la recherche de la classe à laquelle un appel super sera fait) iront. C'est normalement le dictionnaire global du module où la méthode __init__ a été définie à l'origine. Un tel dictionnaire est à propos de dans lequel le cls.__name__ doit être inséré dès le retour de register; nous l'insérons donc nous-mêmes plus tôt.
  3. Nous créons enfin une instance et l'insérons dans le registre. Ceci est dans un bloc try/finally pour nous assurer de supprimer la liaison créée, que la création d'une instance lève ou non une exception; c'est très peu probable d'être nécessaire (puisque 99,999% du temps, le nom est sur le point de rebondir de toute façon), mais il est préférable de garder cette magie aussi isolée que possible afin de minimiser le risque qu'une autre magie étrange interagisse mal avec il.

Cette version de register fonctionnera que ce soit invoqué en tant que décorateur ou par la métaclasse (ce qui, à mon avis, n’est toujours pas un bon usage d’une métaclasse). Il y a des cas obscurs où il échouera cependant:

  1. Je peux imaginer une classe étrange qui non ait une méthode __init__ mais en hérite une qui appelle self.someMethod, et someMethod est surchargé dans la classe en cours de définition et effectue un appel super. Probablement improbable.
  2. La méthode __init__ a peut-être été définie dans un autre module à l'origine, puis utilisée dans la classe en faisant __init__ = externally_defined_function dans le bloc de classe. Cependant, l'attribut func_globals de l'autre module, ce qui signifie que notre liaison temporaire engloberait toute définition du nom de cette classe dans ce module (oops). Encore une fois, peu probable.
  3. Probablement d'autres cas étranges auxquels je n'ai pas pensé.

Vous pourriez essayer d'ajouter plus de piratages pour le rendre un peu plus robuste dans ces situations, mais la nature de Python est à la fois que ce type de piratage est possible et qu'il est impossible de les rendre absolument à l'épreuve des balles.

11
Ben

Depuis Python 3.6, vous n'avez pas besoin de métaclasses pour résoudre ce problème

En python 3.6, une personnalisation plus simple de la création de classes a été introduite ( PEP 487 ).

Un hook __init_subclass__ qui initialise toutes les sous-classes d'une classe donnée.

La proposition comprend l'exemple suivant de enregistrement de sous-classe

class PluginBase:
    subclasses = []

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.subclasses.append(cls)

Dans cet exemple, PluginBase.subclasses contiendra une liste complète de toutes les sous-classes de l’arborescence complète de l’héritage. Il convient de noter que cela fonctionne également bien en tant que classe mixin.

11
matusko

L'appel direct de la classe de base devrait fonctionner (au lieu d'utiliser super ()):

  def __init__(self):
        Base.__init__(self)
1
Andreas Jung

Les réponses ici ne fonctionnaient pas pour moi dans python3 , car __metaclass__ ne fonctionnait pas.

Voici mon code enregistrant toutes les sous-classes d'une classe au moment de leur définition:

registered_models = set()

class RegisteredModel(type):
    def __new__(cls, clsname, superclasses, attributedict):
        newclass = type.__new__(cls, clsname, superclasses, attributedict)
        # condition to prevent base class registration
        if superclasses:
            registered_models.add(newclass)
        return newclass

class CustomDBModel(metaclass=RegisteredModel):
    pass

class BlogpostModel(CustomDBModel):
    pass

class CommentModel(CustomDBModel):
   pass

# prints out {<class '__main__.BlogpostModel'>, <class '__main__.CommentModel'>}
print(registered_models)
0
hlidka