Como encontrar todas as subclasses de uma classe com seu nome?

223

Eu preciso de uma abordagem de trabalho para obter todas as classes que são herdadas de uma classe base em Python.

Roman Prykhodchenko
fonte

Respostas:

316

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 Foocomo 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 importlibpara 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.

unutbu
fonte
Suponha que eu desejasse encontrar todas as subclasses de um módulo, independentemente de o submodulo do módulo que o conter ter sido importado ou não?
Samantha Atkins
1
@SamanthaAtkins: gere uma lista de todos os submódulos do pacote e gere uma lista de todas as classes para cada módulo .
unutbu 30/07/19
Obrigado, foi o que acabei fazendo, mas fiquei curioso para saber se poderia haver uma maneira melhor que eu tinha perdido.
Samantha Atkins
63

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
fletom
fonte
3
Obrigado @fletom! Embora o que eu precisava naquela época fosse apenas __subclasses __ (), sua solução é muito boa. Tome +1;) Btw, acho que pode ser mais confiável usando geradores no seu caso.
Roman Prykhodchenko
3
Não deve all_subclassesser um setpara eliminar duplicatas?
Ryne Everett
@RyneEverett Você quer dizer se você estiver usando herança múltipla? Penso que, caso contrário, você não deve acabar com duplicatas.
fletom
@fletom Sim, a herança múltipla seria necessária para duplicatas. Por exemplo, A(object), B(A), C(A), e D(B, C). get_all_subclasses(A) == [B, C, D, D].
precisa saber é o seguinte
@RomanPrykhodchenko: O título da sua pergunta diz para encontrar todas as subclasses de uma classe com o nome, mas esse e outros trabalhos funcionam apenas com a própria classe, não apenas o nome - apenas o que é?
martineau 24/02
33

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
Kimvais
fonte
2
A abordagem do gerador é realmente limpa.
244
22

Python 3.6 -__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.

Ou Duan
fonte
1
Sim, qualquer subclasse de qualquer tipo acionaria a __init_subclassclasse dos pais.
Ou Duan
9

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'>]
Martineau
fonte
1
@ Chris: Adicionado uma versão que não usa eval()- melhor agora?
martineau
4

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__()]
        )
    )
Peter Brooks
fonte
2

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

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'>}
Martineau
fonte
1

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