Модули
Что это такое
Модуль – кусок кода, который можно использовать в другом коде. В самом простом случае это файл. В любом проекте функциональность разбивается на куски, каждый кусок селится в свой модуль.
Всё, что устанавливается с помощью 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'))
А можно так:
```python
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
Делать так незаконно: это неочевидно, затрудняет отладку и вредит читаемости. Не надо так.