Как я могу обнаружить классы в определенном пакете в python?

У меня есть пакет модулей в стиле плагинов. Это выглядит следующим образом:

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

Каждый файл .py содержит класс, производный от PluginBaseClass . Поэтому мне нужно перечислить каждый модуль в пакете Plugins , а затем искать любые классы, которые реализуют PluginBaseClass . В идеале я хочу иметь возможность сделать что-то вроде этого:

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

Я видел некоторые другие ответы там, но моя ситуация другая. У меня есть фактический импорт в базовый пакет (то есть: import project.Plugins ), и мне нужно найти классы после обнаружения модулей.

8
задан Jason Webb 17 August 2010 в 21:31
поделиться

4 ответа

Сканирование модулей - плохая идея. Если вам нужен реестр классов, вы должны посмотреть метаклассы или использовать существующие решения, такие как zope.interface . Простое решение через метаклассы может выглядеть так:

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())
1
ответ дан 5 December 2019 в 15:16
поделиться

Вы можете (и, вероятно, должны) определить __all__ в __init__. py как список подмодулей вашего пакета; это нужно для того, чтобы поддержать людей, делающих from Plugins import *. Если вы это сделали, вы можете перебирать модули с помощью

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

Причина, по которой другой ответ, опубликованный здесь, не работает, заключается в том, что __import__ импортирует модуль нижнего уровня, но возвращает модуль верхнего уровня (см. docs). Я не знаю почему.

5
ответ дан 5 December 2019 в 15:16
поделиться

Если вы не знаете, что будет в плагинах заранее, вы можете получить список файлов 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']

Извините, это понимание может оказаться непосильной задачей для кого-то относительно новичка в python.Вот более подробная версия (возможно, будет легче следовать):

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)

Затем вы можете использовать __ import __ , чтобы получить модуль:

# get the first one
plugin = __import__('Plugins.' + plugin_modules[0])
1
ответ дан 5 December 2019 в 15:16
поделиться

Edit: вот исправленное решение. Я понял, что допустил ошибку при тестировании предыдущего решения, и оно работает не совсем так, как вы ожидаете. Так что вот более полное решение:

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

Мое предыдущее решение было таким:

Вы можете попробовать что-то вроде:

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)

На самом деле, даже лучше, если вы хотите некоторую модульность:

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
4
ответ дан 5 December 2019 в 15:16
поделиться
Другие вопросы по тегам:

Похожие вопросы: