Wie finde ich alle Unterklassen einer Klasse mit ihrem Namen?

223

Ich brauche einen funktionierenden Ansatz, um alle Klassen zu erhalten, die von einer Basisklasse in Python geerbt wurden.

Roman Prykhodchenko
quelle

Antworten:

313

Klassen neuen Stils (dh Unterklassen von object, was in Python 3 die Standardeinstellung ist) haben eine __subclasses__Methode, die die Unterklassen zurückgibt:

class Foo(object): pass
class Bar(Foo): pass
class Baz(Foo): pass
class Bing(Bar): pass

Hier sind die Namen der Unterklassen:

print([cls.__name__ for cls in Foo.__subclasses__()])
# ['Bar', 'Baz']

Hier sind die Unterklassen selbst:

print(Foo.__subclasses__())
# [<class '__main__.Bar'>, <class '__main__.Baz'>]

Bestätigung, dass die Unterklassen tatsächlich Fooals Basis aufgeführt sind:

for cls in Foo.__subclasses__():
    print(cls.__base__)
# <class '__main__.Foo'>
# <class '__main__.Foo'>

Hinweis: Wenn Sie Unterklassen möchten, müssen Sie Folgendes wiederholen:

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

Beachten Sie, dass wenn die Klassendefinition einer Unterklasse noch nicht ausgeführt wurde - beispielsweise wenn das Modul der Unterklasse noch nicht importiert wurde - diese Unterklasse noch nicht vorhanden ist und __subclasses__sie nicht findet.


Sie haben "seinen Namen gegeben" erwähnt. Da Python-Klassen erstklassige Objekte sind, müssen Sie anstelle der Klasse oder dergleichen keine Zeichenfolge mit dem Klassennamen verwenden. Sie können die Klasse einfach direkt verwenden, und Sie sollten es wahrscheinlich tun.

Wenn Sie eine Zeichenfolge haben, die den Namen einer Klasse darstellt, und die Unterklassen dieser Klasse suchen möchten, gibt es zwei Schritte: Suchen Sie die Klasse mit ihrem Namen und suchen Sie dann die Unterklassen mit __subclasses__wie oben.

Wie Sie die Klasse anhand des Namens finden, hängt davon ab, wo Sie sie erwarten. Wenn Sie erwarten, dass es sich im selben Modul befindet wie der Code, der versucht, die Klasse zu finden, dann

cls = globals()[name]

würde den Job machen, oder in dem unwahrscheinlichen Fall, dass Sie erwarten, ihn bei Einheimischen zu finden,

cls = locals()[name]

Wenn sich die Klasse in einem Modul befinden könnte, sollte Ihre Namenszeichenfolge den vollständig qualifizierten Namen enthalten - so etwas wie 'pkg.module.Foo'statt nur 'Foo'. Verwenden Sie importlibdiese Option , um das Modul der Klasse zu laden und dann das entsprechende Attribut abzurufen:

import importlib
modname, _, clsname = name.rpartition('.')
mod = importlib.import_module(modname)
cls = getattr(mod, clsname)

Wie auch immer Sie die Klasse finden, cls.__subclasses__()würde dann eine Liste ihrer Unterklassen zurückgeben.

unutbu
quelle
Angenommen, ich wollte alle Unterklassen in einem Modul finden, ob das Submodul des Moduls, das es enthält, importiert wurde oder nicht.
Samantha Atkins
1
@SamanthaAtkins: Generieren Sie eine Liste aller Submodule des Pakets und anschließend eine Liste aller Klassen für jedes Modul .
Unutbu
Danke, das habe ich letztendlich getan, war aber neugierig, ob es einen besseren Weg gibt, den ich verpasst habe.
Samantha Atkins
63

Wenn Sie nur direkte Unterklassen möchten, .__subclasses__()funktioniert dies einwandfrei. Wenn Sie alle Unterklassen, Unterklassen von Unterklassen usw. möchten, benötigen Sie eine Funktion, die dies für Sie erledigt.

Hier ist eine einfache, lesbare Funktion, die rekursiv alle Unterklassen einer bestimmten Klasse findet:

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
quelle
3
Danke @fletom! Obwohl ich damals nur __Unterklassen __ () brauchte, ist Ihre Lösung wirklich nett. Nehmen Sie +1;) Übrigens, ich denke, es könnte in Ihrem Fall zuverlässiger sein, Generatoren zu verwenden.
Roman Prykhodchenko
3
Sollte nicht all_subclassesein sein set, um Duplikate zu beseitigen?
Ryne Everett
@ RyneEverett Du meinst, wenn du Mehrfachvererbung verwendest? Ich denke, sonst sollten Sie nicht mit Duplikaten enden.
Fletom
@fletom Ja, für Duplikate wäre eine Mehrfachvererbung erforderlich. Zum Beispiel A(object), B(A), C(A), und D(B, C). get_all_subclasses(A) == [B, C, D, D].
Ryne Everett
@RomanPrykhodchenko: Der Titel Ihrer Frage besagt, dass alle Unterklassen einer Klasse mit ihrem Namen gefunden werden sollen, aber diese und andere Arbeiten funktionieren nur mit der Klasse selbst, nicht nur mit ihrem Namen - also was ist das?
Martineau
32

Die einfachste Lösung in allgemeiner Form:

def get_subclasses(cls):
    for subclass in cls.__subclasses__():
        yield from get_subclasses(subclass)
        yield subclass

Und eine Klassenmethode für den Fall, dass Sie eine einzelne Klasse haben, von der Sie erben:

@classmethod
def get_subclasses(cls):
    for subclass in cls.__subclasses__():
        yield from subclass.get_subclasses()
        yield subclass
Kimvais
quelle
1
Der Generatoransatz ist wirklich sauber.
Four43
22

Python 3.6 -__init_subclass__

Wie bereits erwähnt, können Sie das __subclasses__Attribut überprüfen , um die Liste der Unterklassen abzurufen. Seit Python 3.6 können Sie diese Attributerstellung ändern, indem Sie die __init_subclass__Methode überschreiben .

class PluginBase:
    subclasses = []

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.subclasses.append(cls)

class Plugin1(PluginBase):
    pass

class Plugin2(PluginBase):
    pass

Auf diese Weise können Sie, wenn Sie wissen, was Sie tun, das Verhalten von __subclasses__Unterklassen überschreiben und in dieser Liste weglassen / hinzufügen.

Oder Duan
quelle
1
Ja, jede Unterklasse jeglicher Art würde __init_subclassdie Klasse der Eltern auslösen .
Oder Duan
9

Hinweis: Ich sehe, dass jemand (nicht @unutbu) die referenzierte Antwort so geändert hat, dass sie nicht mehr verwendet wird. Daher vars()['Foo']gilt der primäre Punkt meines Beitrags nicht mehr.

FWIW, hier ist, was ich damit gemeint habe, dass die Antwort von @ unutbu nur mit lokal definierten Klassen funktioniert - und dass die Verwendung von eval()statt vars()mit jeder zugänglichen Klasse funktioniert, nicht nur mit den im aktuellen Bereich definierten.

Für diejenigen, die es nicht mögen eval(), wird auch ein Weg gezeigt, dies zu vermeiden.

Hier ist zunächst ein konkretes Beispiel, das das potenzielle Problem bei der Verwendung veranschaulicht 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'>]

Dies könnte verbessert werden, indem das eval('ClassName')Down in die definierte Funktion verschoben wird, was die Verwendung erleichtert, ohne dass die zusätzliche Allgemeinheit verloren geht, die durch die Verwendung erzielt wird, eval()die im Gegensatz zu vars()nicht kontextsensitiv ist:

# 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'>]

Schließlich ist es möglich und in einigen Fällen sogar wichtig, die Verwendung eval()aus Sicherheitsgründen zu vermeiden. Hier ist eine Version ohne diese:

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
quelle
1
@Chris: Eine Version hinzugefügt, die nicht verwendet wird eval()- jetzt besser?
Martineau
4

Eine viel kürzere Version, um eine Liste aller Unterklassen zu erhalten:

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
quelle
2

Wie finde ich alle Unterklassen einer Klasse mit ihrem Namen?

Wir können dies sicherlich leicht tun, wenn wir auf das Objekt selbst zugreifen, ja.

Einfach seinen Namen zu geben, ist eine schlechte Idee, da es mehrere Klassen desselben Namens geben kann, die sogar im selben Modul definiert sind.

Ich habe eine Implementierung für eine andere Antwort erstellt . Da diese Frage beantwortet wird und etwas eleganter ist als die anderen Lösungen hier, ist sie hier:

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

Verwendung:

>>> 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
quelle
2

Dies ist keine so gute Antwort wie die Verwendung der speziellen integrierten __subclasses__()Klassenmethode, die @unutbu erwähnt, daher präsentiere ich sie lediglich als Übung. Die subclasses()definierte Funktion gibt ein Wörterbuch zurück, das alle Unterklassennamen den Unterklassen selbst zuordnet.

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)

Ausgabe:

{'SubclassB': <class '__main__.SubclassB'>,
 'SubclassA': <class '__main__.SubclassA'>}
Martineau
quelle
1

Hier ist eine Version ohne Rekursion:

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

Dies unterscheidet sich von anderen Implementierungen darin, dass die ursprüngliche Klasse zurückgegeben wird. Dies liegt daran, dass der Code dadurch einfacher wird und:

class Ham(object):
    pass

assert(issubclass(Ham, Ham)) # True

Wenn get_subclasses_gen etwas seltsam aussieht, liegt das daran, dass es durch Konvertieren einer rekursiven Implementierung in einen Schleifengenerator erstellt wurde:

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
quelle