如何找到给定名称的类的所有子类?

Posted

技术标签:

【中文标题】如何找到给定名称的类的所有子类?【英文标题】:How to find all the subclasses of a class given its name? 【发布时间】:2011-04-21 05:03:09 【问题描述】:

我需要一种工作方法来获取从 Python 中的基类继承的所有类。

【问题讨论】:

【参考方案1】:

这是一个简单但高效的代码版本:

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) 当类层次结构是有偏树时。

【讨论】:

【参考方案2】:

这不如使用@unutbu 提到的特殊内置__subclasses__() 类方法那么好,因此我将其仅作为练习提供。定义的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'>

【讨论】:

【参考方案3】:

注意:我看到有人(不是 @unutbu)更改了引用的答案,因此它不再使用 vars()['Foo'] — 所以我的帖子的主要观点不再适用。

FWIW,这就是我所说的 @unutbu's answer 仅与本地定义的类一起使用的意思——使用 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'>]

【讨论】:

【参考方案4】:

新型类(即从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__() 都会返回其子类的列表。

【讨论】:

假设我想查找一个模块中的所有子类,无论包含它的模块的子模块是否已被导入? @SamanthaAtkins:生成一个list of all submodules of the package,然后生成一个list of all classes for each module。 谢谢,这就是我最终做的,但我很好奇是否有更好的方法我错过了。【参考方案5】:

如何找到给定名称的类的所有子类?

如果可以访问对象本身,我们当然可以轻松地做到这一点,是的。

仅仅给出它的名字是一个糟糕的主意,因为可以有多个同名的类,甚至在同一个模块中定义。

我为另一个 answer 创建了一个实现,因为它回答了这个问题,并且比这里的其他解决方案更优雅一点,所以它是:

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

【讨论】:

【参考方案6】:

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__ 的行为并从此列表中省略/添加子类。

【讨论】:

是的,任何类型的任何子类都会触发父类的__init_subclass【参考方案7】:

这是一个没有递归的版本:

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], [])

【讨论】:

【参考方案8】:

获取所有子类列表的更短版本:

from itertools import chain

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

【讨论】:

【参考方案9】:

一般形式的最简单解决方案:

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

【讨论】:

生成器的方法真的很干净。【参考方案10】:

如果您只想要直接子类,那么 .__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

【讨论】:

谢谢@fletom!尽管那时我需要的只是 __subclasses__() 您的解决方案非常好。带你 +1 ;) 顺便说一句,我认为在你的情况下使用生成器可能更可靠。 不应该all_subclassesset 以消除重复吗? @RyneEverett 你的意思是如果你使用多重继承?我认为否则你不应该以重复结束。 @fletom 是的,重复项需要多重继承。例如,A(object)B(A)C(A)D(B, C)get_all_subclasses(A) == [B, C, D, D]. @RomanPrykhodchenko:你的问题的标题说要找到一个给定其名称的类的所有子类,但这以及其他唯一的工作给定类本身,而不仅仅是它的名称 - 所以只是什么是吗?

以上是关于如何找到给定名称的类的所有子类?的主要内容,如果未能解决你的问题,请参考以下文章

idea中如何查看类的所有子类

c#如何获取某一命名空间下的所有的类的信息(方法以及参数)

如何找到实现给定接口的所有类?

java如何判断一个类是不是实现了某个接口?

intellij idea 如何查看一个类或接口的子类关系图

在 Perl/Moose 中,如何将修饰符应用于所有子类中的方法?