Jak znaleźć wszystkie podklasy klasy, biorąc pod uwagę jej nazwę?

223

Potrzebuję praktycznego podejścia do uzyskania wszystkich klas dziedziczonych z klasy podstawowej w Pythonie.

Roman Prykhodchenko
źródło

Odpowiedzi:

315

Klasy w nowym stylu (tj. Podklasy z object, co jest domyślne w Pythonie 3) mają __subclasses__metodę, która zwraca podklasy:

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

Oto nazwy podklas:

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

Oto same podklasy:

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

Potwierdzenie, że podklasy rzeczywiście są wymienione Foojako ich podstawa:

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

Uwaga: jeśli chcesz otrzymać podklasy, musisz powtórzyć:

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

Zauważ, że jeśli definicja klasy podklasy nie została jeszcze wykonana - na przykład, jeśli moduł podklasy nie został jeszcze zaimportowany - wtedy ta podklasa jeszcze nie istnieje i __subclasses__nie będzie mogła jej znaleźć.


Wspomniałeś o „podanej nazwie”. Ponieważ klasy Python są pierwszorzędnymi obiektami, nie musisz używać łańcucha z nazwą klasy w miejscu klasy ani nic podobnego. Możesz po prostu użyć tej klasy bezpośrednio i prawdopodobnie powinieneś.

Jeśli masz ciąg znaków reprezentujący nazwę klasy i chcesz znaleźć podklasy tej klasy, to musisz wykonać dwa kroki: znajdź klasę podaną jej nazwą, a następnie znajdź podklasy z __subclasses__powyższym opisem.

Jak znaleźć klasę na podstawie nazwy, zależy od tego, gdzie chcesz ją znaleźć. Jeśli spodziewasz się znaleźć go w tym samym module co kod, który próbuje zlokalizować klasę, to

cls = globals()[name]

wykona zadanie lub w mało prawdopodobnym przypadku, gdy spodziewasz się go znaleźć u miejscowych,

cls = locals()[name]

Jeśli klasa mogłaby znajdować się w dowolnym module, to ciąg nazwy powinien zawierać w pełni kwalifikowaną nazwę - coś w stylu 'pkg.module.Foo'zamiast po prostu 'Foo'. Użyj, importlibaby załadować moduł klasy, a następnie pobierz odpowiedni atrybut:

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

Gdy znajdziesz klasę, cls.__subclasses__()zwróci listę jej podklas.

unutbu
źródło
Załóżmy, że chciałem znaleźć wszystkie podklasy w module, czy podmoduł zawierający moduł został zaimportowany, czy nie?
Samantha Atkins
1
@SamanthaAtkins: Wygeneruj listę wszystkich podmodułów pakietu , a następnie wygeneruj listę wszystkich klas dla każdego modułu .
unutbu
Dzięki, że tak właśnie skończyłem, ale byłem ciekawy, czy może być lepszy sposób, za którym tęskniłem.
Samantha Atkins
63

Jeśli chcesz tylko bezpośrednie podklasy, to .__subclasses__()działa dobrze. Jeśli chcesz mieć wszystkie podklasy, podklasy podklas itd., Potrzebujesz funkcji, która to zrobi.

Oto prosta, czytelna funkcja, która rekurencyjnie znajduje wszystkie podklasy danej klasy:

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
źródło
3
Dziękuję @fletom! Chociaż w tamtych czasach potrzebowałem tylko __klasy __ (), twoje rozwiązanie jest naprawdę fajne. Daje ci +1;) Przy okazji, myślę, że może być bardziej niezawodne przy użyciu generatorów w twoim przypadku.
Roman Prykhodchenko
3
Czy nie powinno all_subclassesbyć seteliminowanie duplikatów?
Ryne Everett,
@RyneEverett Masz na myśli, jeśli korzystasz z wielokrotnego dziedziczenia? Myślę, że inaczej nie powinieneś skończyć z duplikatami.
fletom
@fletom Tak, wielokrotne dziedziczenie byłoby konieczne dla duplikatów. Na przykład A(object), B(A), C(A), i D(B, C). get_all_subclasses(A) == [B, C, D, D].
Ryne Everett
@RomanPrykhodchenko: Tytuł twojego pytania mówi, aby znaleźć wszystkie podklasy klasy, biorąc pod uwagę jej nazwę, ale to, jak również inne prace, zważywszy na samą klasę, a nie tylko jej nazwę - więc co to takiego?
martineau
33

Najprostsze rozwiązanie w formie ogólnej:

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

I metoda klasy na wypadek, gdybyś miał jedną klasę, z której dziedziczysz:

@classmethod
def get_subclasses(cls):
    for subclass in cls.__subclasses__():
        yield from subclass.get_subclasses()
        yield subclass
Kimvais
źródło
2
Podejście generatora jest naprawdę czyste.
four43
22

Python 3.6 -__init_subclass__

Jak wspomniano w innej odpowiedzi, możesz sprawdzić __subclasses__atrybut, aby uzyskać listę podklas, ponieważ w Pythonie 3.6 możesz modyfikować tworzenie tego atrybutu, zastępując __init_subclass__metodę.

class PluginBase:
    subclasses = []

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

class Plugin1(PluginBase):
    pass

class Plugin2(PluginBase):
    pass

W ten sposób, jeśli wiesz, co robisz, możesz zastąpić zachowanie __subclasses__i pominąć / dodać podklasy z tej listy.

Lub Duan
źródło
1
Tak, każda podklasa z dowolnego rodzaju uruchomiłaby __init_subclassklasę rodzica.
Lub Duan
9

Uwaga: Widzę, że ktoś (nie @unutbu) zmienił odpowiedź, do której się odwołuje, aby nie była już używana vars()['Foo']- więc główny punkt mojego postu nie ma już zastosowania.

FWIW, oto, o co mi chodziło o odpowiedzi @ unutbu działającej tylko z lokalnie zdefiniowanymi klasami - i że użycie eval()zamiast vars()spowoduje, że będzie działać z dowolną dostępną klasą, nie tylko tymi zdefiniowanymi w bieżącym zakresie.

Dla tych, którzy nie lubią używać eval(), pokazano również sposób, aby tego uniknąć.

Najpierw jest konkretny przykład pokazujący potencjalny problem z użyciem 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'>]

Można to poprawić, przesuwając w eval('ClassName')dół do zdefiniowanej funkcji, co ułatwia korzystanie z niej bez utraty dodatkowej ogólności uzyskanej dzięki użyciu, eval()która w przeciwieństwie do tego vars()nie jest zależna od kontekstu:

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

Wreszcie jest możliwe, a może nawet w niektórych przypadkach ważne, aby unikać używania eval()ze względów bezpieczeństwa, więc oto wersja bez niego:

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
źródło
1
@Chris: Dodano wersję, która nie używa eval()- lepiej teraz?
martineau
4

Znacznie krótsza wersja umożliwiająca uzyskanie listy wszystkich podklas:

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
źródło
2

Jak znaleźć wszystkie podklasy klasy, biorąc pod uwagę jej nazwę?

Z pewnością możemy to łatwo zrobić, mając dostęp do samego obiektu, tak.

Samo podanie jego nazwy jest kiepskim pomysłem, ponieważ może istnieć wiele klas o tej samej nazwie, nawet zdefiniowanych w tym samym module.

Stworzyłem implementację dla innej odpowiedzi , a ponieważ odpowiada ona na to pytanie i jest nieco bardziej elegancka niż inne rozwiązania tutaj, oto ona:

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

Stosowanie:

>>> 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
źródło
2

To nie jest tak dobra odpowiedź, jak użycie specjalnej wbudowanej __subclasses__()metody klasy, o której wspomina @unutbu, dlatego przedstawiam ją jedynie jako ćwiczenie. subclasses()Funkcja zdefiniowana zwraca słownika, który odwzorowuje wszystkie nazwy podklasy samych podklasy.

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)

Wynik:

{'SubclassB': <class '__main__.SubclassB'>,
 'SubclassA': <class '__main__.SubclassA'>}
martineau
źródło
1

Oto wersja bez rekurencji:

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

Różni się od innych implementacji tym, że zwraca oryginalną klasę. Jest tak, ponieważ upraszcza kod i:

class Ham(object):
    pass

assert(issubclass(Ham, Ham)) # True

Jeśli get_subclasses_gen wygląda trochę dziwnie, to dlatego, że został utworzony przez konwersję implementacji rekurencyjnej na ogon w generator pętli:

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
źródło