web-dev-qa-db-fra.com

Comment créer dynamiquement des classes dérivées à partir d'une classe de base

Par exemple, j'ai une classe de base comme suit:

class BaseClass(object):
    def __init__(self, classtype):
        self._type = classtype

De cette classe, je tire plusieurs autres classes, par exemple.

class TestClass(BaseClass):
    def __init__(self):
        super(TestClass, self).__init__('Test')

class SpecialClass(BaseClass):
    def __init__(self):
        super(TestClass, self).__init__('Special')

Existe-t-il un moyen agréable, Pythonic, de créer ces classes de manière dynamique par un appel de fonction qui place la nouvelle classe dans ma portée actuelle, comme par exemple:

foo(BaseClass, "My")
a = MyClass()
...

Comme il y aura des commentaires et des questions, j’ai besoin de cela: Les classes dérivées ont toutes exactement la même structure interne, à la différence que le constructeur prend un certain nombre d’arguments précédemment non définis. Ainsi, par exemple, MyClass prend les mots-clés a alors que le constructeur de la classe TestClass prend b et c.

inst1 = MyClass(a=4)
inst2 = MyClass(a=5)
inst3 = TestClass(b=False, c = "test")

Mais ils ne doivent JAMAIS utiliser le type de la classe comme argument d'entrée comme

inst1 = BaseClass(classtype = "My", a=4)

Cela a fonctionné mais je préférerais l’autre façon, c’est-à-dire des objets de classe créés dynamiquement.

79
Alex

Ce morceau de code vous permet de créer de nouvelles classes avec des noms dynamiques et des noms de paramètres. La vérification de paramètre dans __init__ N'autorise pas les paramètres inconnus. Si vous avez besoin d'autres vérifications, telles que le type, ou qu'elles sont obligatoires, ajoutez simplement la logique à cet endroit:

class BaseClass(object):
    def __init__(self, classtype):
        self._type = classtype

def ClassFactory(name, argnames, BaseClass=BaseClass):
    def __init__(self, **kwargs):
        for key, value in kwargs.items():
            # here, the argnames variable is the one passed to the
            # ClassFactory call
            if key not in argnames:
                raise TypeError("Argument %s not valid for %s" 
                    % (key, self.__class__.__name__))
            setattr(self, key, value)
        BaseClass.__init__(self, name[:-len("Class")])
    newclass = type(name, (BaseClass,),{"__init__": __init__})
    return newclass

Et cela fonctionne comme ceci, par exemple:

>>> SpecialClass = ClassFactory("SpecialClass", "a b c".split())
>>> s = SpecialClass(a=2)
>>> s.a
2
>>> s2 = SpecialClass(d=3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 8, in __init__
TypeError: Argument d not valid for SpecialClass

Je vois que vous demandez l’insertion des noms dynamiques dans la portée de dénomination - maintenant, que n’est pas considéré comme une bonne pratique dans Python - vous avez soit une variable les noms, connus au moment du codage, ou les données - et les noms appris au moment de l'exécution sont plus des "données" que des "variables" -

Donc, vous pouvez simplement ajouter vos classes à un dictionnaire et les utiliser à partir de là:

name = "SpecialClass"
classes = {}
classes[name] = ClassFactory(name, params)
instance = classes[name](...)

Et si votre conception a absolument besoin que les noms entrent dans le champ d'application, procédez de même, mais utilisez le dictionnaire renvoyé par l'appel globals() au lieu d'un dictionnaire arbitraire:

name = "SpecialClass"
globals()[name] = ClassFactory(name, params)
instance = SpecialClass(...)

(Il serait en effet possible que la fonction fabrique de classe insère le nom de manière dynamique sur la portée globale de l'appelant - mais c'est encore pire, et n'est pas compatible entre les implémentations de Python. Le chemin faire cela consisterait à obtenir le cadre d'exécution de l'appelant, par le biais de sys._getframe (1) et en définissant le nom de la classe dans le dictionnaire global du cadre dans l'attribut f_globals).

update, tl; dr: Cette réponse était devenue populaire mais restait très spécifique au corps de la question. La réponse générale sur la façon dont "créer dynamiquement des classes dérivées à partir d'une classe de base" dans Python est un simple appel à type en passant le nouveau nom de classe, un tuple avec la classe de base (es) et le corps __dict__ pour la nouvelle classe - comme ceci:

>>> new_class = type("NewClassName", (BaseClass,), {"new_method": lambda self: ...})

mettre à jour
Quiconque en aurait besoin devrait également vérifier le projet aneth - il prétend être en mesure de décaper et de défaire les classes comme le fait le conservateur aux objets ordinaires, et l’avait vécu lors de certains de mes tests. .

122
jsbueno

type() est la fonction qui crée les classes (et en particulier les sous-classes):

def set_x(self, value):
    self.x = value

SubClass = type('SubClass', (BaseClass,), {'set_x': set_x})
# (More methods can be put in SubClass, including __init__().)

obj = SubClass()
obj.set_x(42)
print obj.x  # Prints 42
print isinstance(obj, BaseClass)  # True
74
Eric O Lebigot