¿Cómo encontrar todas las subclases de una clase dado su nombre?

223

Necesito un enfoque funcional para obtener todas las clases que se heredan de una clase base en Python.

Prykhodchenko romano
fuente

Respuestas:

316

Las clases de estilo nuevo (es decir, subclase de object, que es el valor predeterminado en Python 3) tienen un __subclasses__método que devuelve las subclases:

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

Aquí están los nombres de las subclases:

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

Aquí están las subclases mismas:

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

Confirmación de que las subclases realmente enumeran Foocomo su base:

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

Tenga en cuenta que si desea subclases, deberá repetir:

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

Tenga en cuenta que si la definición de clase de una subclase aún no se ha ejecutado, por ejemplo, si el módulo de la subclase aún no se ha importado, entonces esa subclase aún no existe y __subclasses__no la encontrará.


Usted mencionó "dado su nombre". Dado que las clases de Python son objetos de primera clase, no necesita usar una cadena con el nombre de la clase en lugar de la clase ni nada de eso. Puedes usar la clase directamente, y probablemente deberías.

Si tiene una cadena que representa el nombre de una clase y desea encontrar las subclases de esa clase, entonces hay dos pasos: busque la clase con su nombre y luego las subclases __subclasses__como se indica arriba.

Cómo encontrar la clase a partir del nombre depende de dónde espera encontrarla. Si espera encontrarlo en el mismo módulo que el código que está tratando de ubicar la clase, entonces

cls = globals()[name]

haría el trabajo, o en el improbable caso de que espere encontrarlo en locales,

cls = locals()[name]

Si la clase podría estar en cualquier módulo, entonces su cadena de nombre debería contener el nombre completo, algo así como en 'pkg.module.Foo'lugar de solo 'Foo'. Use importlibpara cargar el módulo de la clase, luego recupere el atributo correspondiente:

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

Independientemente de cómo encuentre la clase, cls.__subclasses__()devolvería una lista de sus subclases.

unutbu
fuente
¿Supongamos que quisiera encontrar todas las subclases en un módulo si el submódulo del módulo que lo contiene ha sido importado o no?
Samantha Atkins
1
@SamanthaAtkins: genere una lista de todos los submódulos del paquete y luego genere una lista de todas las clases para cada módulo .
unutbu
Gracias, eso es lo que terminé haciendo, pero tenía curiosidad por saber si podría haber una mejor manera de haberme perdido.
Samantha Atkins
63

Si solo quieres subclases directas, entonces .__subclasses__()funciona bien. Si desea todas las subclases, subclases de subclases, etc., necesitará una función que lo haga por usted.

Aquí hay una función simple y legible que encuentra recursivamente todas las subclases de una clase dada:

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
fuente
3
Gracias @fletom! Aunque lo que necesitaba en esos días era solo __subclasses __ () su solución es realmente agradable. Take you +1;) Por cierto, creo que podría ser más confiable usar generadores en su caso.
Roman Prykhodchenko
3
¿No debería all_subclassesser una setpara eliminar duplicados?
Ryne Everett
@RyneEverett ¿Quieres decir si estás usando herencia múltiple? Creo que de lo contrario no deberías terminar con duplicados.
fletom
@fletom Sí, la herencia múltiple sería necesaria para los duplicados. Por ejemplo, A(object), B(A), C(A), y D(B, C). get_all_subclasses(A) == [B, C, D, D].
Ryne Everett
@RomanPrykhodchenko: El título de tu pregunta dice que debes encontrar todas las subclases de una clase dada su nombre, pero esto, así como otros, solo funcionan dada la clase en sí, no solo su nombre, así que, ¿qué es?
Martineau
33

La solución más simple en forma general:

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

Y un método de clase en caso de que tenga una sola clase de la que herede:

@classmethod
def get_subclasses(cls):
    for subclass in cls.__subclasses__():
        yield from subclass.get_subclasses()
        yield subclass
Kimvais
fuente
2
El enfoque del generador es realmente limpio.
four43
22

Python 3.6 -__init_subclass__

Como se mencionó en otra respuesta, puede verificar el __subclasses__atributo para obtener la lista de subclases, ya que python 3.6 puede modificar la creación de este atributo anulando el __init_subclass__método.

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 esta manera, si sabe lo que está haciendo, puede anular el comportamiento de __subclasses__y omitir / agregar subclases de esta lista.

O Duan
fuente
1
Sí, cualquier subclase de cualquier tipo activaría la __init_subclassclase de los padres.
O Duan
9

Nota: veo que alguien (no @unutbu) cambió la respuesta a la que se hace referencia para que ya no se use vars()['Foo'], por lo que el punto principal de mi publicación ya no se aplica.

FWIW, esto es lo que quise decir sobre que la respuesta de @unutbu solo funciona con clases definidas localmente, y que usarla en eval()lugar de vars()hacerlo funcionaría con cualquier clase accesible, no solo con las definidas en el alcance actual.

Para aquellos que no les gusta usar eval(), también se muestra una forma de evitarlo.

Primero, aquí hay un ejemplo concreto que demuestra el problema potencial con el uso 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'>]

Esto podría mejorarse moviendo eval('ClassName')hacia abajo a la función definida, lo que facilita su uso sin perder la generalidad adicional obtenida al usar lo eval()que, a diferencia, vars()no es sensible al contexto:

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

Por último, es posible, y quizás incluso importante en algunos casos, evitar el uso eval()por razones de seguridad, así que aquí hay una versión sin ella:

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
fuente
1
@ Chris: Se agregó una versión que no usa eval(), ¿mejor ahora?
Martineau
4

Una versión mucho más corta para obtener una lista de todas las subclases:

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

¿Cómo puedo encontrar todas las subclases de una clase dado su nombre?

Ciertamente podemos hacer esto fácilmente dado el acceso al objeto en sí, sí.

Simplemente dado su nombre es una mala idea, ya que puede haber múltiples clases del mismo nombre, incluso definidas en el mismo módulo.

Creé una implementación para otra respuesta , y dado que responde a esta pregunta y es un poco más elegante que las otras soluciones aquí, aquí está:

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

Esta no es una respuesta tan buena como usar el __subclasses__()método de clase incorporado especial que @unutbu menciona, así que lo presento simplemente como un ejercicio. La subclasses()función definida devuelve un diccionario que asigna todos los nombres de subclase a las propias subclases.

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)

Salida:

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

Aquí hay una versión sin recursividad:

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

Esto difiere de otras implementaciones en que devuelve la clase original. Esto se debe a que simplifica el código y:

class Ham(object):
    pass

assert(issubclass(Ham, Ham)) # True

Si get_subclasses_gen se ve un poco extraño, es porque fue creado al convertir una implementación recursiva de la cola en un generador de bucle:

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
fuente