J'ai besoin d'une approche de travail pour obtenir toutes les classes héritées d'une classe de base en Python.
J'ai besoin d'une approche de travail pour obtenir toutes les classes héritées d'une classe de base en Python.
Réponses:
Les classes de nouveau style (c'est-à-dire sous-classées à partir de object
, qui est la valeur par défaut en Python 3) ont une __subclasses__
méthode qui retourne 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.__name__ for cls in Foo.__subclasses__()])
# ['Bar', 'Baz']
Voici les sous-classes elles-mêmes:
print(Foo.__subclasses__())
# [<class '__main__.Bar'>, <class '__main__.Baz'>]
Confirmation que les sous-classes sont bien listées 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 récuser:
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é - alors cette sous-classe n'existe pas encore et __subclasses__
ne la trouvera pas.
Vous avez mentionné "étant donné son nom". Étant donné que 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 comme ça. Vous pouvez simplement utiliser la classe directement, et vous devriez probablement le faire.
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 y a deux étapes: recherchez la classe en fonction de son nom, puis recherchez les sous-classes avec __subclasses__
comme ci-dessus.
Comment trouver la classe à partir du nom dépend de l'endroit où vous vous attendez à le trouver. Si vous vous attendez à le trouver dans le même module que le code qui essaie de localiser la classe, alors
cls = globals()[name]
ferait le travail, ou dans le cas peu probable où vous vous attendez à le trouver chez les locaux,
cls = locals()[name]
Si la classe peut être dans n'importe quel module, alors votre chaîne de nom doit contenir le nom complet - quelque chose comme 'pkg.module.Foo'
au lieu de juste '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)
Quoi que vous trouviez la classe, cls.__subclasses__()
retournerait alors une liste de ses sous-classes.
Si vous voulez juste des sous-classes directes, .__subclasses__()
fonctionne très bien. Si vous voulez toutes les sous-classes, sous-classes de sous-classes, etc., vous aurez besoin d'une fonction pour le faire pour vous.
Voici une fonction simple et lisible qui recherche récursivement 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
all_subclasses
être un set
pour éliminer les doublons?
A(object)
, B(A)
, C(A)
et D(B, C)
. get_all_subclasses(A) == [B, C, D, D]
.
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 dans le cas où 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 d'autres réponses l'ont mentionné, vous pouvez vérifier l' __subclasses__
attribut pour obtenir la liste des sous-classes, car python 3.6 vous pouvez modifier cette création d'attribut en remplaçant la __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 remplacer le comportement de __subclasses__
et omettre / ajouter des sous-classes de cette liste.
__init_subclass
sur la classe du parent.
Remarque: je vois que quelqu'un (pas @unutbu) a changé la réponse référencée afin qu'elle n'utilise plus vars()['Foo']
- donc le point principal de mon message ne s'applique plus.
FWIW, voici ce que je voulais dire à propos de la réponse de @ unutbu ne fonctionnant qu'avec des classes définies localement - et que l'utilisation eval()
au lieu de le vars()
ferait fonctionner avec n'importe quelle classe accessible, pas seulement celles définies dans la portée actuelle.
Pour ceux qui n'aiment pas utiliser eval()
, un moyen est également indiqué pour l'éviter.
Voici d'abord un exemple concret démontrant le problème potentiel de l'utilisation 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 le eval('ClassName')
bas dans la fonction définie, ce qui rend son utilisation plus facile sans perdre la généralité supplémentaire obtenue en utilisant eval()
ce qui contrairement à vars()
n'est pas contextuel:
# 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:
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()
- mieux maintenant?
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__()]
)
)
Comment puis-je trouver toutes les sous-classes d'une classe en fonction de son nom?
Nous pouvons certainement le faire facilement en ayant 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 une autre réponse , et comme elle répond à cette question et qu'elle est un peu plus élégante que les autres solutions ici, la 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'>]
Ce n'est pas une aussi bonne réponse que d'utiliser la méthode spéciale de __subclasses__()
classe intégrée mentionnée par @unutbu, donc je la présente simplement comme un exercice. La subclasses()
fonction définie renvoie un dictionnaire qui mappe tous les noms de sous-classe 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)
Production:
{'SubclassB': <class '__main__.SubclassB'>,
'SubclassA': <class '__main__.SubclassA'>}
Voici une version sans récursivité:
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 qu'il renvoie la classe d'origine. En effet, cela rend le code plus simple et:
class Ham(object):
pass
assert(issubclass(Ham, Ham)) # True
Si get_subclasses_gen a l'air un peu bizarre, c'est parce qu'il a été créé en convertissant une implémentation 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], [])