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

92

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

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

De cette classe je dérive 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 une manière pythonique agréable de créer ces classes dynamiquement par un appel de fonction qui place la nouvelle classe dans ma portée actuelle, comme:

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

Comme il y aura des commentaires et des questions pourquoi j'ai besoin de ceci: Les classes dérivées ont toutes exactement la même structure interne avec la différence que le constructeur prend un certain nombre d'arguments précédemment non définis. Ainsi, par exemple, MyClassprend les mots-clés atandis que le constructeur de la classe TestClassprend bet 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)

J'ai réussi à faire fonctionner cela mais je préférerais l'inverse, c'est-à-dire des objets de classe créés dynamiquement.

Alex
la source
Pour être sûr, vous souhaitez que le type d'instance change en fonction des arguments fournis? Comme si je donne un ace sera toujours MyClasset TestClassne prendra jamais un a? Pourquoi ne pas simplement déclarer les 3 arguments BaseClass.__init__()mais les utiliser par défaut None? def __init__(self, a=None, b=None, C=None)?
bétail
Je ne peux rien déclarer dans la classe de base, car je ne connais pas tous les arguments que je pourrais utiliser. Je pourrais avoir 30 classes différentes avec 5 arguments différents chacune, donc déclarer 150 arguments dans le constructur n'est pas une solution.
Alex

Réponses:

141

Ce bit de code vous permet de créer de nouvelles classes avec des noms dynamiques et des noms de paramètres. La vérification des paramètres dans __init__n'autorise tout simplement pas les paramètres inconnus, si vous avez besoin d'autres vérifications, comme le type, ou qu'elles sont obligatoires, ajoutez simplement la logique ici:

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 d'insérer les noms dynamiques dans la portée de dénomination - maintenant, cela n'est pas considéré comme une bonne pratique en Python - vous avez soit des noms de variables, connus au moment du codage, soit des données - et les noms appris au moment de l'exécution le sont davantage " data "que" variables "-

Ainsi, 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 la portée, faites de même, mais utilisez le dictionnaire renvoyé par l' globals() appel au lieu d'un dictionnaire arbitraire:

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

(Il serait en effet possible pour la fonction de fabrique de classes d'insérer le nom dynamiquement sur la portée globale de l'appelant - mais c'est une pratique encore pire, et n'est pas compatible entre les implémentations Python. La façon de faire serait d'obtenir l'appelant frame d'exécution, via sys._getframe (1) et en définissant le nom de la classe dans le dictionnaire global du frame dans son f_globalsattribut).

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

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

update
Quiconque a besoin de cela devrait également vérifier le projet d' aneth - il prétend être capable de décaper et de décoller des classes comme le fait pickle pour des objets ordinaires, et y avait vécu dans certains de mes tests.

jsbueno
la source
2
Si je me souviens bien, ce BaseClass.__init__()serait mieux comme le plus général super(self.__class__).__init__(), qui joue plus bien lorsque les nouvelles classes sont sous-classées. (Référence: rhettinger.wordpress.com/2011/05/26/super-consemed-super )
Eric O Lebigot
@EOL: Ce serait pour les classes déclarées statiquement - mais comme vous n'avez pas le nom de classe réel à coder en dur comme premier paramètre de Super, cela nécessiterait beaucoup de danse. Essayez de le remplacer par superci-dessus et créez une sous-classe d'une classe créée dynamiquement pour la comprendre; Et, d'un autre côté, dans ce cas, vous pouvez avoir la classe de base comme objet général à partir duquel appeler __init__.
jsbueno
J'ai maintenant eu le temps d'examiner la solution proposée, mais ce n'est pas tout à fait ce que je souhaite. Premièrement, il ressemble à __init__de BaseClassest appelé avec un argument, mais en fait BaseClass.__init__prend toujours une liste arbitraire d'arguments de mots-clés. Deuxièmement, la solution ci-dessus définit tous les noms de paramètres autorisés en tant qu'attributs, ce qui n'est pas ce que je souhaite. TOUT argument DOIT aller BaseClass, mais celui que je connais lors de la création de la classe dérivée. Je vais probablement mettre à jour la question ou en poser une plus précise pour la clarifier.
Alex
@jsbueno: C'est vrai, en utilisant le super()donne que je mentionnais TypeError: must be type, not SubSubClass. Si je comprends bien, cela vient du premier argument selfde __init__(), qui est un SubSubClassoù un typeobjet est attendu: cela semble lié au fait super(self.__class__)est un super objet non lié. Quelle est sa __init__()méthode? Je ne sais pas quelle méthode pourrait nécessiter un premier argument de type type. Pourriez-vous expliquer? (Note latérale: mon super()approche n'a en effet pas de sens, ici, car __init__()a une signature variable.)
Eric O Lebigot
1
@EOL: le problème majeur est en fait si vous créez une autre sous-classe de la classe factorisée: self .__ class__ fera référence à cette sous-classe, pas à la classe dans laquelle "super" est appelé - et vous obtenez une récursivité infinie.
jsbueno
89

type() est la fonction qui crée des classes (et en particulier des 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
Eric O Lebigot
la source
En essayant de comprendre cet exemple en utilisant Python 2.7, j'ai obtenu un TypeErrorqui dit __init__() takes exactly 2 arguments (1 given). J'ai trouvé que l'ajout de quelque chose (quelque chose?) Pour combler le vide suffirait. Par exemple, obj = SubClass('foo')s'exécute sans erreur.
DaveL17
Ceci est normal, car il SubClasss'agit d'une sous-classe de BaseClassdans la question et BaseClassprend un paramètre ( classtype, qui est 'foo'dans votre exemple).
Eric O Lebigot
-2

Pour créer une classe avec une valeur d'attribut dynamique, consultez le code ci-dessous. NB. Ce sont des extraits de code en langage de programmation Python

def create_class(attribute_data, **more_data): # define a function with required attributes
    class ClassCreated(optional extensions): # define class with optional inheritance
          attribute1 = adattribute_data # set class attributes with function parameter
          attribute2 = more_data.get("attribute2")

    return ClassCreated # return the created class

# use class

myclass1 = create_class("hello") # *generates a class*
AdefemiGreat
la source