Модули и пакеты в Python. Глубокое погружение

Автор: | 17.05.2018

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

Тема модулей и пакетов уже обсуждалась нами в 13-ом уроке проекта “Python.Уроки”. Основное внимание там было уделено различным способам импортирования модулей, и использованию хранимых в них функций, классов и переменных. Часть информации в этой статье может повторять уже изложенный материал, это сделано для удобства чтения, чтобы не приходилось переключаться между разными источниками.

Введение

В книге “Совершенный Код” Стив Макконнелл формулирует главный технический императив программирования – это управление сложностью. Основная суть, которого заключается в том, что на каждом этапе разработки ПО мы должны прикладывать максимум усилий для того чтобы сложность нашего проекта не “вышла из берегов”. Показателем этого является возможность одновременно удержать в голове основные компоненты проекта на всех уровнях абстракции. В моделировании систем (да и не только там) выделят такой инструмент как декомпозиция – разделение целого на части, этот принцип является одним из наиболее часто используемых способов работать со сложностью. Декомпозицию можно делать на логическом и на физическом уровне. Для реализации последней цели (декомпозиция на физическом уровне) в программном проекте на Python могут служить модули и пакеты.

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

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

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

Быстрый старт

Перейдем к практике. Если вы знакомы с модулями, то можете пропустить этот раздел. Сейчас мы выполним несколько примеров, которые позволят нам получить интуитивное понимание этой темы.

Модули

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

simplemath.py

def add(a, b):
   return a + b

def sub(a, b):
   return a - b

def mul(a, b):
   return a * b

def div(a, b):
   return a / b

Создадим ещё один модуль worker.py, который будет использовать функции из simplemath.py. Если мы хотим импортировать все функции, то оператор import для нас отлично подойдет. Это будет выглядеть так.

worker.py

import simplemath

print("\"import\" sample")
print(simplemath.add(1, 2))
print(simplemath.sub(1, 2))
print(simplemath.mul(1, 2))
print(simplemath.div(1, 2))

Получим следующий результат.

"import" sample
3
-1
2
0.5

Если же нам нужна только функция сложения, то в таком случае лучше воспользоваться оператором from.

worker_from.py

from simplemath import add

print("\"from\" sample")
print(add(1, 2))
print(sub(1, 2))

Результат выполнения в этом случае будет такой.

"from" sample
3
Traceback (most recent call last):
  File "C:/worker_from.py", line 5, in <module>
    print(sub(1, 2))
NameError: name 'sub' is not defined

Заметьте, что теперь для вызова функции add() нет необходимости указывать, что она находится в модуле simplemath. В первом случае мы ее вызывали так simplemath.add(1, 2), теперь достаточно сделать так: add(1, 2). Вызов функции sub(1, 2) завершился неудачей, т.к. мы его не импортировали.

Пакеты

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

worker_pack.py

import mathpack.simplemath

print("\"import\" sample")
print(mathpack.simplemath.add(1, 2))
print(mathpack.simplemath.sub(1, 2))
print(mathpack.simplemath.mul(1, 2))
print(mathpack.simplemath.div(1, 2))

Это может быть не очень удобным. Можно модифицировать импорт следующим образом:

from mathpack import simplemath

Тогда в остальном коде ничего не придется менять.

worker_pack_from.py

from mathpack import simplemath

print("\"import\" sample")
print(simplemath.add(1, 2))
print(simplemath.sub(1, 2))
print(simplemath.mul(1, 2))
print(simplemath.div(1, 2))

На этом закончим наш “Быстрый старт” и начнем более детально разбирать темы модулей и пакетов.

Работа импорта – взгляд изнутри

Работа процедуры импорта модуля включает три шага: поиск модуля, компиляция и запуск. Начнем с шага – поиск модуля. Поиска модуля, указанного в импорте, интерпретатор Python, выполняет последовательно в ряде директорий, список которых определен в рамках Module Search Path. Этот список выглядит так:

  • домашняя директория;
  • директории, указанные в переменной окружения PYTHONPATH;
  • директории Standard library;
  • пути прописанные в .pth файлах;
  • пакеты сторонних разработчиков.

Домашняя директория – это место, откуда выполняется скрипт. Например, если файлы simplemath.py и worker.py из раздела “Быстрый старт” лежат в директории J:\work, то при запуске скрипта worker.py:

>python j:\work\worker.py

В качестве домашней директории будет выступать  J:\work.

