简体   繁体   English

如何在 python 中发现特定包中的类?

[英]How can I discover classes in a specific package in python?

I have a package of plug-in style modules.我有一个插件风格的模块包。 It looks like this:它看起来像这样:

/Plugins 
/Plugins/__init__.py
/Plugins/Plugin1.py
/Plugins/Plugin2.py 
etc...

Each .py file contains a class that derives from PluginBaseClass .每个 .py 文件都包含一个派生自PluginBaseClass的类。 So I need to list every module in the Plugins package and then search for any classes that implement PluginBaseClass .所以我需要列出Plugins包中的每个模块,然后搜索任何实现PluginBaseClass的类。 Ideally I want to be able to do something like this:理想情况下,我希望能够做这样的事情:

for klass in iter_plugins(project.Plugins):
    action = klass()
    action.run()

I have seen some other answers out there, but my situation is different.我已经看到了其他一些答案,但我的情况有所不同。 I have an actual import to the base package (ie: import project.Plugins ) and I need to find the classes after discovering the modules.我有一个对基本包的实际导入(即: import project.Plugins ),我需要在发现模块后找到这些类。

Edit: here's a revised solution.编辑:这是一个修改后的解决方案。 I realised I was making a mistake while testing my previous one, and it doesn't really work the way you would expect.我意识到我在测试我之前的测试时犯了一个错误,它并没有像你期望的那样工作。 So here is a more complete solution:所以这里有一个更完整的解决方案:

import os
from imp import find_module
from types import ModuleType, ClassType

def iter_plugins(package):
    """Receives package (as a string) and, for all of its contained modules,
    generates all classes that are subclasses of PluginBaseClass."""

    # Despite the function name, "find_module" will find the package
    # (the "filename" part of the return value will be None, in this case)
    filename, path, description = find_module(package)

    # dir(some_package) will not list the modules within the package,
    # so we explicitly look for files. If you need to recursively descend
    # a directory tree, you can adapt this to use os.walk instead of os.listdir
    modules =  sorted(set(i.partition('.')[0]
                          for i in os.listdir(path)
                          if i.endswith(('.py', '.pyc', '.pyo'))
                          and not i.startswith('__init__.py')))
    pkg = __import__(package, fromlist=modules)
    for m in modules:
        module = getattr(pkg, m)
        if type(module) == ModuleType:  
            for c in dir(module):
                klass = getattr(module, c)
                if (type(klass) == ClassType and
                    klass is not PluginBaseClass and
                    issubclass(klass, PluginBaseClass)):
                    yield klass

My previous solution was:我之前的解决方案是:

You could try something like:您可以尝试以下方法:

from types import ModuleType
import Plugins

classes = []
for item in dir(Plugins):
    module = getattr(Plugins, item)
    # Get all (and only) modules in Plugins
    if type(module) == ModuleType:
        for c in dir(module):
            klass = getattr(module, c)
            if isinstance(klass, PluginBaseClass):
                classes.append(klass)

Actually, even better, if you want some modularity:实际上,如果您想要一些模块化,那就更好了:

from types import ModuleType

def iter_plugins(package):
    # This assumes "package" is a package name.
    # If it's the package itself, you can remove this __import__
    pkg = __import__(package)
    for item in dir(pkg):
        module = getattr(pkg, item)
        if type(module) == ModuleType:  
            for c in dir(module):
                klass = getattr(module, c)
                if issubclass(klass, PluginBaseClass):
                    yield klass

You may (and probably should) define __all__ in __init__.py as a list of the submodules in your package;您可以(并且可能应该)将__init__.py中的__all__定义为包中子模块的列表; this is so that you support people doing from Plugins import * .这是为了让你支持人们from Plugins import *做。 If you have done so, you can iterate over the modules with如果你这样做了,你可以用

import Plugins
import sys
modules = { }
for module in Plugins.__all__:
    __import__( module )
    modules[ module ] = sys.modules[ module ]
    # iterate over dir( module ) as above

The reason another answer posted here fails is that __import__ imports the lowest-level module, but returns the top-level one (see the docs ).此处发布的另一个答案失败的原因是__import__导入了最低级别的模块,但返回了顶级模块(请参阅文档)。 I don't know why.我不知道为什么。

Scanning modules isn't good idea.扫描模块不是一个好主意。 If you need class registry you should look at metaclasses or use existing solutions like zope.interface .如果您需要类注册表,您应该查看元类或使用现有的解决方案,如zope.interface Simple solution through metaclasses may look like that:通过元类的简单解决方案可能如下所示:

from functools import reduce
class DerivationRegistry(type):
    def __init__(cls,name,bases,cls_dict):
        type.__init__(cls,name,bases,cls_dict)
        cls._subclasses = set()
        for base in bases:
            if isinstance(base,DerivationRegistry):
                base._subclasses.add(cls)

    def getSubclasses(cls):
        return reduce( set.union,
                       ( succ.getSubclasses() for succ  in cls._subclasses if isinstance(succ,DerivationRegistry)),
                       cls._subclasses)

class Base(object):
    __metaclass__ = DerivationRegistry

class Cls1(object):
    pass

class Cls2(Base):
    pass

class Cls3(Cls2,Cls1):
    pass

class Cls4(Cls3):
    pass

print(Base.getSubclasses())

If you don't know what's going to be in Plugins ahead of time, you can get a list of python files in the package's directory, and import them like so:如果你不提前知道Plugins中会有什么,你可以在包的目录中获取 python 文件的列表,然后像这样导入它们:

# compute a list of modules in the Plugins package

import os
import Plugins
plugin_modules = [f[:-3] for f in os.listdir(os.path.dirname(Plugins.__file__)) 
                  if f.endswith('.py') and f != '__init__.py']

Sorry, that comprehension might be a mouthful for someone relatively new to python.抱歉,对于 python 相对较新的人来说,这种理解可能是满口的。 Here's a more verbose version (might be easier to follow):这是一个更详细的版本(可能更容易理解):

plugin_modules = []

package_path = Plugins.__file__
file_list = os.listdir(os.path.dirname(package_path))
for file_name in file_list:
    if file_name.endswith('.py') and file_name != '__init__.py':
        plugin_modules.append(file_name)

Then you can use __import__ to get the module:然后你可以使用__import__来获取模块:

# get the first one
plugin = __import__('Plugins.' + plugin_modules[0])

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM