Методы подключения дополнительных файлов в программах Python
Работа с ресурсными файлами в Python
При разработке приложений часто требуется включать изображения, конфигурационные файлы, звуки или данные. После упаковки программы (например, с помощью PyInstaller или cx_Freeze) обычные относительные пути перестают работать. Существует несколько подходов для организации доступа к таким ресурсам.
Как организовать доступ к файлам внутри установленного пакета?
Начиная с Python 3.7 рекомендуется использовать модуль importlib.resources. Этот способ является стандартным и не требует установки дополнительных библиотек. Он позволяет получать доступ к файлам, входящим в состав пакета, даже после упаковки приложения.
# структура проекта:
# myapp/
# __init__.py
# resources/
# data.txt
# image.png
# main.py
# main.py
from importlib import resources
def read_data():
with resources.open_text('myapp.resources', 'data.txt') as f:
return f.read()
def get_image_path():
return resources.path('myapp.resources', 'image.png')
print(read_data())
print(get_image_path())Python program to exe (преобразование python в exe)
Возможные проблемы: если ресурсный каталог не является частью пакета (отсутствует __init__.py), importlib.resources не сможет его найти. Решение - создать пустой файл __init__.py внутри папки с ресурсами. Также при использовании PyInstaller необходимо указать эту папку как ресурсную (через --additional-hooks-dir или --add-data), иначе файлы не будут включены в сборку.
Каким образом использовать устаревший модуль pkg_resources?
Ранее широко применялся модуль pkg_resources из библиотеки setuptools. Он также умеет находить файлы внутри пакета, но работает медленнее и не поддерживается в будущих версиях Python.
from pkg_resources import resource_string, resource_filename
data = resource_string(__name__, 'resources/data.txt')
print(data.decode('utf-8'))Resource file python (работа с ресурсными файлами)
Типичные ошибки: имя пакета (__name__) должно соответствовать реальной структуре. Если файл не найден, возникает исключение. В упакованном приложении необходимо указывать пути относительно sys._MEIPASS при работе с PyInstaller, так как pkg_resources не адаптируется к временной папке автоматически.
Как добавить ресурсы при сборке через PyInstaller?
PyInstaller не копирует файлы, не связанные с импортируемыми модулями. Для включения дополнительных файлов используется ключ --add-data.
pyinstaller --add-data "src/resources:resources" main.py
Python exe windows (создание exe из python на windows)
В коде после сборки путь к ресурсам лежит через sys._MEIPASS. При использовании importlib.resources проблем не возникает, если ресурсы оформлены как пакет. Иначе нужно самостоятельно вычислять путь:
import sys, os
def resource_path(relative):
base = getattr(sys, '_MEIPASS', os.path.abspath('.'))
return os.path.join(base, relative)Exe file python (создание exe файла в python)
Проблема: если не перенаправить пути, после сборки программа будет искать файлы в папке с exe, но их там нет. Также важно, чтобы папки с ресурсами содержали __init__.py, если они оформлены как Python-пакеты.
Когда целесообразно встраивать ресурсы прямо в код?
Для небольших файлов (иконки, конфиги) можно закодировать их в base64 и сохранить как строку в исходном коде. Это упрощает распространение - один файл без дополнительных ресурсов.
import base64
# закодированный файл (можно получить из командной строки: base64 image.png > encoded.txt)
encoded = "iVBORw0KGgoAAAANSUhEUg..."
with open('temp_image.png', 'wb') as f:
f.write(base64.b64decode(encoded))Недостатки: рост размера исходного файла, невозможность менять ресурсы без перекомпиляции, сложность с большими файлами.
Как задать ресурсы в setup.py для дистрибутива?
При сборке пакета с помощью setuptools можно включить статические файлы через package_data или data_files. Это гарантирует, что при установке через pip файлы окажутся в нужном месте.
from setuptools import setup, find_packages
setup(
name='myapp',
packages=find_packages(),
package_data={
'myapp.resources': ['*.txt', '*.png'],
},
include_package_data=True,
)Ошибка: если не указать include_package_data=True, файлы могут не копироваться. Также требуется явно указывать расширения в package_data.
Расширенные примеры работы с ресурсными файлами
Пример 1: Иерархическая структура ресурсов с importlib.resources
Предположим, у пакета есть вложенные папки с файлами разных типов.
# myapp/
# __init__.py
# resources/
# __init__.py
# icons/
# __init__.py
# save.png
# load.png
# docs/
# help.pdf
# main.py
from importlib.resources import files
# Получение всех файлов внутри подпапки icons
icon_files = files('myapp.resources.icons').iterdir()
for f in icon_files:
print(f.name)
# Открытие файла из подпапки
data = files('myapp.resources.docs').joinpath('help.pdf').read_bytes()
print(len(data))save.png load.png 123456
Пример 2: Динамическая загрузка ресурсов из sys._MEIPASS при использовании PyInstaller
Код адаптируется к среде исполнения: при запуске из интерпретатора используются относительные пути, после сборки - временная папка PyInstaller.
import sys, os, json
def get_config():
if getattr(sys, 'frozen', False):
base = sys._MEIPASS
else:
base = os.path.dirname(__file__)
path = os.path.join(base, 'resources', 'config.json')
with open(path) as f:
return json.load(f)
config = get_config()
print(config['version'])'2.1.0'
Пример 3: Использование вместе с PyInstaller хуками для автоматического включения ресурсов
Можно создать хуки, которые PyInstaller будет вызывать для поиска скрытых импортов. Но для обычных файлов проще использовать --add-data или встроенные возможности importlib.resources, которые PyInstaller частично понимает. Тем не менее, для сложных проектов пишут хуки.
# hook-myapp.py
from PyInstaller.utils.hooks import collect_data_files
datas = collect_data_files('myapp.resources')
# возвращает список кортежей (исходный путь, целевая папка)Пример 4: Встраивание бинарного файла в код через base64 и его использование без сохранения на диск
Для иконки, которая будет загружена через PIL или Tkinter, можно не создавать временный файл, а работать напрямую с байтами.
import base64, tkinter as tk
from io import BytesIO
# заранее закодированная иконка (часть данных)
encoded_icon = "R0lGODlhEAAQAMQAAORHHOVSKu..."
icon_bytes = base64.b64decode(encoded_icon)
root = tk.Tk()
photo = tk.PhotoImage(data=icon_bytes) # Tkinter поддерживает данные base64
label = tk.Label(root, image=photo)
label.pack()
root.mainloop()(окно с иконкой)
Пример 5: Использование zipimporter для упаковки ресурсов в архив
Можно создать ZIP-архив со всеми ресурсами и добавить его в Python path. Тогда importlib.resources сможет извлекать файлы из архива.
import sys, zipimport
# добавляем архив с ресурсами в sys.path
sys.path.insert(0, 'resources.zip')
# теперь можно импортировать пакет из архива
from importlib.resources import open_text
text = open_text('packed_resources', 'data.txt').read()
print(text)Содержимое data.txt