Мне нужен рабочий подход для получения всех классов, которые унаследованы от базового класса в Python.
Мне нужен рабочий подход для получения всех классов, которые унаследованы от базового класса в Python.
Ответы:
Классы нового стиля (то есть подклассы из 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__()
который затем вернет список его подклассов.
Если вы просто хотите прямые подклассы, то .__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
all_subclasses
быть, set
чтобы устранить дубликаты?
A(object)
, B(A)
, C(A)
и D(B, C)
. get_all_subclasses(A) == [B, C, D, D]
,
Самое простое решение в общем виде:
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
__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
класс родителя.
Примечание: я вижу, что кто-то (не @unutbu) изменил ссылочный ответ, чтобы он больше не использовался vars()['Foo']
- поэтому основной пункт моего поста больше не применяется.
Кстати, вот что я хотел сказать об ответе @ 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'>]
eval()
- лучше сейчас?
Гораздо более короткая версия для получения списка всех подклассов:
from itertools import chain
def subclasses(cls):
return list(
chain.from_iterable(
[list(chain.from_iterable([[x], subclasses(x)])) for x in cls.__subclasses__()]
)
)
Как я могу найти все подклассы класса по имени?
Конечно, мы можем легко сделать это при наличии доступа к самому объекту, да.
Просто учитывая его имя - плохая идея, так как может быть несколько классов с одним и тем же именем, даже определенных в одном модуле.
Я создал реализацию для другого ответа , и поскольку он отвечает на этот вопрос, и он немного более элегантен, чем другие решения, вот он:
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'>]
Это не такой хороший ответ, как использование специального встроенного __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'>}
Вот версия без рекурсии:
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], [])