Переменная окружения PYTHONPATH задается также как любая другая переменная окружения в используемой вами операционной системе, в ней перечисляются пути, по которым может находиться модуль, указанный в импорте. Например, если модуль simplemath.py, перенести в каталог J:\work\sm, то попытка запустить worker.py завершится неудачей, т.к. не будет найден модуль simplemath.py. После добавления пути J:\work\sm в PYTHONPATH все будет ОК, только не забудьте перезапустить пользовательский сеанс.

При установке Python на вашем компьютере вместе с интерпретатором установится и  стандартная библиотека (Standard library), вот в ней и будет осуществляться поиск модуля, если он не будет найден в рабочей директории и по путям из переменной окружения PYTHONPATH.

Следующее место, где будет искать модуль интерпретатор Python, если не найдет его в стандартной библиотеке – эти пути, прописанные в файлах с расширением .pth. Это обычные текстовые файлы, в которых указываются пути, по которым необходимо производить поиск модулей (каждый путь должен начинаться с новой строки). Данные файлы следует располагать в каталоге Python или в <python-dir>/lib/site-python. В рамках нашего примера мы удалим в своей системе переменную окружения PYTHONPATH, созданную чуть раньше (если у вас она используется, то можно убрать из нее добавленный нами путь J:\work\sm). После этого создадим в директории C:\Python35-32 файл workerpath.pth со следующим содержимым.

workerpath.pth

j:\work\sm

Этого будет достаточно, чтобы скрипт worker.py запустился удачно.

Ну и последняя на очереди директория, в которой будет осуществляться поиск импортированного модуля – это lib/site-packages. В ней, как правило, располагаются пакеты, от сторонних разработчиков, устанавливаемые средствами Python.

После того, как модуль был найден, производится его компиляция в байт-код, если это необходимо. Это делается в том случае, если байт-код более старый по сравнению с файлом с исходным кодом (или запускается в другом интерпретаторе). В Python 3 после компиляции создается каталог с именем __pycache__, в нем располагаются файлы с расширением .pyc, в них содержится байт-код.

После этого следует процедура запуска модуля, которая заключается в последовательном выполнении строк кода в соответствующем модуле. Важно помнить, что модуль импортируется только один раз, при этом, если вы еще раз выполните процедуру импорта, то модуль загружен не будет. Перезагрузить модуль можно с помощью команды imp.reload.

Модули

Создание и импортирование модулей

Как мы уже говорили ранее модули – это файлы с расширением .py. Если мы импортируем модуль с помощью import, то получаем доступ к глобальным переменным модуля и его функциями.

Например, импортируем модуль с именем module.

import module

Пусть в module содержится глобальная переменная value и функция calc_value(), тогда доступ к ним мы можем получить так.

module.value
module.calc_value()

Такая работа напоминает взаимодействие с объектами классов, когда мы получаем доступа к атрибутам и методам объекта. Имена модулей должны выбираться также как имена переменных, иначе их нельзя будет импортировать. Так, если вы назовете модуль while.py, то импортировать его будет нельзя.

Далее, кратко пройдемся по тому, как можно импортировать модули. Для примера будем использовать вышесозданный simplemath.py.

Первый способ импорта модуля

import simplemath

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

simplemath.add(1, 2)

Второй способ импорта модуля

from simplemath import add

В этом случае функцию add() можно вызывать напрямую, без указания имени модуля. При этом остальные функции из simplemath, такие как sub(), mul() и div() будут недоступны.

Третий способ

from simplemath import *

Такой подход позволяет импортировать все доступные элементы модуля (переменные, функции, классы), для работы с ними не требуется использовать имя модуля.

Что ещё нужно знать про импорт модулей?

Следует помнить о том, что import и from – это исполняемые выражения, и выполняются они в процессе исполнения скрипта, а не на этапе компиляции. Это позволяет гибко подходить к процессу импортирования: использовать условные выражения и т.п.

В нашем распоряжении есть модуль simplemath.py. Создадим ещё один модуль с иметем advmath.py.

advmath.py

def sqrt(value):
    return value**0.5

def pow(value, magn):
    return value**magn

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

selectimport.py

val = int(input("simple math lib [1], adv math lib [2]: "))
if val==1:
    import simplemath
    print(simplemath.add(1, 2))
elif val==2:
    import advmath
    print(advmath.sqrt(4))
else:
    print("Type only 1 or 2")

Теперь обсудим вопрос модификации данных в импортируемых модулях. Если вы импортируете модуль через оператор import, то в пространстве имен вашей программы появляется объект-модуль с соответствующим именем, аргументы которого – это переменные в модуле.

Создадим модуль vars.py.

vars.py

