Файл конфигурации приложения Python, общий для всех подпакетов

Я создаю приложение для управления некоторым оборудованием. У меня есть различное оборудование, которое я реализовал в пакетах: двигатели и измерительные устройства. Моя файловая структура следующая:

name_of_my_app/
    __init__.py
    main.py
    config.ini
    CONFIG.py
    motors/
        __init__.py
        one_kind_of_motor.py
    measurement_devices/
        __init__.py
        one_kind_of_measurement_device.py

Я хотел бы иметь один файл конфигурации (ini) для хранения настроек двигателей и измерительных устройств.

Я начал с ответа пользователя u/symmitchry на этот пост на Reddit: https://www.reddit.com/r/learnpython/comments/2hjxk5/whats_the_proper_way_to_use_configparser_across/ поскольку это казалось самым разумным поступком. Насколько я понимаю, я мог бы иметь config.ini в корне моего приложения вместе с CONFIG.py. CONFIG.py позаботится о чтении настроек из config.ini и сделает их доступными посредством импорта CONFIG.py. Это работает, когда я импортирую конфигурацию из main.py. Однако, когда я пытаюсь импортировать конфигурацию из one_kind_of_motor.py с помощью:

from ... import CONFIG

как предложено @f3lix в этом посте: Импортируя модули из родительской папки, я получаю:

ImportError: attempted relative import with no known parent package

Именно тогда я добавил __init__.py в корень, надеясь, что это решит мою проблему, но этого не произошло. Я также попробовал использовать .. и . вместо ..., и единственное, что это сделало, это показало, как мало я понимаю в этой теме... (он продолжает выдавать ту же ошибку выше).

В настоящее время, чтобы обойти эту проблему, я читаю нужные мне настройки из config.ini в каждом файле. Но я знаю, что это неоптимально.

Я программирую на Python как ученый, т. е. у меня нет опыта разработчика программного обеспечения, и я обычно использую базовый Python, но я хотел бы научиться простым способам распространения своих исследований. Я видел несколько постов с подобными вопросами (и вы можете позвать меня по этому поводу), но мне не удалось понять их ответ. Можете ли вы объяснить мне это с точки зрения непрофессионала?

Обновлено: В ответ на подробный ответ @ImpecableChicken я создал упрощенную файловую структуру и загрузил ее на GitHub, чтобы продемонстрировать проблему. С использованием

from .. import CONFIG

выдает ошибку

ImportError: attempted relative import with no known parent package

Я использую компьютер под управлением Windows и выполняю свои сценарии из визуального кода.

Как вы используете свое приложение?

Mr_and_Mrs_D 19.07.2024 15:51

Я установил Visual Code и нажимаю «Выполнить»..Выполнить без отладки.

Omnistic 19.07.2024 16:00
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
2
86
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Этот ответ вместе с его верхними комментариями довольно хорошо объясняет относительный импорт. Важно отметить, что для относительного импорта точки указывают на то, что система импорта должна двигаться вверх в иерархии пакетов, а не в дереве файловой системы.

Импорт файла

Проблема

Предположим, что вы импортируете one_kind_of_motor.py в main.py:

  • Запуск python main.py добавляет родительский каталог main.py в sys.path (т. е. name_of_my_app/ будет искать пакеты и модули).
  • Внутри модуля __main__ (соответствующего файлу main.py) import motors.one_kind_of_motor создаётся пакет motors с подмодулем motors.one_kind_of_motor. Пакет motors не имеет родительского пакета; это видно из того, что в его названии нет точек. Родителем модуля motors.one_kind_of_motor является motors.
  • Во время импорта motors.one_kind_of_motorfrom .. import CONFIG указывает, что система импорта должна подняться на два уровня вверх в иерархии пакетов. Первый . соответствует пакету motors, который является родительским для текущего модуля. Второй . соответствует родительскому элементу motors, но motors не имеет родительского пакета, что приводит к ошибке.

Возможно, будет полезно помнить, что в большинстве случаев количество начальных точек в операторе относительного импорта не может превышать количество точек в имени модуля, содержащего этот оператор. (Хотя это правило весьма полезно, в общем случае оно неверно; см., например, __package__ и __spec__.)

Важно отметить, что основной причиной проблемы является не оператор относительного импорта в one_kind_of_motor.py, а скорее способ импорта модуля в main.py. Относительный импорт в one_kind_of_motor.py не удался, поскольку one_kind_of_motor.py импортируется как motors.one_kind_of_motor в другой файл.

Многопакетное решение

Если вы собираетесь запускать только сценарии верхнего уровня (т. е. сценарии непосредственно в name_of_my_app/), вы можете импортировать конфигурацию из one_kind_of_motor.py с помощью

import CONFIG

При запуске python main.py импорт завершается успешно, поскольку name_of_my_app/ находится в sys.path.

(Для этого решения файл __init__.py в name_of_my_app/ не нужен.)

Единое решение

Если вы хотите, чтобы относительный импорт работал, вы можете избежать ошибки, обеспечив, чтобы все файлы, предназначенные для импорта, были вложены в каталог, соседний со сценариями. Например, вы можете организовать свой проект следующим образом:

name_of_my_app/
    main.py
    devices/
        __init__.py
        config.ini
        CONFIG.py
        motors/
            __init__.py
            one_kind_of_motor.py
        measurement_devices/
            __init__.py
            one_kind_of_measurement_device.py

Затем вы можете соответствующим образом настроить импорт в main.py. С помощью этой структуры вы можете импортировать конфигурацию из one_kind_of_motor.py с помощью

from .. import CONFIG

При запуске python main.pyone_kind_of_motor.py теперь соответствует модулю devices.motors.one_kind_of_motor, а оператор относительного импорта эквивалентен

from devices import CONFIG

Запуск файла

Проблема

Предполагая, что вы используете one_kind_of_motor.py как скрипт:

  • Запуск python one_kind_of_motor.py добавляет родительский каталог one_kind_of_motor.py в sys.path (т. е. name_of_my_app/motors/ будет искать пакеты и модули).
  • В модуле __main__ (соответствует файлу one_kind_of_motor.py) from .. import CONFIG указывает, что система импорта должна подняться на два уровня вверх в иерархии пакетов. Первый . соответствует родителю __main__. В этом случае система импорта не может найти родительский пакет __main__, поскольку __main__.__spec__ — это None. Такое поведение описано в документации Python:

Обратите внимание, что __main__.__spec__ всегда является None в последнем случае, даже если технически файл можно импортировать напрямую как модуль. Используйте переключатель -m, если в __main__ требуются действительные метаданные модуля.

Решение

Чтобы избежать ошибки, вы можете вызвать команду python с ключом -m и указать имя модуля. В частности, настройте операторы импорта и структуру проекта, как описано в одном из решений выше, и убедитесь, что вы находитесь в каталоге name_of_my_app/. Затем, для случая с несколькими пакетами, запустите

python -m motors.one_kind_of_motor

или, в случае с одним пакетом, запустите

python -m devices.motors.one_kind_of_motor

Это позволит гарантировать, что модуль __main__ (соответствующий файлу one_kind_of_motor.py) содержит действительные метаданные модуля (как описано здесь).

Однако в документации упоминается важное предостережение, связанное с этим подходом:

Также обратите внимание, что даже если __main__ соответствует импортируемому модулю и __main__.__spec__ установлено соответствующим образом, они все равно считаются отдельными модулями.

Следствием этого является то, что если one_kind_of_motor.py является частью цикла импорта (т. е. запуск one_kind_of_motor.py косвенно вызывает импорт самого себя), в итоге вы получите две копии всех классов и функций, определенных в one_kind_of_motor.py. Если цикл импорта необходим (по возможности избегайте его), вы можете определить функцию точки входа в one_kind_of_motor.py и вызывать эту функцию из main.py или другого скрипта верхнего уровня. То есть в one_kind_of_motor.py создайте функцию, содержащую нужный код точки входа:

# Name this whatever makes sense
def main():
    ...

Затем где-нибудь в main.py или в другом скрипте в name_of_my_app/ вызовите эту функцию:

import devices.motors.one_kind_of_motor
...
# Call the entry point
# Alternatively, call this only for certain script arguments or other conditions
devices.motors.one_kind_of_motor.main()

Теперь вместо запуска one_kind_of_motor.py напрямую запустите скрипт верхнего уровня, содержащий вызов импорта и точки входа.

Спасибо за подробный ответ @ImpecableChicken. Это понятно, но у меня почему-то это не работает. Должно быть, я совершаю ошибку, но я не знаю, что делаю не так. Я создал репозиторий, чтобы показать свою проблему. Когда я выполняю импорт из one_kind_of_motor.py с помощью from .. import CONFIG, я получаю: ImportError: попытка относительного импорта без известного родительского пакета. Я использую Windows, если это имеет значение. Не могли бы вы проверить мой репозиторий? Я обновлю свой пост соответствующим образом, если вы найдете что-то актуальное.

Omnistic 19.07.2024 12:20

@Omnistic, я никогда не думал, что ты можешь выполнять one_kind_of_motor.py напрямую. Я обновил свой ответ, включив в него решение и для этого случая.

ImpeccableChicken 20.07.2024 21:09

Спасибо @ImpecableChicken за ваш ответ. В терминале VS Code, если я наберу python -m devices.motors.one_kind_of_motor, это сработает и ошибок больше не будет. Однако я все еще обрабатываю всю эту информацию. Люди, которые программируют на Python с помощью VS Code, обычно запускают свои сценарии с помощью терминала вместо использования «Выполнить..Выполнить без отладки»?

Omnistic 22.07.2024 11:39

@Omnistic, я никогда не использовал VS Code, но кажется этот ответ может помочь вам запустить любой файл в качестве модуля (хотя для этого требуется установка расширения).

ImpeccableChicken 22.07.2024 19:01

Спасибо @ImpeccableChicken. Я не знаю, как я продвинулся так далеко в Python, даже не используя терминал, не говоря уже о «-m». Очень признателен.

Omnistic 24.07.2024 08:36

Я не знаком с кодом VS, но в конфигурации запуска необходимо использовать ключ -m. То, что делает VS, вероятно, python.exe main.py из папки name_of_my_app. Вместо этого вам нужно бежать python -m name_of_my_app.main # note no .py. Это сообщает Python, что в пакете name_of_my_app запустите модуль main.py как скрипт. Родительский пакет, который ищет Python, — это именно имя_my_app (должно содержать только символы, которые могут присутствовать в имени пакета Python).

Спасибо @Mr_and_Mrs_D за ваш ответ. Я понял, что это -m важно и узнаю об этом больше.

Omnistic 22.07.2024 11:40

Другие вопросы по теме

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

Как изменить класс элементов в списке?
Сгенерируйте n случайных 2D-точек в допустимом регионе
Найдите, встречается ли какое-либо число в отсортированном массиве более n/4 раз
Компромисс между качеством и размером файла: как сохранить очень детальное изображение в файл разумного размера (<1 МБ)?
Сгенерировать ограниченный путь, используя полином кубической или пятой степени
Способ конвертировать латексный файл в PDF только с помощью Python? (без необходимости что-либо устанавливать локально)
Индексация Numba по типу записи (структурированный массив в numpy)
Почему добавление массива float32 и скаляра float64 является массивом float32 в Numpy?
При использовании Google Colab «наборы данных» пакета Python просто исчезли из каталога «сайт-пакеты» виртуального окружения
Противоречивая ошибка при использовании Polars read_csv() с несколькими файлами для csv.gz