101 о том, как совместно использовать код в проекте Python.
Проблема
У вас есть следующая структура проекта:
├── src ├── common ├── __init__.py └── utils.py ├── config └── requirements.txt ├── project1 ├── __init__.py └── main.py ├── project2 ├── __init__.py └── main.py ├── README.md
Где оба подпроекта <project1>
и <project2>
имеют файл main.py
, в котором используется аналогичный код.
Вы (хотите) сохранить этот общий код в файле utils.py
, который находится в третьем каталоге <common>
.
Чтобы использовать любую функцию, хранящуюся в utils.py
, вы пытаетесь записать следующий оператор импорта в файлы main.py
<project1>
и <project2>
:
from common.utils import <function1>, ...
и вы получите следующую ошибку ModuleNotFoundError:
ModuleNotFoundError: No module named 'common'
Затем вы пытаетесь изменить оператор импорта на:
from .utils import <function1>
or
from ..utils import <function1>
возвращает ошибку ImportError:
ImportError: attempted relative import with no known parent package
Как решить эту проблему?
В этой статье я объясню, почему вы получаете эти ошибки и как избежать этой проблемы, используя файл setup.py
, который создает пакет Python.
Во-первых…
Что такое ошибки?
ModuleNotFoundError: нет модуля с именем «общий».
Эта ошибка возникает, когда вы пытаетесь импортировать пакет/модуль Python, который не установлен в вашей (виртуальной) среде. Если вы запустите команду:
pip list
в вашем терминале (mac) или в командной строке (windows) модуль, который вы пытаетесь установить, не будет в списке, выводимом на экран. Например, запустив команду pip list
в моей виртуальной среде (venv), после получения ModuleNotFoundError
я получаю:
Package Version -------------- ------- pip 21.3.1 setuptools 59.0.1
Это не содержит никаких ссылок на utils.py
внутри папки пакета <common>
.
ImportError: попытка относительного импорта без известного родительского пакета
Относительные ошибки импорта возникают, когда вы пытаетесь импортировать модуль из родительского пакета, но Python имеет другой родительский пакет, хранящийся в __main__
. По сути, вы говорите Python импортировать из пакета, о котором он не знает.
Отличное объяснение того, как Python хранит информацию в своей переменной __main__
, можно найти в [Script vs. Module] StackOverflow answer здесь.
Примечание. Относительный импорт не является питоническим, и его следует по возможности избегать, чтобы избежать путаницы и улучшить читаемость кода.
Решения, отличные от Python
Некоторые возможные непитоновские решения, которые вы, возможно, пробовали…
1. sys.path()
Часто вы можете исправить обе эти ошибки, добавив желаемый путь к родительскому пакету в переменную sys.path
перед импортом с ошибкой. Например, добавив следующие строки в файл main.py
:
Однако в целом я бы рекомендовал избегать такого подхода по следующим причинам:
- Грязный: беспорядок.
- Распространение ошибок: его необходимо правильно закодировать, чтобы избежать ошибок при повторном использовании другими из-за разных расположений папок.
- Дублирование кода: его необходимо добавить в каждый файл .py с ошибками импорта.
2. Проект реструктуризации
Вы можете реструктурировать проект и иметь файл main.py
каждого подпроекта в корне, позволяя импорту следовать иерархической структуре каталогов вниз.
Две ключевые причины, по которым следует избегать принятия этого решения:
- Беспорядок. По мере роста вашего проекта этот подход будет становиться неорганизованным, неясным и беспорядочным.
- Настройка: она не позволяет вам структурировать свой проект читабельным и уникальным способом.
- Именование. Вы не можете называть файлы так же, как и все они находятся в одном каталоге, а это означает, что вам придется придумывать уникальные имена, такие как
main1.py
иmain2.py
.
Решение
setup.py
спешит на помощь…
Хорошее эмпирическое правило заключается в том, что если у вас в проекте много кода, который можно повторно использовать во многих модулях, сделайте его пакетом Python.
Что такое пакет Python?
Папка, содержащая __init__.py
, будет распознана Python как пакет. Папка пакета обычно содержит несколько модулей.
Пакеты, установленные в вашей (виртуальной) среде, можно определить с помощью команды pip list
.
Настройка файла setup.py
- Файл
setup.py
содержит информацию о том, как создать собственный пакет Python. Он использует библиотеку Pythonsetuptools
, и базовый файл выглядит примерно так:
Основные три варга setup()
:
name
— Имя вашего пакета Python.install_requires
— указываетsetup.py
на файлrequirements.txt
, который содержит библиотеки Python, необходимые модулям Python, которые станут частью пакета. Строки 3 и 4 позволяютsetup.py
прочитать в пакете требуемые пакеты Python из указанного места.packages
— Пакеты, которые вы хотитеsetup.py
включить в пользовательский пакет Python. Вы можете использоватьsetuptools.find_packages()
, чтобы найти все пакеты в вашем проекте или ввести их вручную.find_packages()
идентифицирует все папки, содержащие файл__init__.py
. Для примера структуры проекта запускfind_packages()
возвращает следующие пакеты в виде пакетов (поскольку это папки, содержащие__init__.py
)
['common', 'project1', 'project2']
- Мы хотим упаковать код только внутри
<common>
, поэтому добавляемproject1
иproject2
в kwargexclude
:
find_packages(exclude=["project1", "project2"])
Установка пакета
Учитывая настройку проекта в разделе «Проблема» этой статьи, ниже представлена обновленная структура при использовании setup.py
снимка экрана выше.
├── src ├── common ├── __init__.py └── utils.py ├── config └── requirements.txt ├── project1 ├── __init__.py └── main.py ├── project2 ├── __init__.py └── main.py ├── setup.py <--- Added in setup.py to the /src folder ├── README.md
Примечание. Файл setup.py
должен находиться в расположении пакетов, которые используются для сборки пользовательского пакета (в пакете setup()
kwarg).
Чтобы создать пакет, вам нужно перейти в директорию родительской папки файла setup.py
и выполнить следующую команду:
pip install -e ./<root of setup.py dir>
Для примера проекта в этой статье вы должны запустить pip install -e ./src
:
-e
запускает установку пакета в редактируемом режиме (динамически), который автоматически обнаруживает любые изменения, которые вы вносите в код при разработке, избегая необходимости постоянно переустанавливать пакет.
После установки вы можете убедиться, что он был успешно установлен, повторно запустив:
pip list
Теперь вы должны увидеть свой пакет, а также перечисленные в requirements.txt
в перечисленных пакетах:
Package Version Editable project location -------------- ------- ----------------------------- DateTime 4.4 pip 21.3.1 pytz 2022.1 setuptools 59.0.1 simple-package 1.0.0 /Users/danielseal/git_local/Sharing_Code_Example/src zope.interface 5.4.0
Обратите внимание на часть Editable project location
вывода pip list
.
Если вы хотите жестко установить пакет (статически), то есть для стабильной версии, в которую вы не собираетесь вносить никаких изменений, вы можете просто удалить -e
и запустить pip install ./src
:
Эта установка является более явной и возвращает следующий вывод после запуска pip list
:
Package Version -------------- ------- DateTime 4.4 pip 21.3.1 pytz 2022.1 setuptools 59.0.1 simple-package 1.0.0 <--- this is the custom package zope.interface 5.4.0 wheel 0.37.1. <--- see 2nd note below
Удаление -e
также создаст колесо для упаковки в вашем проекте в папке /build
, поскольку вы не находитесь в режиме editable
.
Примечание:
- как статические, так и динамические установки добавят в ваш проект файл
.egg-info
. - перед запуском
pip install ./src
рекомендуется установить пакет колеса с помощьюpip install wheel
, чтобы избежать предупреждения о том, что у вас не установлено колесо.
На этом этапе вы успешно установили пользовательский пакет Python.
Теперь, когда вы запускаете ранее ошибочный импорт, вы не должны получать никаких ошибок…
Комментарии
- Внимание:Если вы установили пакет статически (без добавления команды
-e
in install), вам нужно будет повторно запустить команду установки, добавив--upgrade
или-u
в конце, если вы вносите изменения в код внутри пакета папку, которую вы устанавливаете (<common>
для этого примера):
pip install ./src --upgrade
- Редактируемый. Как уже упоминалось, при установке пакета с помощью
pip install -e ./src
пакет будет установлен в редактируемом режиме. Он не добавит в проект папку/build
- Контроль версий. Если вы внедрили новую функцию или исправление в пакет Python, рекомендуется обновить kwarg версия в
setup()
до содействовать хорошему контролю версий.
Эта лента StackOverflow об обновлении локального пакета Python с помощью pip очень хорошо объясняет использование -u (обновление) и -e (редактируемый).
Setup.py
позволяет вам создать собственный пакет Python, позволяющий легко обмениваться кодом в рамках проекта. Просто структурируйте свои подпроекты и общий код в папки, содержащие __init__.py
, и направьте setup()
своим setup.py
на установку кода, содержащегося в общей папке кода.
Запустите pip install -e ./<root of setup.py dir>
(редактируемый режим) или pip install ./<root of setup.py dir>
, и вы установите пакет, определенный файлом setup.py
.
После завершения ваш проект будет иметь два дополнительных скрытых элемента: папку /build
(без -e
) и файл .egg_info
.
Они содержат метаданные о пользовательском пакете Python, позволяющие обмениваться кодом без ошибок ImportError
или ModuleNotfoundError
.
Код, используемый для создания примеров в этой статье, можно найти и клонировать здесь: