Циклические и динамические импорты модулей

PythonPythonBeginner
Практиковаться сейчас

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

В этом лабораторном занятии вы узнаете о двух важных концепциях, связанных с импортом в Python. Импорт модулей в Python иногда может привести к сложным зависимостям, что в свою очередь может привести к ошибкам или неэффективным структурам кода. Циклические импорты, когда два или более модулей импортируют друг друга, создают цикл зависимостей, который может вызвать проблемы, если не будет правильно управлен.

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


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ModulesandPackagesGroup(["Modules and Packages"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python/ModulesandPackagesGroup -.-> python/importing_modules("Importing Modules") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/inheritance("Inheritance") subgraph Lab Skills python/importing_modules -.-> lab-132531{{"Циклические и динамические импорты модулей"}} python/classes_objects -.-> lab-132531{{"Циклические и динамические импорты модулей"}} python/inheritance -.-> lab-132531{{"Циклические и динамические импорты модулей"}} end

Понимание проблемы импорта

Начнем с понимания, что такое импорт модулей. В Python, когда вы хотите использовать функции, классы или переменные из другого файла (модуля), вы используете оператор import. Однако способ организации импортов может привести к различным проблемам.

Теперь мы рассмотрим пример проблемной структуры модулей. В коде файла tableformat/formatter.py импорты разбросаны по всему файлу. На первый взгляд это может показаться несущественным, но это создает проблемы с поддержкой и зависимостями.

Сначала откройте проводник файлов в WebIDE и перейдите в директорию structly. Мы выполним несколько команд, чтобы понять текущую структуру проекта. Команда cd используется для изменения текущей рабочей директории, а команда ls -la выводит список всех файлов и директорий в текущей директории, включая скрытые.

cd ~/project/structly
ls -la

Это покажет вам файлы в директории проекта. Теперь мы посмотрим на один из проблемных файлов с помощью команды cat, которая отображает содержимое файла.

cat tableformat/formatter.py

Вы должны увидеть код, похожий на следующий:

## formatter.py
from abc import ABC, abstractmethod
from .mixins import ColumnFormatMixin, UpperHeadersMixin

class TableFormatter(ABC):
    @abstractmethod
    def headings(self, headers):
        pass

    @abstractmethod
    def row(self, rowdata):
        pass

from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

def create_formatter(name, column_formats=None, upper_headers=False):
    if name == 'text':
        formatter_cls = TextTableFormatter
    elif name == 'csv':
        formatter_cls = CSVTableFormatter
    elif name == 'html':
        formatter_cls = HTMLTableFormatter
    else:
        raise RuntimeError('Unknown format %s' % name)

    if column_formats:
        class formatter_cls(ColumnFormatMixin, formatter_cls):
              formats = column_formats

    if upper_headers:
        class formatter_cls(UpperHeadersMixin, formatter_cls):
            pass

    return formatter_cls()

Обратите внимание на то, что операторы импорта расположены в середине файла. Это проблематично по нескольким причинам:

  1. Это делает код труднее читать и поддерживать. Когда вы просматриваете файл, вы ожидаете увидеть все импорты в начале, чтобы быстро понять, от каких внешних модулей зависит файл.
  2. Это может привести к проблемам с циклическими импортами. Циклические импорты возникают, когда два или более модулей зависят друг от друга, что может вызвать ошибки и сделать поведение вашего кода непредсказуемым.
  3. Это нарушает соглашение Python о размещении всех импортов в верхней части файла. Следование соглашениям делает ваш код более читаемым и легче для понимания другими разработчиками.

В следующих шагах мы более подробно рассмотрим эти проблемы и научимся их решать.

Исследование циклических импортов

Циклический импорт - это ситуация, когда два или более модулей зависят друг от друга. Конкретно, когда модуль A импортирует модуль B, а модуль B также импортирует модуль A, напрямую или косвенно. Это создает цикл зависимостей, который система импорта Python не может правильно разрешить. Проще говоря, Python попадает в цикл, пытаясь понять, какой модуль импортировать первым, и это может привести к ошибкам в вашей программе.

Попробуем провести эксперимент с нашим кодом, чтобы увидеть, как циклические импорты могут вызвать проблемы.

Сначала мы запустим программу для работы с акциями, чтобы проверить, работает ли она с текущей структурой. Этот шаг поможет нам установить базовую линию и увидеть, как программа работает как ожидается, прежде чем мы внесем какие - либо изменения.

cd ~/project/structly
python3 stock.py

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

Теперь мы изменим файл formatter.py. Обычно хорошей практикой является перемещение импортов в верхнюю часть файла. Это делает код более организованным и легче понять сразу.

cd ~/project/structly

Откройте файл tableformat/formatter.py в WebIDE. Мы переместим следующие импорты в верхнюю часть файла, сразу после существующих импортов. Эти импорты предназначены для различных форматеров таблиц, таких как текстовый, CSV и HTML.

from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

Теперь начало файла должно выглядеть так:

## formatter.py
from abc import ABC, abstractmethod
from .mixins import ColumnFormatMixin, UpperHeadersMixin
from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

class TableFormatter(ABC):
    @abstractmethod
    def headings(self, headers):
        pass

    @abstractmethod
    def row(self, rowdata):
        pass

Сохраните файл и попробуйте запустить программу для работы с акциями снова.

python3 stock.py

Вы должны увидеть сообщение об ошибке о том, что TableFormatter не определен. Это явный признак проблемы с циклическим импортом.

Проблема возникает из - за следующей цепочки событий:

  1. formatter.py пытается импортировать TextTableFormatter из formats/text.py.
  2. formats/text.py импортирует TableFormatter из formatter.py.
  3. Когда Python пытается разрешить эти импорты, он попадает в цикл, потому что не может решить, какой модуль импортировать полностью первым.

Вернем наши изменения, чтобы программа снова заработала. Отредактируйте файл tableformat/formatter.py и переместите импорты обратно в исходное место (после определения класса TableFormatter).

## formatter.py
from abc import ABC, abstractmethod
from .mixins import ColumnFormatMixin, UpperHeadersMixin

class TableFormatter(ABC):
    @abstractmethod
    def headings(self, headers):
        pass

    @abstractmethod
    def row(self, rowdata):
        pass

from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

Запустите программу снова, чтобы убедиться, что она работает.

python3 stock.py

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

Реализация регистрации подклассов

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

Сначала разберемся, как можно узнать имя модуля класса. Имя модуля важно, так как мы будем использовать его в нашем паттерне регистрации. Для этого выполним команду Python в терминале.

cd ~/project/structly
python3 -c "from structly.tableformat.formats.text import TextTableFormatter; print(TextTableFormatter.__module__); print(TextTableFormatter.__module__.split('.')[-1])"

При выполнении этой команды вы увидите такой вывод:

structly.tableformat.formats.text
text

Этот вывод показывает, что мы можем извлечь имя модуля из самого класса. Мы будем использовать это имя модуля позже для регистрации подклассов.

Теперь изменим класс TableFormatter в файле tableformat/formatter.py, чтобы добавить механизм регистрации. Откройте этот файл в WebIDE. Мы добавим некоторый код в класс TableFormatter. Этот код поможет нам автоматически регистрировать подклассы.

class TableFormatter(ABC):
    _formats = { }  ## Dictionary to store registered formatters

    @classmethod
    def __init_subclass__(cls):
        name = cls.__module__.split('.')[-1]
        TableFormatter._formats[name] = cls

    @abstractmethod
    def headings(self, headers):
        pass

    @abstractmethod
    def row(self, rowdata):
        pass

Метод __init_subclass__ - это специальный метод в Python. Он вызывается каждый раз, когда создается подкласс TableFormatter. В этом методе мы извлекаем имя модуля подкласса и используем его в качестве ключа для регистрации подкласса в словаре _formats.

Далее нам нужно изменить функцию create_formatter, чтобы использовать словарь регистрации. Эта функция отвечает за создание соответствующего форматера на основе заданного имени.

def create_formatter(name, column_formats=None, upper_headers=False):
    formatter_cls = TableFormatter._formats.get(name)
    if not formatter_cls:
        raise RuntimeError('Unknown format %s' % name)

    if column_formats:
        class formatter_cls(ColumnFormatMixin, formatter_cls):
              formats = column_formats

    if upper_headers:
        class formatter_cls(UpperHeadersMixin, formatter_cls):
            pass

    return formatter_cls()

После внесения этих изменений сохраните файл. Затем проверим, работает ли программа. Мы запустим скрипт stock.py.

python3 stock.py

Если программа запускается корректно, значит, наши изменения не сломали ничего. Теперь посмотрим на содержимое словаря _formats, чтобы увидеть, как работает регистрация.

python3 -c "from structly.tableformat.formatter import TableFormatter; print(TableFormatter._formats)"

Вы должны увидеть такой вывод:

{'text': <class 'structly.tableformat.formats.text.TextTableFormatter'>, 'csv': <class 'structly.tableformat.formats.csv.CSVTableFormatter'>, 'html': <class 'structly.tableformat.formats.html.HTMLTableFormatter'>}

Этот вывод подтверждает, что наши подклассы корректно регистрируются в словаре _formats. Однако у нас все еще есть некоторые импорты в середине файла. На следующем шаге мы исправим эту проблему с помощью динамических импортов.

✨ Проверить решение и практиковаться

Использование динамических импортов

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

Сначала нам нужно удалить инструкции импорта, которые сейчас расположены после класса TableFormatter. Эти импорты - статические, они загружаются при запуске программы. Для этого откройте файл tableformat/formatter.py в WebIDE. После открытия файла найдите и удалите следующие строки:

from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

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

python3 stock.py

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

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

def create_formatter(name, column_formats=None, upper_headers=False):
    if name not in TableFormatter._formats:
        __import__(f'{__package__}.formats.{name}')

    formatter_cls = TableFormatter._formats.get(name)
    if not formatter_cls:
        raise RuntimeError('Unknown format %s' % name)

    if column_formats:
        class formatter_cls(ColumnFormatMixin, formatter_cls):
              formats = column_formats

    if upper_headers:
        class formatter_cls(UpperHeadersMixin, formatter_cls):
            pass

    return formatter_cls()

Самая важная строка в этой функции:

__import__(f'{__package__}.formats.{name}')

Эта строка динамически импортирует модуль на основе имени формата. Когда модуль импортируется, его подкласс TableFormatter автоматически регистрируется. Это благодаря методу __init_subclass__, который мы добавили ранее. Этот метод - специальный метод Python, который вызывается при создании подкласса, и в нашем случае он используется для регистрации класса форматера.

После внесения этих изменений сохраните файл. Затем запустите программу снова, используя следующую команду:

python3 stock.py

Теперь программа должна работать корректно, даже несмотря на то, что мы удалили статические импорты. Чтобы убедиться, что динамический импорт работает как ожидается, мы очистим словарь _formats и вызовем функцию create_formatter. Выполните следующую команду в терминале:

python3 -c "from structly.tableformat.formatter import TableFormatter, create_formatter; TableFormatter._formats.clear(); print('Before:', TableFormatter._formats); create_formatter('text'); print('After:', TableFormatter._formats)"

Вы должны увидеть вывод, похожий на следующий:

Before: {}
After: {'text': <class 'structly.tableformat.formats.text.TextTableFormatter'>}

Этот вывод подтверждает, что динамический импорт загружает модуль и регистрирует класс форматера при необходимости.

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

  1. Теперь все импорты находятся в верхней части файла, что соответствует соглашениям Python. Это делает код легче читать и понимать.
  2. Мы устранили циклические импорты. Циклические импорты могут вызывать проблемы в программе, такие как бесконечные циклы или ошибки, трудно поддающиеся отладке.
  3. Код стал более гибким. Теперь мы можем добавлять новые форматеры без изменения функции create_formatter. Это очень полезно в реальной жизни, когда со временем могут добавляться новые функции.

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

✨ Проверить решение и практиковаться

Резюме

В этом практическом занятии (лабораторной работе) вы изучили важные концепции и техники импорта модулей в Python. Во - первых, вы рассмотрели циклические импорты, поняли, как циклические зависимости между модулями могут привести к проблемам и почему необходимо тщательно обрабатывать их, чтобы избежать таких ситуаций. Во - вторых, вы реализовали регистрацию подклассов, паттерн, при котором подклассы регистрируются у своего родительского класса, исключая необходимость прямого импорта подклассов.

Вы также использовали функцию __import__() для динамических импортов, загружая модули во время выполнения программы только при необходимости. Это делает код более гибким и помогает избежать циклических зависимостей. Эти техники являются важными для создания поддерживаемых Python - пакетов с сложными отношениями между модулями и широко используются в фреймворках и библиотеках. Применение этих паттернов в своих проектах поможет вам создать более модульные, расширяемые и поддерживаемые структуры кода.