Пакеты в 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

Создать пакет

Первым делом нужно добавить рабочую директорию в PYTHONPATH

export PYTHONPATH=/home/andrei/packages/

Обратите внимание, что в PYTHONPATH добавлена директория в которой содержится пакет, а не сама директория с пакетом.

Внутри этой директории создадим следующую структуру проекта

packages └── pac ├── 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/pac/app/double_sum.py", line 1, in <module> from regular_sum import my_sum ModuleNotFoundError: No module named 'regular_sum'

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

from pac.lib.regular_sum import my_sum

Полный код:

# double_sum from pac.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 ├── 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()

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()

Абсолютный импорт

Оба примера ниже используют абсолютный импорт. Указывается полное называние пакета и название модуля.

import demo_reader.compressed.bzipped from demo_reader.compressed import bzipped

Относительный импорт

Оба примера ниже используют относительный импорт. Его можно испльзовать только внутри пакета.

Можно указывать название пакета (подпакета) либо просто указать путь.

from ..module_name import name from ..import name

Преимущества относительного импорта - сокращение кода, возможность менять имя пакета и не менять при этом код модулей.

Тем не менее стандартной практикой является избегание относительного импорта везде где это возможно. С помощью современный IDE , таких как PyCharm можно легко рефакторить код в полуавтоматическом режими. Это делает преимущества относительного импорта несущественным.

Пример с относительным импортом

Изменим наш код, чтобы уменьшить повторы и заодно показать оба типа импорта.

Добавим директорию 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 *

Если __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'