Так вже вийшло, що мені вдруге довелося писати модульну систему на пітоні. Причому вдруге довелося йти по одних і тих самих граблях, бо приклад минулої реалізації залишився на ноуті, який зараз лежить в сервісному центрі. Шлях нижче не являється оптимальним чи там найкращим, але головне, що він 100% робочий і задовольняє моїм потребам 🙂 А потреби полягають в тому, що кожен плагін представляє собою реалізацію класу з певним набором функцій та має засіб для інстанціювання об’єкту цього класу не знаючи його імені. Отож…
Для початку створимо приблизно наступну систему підкаталогів з файлами, а потім почнемо її наповнювати:
+ / plugins
| + / plugin1
| | - __init__.py
| + / plugin2
| | - __init__.py
| | - __init__.py
| - pluginmanager.py
Як неважко здогадатись, модуль plugingmanager.py – буде відповідати за завантаження плагінів. Папка plugins міститиме власне плагіни (кожен з яких буде займати окремий каталог) та файл ініціалізації пакету “plugins” (файл init.py) – його можна лишити порожнім, оскільки надпакет plugins використовується лише для групування.
#!/usr/bin/python
import os
import os.path
plugins = []
def LoadModule(module):
# build package name
packagename = "plugins." + module
# using built-in function __import__() to dynamically load modules;
# ensure that # directory containing "plugins" folder exists in
# python's sys.path list. if not, it can be specified by
# sys.path.append(path_to_plugins) command before calling __import__()
mod = __import__(packagename, globals(), locals(), [])
# if module import was successful
if ( mod ):
# try to get the plugin module itself
components = packagename.split('.')
for component in components[1:]:
mod = getattr(mod, component)
# add the instance of plugin-implemented class to the list
plugins.append(mod.CreateInstance())
# dynamicaly load plugin packages
def LoadPlugins(pluginpath):
# enum plugins directory
for dir in os.listdir(os.path.join(pluginpath, 'plugins')):
# for each non-hidden file or directory
if (dir[0] != "_") and (dir.endswith (".py")):
# try to load corresponding plugin
LoadModule(dir)
LoadPlugins(".")
for plugin in plugins:
plugin.Say("Joey")
Сподіваюсь, коментарі зрозумілі (хоч і англомовні 🙂 ). Отож, ми перебираємо папку з плагінами і для кожого файлу (тобто в нашому випадку каталогу) намагаємось імпортувати його по імені. В пітоні завдяки вищенаведеній структурі будується дерево пакетів. Тобто в нашому випадку у нас є пакет верхнього рівня plugins, який має два підпакети: plugins.plugin1 та plugins.plugin2. Для імпорту використовується вбудована функція import(). Вона повертає клас модуля верхнього рівня (в нашому випадку – “plugins”), але оскільки нам потрібно дійти до пакета нижнього рівня ми “занурюємось” далі використовуючи рекурсивно функцію attrib. В нашому випадку, звісно, можна було зробити простіше, оскільки вкладеність тут має лише один рівень і назва пакету будується в цій же функції, але код я трохи ускладнив спеціально, щоб дати більш загальну та гнучку реалізацію завантаження пакетів чи модулів.
Отримавши клас модуля ми викликаємо його глобальну функцію CreateInstance(), що поверне нам екземпляр реалізованого в модулі класу.
Тепер розглянемо реалізацію власне плагінів. Для нескладних випадків ми можемо запихнути її прямо в опис пакету plugin1 – файл init.py:
#!/usr/bin/python
class Plugin1():
def Say(self, name):
print "Hello, %s!" % name
def CreateInstance():
return Plugin1()
По аналогії можна зробити і реалізацію plugin2. Тепер запустивши програму отримаємо наступне:
$ ./pluginmanager.py
Hello, Joey!
Konnichiwa, Joey!
Набір відповідних плагінів можна тепер збільшувати просто додаючи по аналогії інші підкаталоги-пакети. Звісно, можна переробити це все з підтримкою модулів, а не пакетів (чи не лише пакетів), але мені все ж така структура подобається більше.