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. Он использует библиотеку Python setuptools, и базовый файл выглядит примерно так:

Основные три варга 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 в kwarg exclude:
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.

Код, используемый для создания примеров в этой статье, можно найти и клонировать здесь: