¿Cómo puedo crear dinámicamente clases derivadas a partir de una clase base?

92

Por ejemplo, tengo una clase base de la siguiente manera:

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

De esta clase obtengo varias otras clases, p. Ej.

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

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

¿Existe una forma agradable y pitónica de crear esas clases dinámicamente mediante una llamada de función que coloca la nueva clase en mi alcance actual, como:

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

Como habrá comentarios y preguntas, ¿por qué necesito esto? Todas las clases derivadas tienen exactamente la misma estructura interna con la diferencia de que el constructor toma una serie de argumentos previamente no definidos. Entonces, por ejemplo, MyClasstoma las palabras clave amientras que el constructor de la clase TestClasstoma by c.

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

Pero NUNCA deben usar el tipo de la clase como argumento de entrada como

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

Conseguí que esto funcionara, pero preferiría lo contrario, es decir, objetos de clase creados dinámicamente.

Alex
fuente
Solo para estar seguro, ¿desea que el tipo de instancia cambie según los argumentos proporcionados? ¿Como si le doy un a, siempre será MyClassy TestClassnunca tomaré un a? ¿Por qué no simplemente declarar los 3 argumentos en BaseClass.__init__()pero predeterminarlos todos None? def __init__(self, a=None, b=None, C=None)?
2013
No puedo declarar nada en la clase base, ya que no conozco todos los argumentos que podría usar. Podría tener 30 clases diferentes con 5 argumentos diferentes cada una, por lo que declarar 150 argumentos en el constructor no es una solución.
Alex

Respuestas:

141

Este bit de código le permite crear nuevas clases con nombres dinámicos y nombres de parámetros. La verificación de parámetros en __init__simplemente no permite parámetros desconocidos, si necesita otras verificaciones, como el tipo, o si son obligatorias, simplemente agregue la lógica allí:

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

Y esto funciona así, por ejemplo:

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

Veo que está pidiendo insertar los nombres dinámicos en el ámbito de denominación; ahora, eso no se considera una buena práctica en Python; tiene nombres de variables, conocidos en el momento de la codificación o datos, y los nombres aprendidos en tiempo de ejecución son más " datos "que" variables "-

Por lo tanto, puede agregar sus clases a un diccionario y usarlas desde allí:

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

Y si su diseño necesita absolutamente que los nombres entren en el alcance, simplemente haga lo mismo, pero use el diccionario devuelto por la globals() llamada en lugar de un diccionario arbitrario:

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

(De hecho, sería posible que la función de fábrica de clases insertara el nombre dinámicamente en el alcance global de la persona que llama, pero esa es una práctica aún peor y no es compatible con todas las implementaciones de Python. La forma de hacerlo sería obtener el nombre de la persona que llama marco de ejecución, a través de sys._getframe (1) y estableciendo el nombre de la clase en el diccionario global del marco en su f_globalsatributo).

update, tl; dr: esta respuesta se ha vuelto popular, aún es muy específica para el cuerpo de la pregunta. La respuesta general sobre cómo "crear dinámicamente clases derivadas a partir de una clase base" en Python es una simple llamada a typepasar el nombre de la nueva clase, una tupla con la (s) clase (s) base (s) y el __dict__cuerpo de la nueva clase, como esta:

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

actualización
Cualquiera que necesite esto también debería verificar el proyecto eneldo : afirma ser capaz de encurtir y desacoplar clases tal como lo hace pickle con objetos ordinarios, y lo había cumplido en algunas de mis pruebas.

jsbueno
fuente
2
Si mal no recuerdo, BaseClass.__init__()sería mejor como el más general super(self.__class__).__init__(), que funciona mejor cuando las nuevas clases se subclasifican. (Referencia: rhettinger.wordpress.com/2011/05/26/super-considered-super )
Eric O Lebigot
@EOL: Sería para clases declaradas estáticamente, pero como no tiene el nombre de clase real para codificarlo como el primer parámetro de Super, eso requeriría mucho baile. Intente reemplazarlo con el superanterior y cree una subclase de una clase creada dinámicamente para comprenderlo; Y, por otro lado, en este caso puede tener la clase base como un objeto general desde el cual llamar __init__.
jsbueno
Ahora tuve algo de tiempo para ver la solución sugerida, pero no es exactamente lo que quiero. Primero, parece que se llama a __init__of BaseClasscon un argumento, pero de hecho BaseClass.__init__siempre toma una lista arbitraria de argumentos de palabras clave. En segundo lugar, la solución anterior establece todos los nombres de parámetros permitidos como atributos, que no es lo que quiero. CUALQUIER argumento TIENE que ir BaseClass, pero cuál sé al crear la clase derivada. Probablemente actualizaré la pregunta o haré una más precisa para aclararla.
Alex
@jsbueno: Correcto, usando el super()que estaba mencionando da TypeError: must be type, not SubSubClass. Si lo entiendo correctamente, esto proviene del primer argumento selfde __init__(), que es un lugar SubSubClassdonde typese espera un objeto: esto parece estar relacionado con el hecho de que super(self.__class__)es un superobjeto no vinculado . Cual es su __init__()metodo? No estoy seguro de qué método de este tipo podría requerir un primer argumento de tipo type. ¿Podrías explicar? (Nota al margen: mi super()enfoque de hecho no tiene sentido, aquí, porque __init__()tiene una firma variable.)
Eric O Lebigot
1
@EOL: el problema principal es en realidad si crea otra subclase de la clase factorizada: self .__ class__ se referirá a esa subclase, no a la clase en la que se llama "super", y obtendrá una recursividad infinita.
jsbueno
89

type() es la función que crea clases (y en particular subclases):

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
fuente
Al tratar de entender este ejemplo usando Python 2.7, obtuve un TypeErrorque decía __init__() takes exactly 2 arguments (1 given). Descubrí que agregar algo (¿algo?) Para llenar el vacío sería suficiente. Por ejemplo, se obj = SubClass('foo')ejecuta sin errores.
DaveL17
Esto es normal, ya que SubClasses una subclase de BaseClassen la pregunta y BaseClasstoma un parámetro ( classtype, que está 'foo'en su ejemplo).
Eric O Lebigot
-2

Para crear una clase con un valor de atributo dinámico, consulte el siguiente código. NÓTESE BIEN. Estos son fragmentos de código en el lenguaje de programación 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
fuente