При разработке сложных аддонов с большим количеством кода, хранить весь код в одном файле нецелесообразно. Когда в одном файле присутствуют несколько классов, наборов функций и данных, часто логически не связанных между собой, это затрудняет чтение, отладку, поиск нужных фрагментов кода, его повторное использование. Подобная компоновка кода считается очень плохим тоном программирования.
Blender на уровне Python поддерживает модульную систему компоновки кода, что позволяет разносить логические части аддона по отдельным файлам, после чего подключать их для использования там, где необходимо. Даже если вы ни разу не задумывались об использовании модулей, создавая скрипты или аддоны, вы с ними уже сталкивались – любой код, сохраненный в файл *.py, представляет из себя отдельный самостоятельный модуль. Просто ваш аддон состоял всего лишь из одного модуля. Сложные же аддоны могут состоять из нескольких десятков подключенных модулей.
Итак, модуль – это отдельный файл с расширением *.py, содержащий исполняемый код Python.
Для того, чтобы сформировать аддон из нескольких модулей, их объединяют в пакет. Пакет – это просто директория (папка), в которой собраны необходимые для работы аддона модули. Отличительным признаком пакета модулей является наличие в нем файла с именем __init__.py. Для того, чтобы такой аддон был правильно установлен в Blender, необходимо заархивировать в архив *.zip весь пакет (всю директорию целиком, а не только содержащиеся в ней файлы) и при установке аддона – Install from file – указать созданный архив.
Рассмотрим пример создания простейшего многофайлового аддона:
- Создадим пакет:
- В любом удобном менеджере файлов создать директорию с нужным именем. Для примера – d:/Python/TestMultifile/
- Для написания кода удобнее использовать внешнюю IDE, но можно писать код и во встроенном редакторе Blender.
- Создать новый проект в PyCharm
- В качестве рабочей директории указать созданный пакет.
- Создадим первый модуль:
- Создать новый файл.
- Назвать его addCube.py
Важно помнить, что имена модулей (файлов) не должны в точности повторять имя пакета (директории), чтобы не было проблем с их дальнейшем подключением в аддоне.
Не заостряясь на содержании, возьмем простейший код оператора для добавления в сцену дефолтного куба, куда же без него ).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import bpy class addCubeSample(bpy.types.Operator): bl_idname = 'mesh.add_cube_sample' bl_label = 'Add Cube' bl_options = {"REGISTER", "UNDO"} def execute(self, context): bpy.ops.mesh.primitive_cube_add() return {"FINISHED"} def register() : bpy.utils.register_class(addCubeSample) def unregister() : bpy.utils.unregister_class(addCubeSample) |
Здесь создается пользовательский оператор, путем оборачивания в класс системного оператор bpy.ops.mesh.primitive_cube_add(), добавляющий куб в сцену.
Еще одно важное отличие от однофайлового аддона – указывать описание аддона в словаре bl_info в каждом отдельном модуле не требуется, только исполняемый код.
- Создадим второй модуль:
- Создать новый файл.
- Назвать его addCubePanel.py
В этом модуле создадим интерфейс для вызова оператора из первого модуля. Здесь мы следуем концепции MVC (Model-View-Controller), которая предписывает разделять исполняемую часть кода (модель) и код, ответственный за создание интерфейса (представление).
Для пример создадим простейшую вкладку Add Cube в Т-панели с одной кнопкой для вызова созданного ранее оператора.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import bpy class addCubePanel(bpy.types.Panel): bl_idname = "panel.add_cube_panel" bl_label = "AddCube" bl_space_type = "VIEW_3D" bl_region_type = "TOOLS" bl_category = "Add Cube" def draw(self, context): self.layout.operator("mesh.add_cube_sample", icon='MESH_CUBE', text="Add Cube") def register() : bpy.utils.register_class(addCubePanel) def unregister() : bpy.utils.unregister_class(addCubePanel) |
- Создадим индексный файл для связи модулей в пакете:
- Создать новый файл.
- Назвать его __init__.py
В момент активации аддона, из всего пакета выполняется именно этот файл. Поэтому именно в нем следует разместить словарь bl_info с описанием аддона:
1 2 3 4 5 6 |
bl_info = { 'name': 'Test Multifile Addon', 'category': 'All', 'version': (0, 0, 1), 'blender': (2, 78, 0) } |
А так же именно здесь нужно сделать импорт всех остальных модулей.
Индексный файл __init__.py служит только для подключения и регистрации модулей и поэтому в нем не должно быть (хотя и допускается) никакого кода, относящегося к конкретному аддону. Этот файл должен быть как можно более универсален.
Занесем все имена модулей, которые должны подключаться в аддоне в список:
1 |
modulesNames = ['addCube', 'addCubePanel'] |
Имя модуля – это название файла, без расширения .py.
Это единственная изменяемая строка файла. Для того, чтобы создать индексный файл для любого другого аддона, можно использовать приведенный код, лишь изменив в списке имена модулей на нужные.
Если модули подключаются из пакета, в переменных среды Python они будут доступны по идентификатору: Имя_пакета.Имя_модуля. Дополним формат указанных имен модулей до полного вида:
1 2 3 |
modulesFullNames = {} for currentModuleName in modulesNames: modulesFullNames[currentModuleName] = ('{}.{}'.format(__name__, currentModuleName)) |
Теперь созданный словарь modulesFullNames содержит полные имена подключаемых модулей.
Импортируем все модули из полученного словаря, использовав для этого утилиту importlib:
1 2 3 4 5 6 7 8 9 |
import sys import importlib for currentModuleFullName in modulesFullNames.values(): if currentModuleFullName in sys.modules: importlib.reload(sys.modules[currentModuleFullName]) else: globals()[currentModuleFullName] = importlib.import_module(currentModuleFullName) setattr(globals()[currentModuleFullName], 'modulesNames', modulesFullNames) |
Все импортированные модули хранятся в sys.modules. Перезагрузка уже импортированного (importlib.reload) модуля требуется, если в окне подключения аддонов (User Preferences – Add-ons) пользователь нажимает клавишу обновления f8.
Для ускорения работы аддонов Blender, модули хранятся в скомпилированном виде в директории кэша аддонов, откуда они и берутся для исполнения. Если в код какого-либо модуля внесены изменения, Blender все равно будет использовать старую, уже скомпилированную версию модуля, до тех пор, пока аддон не будет переустановлен или обновлен нажатием клавиши f8. Перезагрузка модуля требуется для того, чтобы измененный код перекомпилировался и стал доступен для использования.
Так как importlib.import_module не создает глобальной переменной с именем подключаемого модуля, следует занести в globals такую переменную, указывающую на соответствующий модуль, чтобы в дальнейшем обращаться к модулю по имени.
Для того, чтобы в отдельных модулях можно было обращаться к другим модулям по имени, в каждом из подключаемых модулей создается переменная moduleNames, ссылающаяся на словарь modulesFullNames. Теперь из любого подключенного модуля можно получить доступ к другому подключенному модулю по его имени. Например для доступа из модуля addCubePanel к модулю addCube нужно выполнить следующую команду:
1 |
addCubeModule = sys.modules[modulesNames['addCube']] |
После того, как все необходимые модули подключены, необходимо зарегистрировать содержащиеся в них классы. Так как функция register при активации аддона автоматически выполняется только в индексном файле, внутри нее необходимо выполнить регистрацию для всех модулей аддона.
1 2 3 4 5 |
def register(): for currentModuleName in modulesFullNames.values(): if currentModuleName in sys.modules: if hasattr(sys.modules[currentModuleName], 'register'): sys.modules[currentModuleName].register() |
То же самое необходимо проделать и для разрегистрации классов.
1 2 3 4 5 |
def unregister(): for currentModuleName in modulesFullNames.values(): if currentModuleName in sys.modules: if hasattr(sys.modules[currentModuleName], 'unregister'): sys.modules[currentModuleName].unregister() |
Полный код файла __init__.py имеет следующий вид:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
bl_info = { 'name': 'Test Multifile Addon', 'category': 'All', 'version': (0, 0, 1), 'blender': (2, 78, 0) } modulesNames = ['addCube', 'addCubePanel'] import sys import importlib modulesFullNames = {} for currentModuleName in modulesNames: modulesFullNames[currentModuleName] = ('{}.{}'.format(__name__, currentModuleName)) for currentModuleFullName in modulesFullNames.values(): if currentModuleFullName in sys.modules: importlib.reload(sys.modules[currentModuleFullName]) else: globals()[currentModuleFullName] = importlib.import_module(currentModuleFullName) setattr(globals()[currentModuleFullName], 'modulesNames', modulesFullNames) def register(): for currentModuleName in modulesFullNames.values(): if currentModuleName in sys.modules: if hasattr(sys.modules[currentModuleName], 'register'): sys.modules[currentModuleName].register() def unregister(): for currentModuleName in modulesFullNames.values(): if currentModuleName in sys.modules: if hasattr(sys.modules[currentModuleName], 'unregister'): sys.modules[currentModuleName].unregister() if __name__ == "__main__": register() |
Это чистовой (релизный) вид индексного файла __init__.py, который может быть использован для любого многофайлового аддона. Необходимо лишь указывать нужные имена модулей в списке modulesNames.
- Директория TestMultifile теперь содержит три файла:
- __init__.py
- addCube.py
- addCubePanel.py
Осталось запаковать ее (директорию целиком, а не только файлы) в архив *.zip и можно выполнить установку тестового мультифайлового аддона в Blender.
Бонус: отладочный запуск мультифайлового аддона из Blender
Если для разработки многофайлового аддона используется внешняя IDE, выполнить отладочный запуск скриптом из Blender не получится. При использовании стандартного скрипта, Blender не поймет, что вызывается индексный файл пакета, а не отдельный исполняемый модуль. Для того, чтобы отлаживать многофайловый аддон в вызывающий скрипт необходимо внести изменения.
В первую очередь нужно указать место расположения подключаемых модулей – путь к директории d:/Python/TestMultifile/. Поиск импортируемых модулей Python осуществляет в перечне директорий, указанных в sys.path. Для того, чтобы модули были найдены интерпретатором в момент их подключения, нужно указать директорию с их расположением и добавить ее в sys.path. Имя запускаемого файла для удобства сохраним в отдельную переменную.
1 2 3 4 5 6 |
filesDir = "d:/Python/TestMultifile" initFile = "__init__.py" if filesDir not in sys.path: sys.path.append(filesDir) |
Отладочный запуск аддона отличается от запуска аддона, уже установленного в Blender. Для того, чтобы внутри модуля __init__.py понимать, какой именно тип запуска требуется, при отладочном запуске добавим в перечень входных параметров sys.argv дополнительный параметр “DEBUG_MODE”.
1 2 |
if 'DEBUG_MODE' not in sys.argv: sys.argv.append('DEBUG_MODE') |
Удалим этот параметр после выполнения скрипта.
1 2 |
if 'DEBUG_MODE' in sys.argv: sys.argv.remove('DEBUG_MODE') |
Полный код скрипта, запускающего многофайловый аддон из окна Text Editor в Blender теперь имеет следующий вид:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import os import sys filesDir = "d:/Python/TestMultifile" initFile = "__init__.py" if filesDir not in sys.path: sys.path.append(filesDir) file = os.path.join(filesDir, initFile) if 'DEBUG_MODE' not in sys.argv: sys.argv.append('DEBUG_MODE') exec(compile(open(file).read(), initFile, 'exec')) if 'DEBUG_MODE' in sys.argv: sys.argv.remove('DEBUG_MODE') |
Код скрипта универсален, для разработки других многофайловых аддонов нужно только изменять путь filesDir к месторасположению модулей аддона.
При запуске из окна Text Editor по кнопке Run Script исполняемый файл __init__.py считается отдельным модулем, а не индексным файлом пакета. Поэтому и все подключаемые внутри него модули должны импортироваться не как части пакета, а как такие же отдельные модули. Следовательно полные имена подключаемых модулей будут состоять лишь из имен самих модулей, без приставки с именем пакета.
Используя добавленный в вызывающем скрипте параметр “DEBUG_MODE”, внесем в файл __init__.py условие для отладочного запуска аддона, позволяющее правильно импортировать подключаемые модули. Для этого нужно всего лишь правильно формировать полные имена в словаре modulesFullNames:
1 2 3 4 5 |
for currentModuleName in modulesNames: if 'DEBUG_MODE' in sys.argv: modulesFullNames[currentModuleName] = ('{}'.format(currentModuleName)) else: modulesFullNames[currentModuleName] = ('{}.{}'.format(__name__, currentModuleName)) |
Полный код файла __init__.py, работающего и в отладочном и в релизном режимах:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
bl_info = { 'name': 'Test Multifile Addon', 'category': 'All', 'version': (0, 0, 1), 'blender': (2, 78, 0) } modulesNames = ['addCube', 'addCubePanel'] import sys import importlib modulesFullNames = {} for currentModuleName in modulesNames: if 'DEBUG_MODE' in sys.argv: modulesFullNames[currentModuleName] = ('{}'.format(currentModuleName)) else: modulesFullNames[currentModuleName] = ('{}.{}'.format(__name__, currentModuleName)) for currentModuleFullName in modulesFullNames.values(): if currentModuleFullName in sys.modules: importlib.reload(sys.modules[currentModuleFullName]) else: globals()[currentModuleFullName] = importlib.import_module(currentModuleFullName) setattr(globals()[currentModuleFullName], 'modulesNames', modulesFullNames) def register(): for currentModuleName in modulesFullNames.values(): if currentModuleName in sys.modules: if hasattr(sys.modules[currentModuleName], 'register'): sys.modules[currentModuleName].register() def unregister(): for currentModuleName in modulesFullNames.values(): if currentModuleName in sys.modules: if hasattr(sys.modules[currentModuleName], 'unregister'): sys.modules[currentModuleName].unregister() if __name__ == "__main__": register() |
Для публикации релиза аддона можно использовать как приведенный выше чистовой файл __init__.py, так и модифицированный.
И снова здрасьте 🙂
Еще раз нырнул в этот код с новыми мозгами
Вопрос по этим строкам:
У нас создается переменная в globals(), только при первом запуске кода.
При запуске кода второй раз, когда модуль уже импортирован, в globals() уже ничего нет т.к. мы явно не указываем на это и в текущем контексте уже нет созданных переменных.
Можно явно указать создание переменной в globals() вот так:
Получается создание чего либо в globals() не имеет смысла.
Можно сделать обычную переменную для того чтобы использовать её для setattr:
Например вот так:
Далее заметка вот какая:
Если бы действительно в globals() создавалась переменная “modulesNames”, то нам бы не пришлось добавлять в каждый модуль параметр “modulesNames”, для дальнейшего вызова индивидуально для каждого модуля с одним и тем же словарем продублированным в каждом модуле.
Мы бы тогда могли обращаться к переменной modulesNames в globals() и всегда доставать ссылки на загруженные модули их полные и обычные имена и что угодно еще.
Мне стало интересно, что именно было необходимо достичь?
Мне кажется можно модернизировать этот код так чтобы все хранилось как раз таки в globals() и не создавался дублирующийся в параметрах каждого модуля словарь. Мы бы могли как раз таки один раз формировать переменные модулей.
Привет!
Идея с globals заключается в том, чтобы без дополнительных импортов иметь возможность из одного .py файла ссылаться на другой .py файл.
В данном случае globals() ведь просто используется как нестандартный способ создания переменной.
globals()[currentModuleFullName] = importlib.import_module(currentModuleFullName)
И не отличается от создания обычной переменной
module = importlib.import_module(currentModuleFullName)
Мы ведь просто создаем переменную, только для того чтобы использовать её в setattr. И эта переменная существует только при первом запуске скрипта в Blender. При втором запуске её уже не будет.
Можно ведь просто напостоянку тогда создавать переменную, чтобы она существовала при каждом запуске скрипта и обращаться уже к её содержимому, а не дописывать одни и те же данные к каждому модулю чтобы потом обращаться к ним.
Так будет фиксированное место хранения данных и не будет зависеть от имени загружаемого модуля и модуль будет обращаться к одной и той же переменной.
Соглашусь, так наверное будет эффективнее.
Если не сложно, посмотри/оцени мою версию.
Может тебе тоже станет интересно и появится новая статья 🙂
—–
modulesNames = [‘addCube’, ‘addCubePanel’]
import sys
import importlib
modulesFullNames = {}
for currentModuleName in modulesNames:
if ‘DEBUG_MODE’ in sys.argv:
modulesFullNames[currentModuleName] = (‘{}’.format(currentModuleName))
else:
modulesFullNames[currentModuleName] = (‘{}.{}’.format(__name__, currentModuleName))
del currentModuleName
del modulesNames
for currentModuleFullName in modulesFullNames.values():
if currentModuleFullName in sys.modules:
globals()[currentModuleFullName] = importlib.reload(sys.modules[currentModuleFullName])
else:
globals()[currentModuleFullName] = importlib.import_module(currentModuleFullName)
del currentModuleFullName
# Импорты делать не обязательно,
# Это чтобы код не подчеркивал строки красным
import addCube
import addCubePanel
print(‘test addCube’, addCube.aaa())
print(‘test addCubePanel’, addCubePanel.vvv())
В файле addCube тестовая функция
def aaa():
return 5
В файле addCubePanel
import addCube
def vvv():
return 15
print(‘in vvv: ‘, addCube.aaa())
Blender выводит
in vvv: 5
test addCube 5
test addCubePanel 15
——
Мы получается создаем переменную с именем модуля сразу с ссылкой на модуль и при любом раскладе храним в глобальных переменных, просто в одном случае мы релоадим, в дргуом импортим.
И сделал удаление на ненужные переменные.
И получается сразу можем обращаться к модулю как к переменной.
Я в коде сделал импорты только потому что, что IDE ругается, так даже без них работает все.
Обычно модули друг на друга не ссылаются, но если вдруг захочется, то можно сделать список неправильно загруженных модулей и потом повторно их загрузить. Тогда можно будет вызывать функции из модулей друг из друга и неважно будет кто загрузился первым при импорте. Если что могу доделать.
Ты сам можешь оформить как статью на сайте. Зарегистрируйся и сможешь публиковать статьи от своего имени.
Оо ничёси!
А чё так можно было? 🙂
Ссылка справа верхней части страницы давно висит )
Как только ты это написал, я сразу начал видеть эту ссылку везде.
Это уже видимо как автоматический спам фильтр на глазах 🙂
Биологическая аугментация.
Адблок в комплекте с глазами )
Несмотря на то, что у меня не получилось с помощью урока сделать init файл для сборки, тем не менее, скрипт для тестирования оказался очень полезным.
Спасибо автору за урок!
Рад, что он оказался для вас полезным.
Структура тестового проекта стандартная
D:/testProject
/__init__.py
/add_cube.py (с функцией и панелью)
Я только начинаю разбираться как создавать скрипты, поэтому разработчики могут меня поправить.
Скрипт для дебага в Blender3.x у меня получилось использовать:
1. использовать как __init__ последний файл в статье:
2. запускать его скриптом описанном тут:
Для запаковки в zip, код описанный в статье – не сработал. Покопавшись в штатных дополнениях Blender, я определил, что работающая структура __init__.py файла для запаковки, такая:
Какие изменения произошли, в Blender 3+? Потому как если запускать дебаг скрипт, с указанием пути к __init__.py то дополнение работает корректно (я работаю со своим примером). Но когда пробую собрать в zip и установить, то вроде как дополнение ставится, но ничего не происходит. Нет ошибок, и панель не появляется в UI
Сложно сказать почему так происходит. Принципиальных изменений, таких чтобы повлияли на инсталляцию аддонов, не должно было происходить в Blender-е
Не работает на 2.92 “out of the box”. Выбрасывает ошибку про “has category” в модуле addCubePanel:https://i.imgur.com/PSJFgL9.png
Чинится при помощи комментирования строки bl_category = “Add Cube” в addCubePanel.py.
Нет, для того, чтобы панель нормально отображалась в 2.92, нужно заменить строчку
bl_region_type = “TOOLS”
на
bl_region_type = “UI”
в этом файле
Немного расшарил инит файл!
А что если добавить вот такую часть?
что бы автоматом искать новые модули в директории, ислкючая “__init__.py”, и добавлять их в список “modulesNames”
modulesNames = []
dir_path = os.path.dirname(os.path.realpath(__file__)) # адрес модуля “__init__.py”
def serchPy():
____import os
____for root, dirs, files in os.walk(dir_path):
________for name in files:
____________if name != ‘__init__.py’:
________________name=name.replace(‘.py’, ”) # удаляем расширение ‘.py’
________________global modulesNames
________________modulesNames.append(name) # добавляем найденный модуль в список
____________else:
________________continue
serchPy()
Хорошая идея!
Я подобное не делю т.к. у меня бывает в директории еще дополнительные файлы, которые не надо включать в текущий проект.
Но для общего случая – очень удобно, не надо прописывать подключаемые модули руками.
Спасибо!
Как всегда очень актуально, доступно и полезно!
Рад, что оказалось полезно.