Bagaimana menemukan semua subclass dari kelas yang diberi namanya?

223

Saya membutuhkan pendekatan kerja untuk mendapatkan semua kelas yang diwarisi dari kelas dasar dengan Python.

Roman Prykhodchenko
sumber

Jawaban:

316

Kelas gaya baru (yaitu subclass dari object, yang merupakan default di Python 3) memiliki __subclasses__metode yang mengembalikan subclass:

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

Berikut adalah nama-nama subclass:

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

Berikut adalah subkelasnya sendiri:

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

Konfirmasi bahwa subclass memang mendaftar Foosebagai basis mereka:

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

Catatan jika Anda ingin subkelas, Anda harus mengulang:

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

Perhatikan bahwa jika definisi kelas dari subclass belum dieksekusi - misalnya, jika modul subclass belum diimpor - maka subclass itu belum ada, dan __subclasses__tidak akan menemukannya.


Anda menyebutkan "diberi nama". Karena kelas Python adalah objek kelas satu, Anda tidak perlu menggunakan string dengan nama kelas sebagai pengganti kelas atau semacamnya. Anda bisa menggunakan kelas secara langsung, dan Anda mungkin harus melakukannya.

Jika Anda memiliki string yang mewakili nama kelas dan Anda ingin menemukan subclass kelas itu, maka ada dua langkah: temukan kelas yang diberi nama, dan kemudian temukan subclass dengan __subclasses__seperti di atas.

Cara menemukan kelas dari nama tergantung pada tempat Anda mengharapkannya. Jika Anda berharap menemukannya dalam modul yang sama dengan kode yang mencoba menemukan kelas, maka

cls = globals()[name]

akan melakukan pekerjaan itu, atau dalam kasus yang tidak Anda harapkan untuk menemukannya di penduduk setempat,

cls = locals()[name]

Jika kelas bisa dalam modul apa pun, maka string nama Anda harus berisi nama yang sepenuhnya memenuhi syarat - sesuatu seperti 'pkg.module.Foo'bukan hanya 'Foo'. Gunakan importlibuntuk memuat modul kelas, lalu ambil atribut yang sesuai:

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

Namun Anda menemukan kelas, cls.__subclasses__()maka akan mengembalikan daftar subkelasnya.

unutbu
sumber
Misalkan saya ingin menemukan semua subclass dalam sebuah modul apakah submodul dari modul yang mengandungnya telah diimpor atau tidak?
Samantha Atkins
1
@ SamanthaAtkins: Hasilkan daftar semua submodul dari paket , dan kemudian buat daftar semua kelas untuk setiap modul .
unutbu
Terima kasih, itulah yang akhirnya saya lakukan tetapi ingin tahu apakah mungkin ada cara yang lebih baik yang saya lewatkan.
Samantha Atkins
63

Jika Anda hanya ingin subclass langsung maka .__subclasses__()berfungsi dengan baik. Jika Anda ingin semua subclass, subclass dari subclass, dan sebagainya, Anda akan memerlukan fungsi untuk melakukannya untuk Anda.

Inilah fungsi sederhana dan mudah dibaca yang secara rekursif menemukan semua subclass dari kelas yang diberikan:

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
sumber
3
@Fletom terima kasih! Meskipun apa yang saya butuhkan kembali ke masa itu hanyalah __subclasses __ () solusi Anda sangat bagus. Bawa Anda +1;) Btw, saya pikir mungkin lebih bisa diandalkan menggunakan generator dalam kasus Anda.
Roman Prykhodchenko
3
Bukankah seharusnya all_subclassesmenjadi setuntuk menghilangkan duplikat?
Ryne Everett
@RyneEverett Maksud Anda jika Anda menggunakan banyak pewarisan? Saya pikir jika tidak, Anda seharusnya tidak memiliki duplikat.
fletom
@fletom Ya, banyak warisan akan diperlukan untuk duplikat. Misalnya, A(object), B(A), C(A), dan D(B, C). get_all_subclasses(A) == [B, C, D, D].
Ryne Everett
@RomanPrykhodchenko: Judul pertanyaan Anda mengatakan untuk menemukan semua subclass dari sebuah kelas yang diberi namanya, tetapi ini juga hanya berfungsi lainnya yang diberikan kelas itu sendiri, bukan hanya namanya — jadi apa itu?
martineau
33

Solusi paling sederhana dalam bentuk umum:

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

Dan metode kelas jika Anda memiliki satu kelas di mana Anda mewarisi dari:

@classmethod
def get_subclasses(cls):
    for subclass in cls.__subclasses__():
        yield from subclass.get_subclasses()
        yield subclass
Kimvais
sumber
2
Pendekatan generator sangat bersih.
four43
22

Python 3.6 -__init_subclass__

Seperti jawaban lain yang disebutkan, Anda dapat memeriksa __subclasses__atribut untuk mendapatkan daftar subclass, karena python 3.6 Anda dapat memodifikasi pembuatan atribut ini dengan mengganti __init_subclass__metode.

class PluginBase:
    subclasses = []

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

class Plugin1(PluginBase):
    pass

class Plugin2(PluginBase):
    pass

Dengan cara ini, jika Anda tahu apa yang Anda lakukan, Anda dapat mengesampingkan perilaku __subclasses__dan menghilangkan / menambahkan subclass dari daftar ini.

Atau Duan
sumber
1
Ya, setiap sub kelas dari jenis apa pun akan memicu __init_subclasspada kelas induknya.
Atau Duan
9

Catatan: Saya melihat bahwa seseorang (bukan @unutbu) mengubah jawaban yang direferensikan sehingga tidak lagi menggunakan vars()['Foo']- sehingga titik utama dari posting saya tidak berlaku lagi.

FWIW, inilah yang saya maksudkan tentang jawaban @ unutbu hanya bekerja dengan kelas yang ditentukan secara lokal - dan bahwa menggunakan eval()bukannya vars()akan membuatnya bekerja dengan kelas yang dapat diakses, tidak hanya yang didefinisikan dalam lingkup saat ini.

Bagi mereka yang tidak suka menggunakan eval(), cara juga ditunjukkan untuk menghindarinya.

Pertama, inilah contoh nyata yang menunjukkan potensi masalah dengan penggunaan 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'>]

Ini dapat ditingkatkan dengan memindahkan fungsi eval('ClassName')ke bawah ke dalam fungsi yang didefinisikan, yang membuatnya lebih mudah tanpa kehilangan generalitas tambahan yang diperoleh dengan menggunakan eval()yang tidak seperti vars()itu tidak peka konteks:

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

Terakhir, itu mungkin, dan mungkin bahkan penting dalam beberapa kasus, untuk menghindari penggunaan eval()karena alasan keamanan, jadi inilah versi tanpa itu:

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
sumber
1
@ Chris: Menambahkan versi yang tidak digunakan eval()- lebih baik sekarang?
martineau
4

Versi yang jauh lebih pendek untuk mendapatkan daftar semua subclass:

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
sumber
2

Bagaimana saya bisa menemukan semua subclass dari kelas yang diberi namanya?

Kita tentu bisa dengan mudah melakukan ini mengingat akses ke objek itu sendiri, ya.

Cukup diberi namanya adalah ide yang buruk, karena mungkin ada beberapa kelas dengan nama yang sama, bahkan didefinisikan dalam modul yang sama.

Saya membuat implementasi untuk jawaban lain , dan karena menjawab pertanyaan ini dan itu sedikit lebih elegan daripada solusi lain di sini, ini dia:

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

Pemakaian:

>>> 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
sumber
2

Ini bukan jawaban yang sebaik menggunakan __subclasses__()metode kelas bawaan yang @unutbu sebutkan, jadi saya menyajikannya hanya sebagai latihan. The subclasses()fungsi yang didefinisikan kembali sebuah kamus yang memetakan semua nama subclass ke subclass sendiri.

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)

Keluaran:

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

Inilah versi tanpa rekursi:

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

Ini berbeda dari implementasi lain karena mengembalikan kelas asli. Ini karena membuat kode lebih sederhana dan:

class Ham(object):
    pass

assert(issubclass(Ham, Ham)) # True

Jika get_subclasses_gen terlihat sedikit aneh itu karena itu dibuat dengan mengubah implementasi tail-recursive menjadi generator looping:

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
sumber