9.1. Теория

Модули и пакеты являются неотъемлемой частью модульного программирования - организации программы как совокупности небольших независимых блоков, структура и поведение которых подчиняются определенным правилам.

Разработка программы как совокупности модулей позволяет:

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

Программный код часто разбивается на несколько файлов, каждый из которых используется отдельно от остальных. Одним из методов написания модульных программ является объектно-ориентированное программирование.

9.1.1. Основные понятия

Модуль (англ. Module) - специальное средство языка программирования, позволяющее объединить вместе данные и функции и использовать их как одну функционально-законченную единицу (например, математический модуль, содержащий тригонометрические и прочие функции, константы \(\pi\), \(\epsilon\) и т.д.).

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

9.1.2. Модули и пакеты в Python

Модуль - отдельный файл с кодом на Python, содержащий функции и данные:

  • имеет расширение *.py (имя файла является именем модуля);
  • может быть импортирован (подключен) (директива import ...);
  • может быть многократно использован.

Пакеты в Python - это способ структуризации модулей. Пакет представляет собой папку, в которой содержатся модули и другие пакеты и обязательный файл __init.py__, отвечающий за инициализацию пакета.

Так, например, пакет xml имеет следующую структуру:

C:\USERS\YURI\APPDATA\LOCAL\PROGRAMS\PYTHON\PYTHON35\LIB\xml
│   __init__.py               Файл инициализации пакета xml
│
├───dom                       Вложенный пакет xml.dom
│       domreg.py
│       expatbuilder.py
│       minicompat.py
│       minidom.py
│       NodeFilter.py
│       pulldom.py
│       xmlbuilder.py
│       __init__.py
│
├───etree                     Вложенный пакет xml.etree
│       cElementTree.py
│       ElementInclude.py
│       ElementPath.py
│       ElementTree.py
│       __init__.py
│
├───parsers                   Вложенный пакет xml.parsers
│       expat.py
│       __init__.py
│
└───sax                       Вложенный пакет xml.sax
        expatreader.py
        handler.py
        saxutils.py
        xmlreader.py
        _exceptions.py
        __init__.py

где каждый модуль (или вложенный пакет) отвечает за свою часть реализации работы с XML-форматом, однако рассматривается как единое целое в виде пакета.

Одна из основных целей использования как модулей, так и пакетов - реализация модели пространства имен, позволяющей логически группировать и в то же время изолировать различные идентификаторы. Например, при наличии глобальной переменной author в модуле A и B не произойдет конфликта, т.к. они находятся в разном пространстве имен: A.author и B.author соответственно.

9.1.2.1. Классификация

Все модули/пакеты в Python можно разделить на 4 категории:

  1. Встроенные (англ. Built-in).

    Модули, встроенные в язык и предоставляющие базовые возможности языка (написаны на языке Си).

    К встроенным относятся как модули общего назначения (например, math или random), так и плаиформозависимые модули (например, модуль winreg, предназначенный для работы с реестром ОС Windows, устанавливается только на соответствующей ОС).

    Список установленных встроенных модулей можно посмотреть следующим образом:

    >>> import sys
    >>>
    >>> print(sys.builtin_module_names)  # Встроенные модули
    ('_ast', '_bisect', '_codecs', '_codecs_cn', '_codecs_hk', '_codecs_iso2022',
    '_codecs_jp', '_codecs_kr', '_codecs_tw', '_collections', '_csv', '_datetime',
    '_functools', '_heapq', '_imp', '_io', '_json', '_locale', '_lsprof', '_md5',
    '_multibytecodec', '_opcode', '_operator', '_pickle', '_random', '_sha1',
    '_sha256', '_sha512', '_signal', '_sre', '_stat', '_string', '_struct',
    '_symtable', '_thread', '_tracemalloc', '_warnings', '_weakref', '_winapi',
    'array', 'atexit', 'audioop', 'binascii', 'builtins', 'cmath', 'errno',
    'faulthandler', 'gc', 'itertools', 'marshal', 'math', 'mmap', 'msvcrt',
    'nt', 'parser', 'sys', 'time', 'winreg', 'xxsubtype', 'zipimport', 'zlib')
    
  2. Стандартная библиотека (англ. Standard Library).

    Модули и пакеты, написанные на Python, предоставляющие расширенные возможности, например, json или os.

  3. Сторонние (англ. 3rd Party).

    Модули и пакеты, которые не входят в дистрибутив Python, и могут быть установлены из каталога пакетов Python (англ. PyPI - the Python Package Index, более 90.000 пакетов) с помощью утилиты pip:

    C:\Users\yuri>pip3 install vk
    Collecting vk
      Downloading vk-2.0.2.tar.gz
    Requirement already satisfied (use --upgrade to upgrade):
      requests<3.0,>=2.8 in c:\users\yuri\appdata\local\programs\python\python35\lib\site-packages (from vk)
    Installing collected packages: vk
      Running setup.py install for vk ... done
    Successfully installed vk-2.0.2
    

    При установке пакета автоматически устанавливаются зависимые пакеты.

  4. Пользовательские (собственные).

    Модули и пакеты, создаваемые разработчиком.

    Примечание

    Создание собственных пакетов не рассматривается в рамках настоящего курса.

В собственной программе рекомендуется выполнять импорт именно в таком порядке: от встроенных до собственных модулей/пакетов.

9.1.2.2. Подключение и использование

Для использования модуля или пакета в коде необходимо его предварительно подключить (импортировать).

import

Импорт модуля или пакета выполняется единожды инструкцией import, располагаемой, как правило, в начале файла.

Выполнить подключение модуля можно несколькими способами:

# Способ №1
# В данном способе обратиться к члену модуля можно с указанием модуля,
# например, module_1.object_1

# Импортирует модуль 'module_1'
import module_1

# Импортирует модули 'module_1', 'module_2',..., 'module_n'
import module_1, module_2, ..., module_n

# Импортирует модуль 'module_1' под псевдонимом 'preferred_name'
import module_1 as preferred_name

# Способ №2
# В данном способе обратиться к члену модуля можно без указания модуля,
# например, object_1

# Импортирует 'object' под псевдонимом 'preferred_name' из модуля 'module_1'
from module_1 import object as preferred_name

# Импортирует объекты 'object_1', ..., 'object_n' из модуля 'module_1'
from module_1 import object_1, object_2, ..., object_n

# Импортирует все объекты из модуля 'module_1'
from module_1 import *

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

# Импортирует пакет 'package_1'
import package_1

# Импортирует модуль 'module_1' из пакета 'package_1'
import package_1.module_1

Различные варианты подключения модуля и пакета приведены в Листингах 9.1.1 (а-г).

Листинг 9.1.1 (а) - Импорт модуля в Python (пример 1) | скачать
import math

# Использование любого элемента модуля math возможно через
# предварительное указание его имени: math.<...>
#
# Это - рекомендуемый способ импортирования модуля

print("sin(30) = {:.1f}".format(math.sin(math.radians(30))))  # sin(30) = 0.5
Листинг 9.1.1 (б) - Импорт модуля в Python (пример 2) | скачать
from math import sin
import math

# Доступ к функции sin() возможен без указания модуля, а
# к функции radians() только с указанием
#
# Данный способ рекомендуется использовать:
# - для повышения читаемости кода;
# - если вероятность конфликта имен с другими участками кода мала.

print("sin(30) = {:.1f}".format(sin(math.radians(30))))  # sin(30) = 0.5
Листинг 9.1.1 (в) - Импорт модуля в Python (пример 3) | скачать
from math import *

# Любую функцию из модуля math можно указывать без ссылки на модуль
#
# Данный способ, хотя и экономит место, НЕ рекомендуется в виду
# высокой вероятности конфликта имен (импортированные таким образом
# члены модуля могут совпасть по имени с другими объектами) и неоднозначности
# поиска идентификатора (откуда импортирован sin?)

print("sin(30) = {:.1f}".format(sin(radians(30))))  # sin(30) = 0.5
Листинг 9.1.1 (г) - Импорт пакета в Python | скачать
import json  # импорт пакета целиком
import os.path  # импорт модуля os.path из пакета os

# Разделитель пути для текущей ОС
print(os.path.sep)  # "\"

