Comment trouver toutes les sous-classes d'une classe en fonction de son nom?

223

J'ai besoin d'une approche de travail pour obtenir toutes les classes héritées d'une classe de base en Python.

Roman Prykhodchenko
la source

Réponses:

316

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 Foocomme 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 importlibpour 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.

unutbu
la source
Supposons que je veuille trouver toutes les sous-classes d'un module, que le sous-module du module le contenant ait été importé ou non?
Samantha Atkins
1
@SamanthaAtkins: Générez une liste de tous les sous-modules du package , puis générez une liste de toutes les classes pour chaque module .
unutbu
Merci, c'est ce que j'ai fini par faire, mais j'étais curieux de savoir s'il y avait un meilleur moyen que j'avais manqué.
Samantha Atkins
63

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
fletom
la source
3
Merci @fletom! Bien que ce dont j'avais besoin à l'époque était juste des __ sous-classes __ (), votre solution est vraiment sympa. Prenez vous +1;) Btw, je pense qu'il pourrait être plus fiable d'utiliser des générateurs dans votre cas.
Roman Prykhodchenko
3
Ne devrait pas all_subclassesêtre un setpour éliminer les doublons?
Ryne Everett
@RyneEverett Vous voulez dire que si vous utilisez l'héritage multiple? Je pense que sinon vous ne devriez pas vous retrouver avec des doublons.
fletom
@fletom Oui, l'héritage multiple serait nécessaire pour les doublons. Par exemple, A(object), B(A), C(A)et D(B, C). get_all_subclasses(A) == [B, C, D, D].
Ryne Everett
@RomanPrykhodchenko: Le titre de votre question dit de trouver toutes les sous-classes d'une classe étant donné son nom, mais cela ainsi que d'autres travaux uniquement étant donné la classe elle-même, pas seulement son nom - alors qu'est-ce que c'est?
martineau
33

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
Kimvais
la source
2
L'approche du générateur est vraiment propre.
four43
22

Python 3.6 -__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.

Ou Duan
la source
1
Oui, toute sous-classe de toute nature déclencherait la __init_subclasssur la classe du parent.
Ou Duan
9

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'>]
martineau
la source
1
@Chris: Ajout d'une version qui n'utilise pas eval()- mieux maintenant?
martineau
4

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__()]
        )
    )
Peter Brooks
la source
2

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'>]
Aaron Hall
la source
2

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'>}
martineau
la source
1

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], [])
Thomas Grainger
la source