Модули

Что это такое

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

Всё, что устанавливается с помощью pip, представляет собой модули. Модули иерархические: ты можешь импортировать модуль markdown и пользоваться им, не зная, что внутри он импортирует ещё десяток других модулей: Питон сам всё разрулит.

Как этим пользоваться

Имя модуля совпадает с именем файла и должно быть нормальным именем переменной в Питоне: например, не содержать знаков минуса.

Предположим, что есть папка 3_bars, в ней файл data_loaders.py с таким содержанием:

import csv
import json


def load_from_json(filepath):
    with open(filepath, 'r') as file_handler:
        return json.load(file_handler)


def load_from_csv(filepath):
    with open(filepath, 'r') as file_handler:
        return list(csv.reader(file_handler))

А рядом есть файл bars.py, в котором мы хотим загрузить данные из csv. Вот что в нём можно написать:

from data_loaders import load_from_csv  # импортируем функцию из модуля


print(load_from_csv('bars.csv'))

А можно так:

import data_loaders  # импортируем модуль целиком


print(data_loaders.load_from_csv('bars.csv'))  # используем функцию с указанием модуля

Есть ещё вариант from data_loaders import *, но он вне закона. Забудьте о нём.

Запуск модуля как скрипта

Когда Питон видит import data_loaders, он находит файл data_loaders.py и выполняет его. Реально выполняет: если в нём есть код, он будет выполнен. Даже если это не просто объявления функций, а их вызов. Представим, что когда мы писали код в data_loaders.py, мы его дебажили. Например, так:

import json


def load_from_json(filepath):
    with open(filepath, 'r') as file_handler:
        return json.load(file_handler)


print(load_from_json('test.json'))

Теперь если мы импортируем этот модуль (import data_loaders), девятая строка выполнится, файл загрузится и выведется на экран. А ведь в bars.py это не нужно! Можно этот код удалить, но тогда будет неудобно дорабатывать функцию load_from_json: при изменении надо будет добавлять отладочный принт, а потом удалять.

Вот правильный способ это обойти:

import json


def load_from_json(filepath):
    with open(filepath, 'r') as file_handler:
        return json.load(file_handler)


if __name__ == '__main__':
    print(load_from_json('test.json'))

Иф на девятой строке значит "выполняй меня только если файл запущен напрямую, а не импортирован". Теперь при запуске python data_loaders.py будет выполняться дебажная загрузка кода, а при импорте этого модуля – не будет. То, что надо.

__name__ – одна из переменных магических переменных. Их можно узнать по двойным подчёркиваниям по краям. Такие переменные доступны всегда и Питон запишет нужные значения в них за нас. В __name__ хранится название модуля, из которого был импортирован данный модуль. Если модуль выполняется напрямую, Питон запишет в эту переменную значение __main__ (доки). Хитро, а?

Подводные камни

Главный подводный камень – рекурсивный импорт. Это если мы импортируем data_loaders из bars, а для data_loaders нужен bars. Вот так:

# bars.py
import data_loaders

# data_loaders.py
import bars

Бах! Всё сломается при запуске.

Иногда бывает ещё веселее: когда импорты замыкаются в трёх и более файлах. Типа того:

# bars.py
import data_loaders

# data_loaders.py
import helpers

# helpers.py
import bars

Всё сломается так же, как в примере выше, но ещё и заставит поломать голову при починке.

Чинить такие случаи просто: разбивать код на максимально независимые модули. В примере выше, например, файлу helpers.py зачем-то нужен bars.py. Так быть не должно: в helpers.py должны жить максимально независимые общие функции, которые используются в других файлах. Не наоборот.

Как работает под капотом

Важнее всего знать, как Питон выбирает файлы для импорта. Сначала он ищет подходящие файлы в рабочей директории, рядом с bars.py. Если не находит, то проходит по папкам в sys.path и ищет нужный файл.

Иногда бывает так, что нужный модуль находится вне тех папок, которые обходит Питон. Один из вариантов побороть это – вручную добавить нужный путь в sys.path (это список). Но это на крайний случай, обычно есть более красивые способы. Например, упаковать код в модуль и установить его с помощью pip. Так что тсс, я вам ничего не говорил.

В памяти все загруженные модули хранятся в sys.modules. Иногда встречаются случаи, когда файла нет, а модуль есть. Это не сложно устроить:

# mod.py
import sys
from types import ModuleType


dynamic_module = ModuleType(__name__)
dynamic_module.x = 5

sys.modules['some_weird_module'] = dynamic_module


# script.py
import mod  # тут выполнился код из mod.py
import some_weird_module  # модуль есть, а файла – нет


print(some_weird_module.x)  # 5

Делать так незаконно: это неочевидно, затрудняет отладку и вредит читаемости. Не надо так.