Jak mogę dynamicznie tworzyć klasy pochodne z klasy bazowej

95

Na przykład mam następującą klasę bazową:

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

Z tej klasy wyprowadzam kilka innych klas, np

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

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

Czy istnieje ładny, pythonowy sposób dynamicznego tworzenia tych klas przez wywołanie funkcji, które umieszcza nową klasę w moim obecnym zakresie, na przykład:

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

Ponieważ pojawią się komentarze i pytania, dlaczego tego potrzebuję: wszystkie klasy pochodne mają dokładnie taką samą strukturę wewnętrzną z tą różnicą, że konstruktor pobiera wiele wcześniej niezdefiniowanych argumentów. Na przykład MyClassprzyjmuje słowa kluczowe, apodczas gdy konstruktor klasy TestClassprzyjmuje bi c.

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

Ale NIGDY nie powinni używać typu klasy jako argumentu wejściowego, takiego jak

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

Mam to do pracy, ale wolałbym inny sposób, tj. Dynamicznie tworzone obiekty klas.

Alex
źródło
Dla pewności chcesz, aby typ wystąpienia zmieniał się w zależności od podanych argumentów? Czy jeśli dam a, zawsze będzie MyClassi TestClassnigdy nie weźmie a? Dlaczego po prostu nie zadeklarować wszystkich 3 argumentów w programie, BaseClass.__init__()ale wszystkie domyślnie są ustawione na None? def __init__(self, a=None, b=None, C=None)?
acattle
Nie mogę nic zadeklarować w klasie bazowej, ponieważ nie znam wszystkich argumentów, których mógłbym użyć. Mogę mieć 30 różnych klas z 5 różnymi argumentami, więc zadeklarowanie 150 argumentów w konstruktorze nie jest rozwiązaniem.
Alex

Odpowiedzi:

145

Ten fragment kodu umożliwia tworzenie nowych klas z dynamicznymi nazwami i nazwami parametrów. Weryfikacja parametrów po __init__prostu nie zezwala na nieznane parametry, jeśli potrzebujesz innych weryfikacji, takich jak typ, lub że są one obowiązkowe, po prostu dodaj tam logikę:

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

A to działa tak, na przykład:

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

Widzę, że prosi o wstawienie nazw dynamicznych w zakresie nazewnictwa - teraz, że nie jest uważane za dobre praktyki w Pythonie - albo masz nazwy zmiennych, znane w momencie pisania kodu lub danych - i nazwiska nabyte w czasie pracy są bardziej " dane "niż" zmienne "-

Możesz więc po prostu dodać swoje klasy do słownika i używać ich z tego miejsca:

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

A jeśli twój projekt absolutnie potrzebuje nazw, które wchodzą w zakres, po prostu zrób to samo, ale użyj słownika zwróconego przez globals() wywołanie zamiast dowolnego słownika:

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

(Rzeczywiście byłoby możliwe, aby funkcja fabryki klasy wstawiała nazwę dynamicznie do globalnego zakresu wywołującego - ale jest to jeszcze gorsza praktyka i nie jest kompatybilna z implementacjami Pythona. Sposobem na to byłoby uzyskanie wywołującego wykonanie ramki, poprzez sys._getframe (1) i ustawienie nazwy klasy w globalnym słowniku ramki w jej f_globalsatrybucie).

update, tl; dr: Ta odpowiedź stała się popularna, ale nadal jest bardzo specyficzna dla treści pytania. Ogólna odpowiedź na temat „dynamicznego tworzenia klas pochodnych z klasy bazowej” w Pythonie jest prostym wywołaniem typeprzekazania nowej nazwy klasy, krotki z __dict__klasami podstawowymi i treścią nowej klasy - na przykład:

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

aktualizacja
Każdy, kto tego potrzebuje, powinien również sprawdzić projekt koperkowy - twierdzi, że jest w stanie marynować i unpickle klasy, tak jak w przypadku zwykłych przedmiotów, i przeżył to w niektórych moich testach.

jsbueno
źródło
2
O BaseClass.__init__()ile dobrze pamiętam, byłoby lepiej jako bardziej ogólne super(self.__class__).__init__(), które gra ładniej, gdy nowe klasy są podklasy. (Odniesienie: rhettinger.wordpress.com/2011/05/26/super-considered-super )
Eric O Lebigot
@EOL: Byłoby to dla klas zadeklarowanych statycznie - ale ponieważ nie masz rzeczywistej nazwy klasy do zakodowania jako pierwszego parametru do Super, wymagałoby to dużo tańca. Spróbuj zastąpić go superpowyższym i utwórz podklasę dynamicznie utworzonej klasy, aby to zrozumieć; Z drugiej strony, w tym przypadku możesz mieć klasę bazową jako ogólny obiekt, z którego chcesz wywołać __init__.
jsbueno
Teraz miałem trochę czasu na przyjrzenie się sugerowanemu rozwiązaniu, ale nie jest to do końca to, czego chcę. Po pierwsze, wygląda na to, że __init__of BaseClassjest wywoływany z jednym argumentem, ale w rzeczywistości BaseClass.__init__zawsze pobiera dowolną listę argumentów słów kluczowych. Po drugie, powyższe rozwiązanie ustawia wszystkie dozwolone nazwy parametrów jako atrybuty, co nie jest tym, czego chcę. KAŻDY argument MUSI przejść BaseClass, ale który z nich znam podczas tworzenia klasy pochodnej. Prawdopodobnie zaktualizuję pytanie lub poproszę o bardziej precyzyjne, aby było jaśniejsze.
Alex
@jsbueno: Tak, używając super()wspomnianego daje daje TypeError: must be type, not SubSubClass. Jeśli dobrze rozumiem, pochodzi to z pierwszego argumentu selfprogramu __init__(), czyli miejsca, w którym oczekuje SubSubClasssię typeobiektu: wydaje się, że ma to związek z faktem, że super(self.__class__)jest to niezwiązany super obiekt. Jaka jest jego __init__()metoda? Nie jestem pewien, która taka metoda może wymagać pierwszego argumentu typu type. Czy możesz wytłumaczyć? (Na marginesie: moje super()podejście rzeczywiście nie ma tu sensu, ponieważ __init__()ma zmienną sygnaturę.)
Eric O Lebigot
1
@EOL: głównym problemem jest w rzeczywistości, jeśli utworzysz kolejną podklasę klasy podzielonej na czynniki: self .__ class__ będzie odnosić się do tej podklasy, a nie do klasy, w której nazywa się „super” - i otrzymujesz nieskończoną rekursję.
jsbueno
91

type() jest funkcją, która tworzy klasy (aw szczególności podklasy):

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
źródło
Próbując zrozumieć ten przykład za pomocą Pythona 2.7, otrzymałem TypeErrorodpowiedź __init__() takes exactly 2 arguments (1 given). Stwierdziłem, że dodanie czegoś (czegokolwiek?), Aby wypełnić lukę, wystarczy. Na przykład obj = SubClass('foo')działa bez błędów.
DaveL17
Jest to normalne, ponieważ SubClassjest to podklasa BaseClassw pytaniu i BaseClassprzyjmuje parametr ( classtypektóry jest 'foo'w twoim przykładzie).
Eric O Lebigot
@EricOLebigot Czy mogę w jakiś sposób wywołać tutaj init BaseClass? jak z super?
mikazz
-3

Aby utworzyć klasę z dynamiczną wartością atrybutu, sprawdź poniższy kod. NB. To są fragmenty kodu w języku programowania 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
źródło