Wie kann ich abgeleitete Klassen dynamisch aus einer Basisklasse erstellen?

92

Zum Beispiel habe ich eine Basisklasse wie folgt:

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

Aus dieser Klasse leite ich mehrere andere Klassen ab, z

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

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

Gibt es eine schöne, pythonische Möglichkeit, diese Klassen dynamisch durch einen Funktionsaufruf zu erstellen, der die neue Klasse in meinen aktuellen Bereich einfügt, wie z.

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

Da es Kommentare und Fragen geben wird, warum ich das brauche: Die abgeleiteten Klassen haben alle genau die gleiche interne Struktur mit dem Unterschied, dass der Konstruktor eine Reihe von zuvor undefinierten Argumenten verwendet. So nimmt zum Beispiel MyClassdie Schlüsselwörter, awährend der Konstruktor der Klasse und TestClassnimmt .bc

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

Sie sollten jedoch NIEMALS den Typ der Klasse als Eingabeargument verwenden

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

Ich habe das zum Laufen gebracht, würde aber den anderen Weg bevorzugen, dh dynamisch erstellte Klassenobjekte.

Alex
quelle
Um sicherzugehen, dass sich der Instanztyp abhängig von den angegebenen Argumenten ändert? Wie wenn ich ein gebe a, wird es immer sein MyClassund TestClasswird es nie nehmen a? Warum nicht einfach alle 3 Argumente deklarieren, BaseClass.__init__()sondern standardmäßig alle auf None? def __init__(self, a=None, b=None, C=None)?
Acattle
Ich kann nichts in der Basisklasse deklarieren, da ich nicht alle Argumente kenne, die ich verwenden könnte. Ich könnte 30 verschiedene Klassen mit jeweils 5 verschiedenen Argumenten haben, daher ist es keine Lösung, 150 Argumente im Konstruktor zu deklarieren.
Alex

Antworten:

141

Mit diesem Code können Sie neue Klassen mit dynamischen Namen und Parameternamen erstellen. Die Parameterüberprüfung in __init__just erlaubt keine unbekannten Parameter. Wenn Sie andere Überprüfungen wie den Typ benötigen oder diese obligatorisch sind, fügen Sie einfach die Logik dort hinzu:

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

Und das funktioniert zum Beispiel so:

>>> 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

Ich sehe, dass Sie darum bitten, die dynamischen Namen in den Namensbereich einzufügen - das wird in Python nicht als gute Vorgehensweise angesehen - Sie haben entweder Variablennamen, die zur Codierungszeit bekannt sind, oder Daten - und Namen, die zur Laufzeit gelernt wurden, sind mehr " Daten "als" Variablen "-

Sie können Ihre Klassen also einfach einem Wörterbuch hinzufügen und von dort aus verwenden:

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

Und wenn Ihr Design unbedingt die Namen benötigt, um in den Geltungsbereich zu gelangen, machen Sie dasselbe, aber verwenden Sie das vom globals() Aufruf zurückgegebene Wörterbuch anstelle eines beliebigen Wörterbuchs:

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

(Es wäre in der Tat möglich, dass die Klassenfactory-Funktion den Namen dynamisch in den globalen Bereich des Aufrufers einfügt. Dies ist jedoch eine noch schlechtere Vorgehensweise und nicht für alle Python-Implementierungen kompatibel. Der Weg dazu besteht darin, den Namen des Aufrufers abzurufen Ausführungsrahmen über sys._getframe (1) und Festlegen des Klassennamens im globalen Wörterbuch des Rahmens in seinem f_globalsAttribut).

update, tl; dr: Diese Antwort war populär geworden, immer noch sehr spezifisch für den Fragetext. Die allgemeine Antwort zum "dynamischen Erstellen abgeleiteter Klassen aus einer Basisklasse" in Python ist ein einfacher Aufruf zum typeÜbergeben des neuen Klassennamens, ein Tupel mit den Basisklassen und dem __dict__Hauptteil für die neue Klasse - wie folgt :

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

Update
Jeder, der dies benötigt, sollte auch das Dill- Projekt überprüfen - es behauptet, in der Lage zu sein, Klassen zu beizen und zu entfernen, genau wie es Beizen bei normalen Objekten tut, und hat es in einigen meiner Tests gelebt.

jsbueno
quelle
2
Wenn ich mich richtig erinnere, BaseClass.__init__()wäre es besser als die allgemeinere super(self.__class__).__init__(), die besser spielt, wenn die neuen Klassen in Unterklassen unterteilt sind. (Referenz: rhettinger.wordpress.com/2011/05/26/super-considered-super )
Eric O Lebigot
@EOL: Dies wäre für statisch deklarierte Klassen der Fall - aber da Sie nicht den tatsächlichen Klassennamen haben, der als erster Parameter für Super fest codiert werden soll, müsste viel herumgetanzt werden. Versuchen Sie, es durch superoben zu ersetzen, und erstellen Sie eine Unterklasse einer dynamisch erstellten Klasse, um es zu verstehen. Andererseits können Sie in diesem Fall die Basisklasse als allgemeines Objekt verwenden, von dem aus Sie aufrufen können __init__.
Jsbueno
Jetzt hatte ich etwas Zeit, um mir die vorgeschlagene Lösung anzusehen, aber es ist nicht ganz das, was ich will. Erstens sieht es so __init__aus, BaseClassals würde von mit einem Argument aufgerufen, aber tatsächlich wird BaseClass.__init__immer eine beliebige Liste von Schlüsselwortargumenten verwendet. Zweitens legt die obige Lösung alle zulässigen Parameternamen als Attribute fest, was ich nicht möchte. JEDES Argument muss verwendet werden BaseClass, aber welches kenne ich beim Erstellen der abgeleiteten Klasse? Ich werde die Frage wahrscheinlich aktualisieren oder eine genauere Frage stellen, um sie klarer zu machen.
Alex
@jsbueno: Richtig, mit dem, was super()ich erwähnte, gibt TypeError: must be type, not SubSubClass. Wenn ich das richtig verstehe, kommt dies aus dem ersten Argument selfvon __init__(), SubSubClassbei dem ein typeObjekt erwartet wird: Dies scheint damit verbunden zu sein, dass super(self.__class__)es sich um ein ungebundenes Superobjekt handelt. Was ist ihre __init__()Methode? Ich bin mir nicht sicher, für welche Methode ein erstes Argument vom Typ erforderlich sein könnte type. Könntest du erklären? (Randnotiz: Mein super()Ansatz macht hier in der Tat keinen Sinn, weil er __init__()eine variable Signatur hat.)
Eric O Lebigot
1
@EOL: Das Hauptproblem besteht darin, dass Sie eine andere Unterklasse der faktorisierten Klasse erstellen: self .__ class__ bezieht sich auf diese Unterklasse, nicht auf die Klasse, in der "super" aufgerufen wird - und Sie erhalten eine unendliche Rekursion.
Jsbueno
89

type() ist die Funktion, die Klassen (und insbesondere Unterklassen) erstellt:

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
quelle
Beim Versuch, dieses Beispiel mit Python 2.7 zu verstehen, habe ich ein TypeErrorsolches erhalten __init__() takes exactly 2 arguments (1 given). Ich fand, dass das Hinzufügen von etwas (irgendetwas?), Um die Lücke zu füllen, ausreichen würde. Läuft beispielsweise obj = SubClass('foo')fehlerfrei.
DaveL17
Dies ist normal, da SubClasses sich um eine Unterklasse von BaseClassin der Frage handelt und BaseClassein Parameter verwendet wird ( classtypewie 'foo'in Ihrem Beispiel).
Eric O Lebigot
-2

Überprüfen Sie den folgenden Code, um eine Klasse mit einem dynamischen Attributwert zu erstellen. NB. Dies sind Codefragmente in der Programmiersprache 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
quelle