จะค้นหา subclasses ทั้งหมดของคลาสที่ให้ชื่อได้อย่างไร?


คำตอบ:


315

คลาสสไตล์ใหม่ (เช่นคลาสย่อยจาก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'>

หมายเหตุถ้าคุณต้องการ sububclasses คุณจะต้องเรียกคืน:

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 เป็นออบเจ็กต์ชั้นหนึ่งคุณไม่จำเป็นต้องใช้สตริงที่มีชื่อคลาสแทนตำแหน่งหรืออะไรแบบนั้น คุณสามารถใช้คลาสได้โดยตรงและคุณควร

หากคุณมีสตริงที่แสดงชื่อของคลาสและคุณต้องการค้นหาคลาสย่อยของคลาสนั้นมีสองขั้นตอน: ค้นหาคลาสที่ให้ชื่อจากนั้นค้นหาคลาสย่อย__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__()จากนั้นส่งคืนรายการของคลาสย่อย


สมมติว่าฉันต้องการค้นหาคลาสย่อยทั้งหมดในโมดูลว่า submodule ของโมดูลที่มีมันถูกอิมพอร์ตหรือไม่?
Samantha Atkins

1
@SamanthaAtkins: สร้างรายการ submodules ทั้งหมดของแพคเกจแล้วสร้างรายชื่อของทุกชั้นสำหรับแต่ละโมดูล
unutbu

ขอบคุณนั่นคือสิ่งที่ฉันทำลงไป แต่ก็อยากรู้อยากเห็นว่าอาจมีวิธีที่ดีกว่าที่ฉันพลาดไป
Samantha Atkins

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 ฉันคิดว่ามันน่าเชื่อถือกว่าถ้าใช้เครื่องกำเนิดไฟฟ้าในกรณีของคุณ
Roman Prykhodchenko

3
ไม่ควรall_subclassesเป็นการ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: ชื่อคำถามของคุณบอกว่าจะหา subclasses ทั้งหมดของคลาสที่ให้ชื่อ แต่สิ่งนี้รวมถึงงานอื่น ๆ เท่านั้นที่ให้ชั้นเรียนเองไม่ใช่แค่ชื่อเท่านั้น - มันคืออะไร?
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

Python 3.6 -__init_subclass__

ตามคำตอบอื่น ๆ ที่กล่าวถึงคุณสามารถตรวจสอบ__subclasses__แอตทริบิวต์เพื่อรับรายการของคลาสย่อยเนื่องจาก python 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.