Как я могу динамически создавать производные классы из базового класса

95

Например, у меня есть следующий базовый класс:

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

Из этого класса я получил несколько других классов, например

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

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

Есть ли хороший питонический способ динамического создания этих классов с помощью вызова функции, которая помещает новый класс в мою текущую область видимости, например:

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

Поскольку будут комментарии и вопросы, зачем мне это нужно: все производные классы имеют одинаковую внутреннюю структуру с той разницей, что конструктор принимает ряд ранее неопределенных аргументов. Так, например, MyClassпринимает ключевые слова, aа конструктор класса TestClassпринимает bи c.

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

Но они НИКОГДА не должны использовать тип класса в качестве входного аргумента, например

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

Я заставил это работать, но предпочел бы другой способ, то есть динамически созданные объекты класса.

Alex
источник
Чтобы быть уверенным, вы хотите, чтобы тип экземпляра изменялся в зависимости от предоставленных аргументов? Как, если я дам, aэто всегда будет MyClassи TestClassникогда не будет a? Почему бы просто не объявить все 3 аргумента, BaseClass.__init__()но по умолчанию None? def __init__(self, a=None, b=None, C=None)?
acattle
Я не могу ничего объявить в базовом классе, так как не знаю всех аргументов, которые могу использовать. У меня может быть 30 разных классов с 5 разными аргументами в каждом, поэтому объявление 150 аргументов в конструкторе не является решением.
Alex

Ответы:

145

Этот фрагмент кода позволяет вам создавать новые классы с динамическими именами и именами параметров. Проверка параметров __init__просто не позволяет использовать неизвестные параметры, если вам нужны другие проверки, например тип, или что они являются обязательными, просто добавьте туда логику:

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

А работает это, например, так:

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

Я вижу , вы просите для вставки имен динамических в рамках именования - теперь, что не считается хорошей практикой в Python - вы либо имена переменных, известные во время кодирования или данные - и имена выучили во время выполнения более " данные "чем" переменные "-

Итак, вы можете просто добавить свои классы в словарь и использовать их оттуда:

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

И если вашему дизайну абсолютно необходимо, чтобы имена входили в область видимости, просто сделайте то же самое, но используйте словарь, возвращаемый globals() вызовом, вместо произвольного словаря:

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

(Действительно, функция фабрики классов могла бы динамически вставлять имя в глобальную область действия вызывающего, но это еще худшая практика, и она несовместима с реализациями Python. Способ сделать это - получить имя вызывающей стороны фрейм выполнения через sys._getframe (1) и установив имя класса в глобальном словаре фрейма в его f_globalsатрибуте).

update, tl; dr: Этот ответ стал популярным, но все еще очень специфичен для тела вопроса. Общий ответ о том, как «динамически создавать производные классы из базового класса» в Python - это простой вызов для typeпередачи имени нового класса, кортежа с базовым классом (-ами) и __dict__тела нового класса, например:

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

update
Любой, кто в этом нуждается, должен также проверить проект укропа - он утверждает, что может обрабатывать и очищать классы так же, как pickle делает с обычными объектами, и в некоторых из моих тестов он жил в соответствии с этим.

jsbueno
источник
2
Если я правильно помню, BaseClass.__init__()было бы лучше как более общее super(self.__class__).__init__(), которое лучше играет, когда новые классы подклассы. (Ссылка: rhettinger.wordpress.com/2011/05/26/super-considered-super )
Эрик О Лебигот
@EOL: это было бы для статически объявленных классов, но поскольку у вас нет фактического имени класса, которое жестко закодировалось бы в качестве первого параметра для Super, это потребовало бы много танцев. Попробуйте заменить его на superприведенный выше и создать подкласс динамически создаваемого класса, чтобы понять его; И, с другой стороны, в этом случае у вас может быть базовый класс как общий объект, из которого следует вызывать __init__.
jsbueno 06
Теперь у меня было время изучить предложенное решение, но это не совсем то, что мне нужно. Во-первых, похоже, что __init__of BaseClassвызывается с одним аргументом, но на самом деле BaseClass.__init__всегда принимает произвольный список аргументов ключевого слова. Во-вторых, в приведенном выше решении все разрешенные имена параметров устанавливаются как атрибуты, чего я не хочу. ДОЛЖЕН использоваться ЛЮБОЙ аргумент BaseClass, но какой из них я знаю при создании производного класса. Я, наверное, обновлю вопрос или задам более точный, чтобы было понятнее.
Alex
@jsbueno: Верно, используя то, что super()я упоминал, дает TypeError: must be type, not SubSubClass. Если я правильно понимаю, это происходит из первого аргумента selfиз __init__(), который является , SubSubClassгде typeожидается объект: это , кажется , связано с тем , super(self.__class__)является несвязанным объектом супер. Каков его __init__()метод? Я не уверен, для какого такого метода может потребоваться первый аргумент типа type. Могли бы вы объяснить? (Примечание: здесь мой super()подход действительно не имеет смысла, потому что __init__()имеет переменную сигнатуру.)
Эрик О Лебигот,
1
@EOL: основная проблема на самом деле заключается в том, что вы создаете другой подкласс факторизованного класса: self .__ class__ будет ссылаться на этот подкласс, а не на класс, в котором вызывается "super" - и вы получите бесконечную рекурсию.
jsbueno 09
91

type() - это функция, которая создает классы (и, в частности, подклассы):

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
Эрик О Лебигот
источник
Пытаясь понять этот пример с помощью Python 2.7, я получил, TypeErrorчто сказал __init__() takes exactly 2 arguments (1 given). Я обнаружил, что достаточно добавить что-нибудь (что-нибудь?), Чтобы заполнить пробел. Например, obj = SubClass('foo')работает без ошибок.
DaveL17 03
Это нормально, поскольку SubClassявляется подклассом BaseClassв вопросе и BaseClassпринимает параметр ( classtype, который есть 'foo'в вашем примере).
Эрик О Лебигот 03
@EricOLebigot Могу я как-нибудь здесь вызвать init BaseClass? как с супер?
mikazz
-3

Чтобы создать класс со значением динамического атрибута, ознакомьтесь с приведенным ниже кодом. NB. Это фрагменты кода на языке программирования 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*
Адефеми
источник