Python: как зарегистрировать все дочерние классы в родительском классе при создании

У меня есть деревья классов Python, каждое из которых состоит из абстрактного базового класса и множества производных конкретных классов. Я хочу, чтобы все конкретные классы были доступны через метод базового класса, и я не хочу ничего указывать при создании дочернего класса.

Вот как выглядит мое воображаемое решение:

class BaseClassA(object):
    # <some magic code around here>
    @classmethod
    def getConcreteClasses(cls):
        # <some magic related code here>

class ConcreteClassA1(BaseClassA):
    # no magic-related code here

class ConcreteClassA2(BaseClassA):
    # no magic-related code here

Насколько это возможно, я бы предпочел написать «магию» один раз в качестве своего рода шаблона проектирования. Я хочу иметь возможность применять его к разным деревьям классов в разных сценариях (т.е. добавить подобное дерево с «BaseClassB» и его конкретными классами).

Спасибо Интернет!


person Yonatan    schedule 18.07.2011    source источник
comment
Это... не совсем решение...   -  person Ignacio Vazquez-Abrams    schedule 18.07.2011
comment
Будут ли все конкретные классы находиться в одном и том же исходном файле?   -  person Triptych    schedule 18.07.2011
comment
@Ignacio: действительно, это не решение, это то, как я хотел бы использовать решение в своем сценарии. @Triptych: Нет, не обязательно.   -  person Yonatan    schedule 18.07.2011
comment
Отвечает ли это на ваш вопрос? Как автоматически зарегистрировать класс, когда он определен   -  person Aziz Alto    schedule 01.05.2020
comment
В аналогичной сфере основная проблема другого вопроса связана с созданием экземпляров классов с прямым оформлением, а не с косвенным запуском регистрации самих классов. Но спасибо за ссылку!   -  person Yonatan    schedule 02.05.2020


Ответы (4)


вы можете использовать метаклассы для этого:

class AutoRegister(type):
    def __new__(mcs, name, bases, classdict):
        new_cls = type.__new__(mcs, name, bases, classdict)
        #print mcs, name, bases, classdict
        for b in bases:
            if hasattr(b, 'register_subclass'):
                b.register_subclass(new_cls)
        return new_cls


class AbstractClassA(object):
    __metaclass__ = AutoRegister
    _subclasses = []

    @classmethod
    def register_subclass(klass, cls):
        klass._subclasses.append(cls)

    @classmethod
    def get_concrete_classes(klass):
        return klass._subclasses


class ConcreteClassA1(AbstractClassA):
    pass

class ConcreteClassA2(AbstractClassA):
    pass

class ConcreteClassA3(ConcreteClassA2):
    pass


print AbstractClassA.get_concrete_classes()

Лично я очень настороженно отношусь к такого рода магии. Не помещайте слишком много этого в свой код.

person gurney alex    schedule 18.07.2011
comment
Очень элегантный! Я видел похожее решение с метаклассами, но менее гибкое. Спасибо Алекс! - person Yonatan; 18.07.2011

Вы должны знать, что часть ответа, который вы ищете, встроена. Классы нового стиля автоматически сохраняют слабую ссылку на все свои дочерние классы, к которым можно получить доступ с помощью метода __subclasses__:

@classmethod
def getConcreteClasses(cls):
    return cls.__subclasses__()

Это не вернет под-подклассы. Если они вам нужны, вы можете создать рекурсивный генератор, чтобы получить их все:

@classmethod
def getConcreteClasses(cls):
    for c in cls.__subclasses__():
        yield c
        for c2 in c.getConcreteClasses():
            yield c2
person Ned Batchelder    schedule 18.07.2011
comment
Гениально, я понятия не имел, что этот механизм существует. Это здорово, когда не требуется поведение обратного вызова для регистрации, а вместо этого требуется итерация дерева подклассов по запросу. Также приятно генерировать дерево подклассов, начиная со встроенного класса объектов Python. - person Yonatan; 18.07.2011


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

def lister(cls):
    cls.classes = list()
    cls._init = cls.__init__
    def init(self, *args, **kwargs):
        cls = self.__class__
        if cls not in cls.classes:
            cls.classes.append(cls)
        cls._init(self, *args, **kwargs)
    cls.__init__ = init
    @classmethod
    def getclasses(cls):
        return cls.classes
    cls.getclasses = getclasses
    return cls

@lister
class A(object): pass

class B(A): pass

class C(A):
    def __init__(self):
        super(C, self).__init__()

b = B()
c = C()
c2 = C()
print 'Classes:', c.getclasses()

Это будет работать независимо от того, определяет ли базовый класс __init__.

person agf    schedule 18.07.2011
comment
Ограничение «init» вполне приемлемо; однако обратите внимание, что конкретные классы не будут «зарегистрированы», пока не будет создан первый экземпляр. Это не соответствует моему сценарию, когда я хочу выбрать, какой класс создавать, исходя из того, какие конкретные классы доступны. Однако это очень удобно, когда вы хотите зарегистрировать только те классы, которые фактически созданы. - person Yonatan; 18.07.2011
comment
Не думал о том, каков был ваш вариант использования. Кроме того, вернулся, чтобы добавить, что это, вероятно, лучше сделать с помощью __new__, а не __init__, поскольку, возможно, менее вероятно, что он будет переопределен без вызова родительского класса '__new__. - person agf; 18.07.2011