Como posso criar dinamicamente classes derivadas de uma classe base

92

Por exemplo, tenho uma classe base da seguinte forma:

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

Desta classe eu obtenho várias outras classes, por exemplo

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

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

Existe uma maneira legal e pítônica de criar essas classes dinamicamente por uma chamada de função que coloca a nova classe em meu escopo atual, como:

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

Como haverá comentários e perguntas sobre por que preciso disso: Todas as classes derivadas têm exatamente a mesma estrutura interna, com a diferença de que o construtor leva uma série de argumentos previamente indefinidos. Então, por exemplo, MyClasspega as palavras-chave aenquanto o construtor da classe TestClasspega be c.

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

Mas eles NUNCA devem usar o tipo da classe como argumento de entrada como

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

Eu fiz isso funcionar, mas preferiria o outro caminho, ou seja, objetos de classe criados dinamicamente.

Alex
fonte
Apenas para ter certeza, você deseja que o tipo de instância mude dependendo dos argumentos fornecidos? Tipo, se eu der um a, sempre será MyClasse TestClassnunca receberá um a? Por que não apenas declarar todos os 3 argumentos em, BaseClass.__init__()mas padronizá-los todos None? def __init__(self, a=None, b=None, C=None)?
acattle
Não posso declarar nada na classe base, pois não conheço todos os argumentos que posso usar. Posso ter 30 classes diferentes com 5 argumentos diferentes cada, então declarar 150 argumentos no construtor não é uma solução.
Alex

Respostas:

141

Este trecho de código permite criar novas classes com nomes dinâmicos e nomes de parâmetros. A verificação de parâmetros em __init__apenas não permite parâmetros desconhecidos, se você precisar de outras verificações, como tipo, ou que sejam obrigatórias, basta adicionar a lógica aí:

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 isso funciona assim, por exemplo:

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

Vejo que você está pedindo para inserir os nomes dinâmicos no escopo de nomenclatura - agora, isso não é considerado uma boa prática em Python - você tem nomes de variáveis, conhecidos no tempo de codificação ou dados - e os nomes aprendidos em tempo de execução são mais " dados "do que" variáveis ​​"-

Então, você pode simplesmente adicionar suas classes a um dicionário e usá-las a partir daí:

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

E se o seu projeto precisa absolutamente que os nomes entrem no escopo, apenas faça o mesmo, mas use o dicionário retornado pelo globals() chamada em vez de um dicionário arbitrário:

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

(Na verdade, seria possível para a função de fábrica de classe inserir o nome dinamicamente no escopo global do chamador - mas isso é uma prática ainda pior e não é compatível com as implementações Python. A maneira de fazer isso seria obter o autor da chamada frame de execução, através de sys._getframe (1) e definindo o nome da classe no dicionário global do frame em seu f_globalsatributo).

update, tl; dr: Esta resposta se tornou popular, ainda é muito específica para o corpo da pergunta. A resposta geral sobre como "criar dinamicamente classes derivadas de uma classe base" em Python é uma simples chamada para typepassar o nome da nova classe, uma tupla com as classes básicas e o __dict__corpo da nova classe - assim:

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

atualizar
Qualquer pessoa que precise disso também deve verificar o projeto de endro - ele afirma ser capaz de conservar e desempacotar classes exatamente como o pickle faz com objetos comuns, e sobreviveu a isso em alguns de meus testes.

jsbueno
fonte
2
Se bem me lembro, BaseClass.__init__()seria melhor como a mais geral super(self.__class__).__init__(), que funciona mais bem quando as novas classes são subclasses. (Referência: rhettinger.wordpress.com/2011/05/26/super-considered-super )
Eric O Lebigot
@EOL: Seria para classes declaradas estaticamente - mas como você não tem o nome da classe real para codificar como o primeiro parâmetro para Super, isso exigiria muita dança. Tente substituí-lo por superacima e crie uma subclasse de uma classe criada dinamicamente para entendê-lo; E, por outro lado, neste caso você pode ter a classe básica como um objeto geral a partir do qual chamar __init__.
jsbueno
Agora tive algum tempo para examinar a solução sugerida, mas não é bem o que desejo. Primeiro, parece que __init__de BaseClassé chamado com um argumento, mas na verdade BaseClass.__init__sempre leva uma lista arbitrária de argumentos de palavras-chave. Em segundo lugar, a solução acima define todos os nomes de parâmetros permitidos como atributos, o que não é o que desejo. QUALQUER argumento TEM que ir BaseClass, mas qual eu conheço ao criar a classe derivada. Provavelmente atualizarei a pergunta ou farei outra mais precisa para torná-la mais clara.
Alex
@jsbueno: Certo, usando o que super()eu estava mencionando dá TypeError: must be type, not SubSubClass. Se bem entendi, isso vem do primeiro argumento selfde __init__(), que é SubSubClassonde um typeobjeto é esperado: isso parece relacionado ao fato de super(self.__class__)ser um superobjeto não ligado. Qual é o seu __init__()método? Não tenho certeza de qual método pode exigir um primeiro argumento do tipo type. Você poderia explicar? (Nota lateral: minha super()abordagem de fato não faz sentido, aqui, porque __init__()tem uma assinatura variável.)
Eric O Lebigot
1
@EOL: o maior problema é realmente se você criar outra subclasse da classe fatorada: self .__ class__ irá se referir a essa subclasse, não à classe na qual "super" é chamado - e você obtém recursão infinita.
jsbueno
89

type() é a função que cria classes (e em subclasses particulares):

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
fonte
Ao tentar entender este exemplo usando Python 2.7, recebi um TypeErrorque disse __init__() takes exactly 2 arguments (1 given). Descobri que adicionar algo (qualquer coisa?) Para preencher a lacuna seria suficiente. Por exemplo, obj = SubClass('foo')é executado sem erros.
DaveL17,
Isso é normal, pois SubClassé uma subclasse de BaseClassna questão e BaseClassleva um parâmetro ( classtype, que está 'foo'no seu exemplo).
Eric O Lebigot,
-2

Para criar uma classe com um valor de atributo dinâmico, verifique o código abaixo. NB. Estes são trechos de código na linguagem de programação 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
fonte