如何根据名称找到某个类的所有子类?

2024-12-24 08:55:00
admin
原创
95
摘要:问题描述:我需要一种可行的方法来获取从 Python 中的基类继承的所有类。解决方案 1:新式类(即从中子类化object,这是 Python 3 中的默认设置)具有__subclasses__返回子类的方法:class Foo(object): pass class Bar(Foo): pass class...

问题描述:

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


解决方案 1:

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

解决方案 2:

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

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

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

解决方案 4:

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

解决方案 5:

注意:我看到有人(不是@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'>]

解决方案 6:

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

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)

其时间复杂度为,若不存在多重继承,则所有子类的数量为。它比递归创建列表或使用生成器生成类的函数更高效,后者的复杂度可能是 (1)O(n)当类层次结构是平衡树时或 (2)当类层次结构是偏向树时。n`O(nlogn)`O(n^2)

解决方案 7:

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

from itertools import chain

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

解决方案 8:

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

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

解决方案 9:

如何根据类名找到该类的所有子类?

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

简单地给出它的名字并不是一个好主意,因为可能有多个同名的类,即使在同一个模块中定义。

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

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

解决方案 10:

这不是使用__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'>}

解决方案 11:

使用该方法的局限性__subclasses()__是:

  1. 您需要加载这些类(即导入它们),否则它将无法工作。

  2. 如果您有多层继承,则需要递归。例如:class A-> class B(A)-> class C(B).A.__subclasses__()只会给您 classB而不是 class C。这是因为 classC继承自 class 而B不是A

因此,为了解决这两个限制,除了上述出色的答案之外,我还采取了略有不同的方法。

该函数将获取包中的所有类:

def get_classes_from_package_recursively(package: str) -> list[type]:
    """Return a list of classes inside a given package (recurse thorugh any sub-packages).

    Keyword arguments:
    package -- package represented as a string. Must not be relative.
    """
    classes_in_package = []
    # Go through the modules in the package
    for _importer, module_name, is_package in pkgutil.iter_modules(importlib.import_module(package).__path__):
        full_module_name = f"{package}.{module_name}"
        # Recurse through any sub-packages
        if is_package:
            classes_in_subpackage = get_classes_from_package_recursively(package=full_module_name)
            classes_in_package.extend(classes_in_subpackage)

        # Load the module for inspection
        module = importlib.import_module(full_module_name)

        # Iterate through all the objects in the module and
        # using the lambda, filter for class objects and only objects that exist within the module
        for _name, obj in inspect.getmembers(
            module,
            lambda member, module_name=full_module_name: inspect.isclass(member) and member.__module__ == module_name,
        ):
            classes_in_package.append(obj)
    return classes_in_package

如何使用上述类的示例:

my_classes = get_classes_from_package_recursively(package="src.database.models")

my_classes现在看起来像这样[MyModel1, MyModel2, MyModel3, etc...]

得到此信息后,只需像这样过滤列表:

my_subclasses = [class_ for class_ in my_classes if issubclass(class_, parent_class) and class_ is not parent_class]

解决方案 12:

虽然我非常偏爱这种__init_subclass__方法,但如果您有一个非常密集的层次结构并且到处都有多重继承,这将保留定义顺序,并避免组合增长顺序:

def descendents(cls):
    '''Does not return the class itself'''
    R = {}
    def visit(cls):
        for subCls in cls.__subclasses__():
            if not subCls in R:
                R[subCls] = True
                visit(subCls)
    visit(cls)
    return list(R.keys())

这是有效的,因为字典记住了键的插入顺序。

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1124  
  IPD(Integrated Product Development,集成产品开发)流程是一种广泛应用于高科技和制造业的产品开发方法论。它通过跨职能团队的紧密协作,将产品开发周期缩短,同时提高产品质量和市场成功率。在IPD流程中,CDCP(Concept Decision Checkpoint,概念决策检查点)是一个关...
IPD培训课程   79  
  研发IPD(集成产品开发)流程作为一种系统化的产品开发方法,已经在许多行业中得到广泛应用。它不仅能够提升产品开发的效率和质量,还能够通过优化流程和资源分配,显著提高客户满意度。客户满意度是企业长期成功的关键因素之一,而IPD流程通过其独特的结构和机制,能够确保产品从概念到市场交付的每个环节都围绕客户需求展开。本文将深入...
IPD流程   70  
  IPD(Integrated Product Development,集成产品开发)流程是一种以跨职能团队协作为核心的产品开发方法,旨在通过优化资源分配、提高沟通效率以及减少返工,从而缩短项目周期并提升产品质量。随着企业对产品上市速度的要求越来越高,IPD流程的应用价值愈发凸显。通过整合产品开发过程中的各个环节,IPD...
IPD项目管理咨询   82  
  跨部门沟通是企业运营中不可或缺的一环,尤其在复杂的产品开发过程中,不同部门之间的协作效率直接影响项目的成败。集成产品开发(IPD)作为一种系统化的项目管理方法,旨在通过优化流程和增强团队协作来提升产品开发的效率和质量。然而,跨部门沟通的复杂性往往成为IPD实施中的一大挑战。部门之间的目标差异、信息不对称以及沟通渠道不畅...
IPD是什么意思   74  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用