Посмотреть содержимое модуля или пакета и справку по нему возможно с помощью функций dir() и :py:func:` help()`:

>>> import math
>>>
>>> dir(math)
['__doc__', '__loader__', '__name__', '__package__', '__spec__', 'acos', 'acosh',
'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'copysign', 'cos', 'cosh', 'degrees',
'e', 'erf', 'erfc', 'exp', 'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp',
'fsum', 'gamma', 'gcd', 'hypot', 'inf', 'isclose', 'isfinite', 'isinf', 'isnan', 'ldexp',
'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'nan', 'pi', 'pow', 'radians', 'sin',
'sinh', 'sqrt', 'tan', 'tanh', 'trunc']
>>>
>>> help(math)
Help on built-in module math:

NAME
    math

DESCRIPTION
    This module is always available.  It provides access to the
    mathematical functions defined by the C standard.

FUNCTIONS
    ...

9.1.2.3. Область поиска

При импорте модуля или пакета Python выполняет его поиск в следующем порядке:

  1. Встроенный модуль?

  2. Файл module.py / module.pyc, или пакет package есть в списке путей переменной sys.path?

    sys.path - список, при инициализации включающий:

    • рабочую директорию скрипта (основного модуля);
    • переменную окружения PYTHONPATH и пути инсталляции Python.

Если модуль не удается найти, возбуждается исключение ImportError.

Примечание

Для проверки содержимого sys.path можно выполнить следующий код:

>>> import sys
>>>
>>> print(sys.path)
['', 'C:\\Users\\yuri\\AppData\\Local\\Programs\\Python\\Python35\\python35.zip',
'C:\\Users\\yuri\\AppData\\Local\\Programs\\Python\\Python35\\DLLs',
'C:\\Users\\yuri\\AppData\\Local\\Programs\\Python\\Python35\\lib',
'C:\\Users\\yuri\\AppData\\Local\\Programs\\Python\\Python35',
'C:\\Users\\yuri\\AppData\\Local\\Programs\\Python\\Python35\\lib\\site-packages',
'C:\\Users\\yuri\\AppData\\Local\\Programs\\Python\\Python35\\lib\\site-packages\\win32',
'C:\\Users\\yuri\\AppData\\Local\\Programs\\Python\\Python35\\lib\\site-packages\\win32\\lib',
'C:\\Users\\yuri\\AppData\\Local\\Programs\\Python\\Python35\\lib\\site-packages\\Pythonwin']

Предупреждение

Рабочая директория расположена в sys.path в начале списка, поэтому не следует называть собственные модули или пакеты аналогично встроенным (например, json) - интерпретатор в первую очередь найдет пользовательский модуль/пакет

9.1.3. Особенности модулей в Python

9.1.3.1. Специальные атрибуты

Каждый модуль имеет специальные и дополнительные атрибуты.

  1. Специальные атрибуты содержат системную информацию о модуле (путь запуска, имя модуля и др.) и доступны всегда. Некоторые из них:

    __name__

    Полное имя модуля.

    Пример: "math" или "os.path".

    __doc__

    Строка документации.

    __file__

    Полный путь к файлу, из которого модуль был создан (загружен).

    Пример: C:\code\task_09_01_02\fibonacci.py.

  2. Дополнительные (необязательные) атрибуты могут содержать справочную информацию об авторе, версии модуля и т.д. и имеют следующие обозначения: __author__, __copyright__, __credits__, __license__, __version__, __maintainer__, __email__, __status__.

9.1.3.2. Кэширование («компиляция») модулей

Python считается интерпретируемым языком, однако имеет гибридную составляющую: при запуске программы происходит компиляция в промежуточный байт-код (бинарный файл с расширением *.pyc) всех импортированных модулей, после чего код выполняется виртуальной машиной (Рисунок 9.1.1).

_images/09_01_01.png

Рисунок 9.1.1 - Процесс выполнения Python-кода (для реализации CPython) [6].

Файл *.pyc создается заново только в случае, если исходный код был изменен (проверяется дата изменения), иначе используется уже найденная «скомпилированная» версия. При этом компилируются только импортируемые модули (так, текущий запускаемый модуль скомпилирован не будет).

На Рисунке 9.1.2 приведена общая схема загрузки модуля.

_images/09_01_02.png

Рисунок 9.1.2 - Процесс выполнения импорта Python с учетом «компиляции» [7].

Особенности *.pyc-файлов:

  • имеют бинарный формат;
  • загружаются быстрее, чем исходные *.py-файлы; при этом скорость выполнения одинакова в обоих случаях;
  • создаются в папке __pycache__ и имеют имя имя_модуля.cpython-35.pyc (для реализации CPython), где 35 - версия Python;
  • «привязаны» к версии интерпретатора (обязательно в пределах основной версии - 2 или 3) и платформе компиляции.

Примечание

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

В то же время, применяется это не часто, т.к. в отличие от языков программирования, компилируемых непосредственно в машинный код (например, Си), для языков, использующих байт-код (Java, C#, Python и др.), высока вероятность полной декомпиляции (воссоздания исходного кода) скомпилированной программы.

9.1.3.3. Собственные модули

Python предоставляет возможность создания собственных модулей.

Примечание

PEP8.

Соглашение рекомендует использовать:

Для создания собственных модулей нет особенных правил - любой файл с расширением *.py является модулем. Пример создания и подключения собственного модуля приведен в Листинге 9.1.2 (а)-(б).

Листинг 9.1.2 (а) - Модуль вычисления чисел Фибоначчи (файл fibonacci.py) | скачать
""" Модуль для работы с числами Фибоначчи.

- список чисел Фибоначчи, не превышающих 'n';
- проверка на вхождение в ряд Фибоначчи;
- ...
"""


def list_le_than(n):
    """Вернуть список чисел Фибоначчи, не превышающих 'n'."""
    assert isinstance(n, int) and n > 0

    a, b = 1, 1
    result = [a]
    while b <= n:
        result.append(b)
        a, b = b, a + b
    return result


def is_in_row(n):
    """Вернуть True, если 'n' - число Фибоначчи."""
    return n in list_le_than(n)
Листинг 9.1.2 (б) - Использование модуля fibonacci (файл main.py) | скачать
import fibonacci

num = 20
print("Числа до {}: {}".format(num, fibonacci.list_le_than(num)))  # [1, 1, 2, 3, 5, 8, 13]
print("{} входит: {}".format(num, fibonacci.is_in_row(num)))  # False

Примечание

Основной модуль программы, как правило, удобно называть main.py.

9.1.3.4. Использование модуля: запуск или импорт

Любой модуль в Python может быть:

  • запущен автономно (как скрипт, например, в командной строке или через IDE);
  • импортирован (через import).

При написании собственного модуля необходимо предполагать, будет он запускаться автономно или использоваться для импорта. При этом следует избегать обособленного кода, который выполнялся бы в обоих вариантах (Листинг 9.1.3 (а)-(б)).

Листинг 9.1.3 (а) - Модуль fibonacci, включающий код, который будет выполнен и при запуске, и при импорте | скачать
""" Модуль для работы с числами Фибоначчи.

- список чисел Фибоначчи, не превышающих 'n';
- проверка на вхождение в ряд Фибоначчи;
- ...
"""


def list_le_than(n):
    """Вернуть список чисел Фибоначчи, не превышающих 'n'."""
    assert isinstance(n, int) and n > 0

    a, b = 1, 1
    result = [a]
    while b <= n:
        result.append(b)
        a, b = b, a + b
    return result


def is_in_row(n):
    """Вернуть True, если 'n' - число Фибоначчи."""
    return n in list_le_than(n)


print(list_le_than(10))  # Этот код выполнится, как при запуске, так и при импорте
Листинг 9.1.3 (б) - Использование модуля fibonacci (файл main.py) | скачать.
import fibonacci  # Здесь будет "неожиданный" вывод на экран: [1, 1, 2, 3, 5, 8]

num = 20
print("Числа до {}: {}".format(num, fibonacci.list_le_than(num)))  # [1, 1, 2, 3, 5, 8, 13]
print("{} входит: {}".format(num, fibonacci.is_in_row(num)))  # False

Чтобы выполнить различный код в зависимости от того, запущен модуль или импортирован, достаточно использовать специальный идентификатор __name__, который содержит:

  • имя модуля, если он был импортирован (например, "fibonacci" или "math");
  • специальное наименование "__main__", если модуль был запущен автономно (Листинг 9.1.4 (а)-(б)).
Листинг 9.1.4 (а) - Модуль вычисления чисел Фибоначчи с проверкой __name__ (файл fibonacci.py) | скачать
""" Модуль для работы с числами Фибоначчи.

- список чисел Фибоначчи, не превышающих 'n';
- проверка на вхождение в ряд Фибоначчи;
- ...
"""


def list_le_than(n):
    """Вернуть список чисел Фибоначчи, не превышающих 'n'."""
    assert isinstance(n, int) and n > 0

    a, b = 1, 1
    result = [a]
    while b <= n:
        result.append(b)
        a, b = b, a + b
    return result


def is_in_row(n):
    """Вернуть True, если 'n' - число Фибоначчи."""
    return n in list_le_than(n)


if __name__ == "__main__":
    print("Данный код выполняется т.к. модуль был запущен автономно (как скрипт) и не будет выполнен при импорте.")
    print("Тест функций:")
    print("- list_le_than(10) =", list_le_than(10))
    print("- is_in_row(10) =", is_in_row(10))
Листинг 9.1.4 (б) - Сравнение выполнения модуля при самостоятельном запуске и при импорте | скачать.
C:\>python fibonacci.py
Данный код выполняется т.к. модуль был запущен автономно (как скрипт) и не будет выполнен при импорте.
Тест функций:
- list_le_than(10) = [1, 1, 2, 3, 5, 8]
- is_in_row(10) = False

C:\>python
Python 3.5.2 (v3.5.2:4def2a2901a5, Jun 25 2016, 22:18:55) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import fibonacci
>>> fibonacci.list_le_than(20)  # Нет вывода как выше
[1, 1, 2, 3, 5, 8, 13]

Как правило, проверка __name__ используется в основном файле программы, а вспомогательные модули избавляются от кода вне функций и/или классов (Листинг 9.1.5 (а)-(б)).

Листинг 9.1.5 (а) - Модуль вычисления чисел Фибоначчи (файл fibonacci.py) | скачать
""" Модуль для работы с числами Фибоначчи.

- список чисел Фибоначчи, не превышающих 'n';
- проверка на вхождение в ряд Фибоначчи;
- ...
"""


def list_le_than(n):
    """Вернуть список чисел Фибоначчи, не превышающих 'n'."""
    assert isinstance(n, int) and n > 0

    a, b = 1, 1
    result = [a]
    while b <= n:
        result.append(b)
        a, b = b, a + b
    return result


def is_in_row(n):
    """Вернуть True, если 'n' - число Фибоначчи."""
    return n in list_le_than(n)
Листинг 9.1.5 (б) - Проверка режима запуска модуля main (файл main.py) | скачать.
import fibonacci

if __name__ == "__main__":
    num = 20
    print("Числа до {}: {}".format(num, fibonacci.list_le_than(num)))  # [1, 1, 2, 3, 5, 8, 13]
    print("{} входит: {}".format(num, fibonacci.is_in_row(num)))  # False
else:
    # else добавлять не нужно, если никакие специальные действия
    #      в случае неавтономного запуска не планируется
    print("Данный модуль не предназначен для импорта.")

[1]Sebesta, W.S Concepts of Programming languages. 10E; ISBN 978-0133943023.
[2]Python - официальный сайт. URL: https://www.python.org/.
[3]Python - FAQ. URL: https://docs.python.org/3/faq/programming.html.
[4]Саммерфилд М. Программирование на Python 3. Подробное руководство. — М.: Символ-Плюс, 2009. — 608 с.: ISBN: 978-5-93286-161-5.
[5]Лучано Рамальо. Python. К вершинам мастерства. — М.: ДМК Пресс , 2016. — 768 с.: ISBN: 978-5-97060-384-0, 978-1-491-94600-8.
[6]Python is compiled language or interpreted language?. URL: http://www.techdarting.com/2014/04/python-compiled-or-interpreted-language.html.
[7]PEP 3147 – PYC Repository Directories. URL: https://www.python.org/dev/peps/pep-3147/.