如何根据名称找到某个类的所有子类?
- 2024-12-24 08:55:00
- admin 原创
- 95
问题描述:
我需要一种可行的方法来获取从 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()__
是:
您需要加载这些类(即导入它们),否则它将无法工作。
如果您有多层继承,则需要递归。例如:
class A
->class B(A)
->class C(B)
.A.__subclasses__()
只会给您 classB
而不是 classC
。这是因为 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())
这是有效的,因为字典记住了键的插入顺序。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)