Пакеты (package) в Python. Вложенные пакеты

Курс по Python: https://stepik.org/course/100707

На этом занятии речь пойдет о пакетах, узнаем, как их создавать и импортировать. Ранее мы с вами уже говорили о модулях – отдельных файлах с текстами программ, которые можно импортировать в другие программы. А пакет (package) – это специальным образом организованный подкаталог с набором модулей, как правило, решающих сходные задачи.

Давайте в качестве примера создадим пакет из модулей по обучающим курсам:

  • HTML
  • Java
  • PHP
  • Python

Это можно сделать, следующим образом. В PyCharm во вкладке «Project» щелкнуть правой кнопкой мыши и из выпадающего меню выбрать New -> Python Package. Здесь нам нужно придумать название пакета, пусть оно будет:

courses

Нажимаем Enter и пакет в рабочем каталоге создан. Посмотрим, что он из себя представляет. Переходим в рабочий каталог и видим в нем подкаталог с указанным именем courses. Внутри этого каталога находится только один пустой файл __init__.py. Чуть позже мы узнаем для чего он нужен.

Итак, пакет в Python – это обычный каталог, в котором обязательно должен располагаться специальный файл __init__.py.

Но пока наш пакет пустой, в нем нет ни одного модуля. Добавим их, то есть, добавим обычные python-файлы в этот каталог. Пусть они будут такими:

html.py:

def get_html():
    print("курс по HTML")

java.py

def get_java():
    print("Курс по Java")

php.py

def get_php():
    pass
 
 
def get_mysql():
    pass

python.py

def
get_python():
    print("курс по Python")

И, кроме того, в файл __init__.py добавим строчку:

NAME = "package courses"

И обратите внимание. Для корректной обработки модулей в пакете, все файлы следует создавать с кодировкой UTF-8.

Все, мы сформировали пакет и теперь можем его импортировать в нашу программу. Для этого записывается ключевое слово import и указывается имя пакета (название подкаталога):

import courses

Давайте посмотрим, что в итоге было импортировано:

print( dir(courses) )

Мы видим имя нашей переменной NAME и вспомогательные переменные, которые были созданы автоматически средой Python:

['NAME', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__']

Выведем значение переменной NAME:

print( courses.NAME )

И в консоли отображается строчка, прописанная в файле __init__.py. О чем это говорит? О том, что при импорте пакета файл __init__.py был выполнен. В действительности – это инициализатор пакета. В нем мы прописываем то, что следует импортировать при импорте пакета. Например, в нашем каталоге courses присутствуют четыре наших файла, но ни один из них не был использован в момент импорта. Это произошло, как раз, по той причине, что в файле __init__.py мы их никак не обрабатывали.

Чтобы получить доступ к функциям из модулей внутри пакета, их, в свою очередь, нужно импортировать в инициализаторе __init__.py, например, так:

import courses.python

Я, думаю, вы понимаете, почему вначале указан подкаталог courses? Так как модули находятся по нестандартному пути, то этот путь следует явно прописать. В данном случае, достаточно указать имя подкаталога, а затем, через точку имя файла.

Теперь, после запуска программы, мы видим, что в пространстве имен пакета courses появилось имя python. Поэтому, мы можем обратиться к этому модулю и вызвать его функцию:

courses.python.get_python()

Но, обычно, в инициализаторе пакетов импорт выполняют с помощью конструкции:

from courses.python import get_python

чтобы, затем, не указывать имя модуля в основной программе:

courses.get_python()

Но, все же, у такого способа импортирования модулей в инициализаторе пакета есть один существенный недостаток – мы, фактически, указываем полный путь к модулю (это называется абсолютным импортом). Представьте, например, что в будущем, по каким-либо причинам, придется поменять название пакета. Тогда и все абсолютные импорты также придется переписывать. Поэтому здесь лучше воспользоваться относительным способом импортирования. Для этого, вместо имени основного подкаталога courses, ставится точка:

from .python import get_python

Эта точка означает «использовать текущий каталог». Так гораздо практичнее и мы теперь совершенно не привязаны к названию пакета.

Или, можно записать так:

from . import html, java, php, python

тогда импортируем все модули в текущем пакете. Но, мы вернем прежний импорт и запишем его со звездочкой для получения всех данных из файла:

from .python import *

Ранее, я вам говорил, что в программах так лучше не делать. Однако, внутри пакетов делают – это исключение из общего правила. Почему такой импорт целесообразен? Дело в том, что модули внутри пакетов постоянно совершенствуются. Появляются новые функции, переменные, классы. И, чтобы каждый раз не менять их список в импорте, прописывают звездочку, так удобнее. При этом конфликта имен можно избежать, контролируя импортируемые данные. Для этого внутри модуля прописывается специальная переменная __all__ со списком импортируемых данных, если используется оператор *. Например, если в модуле php.py, прописать:

__all__ = ['get_php', 'get_mysql']

то в пакет будут импортированы обе функции и мы их можем вызвать:

courses.get_php()
courses.get_mysql()

ошибок никаких не будет. Но, если оставить только какую-нибудь одну:

__all__ = ['get_php']

то прежняя программа выполнится с ошибками, так как вторая функция не была импортирована.

Эту же коллекцию __all__ можно записывать и в инициализаторе, например, так:

from .python import *
from .php import *
 
NAME = "package courses"
 
__all__ = ['python', 'php']

Тогда модули python и php импортируются, а переменная NAME нет.

И последнее, о чем я, думаю, следует сказать – это возможность создавать вложенные пакеты. Делается это очевидным образом, создаем еще один вложенный каталог, например, с именем:

doc

через PyCharm (также). И, далее, разместим в этом вложенном пакете два файла:

java_doc.py:

doc = """Документация по языку Java"""

python_doc.py:

doc = """Документация по языку Python"""

Затем, в файле инициализатора этого пакета запишем импорт этих модулей:

from . import python_doc, java_doc

А в инициализаторе основного пакета – импорт вложенного пакета:

from .doc import *

и скорректируем коллекцию:

__all__ = ['python', 'php', "doc"]

Все, теперь в основной программе можно обращаться к переменным вложенного пакета, следующим образом:

print(courses.python_doc.doc)

Мало того, в самих модулях вложенного пакета можно обращаться к модулям внешнего пакета. Для этого следует при импорте указать две точки (переход во внешний каталог) и далее имя модуля, например (в файле python_doc.py):

from ..python import get_python

Затем, можно вызвать эту функцию:

doc = """Документация по языку Python: """ + get_python()

соответственно, ее переписав (в файле python.py):

def get_python():
    return "курс по Python"

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

Курс по Python: https://stepik.org/course/100707

Видео по теме