Как найти все подклассы класса по его имени?

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


person Roman Prykhodchenko    schedule 05.10.2010    source источник


Ответы (10)


Классы нового стиля (т.е. унаследованные от object, который используется по умолчанию в Python 3) имеют метод __subclasses__, который возвращает подклассы:

class Foo(object): pass
class Bar(Foo): pass
class Baz(Foo): pass
class Bing(Bar): pass

Вот названия подклассов:

print([cls.__name__ for cls in Foo.__subclasses__()])
# ['Bar', 'Baz']

Вот сами подклассы:

print(Foo.__subclasses__())
# [<class '__main__.Bar'>, <class '__main__.Baz'>]

Подтверждение того, что подклассы действительно перечисляют Foo в качестве своей базы:

for cls in Foo.__subclasses__():
    print(cls.__base__)
# <class '__main__.Foo'>
# <class '__main__.Foo'>

Обратите внимание: если вам нужны подклассы, вам нужно будет выполнить рекурсию:

def all_subclasses(cls):
    return set(cls.__subclasses__()).union(
        [s for c in cls.__subclasses__() for s in all_subclasses(c)])

print(all_subclasses(Foo))
# {<class '__main__.Bar'>, <class '__main__.Baz'>, <class '__main__.Bing'>}

Обратите внимание: если определение класса подкласса еще не было выполнено - например, если модуль подкласса еще не был импортирован - тогда этот подкласс еще не существует, и __subclasses__ его не найдет.


Вы упомянули «дали свое имя». Поскольку классы Python являются объектами первого класса, вам не нужно использовать строку с именем класса вместо класса или что-то в этом роде. Вы можете просто использовать класс напрямую, и, вероятно, вам следует это сделать.

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

Как найти класс по имени, зависит от того, где вы ожидаете его найти. Если вы ожидаете найти его в том же модуле, что и код, пытающийся найти класс, тогда

cls = globals()[name]

сработает, или, в том маловероятном случае, если вы ожидаете найти его у местных жителей,

cls = locals()[name]

Если класс может быть в любом модуле, тогда ваша строка имени должна содержать полное имя - что-то вроде 'pkg.module.Foo', а не просто 'Foo'. Используйте importlib для загрузки модуля класса, затем получите соответствующий атрибут:

import importlib
modname, _, clsname = name.rpartition('.')
mod = importlib.import_module(modname)
cls = getattr(mod, clsname)

Как бы вы ни нашли класс, cls.__subclasses__() тогда вернет список его подклассов.

person unutbu    schedule 05.10.2010
comment
Предположим, я хочу найти все подклассы в модуле, был ли импортирован подмодуль модуля, который его содержит, или нет? - person Samantha Atkins; 30.07.2019
comment
@SamanthaAtkins: создайте список всех подмодулей пакета, а затем сгенерируйте список всех классов для каждого модуля. - person unutbu; 30.07.2019
comment
Спасибо, это то, чем я закончил, но мне было любопытно, может ли быть лучший способ, который я пропустил. - person Samantha Atkins; 31.07.2019

Если вам просто нужны прямые подклассы, тогда .__subclasses__() отлично подойдет. Если вам нужны все подклассы, подклассы подклассов и т. Д., Вам понадобится функция, которая сделает это за вас.

Вот простая и удобочитаемая функция, которая рекурсивно находит все подклассы данного класса:

def get_all_subclasses(cls):
    all_subclasses = []

    for subclass in cls.__subclasses__():
        all_subclasses.append(subclass)
        all_subclasses.extend(get_all_subclasses(subclass))

    return all_subclasses
person fletom    schedule 22.06.2013
comment
Спасибо @fletom! Хотя в те дни мне были нужны только __subclasses __ (), ваше решение действительно хорошее. Возьмите вам +1;) Кстати, думаю, в вашем случае было бы надежнее использовать генераторы. - person Roman Prykhodchenko; 05.07.2013
comment
Разве all_subclasses не должно set устранять дубликаты? - person Ryne Everett; 04.09.2016
comment
@RyneEverett Вы имеете в виду, если вы используете множественное наследование? Думаю, иначе у вас не должно получиться дубликатов. - person fletom; 22.09.2016
comment
@fletom Да, для дубликатов необходимо множественное наследование. Например, A(object), B(A), C(A) и D(B, C). get_all_subclasses(A) == [B, C, D, D]. - person Ryne Everett; 22.09.2016
comment
@RomanPrykhodchenko: В заголовке вашего вопроса говорится, что нужно найти все подклассы класса по его имени, но это, как и другие, работает только с учетом самого класса, а не только его имени - так что это? - person martineau; 24.02.2018
comment
Как правильно добавить аннотации типов к предлагаемой функции? - person Peter Bašista; 09.03.2021

