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

Resuelto Roman Prykhodchenko asked hace 14 años • 11 respuestas

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

Roman Prykhodchenko avatar Oct 05 '10 16:10 Roman Prykhodchenko
Aceptado

Las clases de nuevo estilo (es decir, subclasificadas 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 efectivamente figuran Foocomo su base:

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

Tenga en cuenta que si desea subsubclases, deberá recurrir a:

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á.


Mencionaste "dado su nombre". Dado que las clases de Python son objetos de primera clase, no es necesario utilizar una cadena con el nombre de la clase en lugar de la clase ni nada por el estilo. Puedes usar la clase directamente, y probablemente deberías hacerlo.

Si tiene una cadena que representa el nombre de una clase y desea encontrar las subclases de esa clase, entonces hay dos pasos: encontrar la clase dada su nombre y luego encontrar las subclases como se indicó __subclasses__anteriormente.

La forma de 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 intenta localizar la clase, entonces

cls = globals()[name]

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

cls = locals()[name]

Si la clase pudiera estar en cualquier módulo, entonces su cadena de nombre debería contener el nombre completo, algo así como 'pkg.module.Foo'en lugar de solo 'Foo'. Úselo importlibpara cargar el módulo de la clase y 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á una lista de sus subclases.

unutbu avatar Oct 05 '2010 10:10 unutbu

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 busca recursivamente todas las subclases de una clase determinada:

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 avatar Jun 22 '2013 02:06 fletom

La solución más sencilla 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 avatar Nov 09 '2015 10:11 Kimvais

Pitón 3.6 -__init_subclass__

Como se mencionó en otra respuesta, puede verificar el __subclasses__atributo para obtener la lista de subclases, desde 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.

Or Duan avatar Mar 27 '2017 21:03 Or Duan

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 acerca de que la respuesta de @unutbu solo funciona con clases definidas localmente, y que usar eval()en lugar de vars()haría que funcione con cualquier clase accesible, no solo con aquellas definidas en el alcance actual.

Para aquellos a quienes no les gusta usar eval(), también se muestra una manera 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 hacia eval('ClassName')abajo a la función definida, lo que facilita su uso sin perder la generalidad adicional obtenida al usar eval()el cual, a diferencia, vars()no depende del 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 su uso eval()por razones de seguridad, así que aquí hay una versión sin él:

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 avatar Jan 20 '2015 22:01 martineau