J'ai besoin d'une approche de travail pour obtenir toutes les classes héritées d'une classe de base en Python.
Les classes de style nouveau (c'est-à-dire les sous-classes de object
, qui est la valeur par défaut de Python 3) ont une méthode __subclasses__
Qui renvoie les sous-classes:
class Foo(object): pass
class Bar(Foo): pass
class Baz(Foo): pass
class Bing(Bar): pass
Voici les noms des sous-classes:
print([cls.__for cls in Foo.__subclasses__()])
# ['Bar', 'Baz']
Voici les sous-classes elles-mêmes:
print(Foo.__subclasses__())
# [<class '__main__.Bar'>, <class '__main__.Baz'>]
La confirmation que les sous-classes listent bien Foo
comme base:
for cls in Foo.__subclasses__():
print(cls.__base__)
# <class '__main__.Foo'>
# <class '__main__.Foo'>
Notez que si vous voulez des sous-classes, vous devrez recurse:
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'>}
Notez que si la définition de classe d'une sous-classe n'a pas encore été exécutée - par exemple, si le module de la sous-classe n'a pas encore été importé - cette sous-classe n'existe pas encore et __subclasses__
Ne trouvera pas il.
Vous avez mentionné "étant donné son nom". Puisque les classes Python sont des objets de première classe, vous n’avez pas besoin d’utiliser une chaîne avec le nom de la classe à la place de la classe ou quelque chose du genre. Vous pouvez simplement utiliser la classe directement, et vous devriez probablement.
Si vous avez une chaîne représentant le nom d'une classe et que vous souhaitez rechercher les sous-classes de cette classe, il existe deux étapes: rechercher la classe en lui donnant son nom, puis rechercher les sous-classes avec __subclasses__
Comme ci-dessus.
Comment trouver la classe à partir de son nom dépend de l'endroit où vous vous attendez à la trouver. Si vous pensez le trouver dans le même module que le code qui tente de localiser la classe, alors
cls = globals()[name]
ferait le travail, ou dans le cas peu probable où vous vous attendez à le trouver dans les locaux,
cls = locals()[name]
Si la classe peut figurer dans n’importe quel module, votre chaîne de nom doit contenir le nom qualifié complet, quelque chose comme 'pkg.module.Foo'
Au lieu de 'Foo'
. Utilisez importlib
pour charger le module de la classe, puis récupérez l'attribut correspondant:
import importlib
modname, _, clsname = name.rpartition('.')
mod = importlib.import_module(modname)
cls = getattr(mod, clsname)
Cependant, si vous trouvez la classe, cls.__subclasses__()
renverrait alors une liste de ses sous-classes.
Si vous voulez juste des sous-classes directes, alors .__subclasses__()
fonctionne bien. Si vous voulez toutes les sous-classes, sous-classes de sous-classes, etc., vous aurez besoin d'une fonction pour le faire à votre place.
Voici une fonction simple et lisible qui trouve de manière récursive toutes les sous-classes d'une classe donnée:
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
La solution la plus simple sous forme générale:
def get_subclasses(cls):
for subclass in cls.__subclasses__():
yield from get_subclasses(subclass)
yield subclass
Et une méthode de classe si vous avez une seule classe dont vous héritez:
@classmethod
def get_subclasses(cls):
for subclass in cls.__subclasses__():
yield from subclass.get_subclasses()
yield subclass
__init_subclass__
Comme autre réponse mentionnée, vous pouvez vérifier le __subclasses__
attribut pour obtenir la liste des sous-classes, puisque python 3.6 vous pouvez modifier cette création d’attribut en remplaçant __init_subclass__
méthode.
class PluginBase:
subclasses = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.subclasses.append(cls)
class Plugin1(PluginBase):
pass
class Plugin2(PluginBase):
pass
De cette façon, si vous savez ce que vous faites, vous pouvez modifier le comportement de __subclasses__
et omettez/ajoutez des sous-classes de cette liste.
FWIW, voici ce que je voulais dire à propos de réponse de @ unutb ne travaillant qu'avec des classes définies localement - et que l'utilisation de eval()
au lieu de vars()
le ferait fonctionner avec toutes les classes accessibles. classe, pas seulement ceux définis dans la portée actuelle.
Pour ceux qui n'aiment pas utiliser eval()
, un moyen de l'éviter est également indiqué.
Tout d'abord, voici un exemple concret démontrant le problème potentiel lié à l'utilisation de 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'>]
Cela pourrait être amélioré en déplaçant la eval('ClassName')
dans la fonction définie, ce qui facilite son utilisation sans perte de la généralité supplémentaire obtenue en utilisant eval()
qui, contrairement à vars()
n'est pas sensible au contexte:
# 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'>]
Enfin, il est possible, et peut-être même important dans certains cas, d'éviter d'utiliser eval()
pour des raisons de sécurité, voici donc une version sans celle-ci:
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'>]
Une version beaucoup plus courte pour obtenir une liste de toutes les sous-classes:
from itertools import chain
def subclasses(cls):
return list(
chain.from_iterable(
[list(chain.from_iterable([[x], subclasses(x)])) for x in cls.__subclasses__()]
)
)
Ce n'est pas une aussi bonne réponse que d'utiliser la méthode spéciale intégrée dans in__subclasses__()
class mentionnée par @unutbu, je la présente donc simplement comme un exercice. La fonctionsubclasses()
définie renvoie un dictionnaire qui mappe tous les noms de sous-classes aux sous-classes elles-mêmes.
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)
Sortie:
{'SubclassB': <class '__main__.SubclassB'>,
'SubclassA': <class '__main__.SubclassA'>}
Comment trouver toutes les sous-classes d'une classe étant donné son nom?
Nous pouvons certainement le faire facilement, étant donné l’accès à l’objet lui-même, oui.
Donner simplement son nom est une mauvaise idée, car il peut y avoir plusieurs classes du même nom, même définies dans le même module.
J'ai créé une implémentation pour un autre réponse , et comme il répond à cette question et qu'il est un peu plus élégant que les autres solutions ici, le voici:
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
Usage:
>>> 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'>]
Voici une version sans récursion:
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], [])
Cela diffère des autres implémentations en ce sens qu'il retourne la classe d'origine. En effet, cela simplifie le code et:
class Ham(object):
pass
assert(issubclass(Ham, Ham)) # True
Si get_subclasses_gen semble un peu bizarre, c'est parce qu'il a été créé en convertissant une implémentation tail-récursive en un générateur de boucles:
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], [])
Je ne peux pas imaginer un cas d'utilisation réel du monde, mais une méthode robuste (même sur Python 2 anciennes classes de style)) consisterait à analyser l'espace de noms global:
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
Cela fonctionne sur Python 2 nouvelles classes de style et Python 3 classes ainsi que sur Python 2 classique) classes