Мне нужен рабочий подход для получения всех классов, унаследованных от базового класса в 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 vars()['Foo'].__subclasses__()])
# ['Bar', 'Baz']
Вот сами подклассы:
print(vars()['Foo'].__subclasses__())
# [<class '__main__.Bar'>, <class '__main__.Baz'>]
Подтверждение того, что подклассы действительно перечисляют Foo
в качестве базы:
for cls in vars()['Foo'].__subclasses__():
print(cls.__base__)
# <class '__main__.Foo'>
# <class '__main__.Foo'>
Обратите внимание, что если вы хотите subsubclasses, вам придется рекурсивно:
def all_subclasses(cls):
return cls.__subclasses__() + [g for s in cls.__subclasses__()
for g in all_subclasses(s)]
print(all_subclasses(vars()['Foo']))
# [<class '__main__.Bar'>, <class '__main__.Baz'>, <class '__main__.Bing'>]
Ответ 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 в случае, если у вас есть один класс, на который вы наследуете:
@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
FWIW, вот что я имел в виду @unutbu answer, только работая с локально определенными классами — и что использование eval()
вместо vars()
сделает работу с любым доступным классом, а не только теми, которые определены в текущей области.
Для тех, кому не нравится использование eval()
, также показано, как избежать этого.
Сначала здесь приведен конкретный пример, демонстрирующий потенциальную проблему с использованием vars()
:
class Foo(object): pass
class Bar(Foo): pass
class Baz(Foo): pass
class Bing(Bar): pass
# unutbu 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 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):
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
Более короткая версия для получения списка всех подклассов:
from itertools import chain
def subclasses(cls):
return list(
chain.from_iterable(
[list(chain.from_iterable([[x], subclasses(x)])) for x in cls.__subclasses__()]
)
)
Ответ 7
Это не такой хороший ответ, как использование специального встроенного метода класса __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'>}
Ответ 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
Я не могу представить себе реальный случай использования в мире, но надежный способ (даже для классов старого стиля Python 2) - отсканировать пространство имен глобальных переменных:
def has_children(cls):
g = globals().copy() # use a copy to make sure it will not change during iteration
g.update(locals()) # add local symbols
for k, v in g.items(): # iterate over all globals object
try:
if (v is not cls) and issubclass(v, cls): # found a strict sub class?
return True
except TypeError: # issubclass raises a TypeError if arg is not a class...
pass
return False
Он работает на Python 2 новых классах стилей и классах Python 3, а также на классических классах Python 2
Ответ 10
Как найти все подклассы класса с его именем?
Мы можем, конечно, легко сделать это, получив доступ к самому объекту, да.
Просто потому, что его имя плохое, так как может быть несколько классов с одним и тем же именем, даже определенное в том же модуле.
Я создал реализацию для другого 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'>]