Eu preciso de uma abordagem de trabalho para obter todas as classes que são herdadas de uma classe base em Python.
Eu preciso de uma abordagem de trabalho para obter todas as classes que são herdadas de uma classe base em Python.
Respostas:
As classes de novo estilo (por exemplo, subclassificadas de object
, que é o padrão no Python 3) têm um __subclasses__
método que retorna as subclasses:
class Foo(object): pass
class Bar(Foo): pass
class Baz(Foo): pass
class Bing(Bar): pass
Aqui estão os nomes das subclasses:
print([cls.__name__ for cls in Foo.__subclasses__()])
# ['Bar', 'Baz']
Aqui estão as próprias subclasses:
print(Foo.__subclasses__())
# [<class '__main__.Bar'>, <class '__main__.Baz'>]
Confirmação de que as subclasses realmente listam Foo
como sua base:
for cls in Foo.__subclasses__():
print(cls.__base__)
# <class '__main__.Foo'>
# <class '__main__.Foo'>
Observe que se você quiser sub-classes, precisará recursar:
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'>}
Observe que, se a definição de classe de uma subclasse ainda não foi executada - por exemplo, se o módulo da subclasse ainda não foi importado -, essa subclasse ainda não existe e __subclasses__
não a encontra.
Você mencionou "dado o nome". Como as classes Python são objetos de primeira classe, você não precisa usar uma string com o nome da classe no lugar da classe ou algo parecido. Você pode simplesmente usar a classe diretamente e provavelmente deveria.
Se você possui uma string que representa o nome de uma classe e deseja encontrar as subclasses dessa classe, existem duas etapas: encontre a classe com seu nome e, em seguida, encontre as subclasses __subclasses__
como acima.
Como encontrar a classe a partir do nome depende de onde você espera encontrá-la. Se você espera encontrá-lo no mesmo módulo que o código que está tentando localizar a classe,
cls = globals()[name]
faria o trabalho ou, no caso improvável que você espera encontrá-lo nos locais,
cls = locals()[name]
Se a classe puder estar em qualquer módulo, sua string de nome deverá conter o nome completo - algo como, em 'pkg.module.Foo'
vez de apenas 'Foo'
. Use importlib
para carregar o módulo da classe e, em seguida, recupere o atributo correspondente:
import importlib
modname, _, clsname = name.rpartition('.')
mod = importlib.import_module(modname)
cls = getattr(mod, clsname)
No entanto, você encontra a classe e, cls.__subclasses__()
em seguida, retorna uma lista de suas subclasses.
Se você apenas deseja subclasses diretas, .__subclasses__()
funciona bem. Se você deseja todas as subclasses, subclasses de subclasses e assim por diante, precisará de uma função para fazer isso por você.
Aqui está uma função simples e legível que encontra recursivamente todas as subclasses de uma determinada 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
all_subclasses
ser um set
para eliminar duplicatas?
A(object)
, B(A)
, C(A)
, e D(B, C)
. get_all_subclasses(A) == [B, C, D, D]
.
A solução mais simples em geral:
def get_subclasses(cls):
for subclass in cls.__subclasses__():
yield from get_subclasses(subclass)
yield subclass
E um método de classe, caso você tenha uma única classe da qual herda:
@classmethod
def get_subclasses(cls):
for subclass in cls.__subclasses__():
yield from subclass.get_subclasses()
yield subclass
__init_subclass__
Como outra resposta mencionada, você pode verificar o __subclasses__
atributo para obter a lista de subclasses, já que o python 3.6 pode modificar essa criação de atributo substituindo o __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
Dessa forma, se você souber o que está fazendo, poderá substituir o comportamento de __subclasses__
e omitir / adicionar subclasses desta lista.
__init_subclass
classe dos pais.
Nota: Vejo que alguém (não o @unutbu) alterou a resposta referenciada para que ela não seja mais usada vars()['Foo']
- portanto, o ponto principal da minha postagem não se aplica mais.
FWIW, eis o que eu quis dizer sobre a resposta do @ unutbu apenas trabalhando com classes definidas localmente - e que o uso em eval()
vez de vars()
o faria funcionar com qualquer classe acessível, não apenas com as definidas no escopo atual.
Para quem não gosta de usar eval()
, também é mostrado um meio de evitá-lo.
Primeiro, aqui está um exemplo concreto que demonstra o possível problema com o 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'>]
Isso pode ser aprimorado movendo o eval('ClassName')
botão para baixo para a função definida, o que facilita o uso sem perda da generalidade adicional obtida pelo uso eval()
que diferentemente vars()
não é sensível ao 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 fim, é possível, e talvez até importante em alguns casos, evitar o uso eval()
por motivos de segurança, então aqui está uma versão sem ele:
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()
- melhor agora?
Uma versão muito mais curta para obter uma lista de todas as subclasses:
from itertools import chain
def subclasses(cls):
return list(
chain.from_iterable(
[list(chain.from_iterable([[x], subclasses(x)])) for x in cls.__subclasses__()]
)
)
Como posso encontrar todas as subclasses de uma classe com esse nome?
Certamente, podemos facilmente fazer isso com acesso ao próprio objeto, sim.
Basta dar um nome para ele é uma péssima idéia, pois pode haver várias classes com o mesmo nome, mesmo definidas no mesmo módulo.
Criei uma implementação para outra resposta e, como ela responde a essa pergunta e é um pouco mais elegante que as outras soluções aqui, aqui 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 não é uma resposta tão boa quanto usar o __subclasses__()
método de classe interno especial que o @unutbu menciona, então a apresento apenas como um exercício. A subclasses()
função definida retorna um dicionário que mapeia todos os nomes das subclasses para as próprias subclasses.
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)
Resultado:
{'SubclassB': <class '__main__.SubclassB'>,
'SubclassA': <class '__main__.SubclassA'>}
Aqui está uma versão sem recursão:
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], [])
Isso difere de outras implementações, pois retorna a classe original. Isso ocorre porque simplifica o código e:
class Ham(object):
pass
assert(issubclass(Ham, Ham)) # True
Se get_subclasses_gen parece um pouco estranho, é porque foi criado pela conversão de uma implementação recursiva da cauda em um gerador de 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], [])