У меня есть пакет модулей в стиле плагинов. Это выглядит следующим образом:
/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
), и мне нужно найти классы после обнаружения модулей.
Сканирование модулей - плохая идея. Если вам нужен реестр классов, вы должны посмотреть метаклассы или использовать существующие решения, такие как 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())
Вы можете (и, вероятно, должны) определить __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). Я не знаю почему.
Если вы не знаете, что будет в плагинах
заранее, вы можете получить список файлов 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])
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