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?
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__
]
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:
__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.__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.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:
__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.__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.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.
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.
L'appel direct de la classe de base devrait fonctionner (au lieu d'utiliser super ()):
def __init__(self):
Base.__init__(self)
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)