python

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

  • суббота, 1 февраля 2020 г. в 00:28:01
https://habr.com/ru/company/simbirsoft/blog/486112/
  • Блог компании SimbirSoft
  • Разработка веб-сайтов
  • Python
  • Программирование
  • Проектирование и рефакторинг


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-приложений, связанную с циклическими импортами. С их помощью вы можете упростить доработку и поддержку ваших приложений.

Спасибо за внимание! Надеемся, что эта статья была вам полезна.