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.
Теперь, когда вы запускаете ранее ошибочный импорт, вы не должны получать никаких ошибок…

Комментарии
- Внимание:Если вы установили пакет статически (без добавления команды
-ein 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.
Код, используемый для создания примеров в этой статье, можно найти и клонировать здесь: