Come trovare tutte le sottoclassi di una classe dato il suo nome?


223

Ho bisogno di un approccio funzionante per ottenere tutte le classi ereditate da una classe base in Python.

Risposte:


316

Le classi di nuovo stile (ovvero la sottoclasse da object, che è l'impostazione predefinita in Python 3) hanno un __subclasses__metodo che restituisce le sottoclassi:

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

Ecco i nomi delle sottoclassi:

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

Ecco le sottoclassi stesse:

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

Conferma che le sottoclassi effettivamente elencano Foocome base:

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

Nota se vuoi le sottoclassi, dovrai ricorrere:

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

Si noti che se la definizione di classe di una sottoclasse non è stata ancora eseguita, ad esempio se il modulo della sottoclasse non è stato ancora importato, tale sottoclasse non esiste ancora e __subclasses__non la troverà.


Hai menzionato "il suo nome". Poiché le classi Python sono oggetti di prima classe, non è necessario utilizzare una stringa con il nome della classe al posto della classe o qualcosa del genere. Puoi semplicemente usare la classe direttamente e probabilmente dovresti.

Se hai una stringa che rappresenta il nome di una classe e vuoi trovare le sottoclassi di quella classe, allora ci sono due passaggi: trova la classe con il suo nome e poi trova le sottoclassi con __subclasses__come sopra.

Come trovare la classe dal nome dipende da dove ti aspetti di trovarla. Se ti aspetti di trovarlo nello stesso modulo del codice che sta cercando di individuare la classe, allora

cls = globals()[name]

farebbe il lavoro, o nel caso improbabile che ti aspetti di trovarlo nei locali,

cls = locals()[name]

Se la classe potrebbe essere in qualsiasi modulo, la stringa del tuo nome dovrebbe contenere il nome completo - qualcosa di simile 'pkg.module.Foo'anziché solo 'Foo'. Utilizzare importlibper caricare il modulo della classe, quindi recuperare l'attributo corrispondente:

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

Comunque trovi la classe, cls.__subclasses__()restituisce quindi un elenco delle sue sottoclassi.


Supponiamo che volessi trovare tutte le sottoclassi in un modulo se il sottomodulo del modulo che lo contiene era stato importato o no?
Samantha Atkins,


Grazie, è quello che ho finito per fare ma ero curioso di sapere se ci fosse un modo migliore che mi ero perso.
Samantha Atkins,

63

Se vuoi solo sottoclassi dirette, allora .__subclasses__()funziona bene. Se vuoi tutte le sottoclassi, sottoclassi di sottoclassi e così via, avrai bisogno di una funzione per farlo.

Ecco una funzione semplice e leggibile che trova ricorsivamente tutte le sottoclassi di una determinata classe:

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

3
Grazie @fletom! Anche se ciò di cui avevo bisogno a quei tempi era solo __subclasses __ () la tua soluzione è davvero bella. Prendi +1;) A proposito, penso che potrebbe essere più affidabile usando i generatori nel tuo caso.
Roman Prykhodchenko,

3
Non dovrebbe all_subclassesessere un setper eliminare i duplicati?
Ryne Everett,

@RyneEverett Intendi se stai utilizzando l'ereditarietà multipla? Penso che altrimenti non dovresti finire con i duplicati.
fletom,

@fletom Sì, l'ereditarietà multipla sarebbe necessaria per i duplicati. Per esempio, A(object), B(A), C(A), e D(B, C). get_all_subclasses(A) == [B, C, D, D].
Ryne Everett,

@RomanPrykhodchenko: il titolo della tua domanda dice di trovare tutte le sottoclassi di una classe dato il suo nome, ma questo così come altri lavori solo dato alla classe stessa, non solo il suo nome, quindi che cos'è?
martineau,

33

La soluzione più semplice in forma generale:

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

E un metodo di classe nel caso in cui tu abbia una singola classe da cui erediti:

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

2
L'approccio del generatore è davvero pulito.
quattro43,

22

Python 3.6 -__init_subclass__

Come altre risposte menzionate, puoi controllare l' __subclasses__attributo per ottenere l'elenco delle sottoclassi, poiché python 3.6 puoi modificare la creazione di questo attributo sovrascrivendo il __init_subclass__metodo.

class PluginBase:
    subclasses = []

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

class Plugin1(PluginBase):
    pass

class Plugin2(PluginBase):
    pass

In questo modo, se sai cosa stai facendo, puoi ignorare il comportamento di __subclasses__e omettere / aggiungere sottoclassi da questo elenco.


1
Sì, qualsiasi sottoclasse di qualsiasi tipo attiverebbe la __init_subclassclasse genitore.
O Duan,

9

Nota: vedo che qualcuno (non @unutbu) ha cambiato la risposta referenziata in modo che non la usi più vars()['Foo'], quindi il punto principale del mio post non si applica più.

FWIW, ecco cosa intendevo per la risposta di @ unutbu lavorando solo con classi definite localmente - e che l'uso eval()invece di vars()farebbe funzionare con qualsiasi classe accessibile, non solo quelle definite nell'ambito corrente.

Per coloro a cui non piace usare eval(), viene anche mostrato un modo per evitarlo.

Innanzitutto ecco un esempio concreto che dimostra il potenziale problema con l'uso di 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'>]

Ciò potrebbe essere migliorato spostando il eval('ClassName')basso nella funzione definita, il che rende l'utilizzo più semplice senza perdita della generalità aggiuntiva acquisita utilizzando il eval()quale a differenza vars()non è sensibile al contesto:

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

Infine, è possibile, e forse anche importante in alcuni casi, evitare di utilizzarlo eval()per motivi di sicurezza, quindi ecco una versione senza di essa:

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

1
@Chris: aggiunta una versione che non usa eval()- meglio ora?
martineau,

4

Una versione molto più breve per ottenere un elenco di tutte le sottoclassi:

from itertools import chain

def subclasses(cls):
    return list(
        chain.from_iterable(
            [list(chain.from_iterable([[x], subclasses(x)])) for x in cls.__subclasses__()]
        )
    )

2

Come posso trovare tutte le sottoclassi di una classe dato il suo nome?

Possiamo certamente farlo facilmente, dato l'accesso all'oggetto stesso, sì.

Semplicemente dato il suo nome è una cattiva idea, in quanto possono esserci più classi con lo stesso nome, anche definite nello stesso modulo.

Ho creato un'implementazione per un'altra risposta , e poiché risponde a questa domanda ed è un po 'più elegante rispetto alle altre soluzioni qui, eccola qui:

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

Uso:

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

2

Questa non è una buona risposta come l'uso dello speciale __subclasses__()metodo di classe incorporato che menziona @unutbu, quindi lo presento semplicemente come un esercizio. La subclasses()funzione definita restituisce un dizionario che associa tutti i nomi delle sottoclassi alle sottoclassi stesse.

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)

Produzione:

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

1

Ecco una versione senza ricorsione:

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

Ciò differisce da altre implementazioni in quanto restituisce la classe originale. Questo perché rende il codice più semplice e:

class Ham(object):
    pass

assert(issubclass(Ham, Ham)) # True

Se get_subclasses_gen sembra un po 'strano, perché è stato creato convertendo un'implementazione ricorsiva della coda in un generatore di loop:

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], [])
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.