Модули и пакеты — это ядро ​​любого крупного проекта. Я покажу несколько советов, связанных с ними, например, как организовать пакеты и создать пространства имен.

Когда я проверяю сложные проекты на Github, я обычно теряюсь среди бесконечного количества папок и исходных файлов. Авторы этих отчётов считают их кастомизацию довольно очевидной для себя, и это вполне понятно; К сожалению, я изо всех сил пытаюсь иметь такое же представление о структурировании различных папок и исходных файлов безрезультатно.

Как насчет демистификации некоторых распространенных рефлексов при работе с пакетами и модулями?

В этом кратком руководстве я потратил время на моделирование проекта со следующей глобальной структурой:

А в деталях древовидную структуру нашего проекта можно представить так:

Нас больше всего будет интересовать содержимое superlibrary:

Dataloader обрабатывает все виды загрузчиков данных.

Learners содержит модели, разделенные по типу обучения.

Preprocessor содержит всевозможные модули препроцессора с Pandas, NumPy и Scikit-Learn.

Tests — это место, где вы централизуете все свои тесты.

Utils обычно содержит множество функций, которые выполняют оптимизацию или действуют как функции стоимости.

Каждая функция — это просто простая функция печати, которая также может служить полной реализацией.

Мы будем проводить хорошие и быстрые эксперименты, чтобы выделить некоторые концепции, и будем давать примеры, чтобы ответить на такие вопросы, как:

Как изготавливаются пакеты?

Как контролировать импортированный пакет?

Как выполнить пакет в качестве основной функции входа?

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

Эксперимент

Как изготавливаются пакеты?

Если бы вы проверили структуру нашего смоделированного проекта, вы бы заметили рассеянные __init__.py на разных уровнях наших папок.

Каждая папка, содержащая такой файл, считается пакетом. Файлы __init__.py предназначены для включения дополнительного кода инициализации, который запускается при обнаружении различных уровней пакета.

Например, поместим себя в папку preprocessor. Мы напишем несколько строк кода, выполняющих импорт.

Запишем в файл preprocessor/__init__.py следующие строки:

Вернемся на уровень выше каталога препроцессора (чтобы мы могли просмотреть каталоги preprocessor, learner, dataloader, tests и utils), откроем интерпретатор Python и сделаем следующее:

>>> from preprocessor import numpy_prep, pd_prep, sk_prep
>>> numpy_prep()
This is the numpy implementation for the preprocessor.
>>> pd_prep()
This is the Pandas implementation for the preprocessor.
>>> sk_prep()
This is the sklearn implementation for the preprocessor.
>>> ...

Важно получить представление о том, что здесь было сделано; файл __init__.py в каталоге preprocessor склеил воедино все необходимые фрагменты функций, которые нам нужно было бы вызывать на более высоком уровне.

Тем самым мы снабдили пакет preprocessor дополнительными логическими возможностями, которые сэкономят ваше время и избавят от более сложных строк импорта.

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

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

Вы бы написали:

from module import * .

Давайте изменим файл preprocessor/__init__.py, добавив свойство new__all__:

__all__ получает ограниченный список атрибутов.

Посмотрите, что произойдет, если мы запустим интерпретатор уровнем выше каталога preprocessor:

>>> from preprocessor import *
>>> numpy_prep()
This is the numpy implementation for the preprocessor.
>>> pd_prep()
This is the Pandas implementation for the preprocessor.
>>> sk_prep()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'sk_prep' is not defined
>>> ...

Функция sk_prep была опущена и не включена в свойство __all__. Это защищает исходный пакет, из которого был загружен sk_prep, от любого непреднамеренного поведения.

Несмотря на то, что использование from module import * обычно не рекомендуется, тем не менее, оно часто используется в модулях, объявляющих большое количество имен.

По умолчанию этот импорт будет экспортировать все имена, которые не начинаются с подчеркивания. Однако если указано __all__, будут экспортированы только те имена, которые указаны явно.

Ничего не экспортируется, если __all__ определяется как пустой список. Если __all__ включает неопределенные имена, при импорте выдается AttributeError.

Как выполнить пакет в качестве основной функции входа?

Вы, несомненно, знакомы с написанием чего-то вроде этого:

If __name__ == "__main__" :
...

Как насчет того, чтобы перейти на другой уровень? Можно ли запустить пакет learner в качестве основного модуля?

Перейдем к папке learner:

Далее мы хотели бы убедиться, что при импорте каждого из трех пакетов мы одновременно импортируем функции, связанные с каждым типом обучения.

Для пакета clustering пишем следующие строки кода в файл clustering/__init__.py:

То же самое для пакета supervised_learning:

И для пакета reinforcement_learning:

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

Мы создаем новый файл, который мы называем __main__.py:

Мы возвращаемся в основной каталог superlibrary, так что мы будем на уровень выше каталога learner и запускаем следующее:

Username@Host current_directory % python learner 
Methods for clustering :
This is the implementation for model1.
This is the implementation for model2.
Methods for reinforcement learning :
This is the implementation for model1.
This is the implementation for model2.
Methods for supervised learning :
This is the implementation for model1.
This is the implementation for model2.

Ура! Атрибут __main__, кажется, делает гораздо больше, чем то, к чему вы привыкли. Он может выйти за рамки простого файла и взять под контроль весь пакет, чтобы вы могли либо импортировать его, либо запустить.

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

Подходя к нашему последнему разделу, предположим, что вы немного изменили содержимое папки utils, чтобы оно выглядело следующим образом:

Итак, у вас есть в каждом subpackage новый модуль с именем modules, у которого нет __init__.py файла инициализации.

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

С технической точки зрения он считается namespaceпакетом.
Оказывается, с его помощью можно добиться интересных вещей.

Вы можете создать некоторое общее пространство имен, содержащееся в каждом из пакетов backend_connectors и custom_functions, чтобы modules действовал как модуль single.

Не достаточно убежден? Напишем что-нибудь в интерпретаторе на уровень выше двух ранее упомянутых пакетов:

>>> import sys
>>> sys.path.extend(['backend_connectors','custom_functions'])
>>> from modules import cloud_connectors, optimizers
>>> cloud_connectors
<module 'modules.cloud_connectors' from 'path_to_current_directory/cloud_connectors.py'>
>>> optimizers
<module 'modules.optimizers' from 'path_to_current_directory/optimizers.py'>
>>> ...

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

Вот совет о том, как Python воспринимает modules при импорте.

>>> import modules
>>> modules.__path__
_NamespacePath(['backend_connectors/modules','custom_functions/modules'])
>>> modules.__file__
>>> modules
<module 'modules' (namespace)>
>>> ...

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

Чтобы полностью использовать все возможности пакетов пространства имен, и особенно в этом случае, вы не должны включать файлы __init__.py ни в один из каталогов modules. Предположим, вы действительно добавили их:

Посмотрите внимательно, что происходит, когда вы пытаетесь объединить каталоги modules’:

>>> import sys
>>> sys.path.extend(['backend_connectors','custom_functions'])
>>> from modules import cloud_connectors, optimizers
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: cannot import name 'optimizers' from 'modules' (path_to_project_directory/superlibrary/utils/backend_connectors/modules/__init__.py)
...

Как и ожидалось, Python не может загрузить ваши функции, если вы рассматриваете обычные пакеты как пакеты пространства имен.

Заключение

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

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

Спасибо за ваше время.