Як знайти всі підкласи класу за його назвою?


Відповіді:


315

Класи нового стилю (тобто підкласи з 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__()а потім повернете список його підкласів.


Припустимо, я хотів знайти всі підкласи у модулі, імпортований чи ні підмодуль модуля, що містить його?
Саманта Аткінс


Дякую, саме цим я і закінчився, але мені було цікаво, чи може бути кращий спосіб, який я пропустив.
Саманта Аткінс

63

Якщо ви просто хочете прямі підкласи, то це .__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

3
Дякую @fletom! Хоча те, що мені потрібно було в ті часи, було лише __класи __ (), ваше рішення справді приємне. Візьмемо вас +1;) Btw, я думаю, що у вашому випадку може бути надійніше використовувати генератори.
Роман Приходченко

3
Чи не повинно all_subclassesбути setудаленням дублікатів?
Ryne Everett

@RyneEverett Ви маєте на увазі, якщо ви використовуєте багатократну спадщину? Я думаю, інакше вам не слід закінчувати дублікатами.
флетом

@fletom Так, для дублікатів знадобиться багатократне успадкування. Так , наприклад, A(object), B(A), C(A)і D(B, C). get_all_subclasses(A) == [B, C, D, D].
Ryne Everett

@ Роман Приходченко: Назва вашого запитання говорить про те, щоб знайти всі підкласи класу з його назвою, але це, як і інша робота, дана саме класу, а не лише його назвою, так що тільки це?
мартіно

33

Найпростіше рішення в загальному вигляді:

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

2
Підхід генератора дійсно чистий.
чотири43

22

Python 3.6 -__init_subclass__

Як і інша згадана відповідь, ви можете перевірити __subclasses__атрибут, щоб отримати список підкласів, оскільки python 3.6 ви можете змінити створення цього атрибуту, замінивши __init_subclass__метод.

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__та пропустити / додати підкласи із цього списку.


1
Так, будь-який підклас будь-якого виду може викликати __init_subclassклас батьків.
Або Дуан

9

Примітка. Я бачу, що хтось (не @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'>]

1
@Chris: Додана версія, яка не використовується eval()- краще зараз?
мартіно

4

Набагато коротша версія для отримання списку всіх підкласів:

from itertools import chain

def subclasses(cls):
    return list(
        chain.from_iterable(
            [list(chain.from_iterable([[x], subclasses(x)])) for x in cls.__subclasses__()]
        )
    )

2

Як я можу знайти всі підкласи класу з назвою його назви?

Ми, звичайно, можемо легко зробити це за умови доступу до самого об’єкта, так.

Просто назва його ім'я є поганою ідеєю, оскільки може бути кілька класів одного і того ж імені, навіть визначених в одному модулі.

Я створив реалізацію для іншої відповіді , і оскільки вона відповідає на це запитання, і це трохи елегантніше, ніж інші рішення тут, ось це:

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

2

Це не така відповідь, як використання спеціального вбудованого __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'>}

1

Ось версія без рекурсії:

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