Самое простое решение в общем виде:

def get_subclasses(cls):
    for subclass in cls.__subclasses__():
        yield from get_subclasses(subclass)
        yield subclass

И метод класса, если у вас есть единственный класс, от которого вы наследуете:

@classmethod
def get_subclasses(cls):
    for subclass in cls.__subclasses__():
        yield from subclass.get_subclasses()
        yield subclass
person Kimvais    schedule 09.11.2015
comment
Генераторный подход действительно чистый. - person four43; 24.02.2018

Python 3.6 - __init_subclass__

Как упоминалось в другом ответе, вы можете проверить атрибут __subclasses__, чтобы получить список подклассов, поскольку в python 3.6 вы можете изменить создание этого атрибута, переопределив _ 3_.

class PluginBase:
    subclasses = []

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.subclasses.append(cls)

class Plugin1(PluginBase):
    pass

class Plugin2(PluginBase):
    pass

Таким образом, если вы знаете, что делаете, вы можете переопределить поведение __subclasses__ и исключить / добавить подклассы из этого списка.

person Or Duan    schedule 27.03.2017
comment
Да, любой подкласс любого типа вызовет __init_subclass в родительском классе. - person Or Duan; 12.06.2017

Примечание. Я вижу, что кто-то (не @unutbu) изменил указанный ответ так, что он больше не использует vars()['Foo'], поэтому основной пункт моего сообщения больше не применяется.

FWIW, вот что я имел в виду, говоря о том, что ответ @unutbu работает только с локально определенными классами и что использование eval() вместо vars() сделает его работать с любым доступным классом, а не только с теми, которые определены в текущей области.

Для тех, кто не любит использовать eval(), также показан способ избежать этого.

Во-первых, вот конкретный пример, демонстрирующий потенциальную проблему с использованием vars():

class Foo(object): pass
class Bar(Foo): pass
class Baz(Foo): pass
class Bing(Bar): pass

# unutbu's approach
def all_subclasses(cls):
    return cls.__subclasses__() + [g for s in cls.__subclasses__()
                                       for g in all_subclasses(s)]

print(all_subclasses(vars()['Foo']))  # Fine because  Foo is in scope
# -> [<class '__main__.Bar'>, <class '__main__.Baz'>, <class '__main__.Bing'>]

def func():  # won't work because Foo class is not locally defined
    print(all_subclasses(vars()['Foo']))

try:
    func()  # not OK because Foo is not local to func()
except Exception as e:
    print('calling func() raised exception: {!r}'.format(e))
    # -> calling func() raised exception: KeyError('Foo',)

print(all_subclasses(eval('Foo')))  # OK
# -> [<class '__main__.Bar'>, <class '__main__.Baz'>, <class '__main__.Bing'>]

# using eval('xxx') instead of vars()['xxx']
def func2():
    print(all_subclasses(eval('Foo')))

func2()  # Works
# -> [<class '__main__.Bar'>, <class '__main__.Baz'>, <class '__main__.Bing'>]

Это можно улучшить, переместив eval('ClassName') вниз в определенную функцию, что упрощает ее использование без потери дополнительной универсальности, полученной за счет использования eval(), который, в отличие от vars(), не зависит от контекста:

# easier to use version
def all_subclasses2(classname):
    direct_subclasses = eval(classname).__subclasses__()
    return direct_subclasses + [g for s in direct_subclasses
                                    for g in all_subclasses2(s.__name__)]

# pass 'xxx' instead of eval('xxx')
def func_ez():
    print(all_subclasses2('Foo'))  # simpler

func_ez()
# -> [<class '__main__.Bar'>, <class '__main__.Baz'>, <class '__main__.Bing'>]

Наконец, возможно, а в некоторых случаях даже важно избегать использования eval() по соображениям безопасности, поэтому вот версия без него:

def get_all_subclasses(cls):
    """ Generator of all a class's subclasses. """
    try:
        for subclass in cls.__subclasses__():
            yield subclass
            for subclass in get_all_subclasses(subclass):
                yield subclass
    except TypeError:
        return

def all_subclasses3(classname):
    for cls in get_all_subclasses(object):  # object is base of all new-style classes.
        if cls.__name__.split('.')[-1] == classname:
            break
    else:
        raise ValueError('class %s not found' % classname)
    direct_subclasses = cls.__subclasses__()
    return direct_subclasses + [g for s in direct_subclasses
                                    for g in all_subclasses3(s.__name__)]

# no eval('xxx')
def func3():
    print(all_subclasses3('Foo'))

