Модули и пакеты

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

Модулем в Python называется обычный файл с расширением .py.

В рамках программы можно использовать 3 типа модулей:

  1. Встроенные в стандартную библиотеку Python
  2. Пользовательские, написанные другими пользователями, такие модули можно загрузить c pypi.org через установщик пакетов pip.
    Pypi (Python package index) - это каталог пакетов, написанных сообществом Python, на начало 2020 года там содержиться ~ 229 000 пакетов
  3. Собственные модули.

Установка модуля

В случае использования встроенных или собственных модулей их требуется просто импортировать, но в случае использования сторонних модулей их сначала необходимо установить.
Например в нашет приложении требуется парсить html-страницы и мы хотим установить пользовательский модуль.
Мы можем искать модуль на сайте pypi по ключевым словам или через консольную утилиту pip. Смотрите полный набор возможносте pip в документации. Здесь мы разберем базовые сценарии.

Перед работой в консоли с pip убедитесь что вы используете нужный вам установщик.

! Если в системе установлен Python 2 и Python 3, то для корректной работы в консоли с python 3 нужно будет вызывать команды как python3 и pip3.
pip --version
# pip 20.0.2 from /home/user/.local/lib/python3.6/site-packages/pip (python 3.6)
pip search "Beautiful Soup"

# ...
# soup (0.1.0)                 - A Python package - soup
# xy-soup (1.0.0)              - Get The chicken soup for the soul
# fast-soup (1.1.0)            - BeautifulSoup interface for lxml
# alphabet-soup-lambert (0.1)  - A word search project
# ratsoup (0.2)                - Make soup for the rats
# visaplan.kitchen (1.0)       - A kitchen for (beautiful) soup
# broth (0.8)                  - Convenient Wrapper for Beautiful Soup
# bs4 (0.0.1)                  - Dummy package for Beautiful Soup
# ...
После прочтения описания, посещения офф. страницы модуля мы выбираем модуль "bs4"
Дальше необходимо установить его:
pip install bs4
# Defaulting to user installation because normal site-packages is not writeable
# Processing /home/user/.cache/pip/wheels/19/f5/6d/a97dd4f22376d4472d5f4c76c7646876052ff3166b3cf71050/bs4-0.0.1-py3-none-any.whl
# Requirement already satisfied: beautifulsoup4 in /home/user/.local/lib/python3.6/site-packages (from bs4) (4.9.0)
# Requirement already satisfied: soupsieve>1.2 in /home/user/.local/lib/python3.6/site-packages (from beautifulsoup4->bs4) (2.0)
# Installing collected packages: bs4
# Successfully installed bs4-0.0.1
Обратите внимание на строчки лога, проверяется список зависимостей пакета. Теперь мы можем убедиться что пакет установлен используя команду show:
pip show bs4
# Name: bs4
# Version: 0.0.1
# Summary: Screen-scraping library
# Home-page: https://pypi.python.org/pypi/beautifulsoup4
# Author: Leonard Richardson
# Author-email: leonardr@segfault.org
# License: MIT
# Location: /home/user/.local/lib/python3.6/site-packages
# Requires: beautifulsoup4
# Required-by:
Удалить пакет мы можем используя команду uninstall:
pip uninstall bs4
# Found existing installation: bs4 0.0.1
# Uninstalling bs4-0.0.1:
#   Would remove:
#     /home/user/.local/lib/python3.6/site-packages/bs4-0.0.1.dist-info/*
# Proceed (y/n)? y
#   Successfully uninstalled bs4-0.0.1
Можно установить желаемую или конкретную версию пакета, используйте правила:

  1. pip install <имя пакета>==<версия> - конкретная версия.
  2. pip install '<имя пакета>><версия>' # требуется пакет версии больше чем указнная

Для второго случая допустимо использовать знаки сравнения <, <=, >, >=, !=
pip install 'bs4>0.0.0'
# Установит последнюю версию старше 0.0.0
pip install bs4==0.0.1
# Установит конкретную версию 0.0.1
Фиксирование списка зависимостей
Если для работы программы требуются сторонние модули, то при переносе программы на другой компютер эта информация будет утерена, программы могут использовать десятки или сотни пакетов в качестве зависимостей и потеря этой информации является настоящей катастрофой.
Для фиксирования списка зависимостей используется файл requirements.txt (имя файла является внегласным соглашением).

Просмотрим список установленных сторонних пакетов:
pip list
# Package     Version   
# ----------- ----------
# ...
# pyparsing   2.4.7     
# bs4         0.0.1
# setuptools  46.1.3    
# six         1.14.0    
# urllib3     1.25.9  
# ...
Зафиксируем список в файл requirements.txt
pip install freeze
pip freeze > requirements.txt
# Содержимое файла теперь содержит список сторонних пакетов и их версии
# Теперь при переносе программы на другой компьютер можно установить все зафиксированные пакеты командой:
pip install -r requirements.txt
Этот способ работы с зависимостями является стандартным, но уже устаревшим. При таком подходе существует проблема - при каждом изменении зависимостей необходимо вручную обновлять файл requirements.txt, иногда установка одного пакета обновляет версии уже установленных и за этим очень не удобно следить.

Используйте утилиту Pipenv для решения этой проблемы.
Загрузка модуля
Нужный модуль установлен и теперь можно его импортировать в программу, но по какому принципу python импортирует модули, давайте подробнее рассмотрим этот аспект. При импорте модулей python ищет их в порядке определенном переменной sys.path
import sys

for p in sys.path:
    print(p)
# Вывод:    
# 1) 
#    /usr/lib/python36.zip
# 3) /usr/lib/python3.6
#    /usr/lib/python3.6/lib-dynload
# 5) /home/user/.local/lib/python3.6/site-packages
#    /usr/local/lib/python3.6/dist-packages
#    /usr/lib/python3/dist-packages
Рассмотрим основные каталоги:

1. Пустая строка - указывает на каталог, содержащий запущенную программу
3. Стандартные модули Python
5. Пакеты установленные в учетную запись пользователя


Т.е модуль с указанным именем будет импортирован по приоритету: Каталог скрипта -> Стандартная библиотека -> Каталог пакетов в учетной записи пользователя

Узнать в какой из каталогов установлен модуль можно командой pip show:
pip show bs4
# Name: bs4
# Version: 0.0.1
# Summary: Screen-scraping library
# Home-page: https://pypi.python.org/pypi/beautifulsoup4
# Author: Leonard Richardson
# Author-email: leonardr@segfault.org
# License: MIT
# Вот здесь --> Location: /home/user/.local/lib/python3.6/site-packages
# Requires: beautifulsoup4
# Required-by:
Таким образом все устанавливаемые нами пакеты будут размещены в директории пакетов на уровне пользователя, при работе с разными программами, у которых разные зависимости это создает неудобства, т.к. разные программы могут требовать разные версии пакетов. Для решения этой проблемы в Python предусмотенна возможность создавать виртуальные окружения, добавляя еще один уровень - четвертый. Таким образом каждая программа может иместь собственное виртуальное окружение и загружать пакеты оттуда. Эту тему рассмотрим отдельно.
Импорт модуля
Модуль необходимо импортировать для того чтобы его код можно было использовать. Существует несколько рекомендаций импортирования:

  1. Порядок импорта: стандартные, установленные, собственные модули
  2. В алфавитном порядке
  3. В начале файла после строки hasbang и строки документации модуля
Подключить модуль можно с помощью инструкции import. После ключевого слова import указывается название модуля. Одной инструкцией можно подключить несколько модулей, хотя этого не рекомендуется делать, так как это снижает читаемость кода. После импортирования модуля его название становится переменной, через которую можно получить доступ к атрибутам модуля.
import os               # Импорт всего модуля
import os, sys          # Импорт нескольких модулей
import os as os_tools   # Присвоение импортируемому пакету другого имени (псевдонима) в рамках программы, помогает избежать конфликтов имен
import os.path as path  # Если у модуля есть атрибуты, то можно импортировать нужный атрибут
Каждый модуль, как и все в Python является объектом, все публичное содержимое модуля: функции, константы, коллекции, являются атрибутами этого модуля.
Подключить определенные атрибуты модуля можно с помощью инструкции from. Она имеет несколько форматов:
from os import path                # Импортирование атрибута модуля
from os import path as path_tools  # Импортирвоание атрибута модуля с присвоением другого имени (псевдонима)
from os import path, environ       # Импортирование нескольких атрибутов
from os import path as path_tools, environ as env  # Импортирование нескольких атрибутов с псеводнимами
from os import *                   # Импортировать все публичные атрибуты модуля (те что не начинаются с "_")
from os.path import join           # Импортирование вложенного атрибута модуля
Если импорт невозможно выполнить - будет поднято исключение ImportError. Если запрашиваемая часть модуля не будет найдена, то будет поднято исключение AttributeError.
Создание собственного модуля
Создадим простой проект со следующей структурой папок:
# Структура проекта:
# ------- consts.py
# ------- main.py

# ------- Содержимое consts.py --------- 
START_HEALTH = 10
START_DEFENCE = 0

# ------- Содержимое main.py ---------
# -*- coding: utf-8 -*-
import conf


class Boy(object):

    def __init__(self, name: str):
        self.name = name
        self.health = consts.START_HEALTH
        self.defence = consts.START_DEFENCE

    def __str__(self):
        return self.name

if __name__ == '__main__':
    boy = Boy(name='Archer')
    print(boy)
  • consts.py - представляет из мебя модуль, хранящий константы проекта
  • main.py - представляет из себя программу, которую можно запускать из консоли благодаря блоку "__name__ == '__main__'"
Модуль consts.py импортируется по относительному пути. Конструктор класса "Boy" использует его константы "START_HEALTH" и "START_DEFENCE" при создании объектов.
Пакеты
Пакет - это каталог содержащий файл __init__.py (возможно пустой). Обычно пакет содержит набор модулей или других пакетов, объединенных по общему признаку. Имея знания о классах и объектах в python мы понимаем что импортированны модуль это тоже объект, а файл __init__.py, по аналогии с классами - это своего рода "конструктор" пакета. Когда вы импортируете пакет - содержимое файла __init__.py выполнится перед созданием объекта пакета. Если вам нужно сделать что-то перед импортом модуля - делайте это в __init__.py

Рассмотрим пример на базе предыдущего. Допустим в нашем проекте потребовалось вести логирование некотрых дейстивий, например записывать в лог информацию о каждом созданном объекте boy, доработаем код:

  • Подключим стандартный модуль logging
  • Создадим пакет utils и поместим туда файл consts.py
  • Создадим в пакете utils файл conf.py для хранения настроек логгера
# Структура проекта:
# ------- utils/
# ------- ------- __init__.py
# ------- ------- consts.py
# ------- ------- conf.py
# ------- main.py

# ------- Содержимое consts.py --------- 
START_HEALTH = 10
START_DEFENCE = 0

# ------- Содержимое conf.py --------- 
import os

PROJECT_DIR = os.path.dirname(os.path.dirname(__file__))

LOG_DIR = os.path.join(PROJECT_DIR, 'logs') # PROJECT_DIR/logs/
LOG_PATH = os.path.join(LOG_DIR, 'main.log') # PROJECT_DIR/logs/main.log
LOG_FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'

DEBUG = True

# ------- Содержимое main.py --------- 
# -*- coding: utf-8 -*-
import logging
from utils import conf, consts


logging.basicConfig(
    filename=conf.LOG_PATH,
    format=conf.LOG_FORMAT,
    level=logging.DEBUG if conf.DEBUG else logging.INFO,
)

class Boy(object):

    def __init__(self, name: str):
        self.name = name
        self.health = consts.START_HEALTH
        self.defence = consts.START_DEFENCE

        self.logger = logging.getLogger('boy')
        self.logger.info(f'Инициализация нового объекта: "{name}"')

    def __str__(self):
        return self.name


if __name__ == '__main__':
    boy = Boy(name='Archer')
    father = Boy(name='Berserker')
Содержимое файла conf.py:

  • PROJECT_DIR - вычисляет корневую директрию проекта относительно __file__, т.е относительно файла conf.py выше на два уровня. Все дальнейшие директории проекта будем считать отсносительно PROJECT_DIR - такой подход не привязывает код к конкретной директории на компьютере, а делает его свобобнопортируемым.
  • LOG_DIR - это путь до каталога где должен храниться файл лога.
  • LOG_PATH - это путь до файла лога.
В конструкторе класса "Boy" мы добавили инициализацию логера и запись в лог.

Давайте запустим программу main.py - получаем ошибку:
FileNotFoundError: [Errno 2] No such file or directory: '/home/user/project/logs/main.log'
Дело в том что логгер пытается создать файл лога, но каталог logs не существует. Строго говоря папка с логами не должна входить в репозиторий проекта и это значит что каждый раз перенося программу на новый компьютер вы будете получать эту ошибку и придется вручную создавать каталог logs. Но этого можно избежать если инициализировать создание каталога перед тем он потребуется:
# Структура проекта:
# ------- utils/
# ------- ------- __init__.py
# ------- ------- consts.py
# ------- ------- conf.py
# ------- main.py

# ------- Содержимое файла __init__.py ---------

import os
from utils import conf

os.makedirs(conf.LOG_DIR, exist_ok=True)
Теперь каждый раз при импорте модуля utils или его атрибутов будет выполняться код из __init__.py и каталог logs будет создаваться автоматически, если его не существует.

Запустим программу main.py еще раз и увидем что каталог logs создался автоматически, logger создал файл лога и записал в него 2 записи:
# Структура проекта:
# ------- logs/
# ------- ------- main.log
# ------- utils/
# ------- ------- __init__.py
# ------- ------- consts.py
# ------- ------- conf.py
# ------- main.py

# ------- Содержимое файла main.log ---------
2020-04-23 13:47:06,978 - boy - INFO - Инициализация нового объекта: "Archer"
2020-04-23 13:47:06,978 - boy - INFO - Инициализация нового объекта: "Berserker"