Пакеты в Python
Введение
Перед изучением этой статьи убедитесь, что вам знакома тема sys.path в Python
По умолчанию основным инструментом для организации программ является модуль.
Обычно модуль это файл с кодом на Python и расширением .py
Один модуль можно подгрузить в другой модуль или в REPL с помощью import.
У модуля как и у всего остального есть представление в виде объекта.
Пакет это такой тип модуля, который может содержать другие модули а также другие пакеты.
Рассмотрим на примере urllib
>>> import urllib
>>> import urllib.request
>>> type(urllib)
<class 'module'>
>>> type(urllib.request)
<class 'module'>
И urllib и urllib.request имеют тип module
При таком импорте вызвать request без указания родительского модуля нельзя.
Рассмотрим другой способ импорта
>>> from urllib import request
>>> request
<module 'urllib.request' from 'C:\\Users\\Andrei\\AppData\\Local\\Programs\\Python\\Python38-32\\lib\\urllib\\request.py'>
Видно, что request это дочерний модуль urllib
__path__
Разницу между urllib и urllib.request можно заметить по отсутствию у request атрибута __path__
>>> urllib.__path__
['C:\\Users\\Andrei\\AppData\\Local\\Programs\\Python\\Python38-32\\lib\\urllib']
Начиная с Python 3.3+ __path__ это список до этого он был просто строкой
>>> urllib.request.__path__
Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: module 'urllib.request' has no attribute '__path__'
urllib это пакет. Пакеты обычно являются директориями, модули это обычно простые файлы.
sys.path
Перед тем как создавать свои пакеты изучите статью
sys.path
Из неё вы узнаете о том, как сделать модуль видимым для Python
с помощью
sys.path.append
или с помощью
PYTHONPATH
Импорт из других модулей
Рассмотрим простейшую ситуацию: два модуля находятся в одной директории
pac |-- __init__.py |-- a.py `-- b.py
# a.py def printer() -> str: return "A"
Чтобы импортировать функцию printer() из a.py в b.py можно применить один из следующих способов
# b.py from a import printer print(printer())
# b.py import a print(a.printer())
python b.py
A
Абсолютный импорт
Ещё один способ импортировать из модуля - указать его вместе с названием пакета. В нашем случае модуль никуда не вложен, поэтому его нужно указать через точку после названия пакета. Названием пакета будет имя директории, в нашем примере это pac.
# b.py from pac.a import printer print(printer())
Для вложенных модулей логика следующая: нужно добавлять все названия вложенных директорий после точки
pac |-- __init__.py |-- a.py `-- nested |-- __init__.py `-- c.py
# c.py from pac.a import printer print(printer())
python c.py
A
a.py импортируется одинаково как из соседнего модуля b.py так и из с.py , который находится в поддиректории nested - это является преимуществом абсолютного импорта.
Если нужно импортировать в обратном направлении - из с.py в a.py нужно указать путь до с.py
# a.py from pac.nested.c import nested_printer print(nested_printer())
# c.py def nested_printer() -> str: return("C")
python a.py
C
Относительный импорт
Оба примера ниже используют относительный импорт. Его можно испльзовать только внутри пакета.
Можно указывать название пакета (подпакета) либо просто указать путь.
from ..module_name import name from ..import name
Преимущества относительного импорта - сокращение кода, возможность менять имя пакета и не менять при этом
код модулей.
Тем не менее стандартной практикой является избегание относительного импорта везде где это
возможно. С помощью современный
IDE
, таких как
PyCharm
можно легко рефакторить код в полуавтоматическом режими. Это делает преимущества относительного
импорта несущественным.
Применим относительный импорт к простейшему примеру
pac |-- __init__.py |-- a.py `-- b.py
# b.py from .a import printer print(printer())
Если запустить такой скрипт предыдущим способом будет ошибка
python b.py
Traceback (most recent call last): File "C:\Users\Andrei\pac\b.py", line 2, in module> from .a import printer ImportError: attempted relative import with no known parent package
Нужно использовать следующий способ запуска
python -m pac.b
A
В случае со вложенными модулями подход такой же
pac |-- __init__.py |-- a.py `-- nested |-- __init__.py `-- c.py
# c.py from ..a import printer print(printer())
python -m pac.nested.c
A
Создать пакет
Первым делом нужно добавить рабочую директорию в PYTHONPATH
export PYTHONPATH=/home/andrei/packages/
Обратите внимание, что в PYTHONPATH добавлена директория в которой содержится пакет, а не сама директория с пакетом.
Внутри этой директории создадим следующую структуру проекта
packages └── sumpac ├── app │ └── double_sum.py └── lib └── regular_sum.py
Скрипт reqular_sum.py складывает два числа
# regular_sum.py def my_sum(a: float, b: float) -> float: return a + b if __name__ == "__main__": my_sum(4, 7)
Скрипт double_sum.py складывает два числа с помощью импортированной из regular_sum.py функции my_sum() и удваивает этот результат
# double_sum from regular_sum import my_sum def double_sum(a: float, b: float) -> float: s = my_sum(a, b) return s * 2 if __name__ == "__main__": print(double_sum(7, 11)) # expected result: 36
Если сходу запустить скрипт double_sum.py будет получено сообщение об ошибке
python double_sum.py
Traceback (most recent call last): File "/home/andrei/packages/sumpac/app/double_sum.py", line 1, in <module> from regular_sum import my_sum ModuleNotFoundError: No module named 'regular_sum'
Нужно изменить импорт указав из какой директории брать функцию. Вложенные директории указываются через точку.
from sumpac.lib.regular_sum import my_sum
Полный код:
# double_sum from sumpac.lib.regular_sum import my_sum def double_sum(a: float, b: float) -> float: s = my_sum(a, b) return s * 2 if __name__ == "__main__": print(double_sum(7, 11)) # expected result: 36
python double_sum.py
36
Пример с os.path.splitext
Подробнее про метод os.path вы можете прочитать в статье «Модуль os в Python»
import os my_file = "demo.custom" filename = os.path.splitext(my_file)[0] extension = os.path.splitext(my_file)[1] print(filename) print(extension)
demo
.custom
__init__.py
В
современном
Python
описаных выше действий достаточно для создания пакета.
Тем не менее рекомендуется в директории, которые входят в состав пакета добавлять файлы
__init__.py
. Они могут быть как пустыми, так и содержащими некоторые настройки.
Почему это полезно:
- Сохранится совместимость с Python ниже 3.3
- Явно будет видно, что подразумевается пакет
- В этих файлах можно задавать настройки, например для __all__
- Некоторые IDE по-прежнему полагаются на наличие/отсутствие файлов __init__.py при подключении модулей из других директорий.
Рассмотрим как можно изменить правила импорта с помощью __init__.py в простейшем примере
pac |-- __init__.py |-- a.py `-- nested |-- __init__.py `-- c.py
Напоминаю, что при пустом __init__.py (речь о том, который в nested) импорт осуществлялся следующим образом
from pac.nested.c import nested_printer
Чтобы избавится от необходимости дописывать после nested .c внесем следующие изменения в __init__.py из директории nested
# nested/__init__.py from .c import nested_printer
Теперь можно не указывать .c при импорте nested_printer
# a.py from pac.nested import nested_printer print(nested_printer())
# c.py def nested_printer() -> str: return("C")
python a.py
C
С __init__.py файлами структура пакета sumpac будет выглядеть так
sumpac ├── app │ ├── double_sum.py │ └── __init__.py ├── __init__.py └── lib ├── __init__.py └── regular_sum.py
Пример: demoreader
Рассмотрим пример применения __init__.py
mkdir demo_reader touch demo_reader/__init__.py python >>> import demo_reader >>> type(demo_reader)
<class 'module'>
>>> demo_reader.__file__
'/home/andrei/demo_reader/__init__.py'
Сделаем так чтобы __init__.py каждый раз информировал нас о том, что пакет импортирован
echo 'print("demo reader is being imported")' >> demo_reader/__init__.py
python >>> import demo_reader
demo reader is being imported
Добавим в пакет demo_reader файл multireader.py
demo_reader ├── __init__.py └── multireader.py
# demo_reader/__init__.py print("demo reader is being imported")
# demo_reader/multireader.py class MultiReader: def __init__(self, filename): self.filename = filename self.f = open(filename, 'rt') def close(self): self.f.close() def read(self): return self.f.read()
python >>> import demo_reader.multireader
demo reader is being imported
>>> r = demo_reader.multireader.MultiReader('demo_reader/__init__.py') >>> r.read()
'# demo_reader/__init__.py\n\nprint("demo reader is being imported")\n'
>>> r.close()
Добавим возможность читать файлы, сжатые, с помощью bz2 и gzip
demo_reader ├── compressed │ ├── bzipped.py │ ├── gzipped.py │ └── __init__.py ├── __init__.py └── multireader.py
# demo_reader/compressed/gzipped.py import gzip import sys opener = gzip.open # Alias for gzip.open # Decompresses during read if __name__ == '__main__': # Use gzip to create compressed file f = gzip.open(sys.argv[1], mode='wt') # Join to space-separaded string f.write(' '.join( sys.argv[2:]) # The data to compress ) f.close()
python -m demo_reader.compressed.gzipped test.gz data compressed with gz
Опция -m говорит о том, что нужно запустить модуль
demo_reader.compressed.gzipped - это имя модуля. Оно должно быть в формате
FQMN - Fully-qualified module name, то есть содержать все нужные директории, разделённые точками.
test.gz - это argv[1] то есть имя файла, в который идёт запись
Всё что идёт после это argv[2:], в данном случае argv[2], argv[3], argv[4] и argv[5]
- то есть данные, которые будут записаны в файл
Аналогичный скрипт для bz2
# demo_reader/compressed/bzipped.py import bz2 import sys opener = bz2.open if __name__ == '__main__': f = bz2.open(sys.argv[1], mode='wt') f.write(' '.join(sys.argv[2:])) f.close()
python -m demo_reader.compressed.bzipped test.bz2 data compressed with bz2
В результате у нас появилось два файла test.gz и test.bz2
Все модули и пакеты можно импортировать
python >>> import demo_reader demo reader is being imported >>> import demo_reader.multireader >>> import demo_reader.compressed >>> import demo_reader.compressed.gzipped >>> import demo_reader.compressed.bzipped
Изменим скрипт multireader.py
чтобы использовать в нём новые модули.
Про метод
get()
читайте в статье
словари в Python
# demo_reader/multireader.py import os from demo_reader.compressed import bzipped, gzipped extension_map = { '.bz2': bzipped.opener, '.gz': gzipped.opener } class MultiReader: def __init__(self, filename): extension = os.path.splitext(filename)[1] opener = extension_map.get(extension, open) self.f = opener(filename, 'rt') def close(self): self.f.close() def read(self): return self.f.read()
В этом примере используется абсолютный импорт
from demo_reader.compressed import bzipped
Можно было использовать такой вариант абсолютного импорта
import demo_reader.compressed.bzipped
Запустим скрипт
python >>> from demo_reader.multireader import MultiReader
demo reader is being imported
>>> r = MultiReader('test.bz2') >>> r.read()
'data compressed with bz2'
>>> r.close() >>> r = MultiReader('test.gz') >>> r.read()
'data compressed with gzip'
>>> r.close() >>> r = MultiReader('demo_reader/__init__.py')
>>> r.read()
'# demo_reader/__init__.py\n\nprint("demo reader is being imported")\n'
>>> r.close()
Пример с относительным импортом
Изменим наш код, чтобы уменьшить повторы и заодно показать оба типа импорта.
Добавим директорию
util
а в ней файлы
__init__.py
и
writer.py
demo_reader ├── compressed │ ├── bzipped.py │ ├── gzipped.py │ └── __init__.py ├── __init__.py ├── multireader.py └── util ├── __init__.py └── writer.py
# demo_reader/util/writer.py import sys def main(opener): f = opener(sys.argv[1], mode='wt') f.write(' '.join(sys.argv[2:])) f.close()
Теперь
bzipped.py
и
gzipped.py
могут пользоваться
writer.py
Для этого
bzipped.py
импортирует
writer.py
через абсолютный путь а
gzipped.py
через отностиельный
# demo_reader/compressed/bzipped.py import bz2 from demo_reader.util import writer opener = bz2.open if __name__ == '__main__': writer.main(opener)
Обратите внимание на то, как вызывается функция main - через точку после названия модуля
# demo_reader/compressed/gzipped.py import gzip from ..util import writer opener = gzip.open if __name__ == '__main__': writer.main(opener)
На примере
demo_reader/compressed/bzipped.py
Рассмотрим как выглядят абсолютные и относительные пути для импорта
Относительный | Абсолютный |
---|---|
from . import name | from demo_reader.compressed import name |
from .. import name | from demo_reader import name |
from ..util import name | from demo_reader.util import name |
from .. import util | from demo_reader import util |
__all__
__all__ это атрибут модуля. Он отвечает за то, что будет импортировано если кто-то захочеть сделать
from module import *
Не рекомендуется использовать такой импорт.
Это ухудшает читаемость кода и усложняет дебаг.
Ваш
IDE
не сможет подсказать ничего про импортируемые объекты.
Если __all__ не задан, то после import * будут импортированы все публичные имена
__all__ должен быть списком строк каждым элементом которого является имя для импорта
Отредактируем файл __init__.py в директории compressed
from demo_reader.compressed.bzipped import opener as bz2_opener from demo_reader.compressed.gzipped import opener as gzip_opener
Без __all__
python >>> from pprint import pprint >>> pprint(locals())
{'__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__doc__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__name__': '__main__', '__package__': None, '__spec__': None, 'pprint': <function pprint at 0x7fe88894f3a0>}
>>> from demo_reader.compressed import *
demo reader is being imported
>>> pprint(locals())
{'__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__doc__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__name__': '__main__', '__package__': None, '__spec__': None, 'bz2_opener': <function open at 0x7fe888921ee0>, 'bzipped': <module 'demo_reader.compressed.bzipped' from '/home/avorotyn/python/lessons/pluralsight/organizing_larger_programs/chapter3/demo_reader/compressed/bzipped.py'>, 'gzip_opener': <function open at 0x7fe8888d34c0>, 'gzipped': <module 'demo_reader.compressed.gzipped' from '/home/avorotyn/python/lessons/pluralsight/organizing_larger_programs/chapter3/demo_reader/compressed/gzipped.py'>, 'pprint': <function pprint at 0x7fe88894f3a0>}
С помощью __all__ можно импортировать только bz2_opener и gzip_opener
# demo_reader/compressed/__init__.py from demo_reader.compressed.bzipped import opener as bz2_opener from demo_reader.compressed.gzipped import opener as gzip_opener __all__ = ['bz2_opener', 'gzip_opener']
python >>> from pprint import pprint >>> pprint(locals())
{'__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__doc__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__name__': '__main__', '__package__': None, '__spec__': None, 'pprint': <function pprint at 0x7fe88894f3a0>}
>>> from demo_reader.compressed import *
demo reader is being imported
>>> pprint(locals())
{'__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__doc__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__name__': '__main__', '__package__': None, '__spec__': None, 'bz2_opener': <function open at 0x7fe888921ee0>, 'gzip_opener': <function open at 0x7fe8888d34c0>, 'pprint': <function pprint at 0x7fe88894f3a0>}
>>> bz2_opener
<function open at 0x7f3db4c0fee0>
>>> gzip_opener
<function open at 0x7f3db4bc34c0>
Полный код
Создание архивов с помощью
python -m demo_reader.compressed.bzipped test.bz2 data compressed with bz2
python -m demo_reader.compressed.gzipped test.gz data compressed with gz
В этом варианте выдаст предупреждение
/home/andrei/.pyenv/versions/3.9.5/lib/python3.9/runpy.py:127: RuntimeWarning: 'demo_reader.compressed.bzipped' found in sys.modules after import of package 'demo_reader.compressed', but prior to execution of 'demo_reader.compressed.bzipped'; this may result in unpredictable behaviour warn(RuntimeWarning(msg))
Архив, тем не менее, будет создан, я пока что обдумываю решение.
Структура
demo_reader ├── compressed │ ├── bzipped.py │ ├── gzipped.py │ └── __init__.py ├── __init__.py ├── multireader.py └── util ├── __init__.py └── writer.py
# demo_reader/compressed/bzipped.py import bz2 from demo_reader.util import writer opener = bz2.open if __name__ == '__main__': writer.main(opener)
# demo_reader/compressed/gzipped.py import gzip from ..util import writer opener = gzip.open if __name__ == '__main__': writer.main(opener)
# demo_reader/compressed/__init__.py from demo_reader.compressed.bzipped import opener as bz2_opener from demo_reader.compressed.gzipped import opener as gzip_opener __all__ = ['bz2_opener', 'gzip_opener']
# demo_reader/__init__.py print("demo reader is being imported")
# demo_reader/multireader.py import os from demo_reader.compressed import bzipped, gzipped extension_map = { '.bz2': bzipped.opener, '.gz': gzipped.opener } class MultiReader: def __init__(self, filename): extension = os.path.splitext(filename)[1] opener = extension_map.get(extension, open) self.f = opener(filename, 'rt') def close(self): self.f.close() def read(self): return self.f.read()
# demo_reader/util/__init__.py
# demo_reader/util/writer.py import sys def main(opener): f = opener(sys.argv[1], mode='wt') f.write(' '.join(sys.argv[2:])) f.close()
python -m demo_reader.compressed.bzipped test.bz2 data compressed with bz2 by Andrei
python >>> from demo_reader.multireader import MultiReader
demo reader is being imported
>>> r = MultiReader('test.bz2') >>> r.read()
'data compressed with bz2 by Andrei'
Пакеты в Python | |
Namespace пакеты в Python | |
Правильная структура пакета | |
setuptools | |
Плагины | |
Python |