Шаг 186.
Python: сборник рецептов.
Модули и пакеты. Разделение модуля на несколько файлов

    На этом шаге мы рассмотрим особенности выполнения этой операции.

Задача

    У вас есть модуль, который вы хотели бы разделить на несколько файлов. Однако вы хотели бы сделать это, не ломая существующий код - сохранив отдельные файлы объединенными в один логический модуль.

Решение

    Программный модуль можно разделить на отдельные файлы путем превращения в пакет. Рассмотрим следующий простой модуль:

# mymodule.py

class A:
    def spam(self): 
        print('A.spam')

class B(A):
    def bar(self): 
        print('B.bar')

    Предположим, что вы хотите разделить mymodule.py на два файла, по одному на каждое определение класса. Чтобы это сделать, начните с замены файла mymodule.py на каталог с именем mymodule. В этом каталоге создайте следующие файлы:

mymodule/
    __init__.py
    a.py
    b.py

    В файл a.py поместите этот код:

# a.py

class A:
    def spam(self): 
        print('A.spam')

    В файл b.py поместите этот код:

# b.py

from .a import A

class B(A):
    def bar(self): 
        print('B.bar')

    И наконец, склейте в файле __init__.py оба этих файла:

# __init__.py

from .a import A
from .b import B

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

>>> import mymodule 
>>> a = mymodule.A()
>>> a.spam()
A.spam
>>> b = mymodule.B()
>>> b.bar()
B.bar 
>>>


Обсуждение

    Основная проблема в этом рецепте - понять, хотите ли вы, чтобы пользователи работали с большим количеством маленьких модулей или одним модулем. Например, если у вас обширная кодовая база, вы можете просто разбить все на отдельные файлы и заставить пользователей писать множество инструкций import:

from mymodule.a import A 
from mymodule.b import B
.  .  .

    Это работает, но на пользователей возлагается бремя - они должны знать, где расположены различные части. Часто проще все объединить и разрешить единый импорт:

from mymodule import A, B

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

    Когда модуль разделяется, вы должны внимательно отнестись к перекрестным ссылкам имен файлов. В этом рецепте, например, класс B нуждается в доступе к классу A как к базовому классу. Чтобы добиться этого, используется относительное импортирование на уровне пакета from .a import A.

    Относительное импортирование на уровне пакета использовано в этом рецепте для устранения прямого прописывания имени модуля высшего уровня в исходном коде. Это делает проще переименование и перемещение модуля (см. 185 шаг).

    Для этого рецепта есть интересное дополнение, которое вводит концепцию "ленивого" импортирования. Как показано выше, __init__.py импортирует все требуемые подкомпоненты за один раз. Однако для очень большого модуля может быть целесообразно импортировать компоненты по мере необходимости. Чтобы сделать это, немного изменим __init__.py:

# __init__.py

def A():
    from .a import A 
    return A()

def B():
    from .b import B 
    return B()

    В этой версии классы A и B заменены функциями, которые загружают нужные классы при первом доступе к ним. С точки зрения пользователя это не слишком большое отличие. Например:

>>> import mymodule 
>>> a = mymodule.A() 
>>> a.spam()
A.spam
>>>
Главный недостаток ленивой загрузки в том, что могут поломаться наследования и проверка типов. Например, вы могли бы немного изменить код:
if isinstance(x, mymodule.A):	# Ошибка
if isinstance(x, mymodule.a.A):	# Ok

    Если вам нужен пример ленивой загрузки из реального мира, посмотрите на исходный код multiprocessing/__init__.py в стандартной библиотеке.

    На следующем шаге мы рассмотрим создание отдельных каталогов с кодом для импорта под общим пространством имен.




Предыдущий шаг Содержание Следующий шаг