Come posso creare dinamicamente classi derivate da una classe base


92

Ad esempio, ho una classe base come segue:

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

Da questa classe derivano molte altre classi, es

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

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

C'è un modo simpatico e pitonico per creare quelle classi dinamicamente da una chiamata di funzione che inserisce la nuova classe nel mio ambito attuale, come:

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

Poiché ci saranno commenti e domande sul motivo per cui ho bisogno di questo: le classi derivate hanno tutte la stessa identica struttura interna con la differenza che il costruttore accetta un numero di argomenti precedentemente indefiniti. Quindi, ad esempio, MyClassaccetta le parole chiave amentre il costruttore della classe TestClassaccetta be c.

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

Ma non dovrebbero MAI usare il tipo della classe come argomento di input come

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

Ho ottenuto questo per funzionare ma preferirei l'altro modo, cioè oggetti di classe creati dinamicamente.


Per essere sicuri, vuoi che il tipo di istanza cambi a seconda degli argomenti forniti? Come se me ne regalassi uno asarà sempre MyClasse TestClassnon lo prenderò mai a? Perché non solo dichiarare tutti e 3 gli argomenti BaseClass.__init__()ma impostarli come predefiniti None? def __init__(self, a=None, b=None, C=None)?
cavalla

Non posso dichiarare nulla nella classe base, poiché non conosco tutti gli argomenti che potrei usare. Potrei avere 30 classi diverse con 5 argomenti diversi ciascuna, quindi dichiarare 150 argomenti nel costruttore non è una soluzione.
Alex

Risposte:


141

Questo bit di codice consente di creare nuove classi con nomi dinamici e nomi di parametri. La verifica dei parametri in __init__appena non consente parametri sconosciuti, se hai bisogno di altre verifiche, come il tipo, o che sono obbligatorie, aggiungi la logica lì:

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

E funziona così, ad esempio:

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

Vedo che stai chiedendo di inserire i nomi dinamici nell'ambito dei nomi - ora, questa non è considerata una buona pratica in Python - o hai nomi di variabili, noti al momento della codifica, o dati - e i nomi appresi in runtime sono più " dati "rispetto a" variabili "-

Quindi, potresti semplicemente aggiungere le tue classi a un dizionario e usarle da lì:

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

E se il tuo progetto ha assolutamente bisogno che i nomi rientrino nell'ambito, fai lo stesso, ma usa il dizionario restituito dalla globals() chiamata invece di un dizionario arbitrario:

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

(Sarebbe davvero possibile per la funzione class factory inserire il nome dinamicamente nell'ambito globale del chiamante, ma questa è una pratica ancora peggiore e non è compatibile con le implementazioni Python. Il modo per farlo sarebbe ottenere il frame di esecuzione, tramite sys._getframe (1) e impostando il nome della classe nel dizionario globale del frame nel suo f_globalsattributo).

update, tl; dr: questa risposta è diventata popolare, ma è ancora molto specifica per il corpo della domanda. La risposta generale su come "creare dinamicamente classi derivate da una classe base" in Python è una semplice chiamata per typepassare il nome della nuova classe, una tupla con le classi base e il __dict__corpo per la nuova classe, come questo:

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

update
Chiunque abbia bisogno di questo dovrebbe anche controllare il progetto Dill - afferma di essere in grado di mettere in saccoccia e dissotterrare le classi proprio come fa Pickle con gli oggetti ordinari, e ci ha vissuto in alcuni dei miei test.


2
Se ricordo bene, BaseClass.__init__()sarebbe meglio come il più generale super(self.__class__).__init__(), che suona più bene quando le nuove classi sono sottoclassi. (Riferimento: rhettinger.wordpress.com/2011/05/26/super-considered-super )
Eric O Lebigot

@EOL: Lo sarebbe per le classi dichiarate staticamente, ma poiché non hai il nome della classe effettivo da codificare come primo parametro di Super, ciò richiederebbe molto ballare. Prova a sostituirlo con supersopra e crea una sottoclasse di una classe creata dinamicamente per capirlo; E, d'altra parte, in questo caso puoi avere la classe base come oggetto generale da cui chiamare __init__.
jsbueno

Ora ho avuto un po 'di tempo per esaminare la soluzione suggerita, ma non è proprio quello che voglio. Primo, sembra che __init__of BaseClasssia chiamato con un argomento, ma in realtà BaseClass.__init__accetta sempre un elenco arbitrario di argomenti di parole chiave. In secondo luogo, la soluzione sopra imposta tutti i nomi dei parametri consentiti come attributi, che non è quello che voglio. QUALSIASI argomento DEVE andare BaseClass, ma quale so quando creo la classe derivata. Probabilmente aggiornerò la domanda o ne chiederò una più precisa per renderla più chiara.
Alex

@jsbueno: Esatto, usando i dati di cui super()parlavo TypeError: must be type, not SubSubClass. Se ho capito bene, questo deriva dal primo argomento selfdi __init__(), che è SubSubClassdove typeci si aspetta un oggetto: questo sembra correlato al fatto che super(self.__class__)è un super oggetto non legato. Qual è il suo __init__()metodo? Non sono sicuro di quale metodo di questo tipo possa richiedere un primo argomento di tipo type. Potresti spiegare? (Nota a super()__init__()
margine

1
@EOL: il problema principale è in realtà se crei un'altra sottoclasse della classe fattorizzata: self .__ class__ farà riferimento a quella sottoclasse, non alla classe in cui è chiamato "super" - e ottieni ricorsione infinita.
jsbueno

89

type() è la funzione che crea le classi (e in particolare le sottoclassi):

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

Nel cercare di capire questo esempio usando Python 2.7, ho ottenuto un TypeErrordetto __init__() takes exactly 2 arguments (1 given). Ho scoperto che sarebbe sufficiente aggiungere qualcosa (qualcosa?) Per colmare il vuoto. Ad esempio, obj = SubClass('foo')viene eseguito senza errori.
DaveL17

Questo è normale, poiché SubClassè una sottoclasse di BaseClassnella domanda e BaseClassaccetta un parametro ( classtype, che è 'foo'nel tuo esempio).
Eric O Lebigot

-2

Per creare una classe con un valore di attributo dinamico, controlla il codice seguente. NB. Questi sono frammenti di codice nel linguaggio di programmazione 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*
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.