Adı verilen bir sınıfın tüm alt sınıfları nasıl bulunur?


223

Python'da bir temel sınıftan miras alınan tüm sınıfları almak için çalışan bir yaklaşıma ihtiyacım var.

Yanıtlar:


317

Yeni stil sınıflarının (yani alt sınıf, objectPython 3'te varsayılan) __subclasses__alt sınıfları döndüren bir yöntemi vardır:

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

Alt sınıfların isimleri:

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

İşte alt sınıfların kendileri:

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

Alt sınıfların Footemel olarak listelediklerini teyit etme:

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

Alt sınıflar istiyorsanız, tekrarlamanız gerektiğini unutmayın:

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

Bir alt sınıfın sınıf tanımı henüz yürütülmediyse (örneğin, alt sınıfın modülü henüz içe aktarılmadıysa), o alt sınıfın henüz mevcut olmadığını ve __subclasses__onu bulamayacağını unutmayın.


"İsmi verildi" den bahsettiniz. Python sınıfları birinci sınıf nesneler olduğundan, sınıfın yerine sınıfın adını taşıyan bir dize veya bunun gibi bir şey kullanmanız gerekmez. Sınıfı doğrudan kullanabilirsiniz ve muhtemelen kullanmalısınız.

Bir sınıfın adını temsil eden bir dizeniz varsa ve bu sınıfın alt sınıflarını bulmak istiyorsanız, iki adım vardır: adı verilen sınıfı bulun ve __subclasses__yukarıdaki gibi alt sınıfları bulun .

Sınıfın adından nasıl bulunacağı, onu nerede bulmayı beklediğinize bağlıdır. Sınıfı bulmaya çalışan kodla aynı modülde bulmayı bekliyorsanız, o zaman

cls = globals()[name]

işi yapar ya da yerel bir yerde bulmayı beklediğiniz olası bir durumda,

cls = locals()[name]

Sınıf herhangi modülünde olabilir, o zaman adın dize tam nitelikli adını içermelidir - gibi bir şey 'pkg.module.Foo'yerine sadece 'Foo'. importlibSınıfın modülünü yüklemek için kullanın , ardından ilgili niteliği alın:

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

Ancak sınıfı bulursanız, cls.__subclasses__()alt sınıflarının bir listesini döndürürsünüz.


Bir modüldeki tüm alt sınıfları içeren modülün alt modülünün içe aktarılıp aktarılmadığını bulmak istediğimi varsayalım?
Samantha Atkins

1
@SamanthaAtkins: Paketin tüm alt modüllerinin bir listesini oluşturun ve ardından her modül için tüm sınıfların bir listesini oluşturun .
unutbu

Teşekkürler, bunu yaptım ama kaçırdığım daha iyi bir yol olup olmadığını merak ediyordum.
Samantha Atkins

63

Sadece doğrudan alt sınıf istiyorsanız o zaman .__subclasses__()iyi çalışıyor. Tüm alt sınıfları, alt sınıfların alt sınıflarını vb. İstiyorsanız, bunu sizin için yapacak bir işleve ihtiyacınız olacaktır.

İşte belirli bir sınıfın tüm alt sınıflarını özyineli olarak bulan basit, okunabilir bir işlev:

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
Teşekkürler @fletom! O günlere kadar ihtiyacım olan şey sadece __subclasses __ () olsa da çözümün gerçekten güzel. Size +1;) Btw, bence durumunuzdaki jeneratörleri kullanmak daha güvenilir olabilir.
Roman Prykhodchenko

3
Yinelemeleri ortadan kaldırmak all_subclassesiçin bir olmamalı set?
Ryne Everett

@RyneEverett Birden fazla miras mı kullanıyorsunuz? Bence aksi halde kopyalar almamalısınız.
fletom

@fletom Evet, kopyalar için çoklu kalıtım gerekli olacaktır. Örneğin, A(object), B(A), C(A), ve D(B, C). get_all_subclasses(A) == [B, C, D, D].
Ryne Everett

