web-dev-qa-db-fra.com

Comment trouver toutes les sous-classes d'une classe étant donné son nom?

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

184

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.

245
unutbu

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
58
fletom

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
25
Kimvais

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

16
Or Duan

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'>]
8
martineau

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__()]
        )
    )
3
Peter Brooks

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

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'>]
1
Aaron Hall

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

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

0
Serge Ballesta