Necesito un enfoque funcional para obtener todas las clases que se heredan de una clase base en Python.
Necesito un enfoque funcional para obtener todas las clases que se heredan de una clase base en Python.
Respuestas:
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 Foo
como 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 importlib
para 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.
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
all_subclasses
ser una set
para eliminar duplicados?
A(object)
, B(A)
, C(A)
, y D(B, C)
. get_all_subclasses(A) == [B, C, D, D]
.
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
__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.
__init_subclass
clase de los padres.
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'>]
eval()
, ¿mejor ahora?
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__()]
)
)
¿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'>]
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'>}
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], [])