@RomanPrykhodchenko: Sorunuzun başlığı, bir sınıfın tüm alt sınıflarını adını verdiğini söylüyor, ancak bu ve diğerlerinin yanı sıra sadece sınıfın kendisi, sadece adı değil, verilen iş - yani sadece ne?
martineau

33

Genel formdaki en basit çözüm:

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

Tek bir sınıfınız olması durumunda, aşağıdakilerden miras aldığınız bir sınıf yöntemi:

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

2
Jeneratör yaklaşımı gerçekten temiz.
four43

22

Python 3.6 -__init_subclass__

Bahsedilen diğer cevaplarda __subclasses__, alt sınıfların listesini almak için özniteliği kontrol edebilirsiniz , çünkü python 3.6, bu özniteliğin oluşturulmasını __init_subclass__yöntemi geçersiz kılarak değiştirebilirsiniz .

class PluginBase:
    subclasses = []

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

class Plugin1(PluginBase):
    pass

class Plugin2(PluginBase):
    pass

Bu şekilde, ne yaptığınızı biliyorsanız __subclasses__, bu listenin alt sınıflarının davranışını geçersiz kılabilir ve alt sınıfları atlayabilirsiniz.


1
Evet, herhangi bir türden herhangi bir alt sınıf __init_subclass, ebeveynin sınıfını tetikler .
Veya Duan

9

Not: Birinin (@unutbu değil) başvurulan yanıtı artık kullanmaması için değiştirdiğini görüyorum - bu yüzden yazımın vars()['Foo']birincil noktası artık geçerli değil.

FWIW, @ unutbu'nun sadece yerel olarak tanımlanmış sınıflarla çalışmasının cevabı hakkında ne demek istediğimi - ve bunun eval()yerine kullanmanın vars()sadece mevcut kapsamda tanımlananlarla değil, erişilebilir herhangi bir sınıfla çalışmasını sağlayacağını düşünüyorum.

Kullanmayı sevmeyenler eval()için bundan kaçınmanın bir yolu da gösterilmiştir.

İlk olarak, kullanımdaki potansiyel sorunu gösteren somut bir örnek 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'>]

Bu, eval('ClassName')aşağıya tanımlanan fonksiyona taşınarak geliştirilebilir , bu da içeriğin eval()aksine vars()içeriğe duyarlı olmayan kazanılmış ek genelliği kaybetmeden kullanmayı kolaylaştırır :

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

Son olarak, eval()güvenlik nedeniyle kullanmaktan kaçınmak ve hatta bazı durumlarda önemli olabilir , bu yüzden onsuz bir sürüm:

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: Kullanılmayan bir sürüm eklendi eval()- şimdi daha iyi mi?
martineau

4

Tüm alt sınıfların bir listesini almak için çok daha kısa bir sürüm:

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

Adı verilen bir sınıfın tüm alt sınıflarını nasıl bulabilirim?

Nesnenin kendisine verilen bu erişimi kesinlikle kolayca yapabiliriz, evet.

Aynı modülde tanımlanmış olsa bile, aynı isimde birden fazla sınıf olabileceğinden, basitçe adı kötü bir fikirdir.

Başka bir cevap için bir uygulama oluşturdum ve bu soruya cevap verdiğinden ve buradaki diğer çözümlerden biraz daha zarif olduğundan, burada:

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

Kullanımı:

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

Bu, __subclasses__()@unutbu'nun bahsettiği özel yerleşik sınıf yöntemini kullanmak kadar iyi bir cevap değil , bu yüzden sadece bir egzersiz olarak sunuyorum. subclasses()Fonksiyon alt sınıfları kendileri için tüm alt sınıf adlarını eşleyen bir sözlük döner tanımladı.

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)

Çıktı:

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

1

İşte özyinelemesiz bir sürüm:

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

Bu, orijinal sınıfı döndürmesi bakımından diğer uygulamalardan farklıdır. Bunun nedeni, kodu daha basit hale getirmesi ve:

class Ham(object):
    pass

assert(issubclass(Ham, Ham)) # True

Get_subclasses_gen biraz garip görünüyorsa, bunun nedeni kuyruk yinelemeli bir uygulamayı döngü oluşturucuya dönüştürerek oluşturulmuş olmasıdır:

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], [])
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.