Ho bisogno di un approccio funzionante per ottenere tutte le classi ereditate da una classe base in Python.
Ho bisogno di un approccio funzionante per ottenere tutte le classi ereditate da una classe base in Python.
Risposte:
Le classi di nuovo stile (ovvero la sottoclasse da object
, che è l'impostazione predefinita in Python 3) hanno un __subclasses__
metodo che restituisce le sottoclassi:
class Foo(object): pass
class Bar(Foo): pass
class Baz(Foo): pass
class Bing(Bar): pass
Ecco i nomi delle sottoclassi:
print([cls.__name__ for cls in Foo.__subclasses__()])
# ['Bar', 'Baz']
Ecco le sottoclassi stesse:
print(Foo.__subclasses__())
# [<class '__main__.Bar'>, <class '__main__.Baz'>]
Conferma che le sottoclassi effettivamente elencano Foo
come base:
for cls in Foo.__subclasses__():
print(cls.__base__)
# <class '__main__.Foo'>
# <class '__main__.Foo'>
Nota se vuoi le sottoclassi, dovrai ricorrere:
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'>}
Si noti che se la definizione di classe di una sottoclasse non è stata ancora eseguita, ad esempio se il modulo della sottoclasse non è stato ancora importato, tale sottoclasse non esiste ancora e __subclasses__
non la troverà.
Hai menzionato "il suo nome". Poiché le classi Python sono oggetti di prima classe, non è necessario utilizzare una stringa con il nome della classe al posto della classe o qualcosa del genere. Puoi semplicemente usare la classe direttamente e probabilmente dovresti.
Se hai una stringa che rappresenta il nome di una classe e vuoi trovare le sottoclassi di quella classe, allora ci sono due passaggi: trova la classe con il suo nome e poi trova le sottoclassi con __subclasses__
come sopra.
Come trovare la classe dal nome dipende da dove ti aspetti di trovarla. Se ti aspetti di trovarlo nello stesso modulo del codice che sta cercando di individuare la classe, allora
cls = globals()[name]
farebbe il lavoro, o nel caso improbabile che ti aspetti di trovarlo nei locali,
cls = locals()[name]
Se la classe potrebbe essere in qualsiasi modulo, la stringa del tuo nome dovrebbe contenere il nome completo - qualcosa di simile 'pkg.module.Foo'
anziché solo 'Foo'
. Utilizzare importlib
per caricare il modulo della classe, quindi recuperare l'attributo corrispondente:
import importlib
modname, _, clsname = name.rpartition('.')
mod = importlib.import_module(modname)
cls = getattr(mod, clsname)
Comunque trovi la classe, cls.__subclasses__()
restituisce quindi un elenco delle sue sottoclassi.
Se vuoi solo sottoclassi dirette, allora .__subclasses__()
funziona bene. Se vuoi tutte le sottoclassi, sottoclassi di sottoclassi e così via, avrai bisogno di una funzione per farlo.
Ecco una funzione semplice e leggibile che trova ricorsivamente tutte le sottoclassi di una determinata classe:
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
essere un set
per eliminare i duplicati?
A(object)
, B(A)
, C(A)
, e D(B, C)
. get_all_subclasses(A) == [B, C, D, D]
.
La soluzione più semplice in forma generale:
def get_subclasses(cls):
for subclass in cls.__subclasses__():
yield from get_subclasses(subclass)
yield subclass
E un metodo di classe nel caso in cui tu abbia una singola classe da cui erediti:
@classmethod
def get_subclasses(cls):
for subclass in cls.__subclasses__():
yield from subclass.get_subclasses()
yield subclass
__init_subclass__
Come altre risposte menzionate, puoi controllare l' __subclasses__
attributo per ottenere l'elenco delle sottoclassi, poiché python 3.6 puoi modificare la creazione di questo attributo sovrascrivendo il __init_subclass__
metodo.
class PluginBase:
subclasses = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.subclasses.append(cls)
class Plugin1(PluginBase):
pass
class Plugin2(PluginBase):
pass
In questo modo, se sai cosa stai facendo, puoi ignorare il comportamento di __subclasses__
e omettere / aggiungere sottoclassi da questo elenco.
__init_subclass
classe genitore.
Nota: vedo che qualcuno (non @unutbu) ha cambiato la risposta referenziata in modo che non la usi più vars()['Foo']
, quindi il punto principale del mio post non si applica più.
FWIW, ecco cosa intendevo per la risposta di @ unutbu lavorando solo con classi definite localmente - e che l'uso eval()
invece di vars()
farebbe funzionare con qualsiasi classe accessibile, non solo quelle definite nell'ambito corrente.
Per coloro a cui non piace usare eval()
, viene anche mostrato un modo per evitarlo.
Innanzitutto ecco un esempio concreto che dimostra il potenziale problema con l'uso di 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'>]
Ciò potrebbe essere migliorato spostando il eval('ClassName')
basso nella funzione definita, il che rende l'utilizzo più semplice senza perdita della generalità aggiuntiva acquisita utilizzando il eval()
quale a differenza vars()
non è sensibile al contesto:
# 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'>]
Infine, è possibile, e forse anche importante in alcuni casi, evitare di utilizzarlo eval()
per motivi di sicurezza, quindi ecco una versione senza di essa:
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()
- meglio ora?
Una versione molto più breve per ottenere un elenco di tutte le sottoclassi:
from itertools import chain
def subclasses(cls):
return list(
chain.from_iterable(
[list(chain.from_iterable([[x], subclasses(x)])) for x in cls.__subclasses__()]
)
)
Come posso trovare tutte le sottoclassi di una classe dato il suo nome?
Possiamo certamente farlo facilmente, dato l'accesso all'oggetto stesso, sì.
Semplicemente dato il suo nome è una cattiva idea, in quanto possono esserci più classi con lo stesso nome, anche definite nello stesso modulo.
Ho creato un'implementazione per un'altra risposta , e poiché risponde a questa domanda ed è un po 'più elegante rispetto alle altre soluzioni qui, eccola qui:
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
Uso:
>>> 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'>]
Questa non è una buona risposta come l'uso dello speciale __subclasses__()
metodo di classe incorporato che menziona @unutbu, quindi lo presento semplicemente come un esercizio. La subclasses()
funzione definita restituisce un dizionario che associa tutti i nomi delle sottoclassi alle sottoclassi stesse.
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)
Produzione:
{'SubclassB': <class '__main__.SubclassB'>,
'SubclassA': <class '__main__.SubclassA'>}
Ecco una versione senza ricorsione:
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], [])
Ciò differisce da altre implementazioni in quanto restituisce la classe originale. Questo perché rende il codice più semplice e:
class Ham(object):
pass
assert(issubclass(Ham, Ham)) # True
Se get_subclasses_gen sembra un po 'strano, perché è stato creato convertendo un'implementazione ricorsiva della coda in un generatore di loop:
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], [])