Web-приложения на Flask: как бороться с циклическими импортами

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



    Flask и циклические импорты


    Разработчики, использующие flask, нередко сталкиваются с проблемой возникновения зависимостей между модулями. Для объявления view и моделей разработчик использует глобальные объекты, созданные и инициализированные в главном модуле («точке входа»). При этом он рискует получить циклические импорты, из-за которых проект будет трудно поддерживать.

    Документация и основные учебники flask для решения этой проблемы предлагают вынести в __init__.py код инициализации проекта, который создает инстанс-классы Flask и производит настройку приложения. Это позволяет получить доступ ко всем глобальным объектам из области видимости пакета.

    При использовании этого подхода структура выглядит примерно так:

    .
    ├── app
    │   ├── __init__.py
    │   ├── forms.py
    │   ├── models.py
    │   ├── views.py
    │   └── templates
    ├── config.py
    └── migrations

    app/__init__.py

    import flask
    from flask_mail import Mail
    # other extensions
    
    app = Flask(__name__)
    mail = Mail(app)
    # configure flask app
    
    from app import views, models

    app/views.py

    from app import app
    
    @app.route('/view_name/'):
    def view_name():
         pass

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

    Как правило, мы решаем эту проблему следующим образом:

    • Избегаем стандартного роутинга.
    • Предпочитаем оригинальные версии библиотек, без «оберток».
    • Используем dependency injection.

    Давайте рассмотрим это подробнее.

    Работа с classy


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

    Пример структуры проекта при использовании библиотеки flask-classful.

    .
    ├── app
    │   ├── static
    │   ├── templates
    │   ├── forms.py
    │   ├── routes.py
    │   ├── views.py
    │   └── tasks.py
    ├── models
    ├── app.py
    ├── config.py
    └── handlers.py

    app.py

    import flask
    from flask_mail import Mail
    # other extensions
    
    from app import routes as app_route
    
    app = Flask(__name__)
    mail = Mail(app)
    # configure flask app
    
    app.register_blueprint(app_route.app_blueprint)

    app/routes.py

    from flask import Blueprint
    from app import views
    
    app_blueprint = Blueprint(...)
    views.AccountView.register(app_blueprint)
    # register other views

    app/views.py

    from flask_classy import FlaskView, route
    from flask_login import login_required
    
    class AccountView(FlaskView):
    
         def login(self):
              pass
    
         # other views
    
         @login_required
         def logout(self):
              pass

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

    Предпочтение оригинальным версиям библиотек


    Приведенный выше код показывает, как flask-classful помогает бороться с циклическими импортами. Причиной этой проблемы в классических проектах flask могут быть как объявление view, так и некоторые расширения. Один из ярких примеров – flask-sqlalchemy.

    Расширение flask-sqlalchemy призвано улучшать интеграцию sqlalchemy и flask, но на практике оно зачастую привносит в проект больше проблем, чем пользы:

    • Расширение пропагандирует использование глобального объекта для взаимодействия с базой данных и в том числе для объявления моделей, что снова приводит к проблеме циклических импортов.
    • Нужно описывать модели, используя свои собственные классы, что приводит к жесткой привязке моделей к flask проекту. В результате эти модели нельзя использовать в подпроектах или во вспомогательном скрипте.

    По этим причинам мы стараемся не использовать flask-sqlalchemy.

    Использование паттерна Dependency injection


    Внедрение classy-подхода и отказ от flask-sqlalchemy – это лишь первые шаги для решения проблемы циклического импорта. Далее нужно реализовать в приложении логику доступа к глобальным объектам. Для этого удобно применять паттерн dependency injection, реализованный в библиотеке dependency-injector.

    Пример использования паттерна в коде с библиотекой dependency-injector:

    app.py

    import dependency_injector.containers as di_cnt
    import dependency_injector.providers as di_prv
    from flask import Flask
    from flask_mail import Mail
    
    from app import views as app_views
    from app import routes as app_routes
    
    app = Flask(__name__)
    mail = Mail(app)
    
    # регистрация blueprints
    app.register_blueprint(app_routes.app_blueprint)
    
    # создание providers
    class DIServices(di_cnt.DeclarativeContainer):
        mail = di_prv.Object(mail)
    
    # injection
    app_views.DIServices.override(DIServices)


    app/routes.py

    from os.path import join
    
    from flask import Blueprint
    
    import config
    from app import views
    
    conf = config.get_config()
    
    app_blueprint = Blueprint(
        'app', __name__, template_folder=join(conf.BASE_DIR, 'app/templates'),
        static_url_path='/static/app', static_folder='static'
    )
    
    views.AccountView.register(app_blueprint, route_base='/')

    app/views.py

    import dependency_injector.containers as di_cnt
    import dependency_injector.providers as di_prv
    from flask_classy import FlaskView
    from flask_login import login_required
    
    class DIServices(di_cnt.DeclarativeContainer):
        mail = di_prv.Provider()
    
    class AccountView(FlaskView):
    
        def registration(self):
            # реализация регистрации
            msg = 'text'
            DIServices.mail().send(msg)
    
        def login(self):
            pass
    
        @login_required
        def logout(self):
            pass

    Перечисленные в статье меры позволяют устранить циклические импорты, а также повысить качество кода. Предлагаем посмотреть, как выглядит flask-проект с использованием описанных выше подходов, на примере игры «Быки и коровы», выполненной в виде web-приложения.

    Вывод


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

    Спасибо за внимание! Надеемся, что эта статья была вам полезна.
    SimbirSoft
    Лидер в разработке современных ИТ-решений на заказ

    Комментарии 4

      +1
      А аннотации или тайп хинтинг вы не используете? А то у меня есть подозрение, что IDE вам не подсвечивает типы, и при попытке это исправить, вы вернетесь к циклическим импортам. Но я могу ошибаться. Можете рассказать об этом?
        +2
        А вот сюда посмотрите.
          +1
          А вот сюда посмотрите.
          Не знаю как вас отблагодарить за ссылку. Плюсану карму.
        0
        del.

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

        Самое читаемое