Làm thế nào để tìm tất cả các lớp con của một lớp được đặt tên của nó?


223

Tôi cần một cách tiếp cận làm việc để có được tất cả các lớp được kế thừa từ một lớp cơ sở trong Python.

Câu trả lời:


316

Các lớp kiểu mới (tức là lớp con từ object, được mặc định trong Python 3) có một __subclasses__phương thức trả về các lớp con:

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

Dưới đây là tên của các lớp con:

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

Dưới đây là các lớp con:

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

Xác nhận rằng các lớp con thực sự liệt kê Foolà cơ sở của chúng:

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

Lưu ý nếu bạn muốn các lớp con, bạn sẽ phải lặp lại:

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

Lưu ý rằng nếu định nghĩa lớp của lớp con chưa được thực thi - ví dụ: nếu mô-đun của lớp con chưa được nhập - thì lớp con đó chưa tồn tại và __subclasses__sẽ không tìm thấy nó.


Bạn đã đề cập "cho tên của nó". Vì các lớp Python là các đối tượng hạng nhất, nên bạn không cần sử dụng một chuỗi có tên của lớp thay cho lớp hoặc bất cứ thứ gì tương tự. Bạn chỉ có thể sử dụng lớp trực tiếp, và có lẽ bạn nên.

Nếu bạn có một chuỗi đại diện cho tên của một lớp và bạn muốn tìm các lớp con của lớp đó, thì có hai bước: tìm lớp được đặt tên của nó, sau đó tìm các lớp con __subclasses__như trên.

Cách tìm lớp từ tên tùy thuộc vào nơi bạn muốn tìm. Nếu bạn đang mong đợi tìm thấy nó trong cùng một mô-đun với mã đang cố định vị lớp, thì

cls = globals()[name]

sẽ thực hiện công việc hoặc trong trường hợp không mong muốn mà bạn mong muốn tìm thấy nó ở địa phương,

cls = locals()[name]

Nếu lớp có thể nằm trong bất kỳ mô-đun nào, thì chuỗi tên của bạn sẽ chứa tên đủ điều kiện - giống như 'pkg.module.Foo'thay vì chỉ 'Foo'. Sử dụng importlibđể tải mô-đun của lớp, sau đó lấy thuộc tính tương ứng:

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

Tuy nhiên, bạn tìm thấy lớp, cls.__subclasses__()sau đó sẽ trả về một danh sách các lớp con của nó.


Giả sử tôi muốn tìm tất cả các lớp con trong một mô-đun cho dù mô hình con của mô-đun chứa nó đã được nhập khẩu hay chưa?
Samantha Atkins


Cảm ơn, đó là những gì tôi đã làm nhưng tò mò liệu có cách nào tốt hơn tôi đã bỏ lỡ.
Samantha Atkins

63

Nếu bạn chỉ muốn các lớp con trực tiếp thì .__subclasses__()hoạt động tốt. Nếu bạn muốn tất cả các lớp con, lớp con của lớp con, v.v., bạn sẽ cần một hàm để làm điều đó cho bạn.

Đây là một hàm đơn giản, dễ đọc, tìm đệ quy tất cả các lớp con của một lớp nhất định:

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
Cảm ơn bạn @fletom! Mặc dù những gì tôi cần trở lại những ngày đó chỉ là __subgroupes __ () giải pháp của bạn thực sự tốt đẹp. Đưa bạn +1;) Btw, tôi nghĩ rằng nó có thể đáng tin cậy hơn bằng cách sử dụng máy phát điện trong trường hợp của bạn.
Roman Prykhodchenko

3
Không nên all_subclasseslà một setđể loại bỏ trùng lặp?
Ryne Everett

@RyneEverett Ý bạn là nếu bạn đang sử dụng nhiều kế thừa? Tôi nghĩ rằng bạn không nên kết thúc với các bản sao.
fletom

@fletom Có, nhiều kế thừa sẽ là cần thiết cho các bản sao. Ví dụ, A(object), B(A), C(A), và D(B, C). get_all_subclasses(A) == [B, C, D, D].
Ryne Everett

@RomanPrykhodchenko: Tiêu đề câu hỏi của bạn nói rằng hãy tìm tất cả các lớp con của một lớp được đặt tên, nhưng điều này cũng như các công việc khác chỉ được cung cấp cho chính lớp đó, không chỉ tên của nó, vậy nó là gì?
martineau

33

Giải pháp đơn giản nhất ở dạng chung:

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

Và một lớp đối xứng trong trường hợp bạn có một lớp duy nhất mà bạn kế thừa từ:

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

2
Cách tiếp cận máy phát điện thực sự sạch sẽ.
bốn43

22

Python 3.6 -__init_subclass__

Như câu trả lời khác đã đề cập, bạn có thể kiểm tra __subclasses__thuộc tính để lấy danh sách các lớp con, vì python 3.6, bạn có thể sửa đổi việc tạo thuộc tính này bằng cách ghi đè __init_subclass__phương thức.

class PluginBase:
    subclasses = []

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

class Plugin1(PluginBase):
    pass

class Plugin2(PluginBase):
    pass

Bằng cách này, nếu bạn biết bạn đang làm gì, bạn có thể ghi đè hành vi của __subclasses__và bỏ qua / thêm các lớp con từ danh sách này.


1
Có bất kỳ lớp phụ từ bất kỳ loại nào sẽ kích hoạt __init_subclasslớp của cha mẹ.
Hoặc Duẩn

9

Lưu ý: Tôi thấy rằng ai đó (không phải @unutbu) đã thay đổi câu trả lời được tham chiếu để nó không còn sử dụng nữa vars()['Foo']- vì vậy điểm chính của bài đăng của tôi không còn được áp dụng.

FWIW, đây là ý tôi muốn nói về câu trả lời của @ unutbu chỉ hoạt động với các lớp được xác định cục bộ - và việc sử dụng eval()thay vì vars()sẽ làm cho nó hoạt động với bất kỳ lớp có thể truy cập nào, không chỉ các lớp được xác định trong phạm vi hiện tại.

Đối với những người không thích sử dụng eval(), một cách cũng được hiển thị để tránh nó.

Đầu tiên, đây là một ví dụ cụ thể thể hiện vấn đề tiềm ẩn khi sử dụng 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'>]

Điều này có thể được cải thiện bằng cách chuyển eval('ClassName')xuống hàm được xác định, giúp sử dụng dễ dàng hơn mà không mất tính tổng quát bổ sung có được bằng cách sử dụng eval()không giống như vars()không nhạy cảm theo ngữ cảnh:

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

Cuối cùng, có thể, và thậm chí có thể quan trọng trong một số trường hợp, để tránh sử dụng eval()vì lý do bảo mật, vì vậy đây là phiên bản không có:

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: Đã thêm phiên bản không sử dụng eval()- tốt hơn bây giờ?
martineau

4

Một phiên bản ngắn hơn nhiều để có được danh sách tất cả các lớp con:

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

Làm thế nào tôi có thể tìm thấy tất cả các lớp con của một lớp được đặt tên của nó?

Chúng tôi chắc chắn có thể dễ dàng thực hiện điều này khi có quyền truy cập vào chính đối tượng, vâng.

Đơn giản chỉ cần đặt tên của nó là một ý tưởng tồi, vì có thể có nhiều lớp cùng tên, thậm chí được định nghĩa trong cùng một mô-đun.

Tôi đã tạo ra một triển khai cho một câu trả lời khác , và vì nó trả lời câu hỏi này và nó thanh lịch hơn một chút so với các giải pháp khác ở đây, đây là:

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

Sử dụng:

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

Đây không phải là một câu trả lời tốt như sử dụng __subclasses__()phương pháp lớp tích hợp đặc biệt mà @unutbu đề cập, vì vậy tôi chỉ trình bày nó như một bài tập. Các subclasses()chức năng được xác định lợi nhuận một cuốn từ điển mà bản đồ tất cả các tên lớp con đến lớp con mình.

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)

Đầu ra:

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

1

Đây là một phiên bản không có đệ quy:

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

Điều này khác với các triển khai khác ở chỗ nó trả về lớp gốc. Điều này là do nó làm cho mã đơn giản hơn và:

class Ham(object):
    pass

assert(issubclass(Ham, Ham)) # True

Nếu get_subgroupes_gen trông hơi kỳ lạ thì đó là do nó được tạo bằng cách chuyển đổi một triển khai đệ quy đuôi thành một trình tạo vòng lặp:

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], [])
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.