【问题标题】:How to find all the subclasses of a class given its name?如何找到给定名称的类的所有子类?
【发布时间】:2011-04-21 05:03:09
【问题描述】:

我需要一种工作方法来获取从 Python 中的基类继承的所有类。

【问题讨论】:

    标签: python subclass


    【解决方案1】:

    这是一个简单但高效的代码版本:

    def get_all_subclasses(cls):
        subclass_list = []
    
        def recurse(klass):
            for subclass in klass.__subclasses__():
                subclass_list.append(subclass)
                recurse(subclass)
    
        recurse(cls)
    
        return set(subclass_list)
    

    它的时间复杂度是O(n),其中n是没有多重继承时所有子类的数量。 它比使用生成器递归创建列表或产生类的函数更有效,其复杂性可能是 (1) O(nlogn) 当类层次结构是平衡树时或 (2) O(n^2) 当类层次结构是有偏树时。

    【讨论】:

      【解决方案2】:

      这不如使用@unutbu 提到的特殊内置__subclasses__() 类方法那么好,因此我将其仅作为练习提供。定义的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'>}
      

      【讨论】:

        【解决方案3】:

        注意:我看到有人(不是 @unutbu)更改了引用的答案,因此它不再使用 vars()['Foo'] — 所以我的帖子的主要观点不再适用。

        FWIW,这就是我所说的 @unutbu's answer 仅与本地定义的类一起使用的意思——使用 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'>]
        

        【讨论】:

          【解决方案4】:

          新型类(即从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 类是一流的对象,因此您不需要使用带有类名的字符串来代替类或类似的东西。您可以直接使用该类,而且您可能应该这样做。

          如果你确实有一个表示类名的字符串,并且你想找到该类的子类,那么有两个步骤:找到给定其名称的类,然后找到带有__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__() 都会返回其子类的列表。

          【讨论】:

          【解决方案5】:

          如何找到给定名称的类的所有子类?

          如果可以访问对象本身,我们当然可以轻松地做到这一点,是的。

          仅仅给出它的名字是一个糟糕的主意,因为可以有多个同名的类,甚至在同一个模块中定义。

          我为另一个 answer 创建了一个实现,因为它回答了这个问题,并且比这里的其他解决方案更优雅一点,所以它是:

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

          【讨论】:

            【解决方案6】:

            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__ 的行为并从此列表中省略/添加子类。

            【讨论】:

            • 是的,任何类型的任何子类都会触发父类的__init_subclass
            【解决方案7】:

            这是一个没有递归的版本:

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

            【讨论】:

              【解决方案8】:

              获取所有子类列表的更短版本:

              from itertools import chain
              
              def subclasses(cls):
                  return list(
                      chain.from_iterable(
                          [list(chain.from_iterable([[x], subclasses(x)])) for x in cls.__subclasses__()]
                      )
                  )
              

              【讨论】:

                【解决方案9】:

                一般形式的最简单解决方案:

                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
                

                【讨论】:

                • 生成器的方法真的很干净。
                【解决方案10】:

                如果您只想要直接子类,那么 .__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
                

                【讨论】:

                • 谢谢@fletom!尽管那时我需要的只是 __subclasses__() 您的解决方案非常好。带你 +1 ;) 顺便说一句,我认为在你的情况下使用生成器可能更可靠。
                • 不应该all_subclassesset 以消除重复吗?
                • @RyneEverett 你的意思是如果你使用多重继承?我认为否则你不应该以重复结束。
                • @fletom 是的,重复项需要多重继承。例如,A(object)B(A)C(A)D(B, C)get_all_subclasses(A) == [B, C, D, D].
                • @RomanPrykhodchenko:你的问题的标题说要找到一个给定其名称的类的所有子类,但这以及其他唯一的工作给定类本身,而不仅仅是它的名称 - 所以只是什么是吗?
                猜你喜欢
                • 2010-10-04
                • 2012-02-03
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多