Мені потрібен робочий підхід до отримання всіх класів, які успадковані від базового класу в 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']
- тому основний пункт моєї публікації більше не застосовується.
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'>]
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], [])