2010-02-03 5 views
3

Je souhaite conserver un dictionnaire de sous-classes (toutes non incluses immédiatement) dans une classe de base, afin de pouvoir les instancier à partir d'une chaîne. Je fais ceci parce que le CLSID est envoyé par l'intermédiaire d'un formulaire Web, ainsi je veux limiter les choix à ceux choisis des sous-classes. (Je ne veux pas eval()/globals() le nom de la classe).Comment obtenir une référence à la classe actuelle du corps de la classe?

class BaseClass(object): 
    CLSID = 'base' 
    CLASSES = {} 

    def from_string(str): 
     return CLASSES[str]() 

class Foo(BaseClass): 
    CLSID = 'foo' 
    BaseClass.CLASSES[CLSID] = Foo 

class Bar(BaseClass): 
    CLSID = 'bar' 
    BaseClass.CLASSES[CLSID] = Bar 

Cela ne marche évidemment pas. Mais y a-t-il quelque chose comme @classmethod pour init? L'idée est que ce classmethod ne s'exécuterait qu'une seule fois lors de la lecture de chaque classe et enregistrerait la classe avec le baseclass. Quelque chose comme ce qui suit pourrait alors travailler: (sauverait également la ligne supplémentaire dans Foo et Bar)

class BaseClass(object): 
    CLSID = 'base' 
    CLASSES = {} 

    @classmethod 
    def __init__(cls): 
     BaseClass.CLASSES[cls.CLSID] = cls 

    def from_string(str): 
     return CLASSES[str]() 

Je pensais à l'aide __subclasses__ puis filter() sur CLSID, mais qui ne fonctionne que pour les sous-classes immédiates. Donc, en espérant que j'ai expliqué mon but, la question est de savoir comment faire fonctionner ce travail? Ou est-ce que je vais à ce sujet d'une manière complètement fausse?

+3

Habituellement, nous utilisons une usine séparée pour cela, pas la superclasse. Pourquoi n'utilisez-vous pas le modèle de conception ** Factory ** le plus commun? –

+0

Je pensais qu'il serait plus propre de mettre la méthode d'usine dans la super-classe. De plus, je ne veux pas avoir à coder en dur les options pour les classes/ids, je veux aussi que toute sous-classe soit autonome, afin que la classe de base et les méthodes d'usine puissent rester une boîte noire. – noio

+0

Mettre l'usine dans la superclasse est loin d'être propre - comme votre question le révèle. "code dur les options?" n'a pas de sens. L'usine est aussi "black box" que la superclasse, donc je ne comprends pas. –

Répondre

6

lier Irrévocablement cela avec la classe de base:

class AutoRegister(type): 
    def __new__(mcs, name, bases, D): 
    self = type.__new__(mcs, name, bases, D) 
    if "ID" in D: # only register if has ID attribute directly 
     if self.ID in self._by_id: 
     raise ValueError("duplicate ID: %r" % self.ID) 
     self._by_id[self.ID] = self 
    return self 

class Base(object): 
    __metaclass__ = AutoRegister 
    _by_id = {} 
    ID = "base" 

    @classmethod 
    def from_id(cls, id): 
    return cls._by_id[id]() 

class A(Base): 
    ID = "A" 

class B(Base): 
    ID = "B" 

print Base.from_id("A") 
print Base.from_id("B") 

ou de tenir des préoccupations disparates en fait séparés:

class IDFactory(object): 
    def __init__(self): 
    self._by_id = {} 
    def register(self, cls): 
    self._by_id[cls.ID] = cls 
    return cls 

    def __call__(self, id, *args, **kwds): 
    return self._by_id[id](*args, **kwds) 
    # could use a from_id function instead, as above 

factory = IDFactory() 

@factory.register 
class Base(object): 
    ID = "base" 

@factory.register 
class A(Base): 
    ID = "A" 

@factory.register 
class B(Base): 
    ID = "B" 

print factory("A") 
print factory("B") 

Vous avez peut-être ramassé sur lequel je préfère. Défini séparément de la hiérarchie des classes, vous pouvez facilement étendre et modifier, par exemple en vous inscrivant sous deux noms (en utilisant un attribut ID ne permet un):

class IDFactory(object): 
    def __init__(self): 
    self._by_id = {} 

    def register(self, cls): 
    self._by_id[cls.ID] = cls 
    return cls 

    def register_as(self, name): 
    def wrapper(cls): 
     self._by_id[name] = cls 
     return cls 
    return wrapper 

    # ... 

@factory.register_as("A") # doesn't require ID anymore 
@factory.register   # can still use ID, even mix and match 
@factory.register_as("B") # imagine we got rid of B, 
class A(object):   # and A fulfills that roll now 
    ID = "A" 

Vous pouvez également conserver l'instance d'usine « à l'intérieur » de la base tout en le maintenant découplé:

class IDFactory(object): 
    #... 

class Base(object): 
    factory = IDFactory() 

    @classmethod 
    def register(cls, subclass): 
    if subclass.ID in cls.factory: 
     raise ValueError("duplicate ID: %r" % subclass.ID) 
    cls.factory[subclass.ID] = subclass 
    return subclass 

@Base.factory.register # still completely decoupled 
         # (it's an attribute of Base, but that can be easily 
         # changed without modifying the class A below) 
@Base.register # alternatively more coupled, but possibly desired 
class A(Base): 
    ID = "A" 
+0

Merci! La solution avec les décorateurs semble très attrayante. Je vois maintenant pourquoi la première option est une mauvaise idée :). Je pourrais néanmoins inclure/coupler l'usine dans la classe de base, car d'autres 'groupes' de classes (héritant d'une autre classe de base) ont des restrictions de sécurité différentes. – noio

2

Vous pouvez muck avec métaclasses pour faire le travail pour vous, mais je pense qu'une solution plus simple pourrait suffire:

class BaseClass(object): 
    CLASS_ID = None 
    _CLASSES = {} 

    @classmethod 
    def create_from_id(cls, class_id): 
     return CLASSES[class_id]() 

    @classmethod 
    def register(cls): 
     assert cls.CLASS_ID is not None, "subclass %s must define a CLASS_ID" % cls 
     cls._CLASSES[cls.CLASS_ID] = cls 

ensuite pour définir une sous-classe il suffit d'utiliser:

class Foo(BaseClass): 
    CLASS_ID = 'foo' 

Foo.register() 

Et enfin utiliser la méthode usine dans la BaseClass pour créer les instances pour vous:

foo = BaseClass.create_from_id('foo') 

Dans cette solution , après la définition de classe, vous devez appeler la méthode de classe register pour enregistrer la sous-classe dans la classe de base. En outre, le CLASS_ID par défaut est Aucun pour éviter d'écraser la classe de base dans le Registre si un utilisateur oublie de le définir.

+0

+1 pour m'avoir conseillé de ne pas 'muck with metaclasses' – noio

Questions connexes