Пакеты в Python

Содержание
Введение
__path__
sys.path
Импорт из других модулей
Абсолютный импорт
Относительный импорт
Создать пакет
Пример с os.path.splitext
__init__.py
Пример: demoreader
Пример с разными импортами
__all__
Полный код
Похожие статьи

Введение

Перед изучением этой статьи убедитесь, что вам знакома тема 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 . Они могут быть как пустыми, так и содержащими некоторые настройки.

Почему это полезно:

Рассмотрим как можно изменить правила импорта с помощью __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

Поиск по сайту

Подпишитесь на Telegram канал @aofeed чтобы следить за выходом новых статей и обновлением старых

Перейти на канал

@aofeed

Задать вопрос в Телеграм-группе

@aofeedchat

Контакты и сотрудничество:
Рекомендую наш хостинг beget.ru
Пишите на info@urn.su если Вы:
1. Хотите написать статью для нашего сайта или перевести статью на свой родной язык.
2. Хотите разместить на сайте рекламу, подходящую по тематике.
3. Реклама на моём сайте имеет максимальный уровень цензуры. Если Вы увидели рекламный блок недопустимый для просмотра детьми школьного возраста, вызывающий шок или вводящий в заблуждение - пожалуйста свяжитесь с нами по электронной почте
4. Нашли на сайте ошибку, неточности, баг и т.д. ... .......
5. Статьи можно расшарить в соцсетях, нажав на иконку сети: