Як я можу динамічно створювати похідні класи з базового класу


92

Наприклад, у мене є базовий клас наступним чином:

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)

Я змусив це працювати, але віддав би перевагу іншому шляху, тобто динамічно створюваним об’єктам класу.


Щоб бути впевненим, ви хочете, щоб тип екземпляра змінювався залежно від поданих аргументів? Мовляв, якщо я даю, aце завжди буде MyClassі TestClassніколи не прийме a? Чому б просто не оголосити всі 3 аргументи, BaseClass.__init__()а за замовчуванням їх усі None? def __init__(self, a=None, b=None, C=None)?
acattle

Я не можу нічого оголосити в базовому класі, оскільки я не знаю всіх аргументів, які я можу використовувати. У мене може бути 30 різних речень із 5 різними аргументами в кожному, тому оголошення 150 аргументів у конструкторі не є рішенням.
Alex

Відповіді:


141

Цей біт коду дозволяє створювати нові класи з динамічними іменами та іменами параметрів. Перевірка параметрів __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: ...})

оновити
Будь-хто, хто потребує цього, також повинен перевірити проект кропу - він стверджує, що може маринувати та розпаковувати класи так само, як це робить маринований звичайні предмети, і дожив до цього в деяких моїх тестах.


2
Якщо я добре пам'ятаю, BaseClass.__init__()було б краще, як загальніше super(self.__class__).__init__(), що грає приємніше, коли нові класи підкласуються. (Довідка: rhettinger.wordpress.com/2011/05/26/super-considered-super )
Ерік Лебіго,

@EOL: Це було б для статично оголошених класів - але оскільки у вас немає фактичного імені класу, щоб хардкодувати як перший параметр для Super, для цього потрібно буде багато танцювати. Спробуйте замінити його на superвище і створіть підклас динамічно створеного класу, щоб зрозуміти це; І, з іншого боку, у цьому випадку ви можете мати базовий клас як загальний об’єкт, з якого слід викликати __init__.
jsbueno

Зараз у мене було трохи часу, щоб розглянути запропоноване рішення, але воно не зовсім те, що я хочу. По-перше, схоже __init__на те 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

89

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

Це нормально, оскільки SubClassє підкласом BaseClassу питанні і BaseClassприймає параметр ( classtype, який є 'foo'у вашому прикладі).
Eric O Lebigot

-2

Щоб створити клас із значенням динамічного атрибута, перевірте код нижче. Примітка. Це фрагменти коду мовою програмування 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*
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.