이름이 주어진 클래스의 모든 서브 클래스를 찾는 방법은 무엇입니까?


223

파이썬의 기본 클래스에서 상속 된 모든 클래스를 가져 오는 효과적인 접근 방식이 필요합니다.

답변:


315

새로운 스타일의 클래스 (즉 object, Python 3에서 기본값 인 from에서 서브 클래 싱됨)에는 __subclasses__서브 클래스를 반환 하는 메소드가 있습니다.

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

서브 클래스의 이름은 다음과 같습니다.

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

서브 클래스 자체는 다음과 같습니다.

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

서브 클래스가 실제로 Foo기본 으로 나열되는지 확인 :

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

서브 서브 클래스를 원하면 재귀해야합니다.

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

서브 클래스의 클래스 정의가 아직 실행되지 않은 경우 (예 : 서브 클래스의 모듈을 아직 가져 오지 않은 경우) 해당 서브 클래스는 존재하지 __subclasses__않으며 찾을 수 없습니다.


당신은 "이름을 지어주었습니다"라고 언급했습니다. 파이썬 클래스는 일류 객체이므로 클래스 대신 클래스 이름을 가진 문자열을 사용할 필요가 없습니다. 클래스를 직접 사용할 수 있으며 아마도해야합니다.

클래스 이름을 나타내는 문자열이 있고 해당 클래스의 서브 클래스를 찾으려면 두 단계가 있습니다. 이름이 지정된 클래스를 찾은 다음 __subclasses__위와 같이 서브 클래스를 찾으십시오 .

이름에서 수업을 찾는 방법은 원하는 수업에 따라 다릅니다. 클래스를 찾으려고하는 코드와 동일한 모듈에서 찾을 것으로 예상되면

cls = globals()[name]

일을 할 수도 있고, 지역 주민에게서 찾을 것으로 예상되는 경우에는

cls = locals()[name]

클래스가 모든 모듈에있을 수있는 경우 이름 문자열에는 'pkg.module.Foo'just 대신에 정규화 된 이름이 포함되어야합니다 'Foo'. importlib클래스 모듈을로드 한 다음 해당 속성을 검색하는 데 사용하십시오 .

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

그러나 클래스를 찾으면 cls.__subclasses__()하위 클래스 목록을 반환합니다.


모듈을 포함하는 모듈의 하위 모듈을 가져 왔는지 여부에 상관없이 모듈에서 모든 하위 클래스를 찾고 싶다고 가정 해 봅시다.
— 사만다 앳킨스


고마워, 그게 내가 끝났지 만 내가 놓친 더 좋은 방법이 있는지 궁금합니다.
— 사만다 앳킨스

63

직접 서브 클래스를 원한다면 .__subclasses__()잘 작동합니다. 모든 서브 클래스, 서브 클래스의 서브 클래스 등을 원하면이를 수행하는 함수가 필요합니다.

주어진 클래스의 모든 서브 클래스를 재귀 적으로 찾는 간단하고 읽기 쉬운 함수는 다음과 같습니다.

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

3
@fletom 감사합니다! 그 당시에 필요한 것은 __subclasses __ () 였지만 솔루션은 정말 좋습니다. +1;) Btw, 당신의 경우 발전기를 사용하는 것이 더 신뢰할 수 있다고 생각합니다.
— 로마 Prykhodchenko

3
안 all_subclassesA가 될 set중복을 제거하기 위해?
— Ryne Everett

@RyneEverett 다중 상속을 사용하고 있다는 의미입니까? 그렇지 않으면 중복으로 끝나서는 안된다고 생각합니다.
— fletom

@fletom 예, 중복에는 여러 상속이 필요합니다. 예를 들어, A(object), B(A), C(A),와 D(B, C). get_all_subclasses(A) == [B, C, D, D].
— Ryne Everett

@RomanPrykhodchenko : 질문의 제목은 클래스의 이름이 주어진 클래스의 모든 서브 클래스를 찾는 것입니다. 그러나 이것뿐만 아니라 클래스 자체에 주어진 이름뿐만 아니라 다른 클래스의 작업 만이 있습니다.
— martineau

33

일반적인 형태의 가장 간단한 솔루션 :

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

그리고 상속받은 단일 클래스가있는 경우 클래스 메소드 :

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

2
발전기 접근 방식은 정말 깨끗합니다.
— four43

22

파이썬 3.6 -__init_subclass__

다른 답변에서 언급했듯이 __subclasses__하위 클래스 목록을 가져 오기 위해 속성을 확인할 수 있습니다. 파이썬 3.6 이후에는 __init_subclass__메소드 를 재정 의하여이 속성 생성을 수정할 수 있습니다 .

class PluginBase:
    subclasses = []

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.subclasses.append(cls)

class Plugin1(PluginBase):
    pass

class Plugin2(PluginBase):
    pass

이렇게하면 수행중인 작업을 알면 __subclasses__이 목록에서 하위 클래스 의 동작을 무시하고 하위 클래스를 생략 / 추가 할 수 있습니다 .


1
예, 모든 종류의 하위 클래스 __init_subclass는 부모 클래스를 트리거합니다 .
— 또는 Duan

9

참고 : @unutbu가 아닌 누군가가 더 이상 사용하지 않도록 참조 답변을 변경 vars()['Foo']했으므로 내 게시물의 기본 사항이 더 이상 적용되지 않습니다.

FWIW, @unutbu의 답변 은 로컬로 정의 된 클래스에서만 작동하며 그 eval()대신에 사용하는 것입니다.vars() 하면 현재 범위에 정의 된 클래스뿐만 아니라 액세스 가능한 클래스에서도 작동합니다.

사용을 싫어하는 사람들을 위해 eval() 그것을 피하는 방법도 있습니다.

먼저 다음을 사용하여 발생할 수있는 잠재적 인 문제를 보여주는 구체적인 예가 있습니다 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'>]

이는 eval('ClassName')정의 된 함수로 아래로 이동함으로써 개선 될 수 있는데, 이는 문맥에 민감하지 않은 eval()것과 달리 사용함으로써 얻는 추가 일반성을 잃지 않고 쉽게 사용할 수있게 vars()합니다.

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

마지막으로, eval()보안상의 이유로 사용을 피하는 것이 가능하며 경우에 따라 중요 할 수도 있으므로 여기에없는 버전이 있습니다.

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

1
@Chris : eval()더 이상 사용하지 않는 버전을 추가 했습니까?
— martineau

4

모든 서브 클래스 목록을 얻기위한 훨씬 짧은 버전 :

from itertools import chain

def subclasses(cls):
    return list(
        chain.from_iterable(
            [list(chain.from_iterable([[x], subclasses(x)])) for x in cls.__subclasses__()]
        )
    )

2

이름이 지정된 클래스의 모든 하위 클래스를 어떻게 찾을 수 있습니까?

객체 자체에 대한 액세스 권한이 주어지면 확실히 쉽게 할 수 있습니다.

단순히 같은 모듈에 정의 된 동일한 이름의 여러 클래스가있을 수 있기 때문에 단순히 이름을 지정하는 것은 좋지 않습니다.

다른 답변에 대한 구현을 만들었 으며이 질문에 대답하고 다른 솔루션보다 약간 우아하기 때문에 여기에 있습니다.

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

용법:

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

2

이것은 __subclasses__()@unutbu가 언급 한 특수 내장 클래스 메소드를 사용하는 것만 큼 좋은 대답이 아니므로 연습으로 제시합니다. 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)

산출:

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

1

재귀없는 버전은 다음과 같습니다.

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

이것은 원래 클래스를 반환한다는 점에서 다른 구현과 다릅니다. 코드가 단순 해지고 다음과 같은 이유 때문입니다.

class Ham(object):
    pass

assert(issubclass(Ham, Ham)) # True

get_subclasses_gen이 약간 이상하게 보이면 꼬리 재귀 구현을 반복 생성기로 변환하여 생성 되었기 때문입니다.

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], [])
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.