func3()  # Also works
# -> [<class '__main__.Bar'>, <class '__main__.Baz'>, <class '__main__.Bing'>]
person martineau    schedule 20.01.2015
comment
@Chris: Добавлена ​​версия, в которой не используется eval() - теперь лучше? - person martineau; 19.05.2015

Гораздо более короткая версия для получения списка всех подклассов:

from itertools import chain

def subclasses(cls):
    return list(
        chain.from_iterable(
            [list(chain.from_iterable([[x], subclasses(x)])) for x in cls.__subclasses__()]
        )
    )
person Peter Brooks    schedule 06.09.2016

Вот простая, но эффективная версия кода:

def get_all_subclasses(cls):
    subclass_list = []

    def recurse(klass):
        for subclass in klass.__subclasses__():
            subclass_list.append(subclass)
            recurse(subclass)

    recurse(cls)

    return set(subclass_list)

Его временная сложность равна O(n), где n - количество всех подклассов, если нет множественного наследования. Это более эффективно, чем функции, которые рекурсивно создают списки или создают классы с генераторами, сложность которых может быть (1) O(nlogn), если иерархия классов является сбалансированным деревом, или (2) O(n^2), если иерархия классов является смещенным деревом.

person dhnam    schedule 11.09.2020

Вот версия без рекурсии:

def get_subclasses_gen(cls):

    def _subclasses(classes, seen):
        while True:
            subclasses = sum((x.__subclasses__() for x in classes), [])
            yield from classes
            yield from seen
            found = []
            if not subclasses:
                return

            classes = subclasses
            seen = found

    return _subclasses([cls], [])

Это отличается от других реализаций тем, что возвращает исходный класс. Это потому, что это упрощает код и:

class Ham(object):
    pass

assert(issubclass(Ham, Ham)) # True

Если get_subclasses_gen выглядит немного странно, потому что он был создан путем преобразования хвостовой рекурсивной реализации в генератор цикла:

def get_subclasses(cls):

    def _subclasses(classes, seen):
        subclasses = sum(*(frozenset(x.__subclasses__()) for x in classes))
        found = classes + seen
        if not subclasses:
            return found

        return _subclasses(subclasses, found)

    return _subclasses([cls], [])
person Thomas Grainger    schedule 07.06.2017

Как мне найти все подклассы класса по его имени?

Конечно, мы можем легко это сделать, имея доступ к самому объекту, да.

Просто дать его имя - плохая идея, поскольку может быть несколько классов с одним и тем же именем, даже определенных в одном модуле.

Я создал реализацию для другого ответа, и, поскольку он отвечает на этот вопрос и немного более элегантен, чем другие решения здесь, вот:

def get_subclasses(cls):
    """returns all subclasses of argument, cls"""
    if issubclass(cls, type):
        subclasses = cls.__subclasses__(cls)
    else:
        subclasses = cls.__subclasses__()
    for subclass in subclasses:
        subclasses.extend(get_subclasses(subclass))
    return subclasses

Использование:

>>> import pprint
>>> list_of_classes = get_subclasses(int)
>>> pprint.pprint(list_of_classes)
[<class 'bool'>,
 <enum 'IntEnum'>,
 <enum 'IntFlag'>,
 <class 'sre_constants._NamedIntConstant'>,
 <class 'subprocess.Handle'>,
 <enum '_ParameterKind'>,
 <enum 'Signals'>,
 <enum 'Handlers'>,
 <enum 'RegexFlag'>]
person Aaron Hall    schedule 01.11.2017

Это не такой хороший ответ, как использование специального встроенного метода класса __subclasses__(), о котором упоминает @unutbu, поэтому я представляю его просто как упражнение. Определенная функция subclasses() возвращает словарь, который сопоставляет имена всех подклассов с самими подклассами.

def traced_subclass(baseclass):
    class _SubclassTracer(type):
        def __new__(cls, classname, bases, classdict):
            obj = type(classname, bases, classdict)
            if baseclass in bases: # sanity check
                attrname = '_%s__derived' % baseclass.__name__
                derived = getattr(baseclass, attrname, {})
                derived.update( {classname:obj} )
                setattr(baseclass, attrname, derived)
             return obj
    return _SubclassTracer

def subclasses(baseclass):
    attrname = '_%s__derived' % baseclass.__name__
    return getattr(baseclass, attrname, None)


class BaseClass(object):
    pass

class SubclassA(BaseClass):
    __metaclass__ = traced_subclass(BaseClass)

class SubclassB(BaseClass):
    __metaclass__ = traced_subclass(BaseClass)

print subclasses(BaseClass)

Выход:

{'SubclassB': <class '__main__.SubclassB'>,
 'SubclassA': <class '__main__.SubclassA'>}
person martineau    schedule 05.10.2010