value = 1
array = [1, 2, 3]

def set_value(new_value):
    value = new_value

def get_value():
    return value

В этом модуле содержатся две переменные (value и array) и две функции (get_value() и set_value()), позволяющие модифицировать и получать значение переменной value.

Если мы импортируем данный модуль с помощью import, то получим следующие возможности.

Доступ к переменным осуществляется через соответствующие атрибуты объекта-модуля.

>>> import vars
>>> vars.value
1
>>> vars.array
[1, 2, 3]

Вывоз функций организован как вызов методов объекта-модуля.

>>> vars.get_value()
1
>>> vars.set_value(5)
>>> vars.get_value()
5

При этом мы можем менять значение переменных из модуля vars напрямую, используя атрибуты.

>>> vars.value = 7
>>> vars.value
7
>>> vars.array[2] = 17
>>> vars.array
[1, 2, 17]

Если же мы импортируем модуль vars через from, то работа с его элементами будет отличаться от рассмотренного выше сценария. Переменные value и array будут скопированы в текущую область видимости. Это приведет к ряду последствий.

Для доступа к переменных из модуля vars теперь не нужно указывать имя модуля.

>>> from vars import *
>>> value
1
>>> array
[1, 2, 3]

Но при этом переменная value – это уже новая переменная в текущей области видимости, т.е. она находится вне контекста модуля vars.

Если мы дополнительно импортируем модуль через import, то можно показать, что модификация value в текущем контексте не затронет переменную value в модуле vars. Импортируем модуль vars через импорт и выведем значение value в нем.

>>> import vars
>>> vars.value
1

Модифицируем value в текущем контексте.

>>> value = 9
>>> value
9
>>> vars.value
1

Это изменение не коснулось value из модуля vars. Теперь изменим vars.value.

>>> vars.value = 11
>>> vars.value
11
>>> value
9

Т.е. value и vars.value – это две разные переменные, значения которых никак не связаны друг с другом. Другое дело мутабельные переменные, в нашем примере – это array.

>>> array
[1, 2, 3]
>>> vars.array
[1, 2, 3]

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

>>> array[1] = 23
>>> array
[1, 23, 3]
>>> vars.array
[1, 23, 3]

Как видно, изменение значения переменной array и текущем контексте приводит к изменению этой переменной в контексте модуля vars. Это связано с тем, что элементы этих списков ссылаются на одни и те же объекты. Более подробно о том как устроены типы в Python см. тут (https://devpractice.ru/python-lesson-3-data-model/).

Этот эффект наблюдается и в обратную сторону.

>>> vars.array[0] = 15
>>> vars.array
[15, 23, 3]
>>> array
[15, 23, 3]

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

Перезагрузка модуля

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

Импортируем модуль vars.

>>> import vars
>>> vars.value
1

Поменяем в файле vars.py строку value = 1 на value = 7.

>>> # внесли изменения в vars.py
>>> import vars
>>> vars.value
1

Ничего не поменялось.

Для перезагрузки модуля необходимо воспользоваться функцией reload() из модуля imp.

>>> # внесли изменения в vars.py
>>> import vars
>>> vars.value
1

>>> from imp import reload
>>> reload(vars)
<module 'vars' from 'j:\\work\\vars.py'>
>>> vars.value
7

И напоследок, все что импортируется из модуля можно получить через функцию dir().

>>> import vars
>>> dir(vars)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'array', 'get_value', 'set_value', 'value']

Пакеты

В первом приближении пакеты в Python можно воспринимать как каталоги, в которых лежат модули (на самом деле все сложнее).  Для того, чтобы импортировать модуль из пакета, необходимо в import передать весь путь до модуля с перечислением пакетов, разделяя их точками.

Если перенести модуль vars.py (см. предыдущий раздел) в каталог simple, то импорт будет выглядеть так.

>>> import simple.vars
>>> simple.vars.value
7

При этом для доступа к элементу value необходимо указывать всю иерархию вложения.

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

Если в файле __init__.py есть какой-то код, то он будет выполнен сразу после выполнения процедуры импорта.

Тема пакетов будет развита более детально несколько позднее!!!

На этом пока все! Со временем, если будет появляться интересная информация, и какие-то полезные практики использования модулей и пакетов, эта статья будет расширяться

Поделиться
Share on VK
VK
Tweet about this on Twitter
Twitter
Share on Facebook
Facebook
Share on Google+
Google+

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *


Нажимая на кнопку "Отправить комментарий", я даю согласие обработку персональных данных и принимаю политику конфиденциальности.