名前を指定してクラスのすべてのサブクラスを見つける方法は?


回答:


316

新しいスタイルのクラス(つまりobject、Python 3のデフォルトであるからサブクラス化)__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__見つからないことに注意してください。


あなたは「その名前を与えられた」と述べました。Pythonクラスはファーストクラスのオブジェクトなので、クラスの代わりにクラス名などの文字列を使用する必要はありません。クラスを直接使用できますが、おそらく使用する必要があります。

クラスの名前を表す文字列があり、そのクラスのサブクラスを検索したい場合は、2つのステップがあります。名前を指定してクラスを検索し、次に__subclasses__上記のようにサブクラスを検索します。

名前からクラスを見つける方法は、それがどこにあるかを予想している場所によって異なります。クラスを見つけようとしているコードと同じモジュールでそれを見つけることを期待しているなら、

cls = globals()[name]

仕事をするでしょう、あるいは、あなたが地元の人でそれを見つけることを期待していると思われる場合

cls = locals()[name]

クラスは、任意のモジュールにすることができれば、その後、あなたの名前の文字列は、完全修飾名を含める必要があります-のようなもの'pkg.module.Foo'だけではなくのを'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を取りなさい;)ところで、私はあなたの場合ジェネレータを使用する方が信頼性が高いと思います。
Roman Prykhodchenko 2013

3
重複を排除するべきではall_subclassesありませんsetか?
Ryne Everett

@RyneEverett多重継承を使用しているということですか?そうでなければ、あなたは重複で終わるべきではないと思います。
16

@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
ジェネレーターのアプローチは本当にクリーンです。
4

22

Pythonの3.6 -__init_subclass__

他の回答が述べたように、__subclasses__属性をチェックしてサブクラスのリストを取得できます。Python3.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.