Potrzebuję praktycznego podejścia do uzyskania wszystkich klas dziedziczonych z klasy podstawowej w Pythonie.
Potrzebuję praktycznego podejścia do uzyskania wszystkich klas dziedziczonych z klasy podstawowej w Pythonie.
Odpowiedzi:
Klasy w nowym stylu (tj. Podklasy z object
, co jest domyślne w Pythonie 3) mają __subclasses__
metodę, która zwraca podklasy:
class Foo(object): pass
class Bar(Foo): pass
class Baz(Foo): pass
class Bing(Bar): pass
Oto nazwy podklas:
print([cls.__name__ for cls in Foo.__subclasses__()])
# ['Bar', 'Baz']
Oto same podklasy:
print(Foo.__subclasses__())
# [<class '__main__.Bar'>, <class '__main__.Baz'>]
Potwierdzenie, że podklasy rzeczywiście są wymienione Foo
jako ich podstawa:
for cls in Foo.__subclasses__():
print(cls.__base__)
# <class '__main__.Foo'>
# <class '__main__.Foo'>
Uwaga: jeśli chcesz otrzymać podklasy, musisz powtórzyć:
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'>}
Zauważ, że jeśli definicja klasy podklasy nie została jeszcze wykonana - na przykład, jeśli moduł podklasy nie został jeszcze zaimportowany - wtedy ta podklasa jeszcze nie istnieje i __subclasses__
nie będzie mogła jej znaleźć.
Wspomniałeś o „podanej nazwie”. Ponieważ klasy Python są pierwszorzędnymi obiektami, nie musisz używać łańcucha z nazwą klasy w miejscu klasy ani nic podobnego. Możesz po prostu użyć tej klasy bezpośrednio i prawdopodobnie powinieneś.
Jeśli masz ciąg znaków reprezentujący nazwę klasy i chcesz znaleźć podklasy tej klasy, to musisz wykonać dwa kroki: znajdź klasę podaną jej nazwą, a następnie znajdź podklasy z __subclasses__
powyższym opisem.
Jak znaleźć klasę na podstawie nazwy, zależy od tego, gdzie chcesz ją znaleźć. Jeśli spodziewasz się znaleźć go w tym samym module co kod, który próbuje zlokalizować klasę, to
cls = globals()[name]
wykona zadanie lub w mało prawdopodobnym przypadku, gdy spodziewasz się go znaleźć u miejscowych,
cls = locals()[name]
Jeśli klasa mogłaby znajdować się w dowolnym module, to ciąg nazwy powinien zawierać w pełni kwalifikowaną nazwę - coś w stylu 'pkg.module.Foo'
zamiast po prostu 'Foo'
. Użyj, importlib
aby załadować moduł klasy, a następnie pobierz odpowiedni atrybut:
import importlib
modname, _, clsname = name.rpartition('.')
mod = importlib.import_module(modname)
cls = getattr(mod, clsname)
Gdy znajdziesz klasę, cls.__subclasses__()
zwróci listę jej podklas.
Jeśli chcesz tylko bezpośrednie podklasy, to .__subclasses__()
działa dobrze. Jeśli chcesz mieć wszystkie podklasy, podklasy podklas itd., Potrzebujesz funkcji, która to zrobi.
Oto prosta, czytelna funkcja, która rekurencyjnie znajduje wszystkie podklasy danej klasy:
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
być set
eliminowanie duplikatów?
A(object)
, B(A)
, C(A)
, i D(B, C)
. get_all_subclasses(A) == [B, C, D, D]
.
Najprostsze rozwiązanie w formie ogólnej:
def get_subclasses(cls):
for subclass in cls.__subclasses__():
yield from get_subclasses(subclass)
yield subclass
I metoda klasy na wypadek, gdybyś miał jedną klasę, z której dziedziczysz:
@classmethod
def get_subclasses(cls):
for subclass in cls.__subclasses__():
yield from subclass.get_subclasses()
yield subclass
__init_subclass__
Jak wspomniano w innej odpowiedzi, możesz sprawdzić __subclasses__
atrybut, aby uzyskać listę podklas, ponieważ w Pythonie 3.6 możesz modyfikować tworzenie tego atrybutu, zastępując __init_subclass__
metodę.
class PluginBase:
subclasses = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.subclasses.append(cls)
class Plugin1(PluginBase):
pass
class Plugin2(PluginBase):
pass
W ten sposób, jeśli wiesz, co robisz, możesz zastąpić zachowanie __subclasses__
i pominąć / dodać podklasy z tej listy.
__init_subclass
klasę rodzica.
Uwaga: Widzę, że ktoś (nie @unutbu) zmienił odpowiedź, do której się odwołuje, aby nie była już używana vars()['Foo']
- więc główny punkt mojego postu nie ma już zastosowania.
FWIW, oto, o co mi chodziło o odpowiedzi @ unutbu działającej tylko z lokalnie zdefiniowanymi klasami - i że użycie eval()
zamiast vars()
spowoduje, że będzie działać z dowolną dostępną klasą, nie tylko tymi zdefiniowanymi w bieżącym zakresie.
Dla tych, którzy nie lubią używać eval()
, pokazano również sposób, aby tego uniknąć.
Najpierw jest konkretny przykład pokazujący potencjalny problem z użyciem 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'>]
Można to poprawić, przesuwając w eval('ClassName')
dół do zdefiniowanej funkcji, co ułatwia korzystanie z niej bez utraty dodatkowej ogólności uzyskanej dzięki użyciu, eval()
która w przeciwieństwie do tego vars()
nie jest zależna od kontekstu:
# 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'>]
Wreszcie jest możliwe, a może nawet w niektórych przypadkach ważne, aby unikać używania eval()
ze względów bezpieczeństwa, więc oto wersja bez niego:
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()
- lepiej teraz?
Znacznie krótsza wersja umożliwiająca uzyskanie listy wszystkich podklas:
from itertools import chain
def subclasses(cls):
return list(
chain.from_iterable(
[list(chain.from_iterable([[x], subclasses(x)])) for x in cls.__subclasses__()]
)
)
Jak znaleźć wszystkie podklasy klasy, biorąc pod uwagę jej nazwę?
Z pewnością możemy to łatwo zrobić, mając dostęp do samego obiektu, tak.
Samo podanie jego nazwy jest kiepskim pomysłem, ponieważ może istnieć wiele klas o tej samej nazwie, nawet zdefiniowanych w tym samym module.
Stworzyłem implementację dla innej odpowiedzi , a ponieważ odpowiada ona na to pytanie i jest nieco bardziej elegancka niż inne rozwiązania tutaj, oto ona:
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
Stosowanie:
>>> 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'>]
To nie jest tak dobra odpowiedź, jak użycie specjalnej wbudowanej __subclasses__()
metody klasy, o której wspomina @unutbu, dlatego przedstawiam ją jedynie jako ćwiczenie. subclasses()
Funkcja zdefiniowana zwraca słownika, który odwzorowuje wszystkie nazwy podklasy samych podklasy.
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)
Wynik:
{'SubclassB': <class '__main__.SubclassB'>,
'SubclassA': <class '__main__.SubclassA'>}
Oto wersja bez rekurencji:
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], [])
Różni się od innych implementacji tym, że zwraca oryginalną klasę. Jest tak, ponieważ upraszcza kod i:
class Ham(object):
pass
assert(issubclass(Ham, Ham)) # True
Jeśli get_subclasses_gen wygląda trochę dziwnie, to dlatego, że został utworzony przez konwersję implementacji rekurencyjnej na ogon w generator pętli:
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], [])