2026, Jan 01 00:03

DataFrame между запросами в Flask: почему пропадает и как построить Plotly без сессии

Разбираем, почему pandas DataFrame не переживает запросы во Flask, и показываем решение: передавать SQL и заново выполнять для построения Plotly без сессии.

Представления Flask кажутся прямолинейными, пока вы не попытаетесь перенести DataFrame из pandas из одного запроса в следующий. На бумаге типичный сценарий выглядит просто: принять SQL‑запрос, показать результат, дать пользователю выбрать две колонки и отрисовать график в Plotly. Затык возникает, когда вы пытаетесь переиспользовать DataFrame, полученный на шаге «result», в шаге «plot». Вы рендерите шаблон, запрос завершается — и объект исчезает. Сохранять сериализованный DataFrame в сессии помогает лишь при крошечных наборах данных, но быстро приводит в тупик.

Минимальный пример, демонстрирующий проблему

Ниже показан поток: принимаем SQL‑запрос, выполняем его, собираем pandas DataFrame, рендерим HTML‑таблицу и затем пытаемся повторно использовать тот же DataFrame через сессию Flask в маршруте построения графика.

from flask import render_template as render, request as http, session as web_session
from sqlalchemy import text
import pandas as pd
@app.route("/", methods=["GET", "POST"])
def home():
    return render('base.html')
@app.route('/submit', methods=['POST'])
def run_query():
    sql_text = http.form.get('sqlr')  # например, SELECT * FROM Soils
    with engine.connect() as db:
        res = db.execute(text(sql_text))
        cols = res.keys()
        rows = res.fetchall()
    frame = pd.DataFrame(rows, columns=cols)
    # Работает только для небольших фреймов
    web_session['frame'] = frame.to_json()
    return render(
        "result.html",
        request_recap=sql_text,
        request_result=frame.to_html(index=False),
        cols=frame.columns,
    )
@app.route('/plot', methods=["GET", "POST"])
def make_chart():
    # Берём из сессии; для больших фреймов это не масштабируется
    frame = pd.read_json(web_session.get('frame'))
    x_name = http.form.get('var1')
    y_name = http.form.get('var2')
    fig = makefig(table=frame, x=x_name, y=y_name)
    return render("plot.html", fig=fig.to_html())

Что именно идёт не так и почему

render_template возвращает ответ так же, как любая функция Python возвращает значение. Как только ответ отправлен, контекст запроса исчезает, а локальные переменные уходят вместе с ним. HTTP и HTML не сохраняют состояние, поэтому последующий запрос не видит переменных из предыдущего. Каждое представление выполняется изолированно. Если нужно делиться данными между запросами, требуется хранение за пределами локальной области видимости. Фреймворки предлагают механизм сессий, связывающий данные с конкретным пользователем, но складывать туда большие сериализованные DataFrame практически имеет смысл лишь для совсем маленьких объёмов и не масштабируется.

Иными словами, дело не в pandas или Plotly. Проблема в ожидании, что переменная, созданная в одном запросе, останется в памяти к следующему. Жизненный цикл запросов работает не так.

Практичное решение: передавать запрос, а не DataFrame

Простой и надёжный приём — повторно выполнять тот же SQL‑запрос в маршруте построения графика. Вместо перемещения DataFrame между представлениями сохраните исходный текст SQL в скрытом поле формы. Когда пользователь отправляет форму с выбранными колонками, маршрут построения получает SQL, снова выполняет его и строит фигуру Plotly напрямую из сырых строк. Без pandas, без JSON‑нагрузки в сессии, без JavaScript.

from flask import Flask as WebApp
from flask import render_template as show
from flask import request as req
from sqlalchemy import text
import plotly.graph_objects as pgo
app = WebApp(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
@app.route("/", methods=["GET", "POST"])
def landing():
    return show('base.html')
@app.route('/submit', methods=['POST'])
def handle_submit():
    incoming_sql = req.form.get('sqlr')
    with engine.connect() as link:
        outcome = link.execute(text(incoming_sql))
        field_names = list(outcome.keys())
        data_rows = outcome.fetchall()
    # Преобразуем строки для безопасного рендера в шаблоне
    dict_rows = [dict(zip(field_names, r)) for r in data_rows]
    return show(
        "result.html",
        request_recap=incoming_sql,
        request_result=dict_rows,
        cols=field_names,
    )
@app.route('/plot', methods=["POST"])
def render_plot():
    sql_again = req.form.get('sql_query')
    x_field = req.form.get('var1')
    y_field = req.form.get('var2')
    with engine.connect() as link:
        outcome = link.execute(text(sql_again))
        field_names = list(outcome.keys())
        data_rows = outcome.fetchall()
    chart = build_figure(data_rows, field_names, x_field, y_field)
    return show("plot.html", fig=chart.to_html())
def build_figure(rows, cols, x_name, y_name):
    x_idx = cols.index(x_name)
    y_idx = cols.index(y_name)
    xs = [r[x_idx] for r in rows]
    ys = [r[y_idx] for r in rows]
    fig = pgo.Figure()
    fig.add_trace(
        pgo.Scatter(
            x=xs,
            y=ys,
            mode='markers',
            marker=dict(size=8, color='blue', opacity=0.7),
            name=f'{y_name} vs {x_name}',
        )
    )
    fig.update_layout(
        title=f'{y_name} vs {x_name}',
        xaxis_title=x_name,
        yaxis_title=y_name,
        template='plotly_white',
    )
    return fig

Сопровождающие шаблоны принимают обычные словари и вместе с запросом на построение отправляют исходный SQL. Так строки выводятся таблицей и формируется интерфейс выбора осей — целиком на стороне сервера.

<!-- result.html -->
<p>Your Query: {{ request_recap }}</p>
<table>
  <thead>
    <tr>
      {% for col in cols %}
      <th>{{ col }}</th>
      {% endfor %}
    </tr>
  </thead>
  <tbody>
    {% for row in request_result %}
    <tr>
      {% for col in cols %}
      <td>{{ row[col] }}</td>
      {% endfor %}
    </tr>
    {% endfor %}
  </tbody>
</table>
<form action="/plot" method="POST">
  <input type="hidden" name="sql_query" value="{{ request_recap }}" />
  <label for="var1">X-axis:
    <select name="var1" id="var1" required>
      <option value="">Select X column</option>
      {% for col in cols %}
      <option value="{{ col }}">{{ col }}</option>
      {% endfor %}
    </select>
  </label>
  <label for="var2">Y-axis:
    <select name="var2" id="var2" required>
      <option value="">Select Y column</option>
      {% for col in cols %}
      <option value="{{ col }}">{{ col }}</option>
      {% endfor %}
    </select>
  </label>
  <br /><br />
  <button type="submit">Generate Plot</button>
</form>
<!-- plot.html -->
<div>
  {{ fig|safe }}
</div>

Почему это важно понимать

Каждое представление во фреймворке по сути выполняется как отдельная программа. Без серверной сессии, файлов или базы данных один запрос не видит переменные, находившиеся в памяти при другом запросе. Сессии как раз и существуют, чтобы связывать состояние запроса с конкретным пользователем, но они плохо подходят для больших полезных нагрузок вроде целых DataFrame. Передача SQL и повторное выполнение по мере необходимости сохраняют корректность, избегают раздутых объектов в сессии и соответствуют природе HTTP без сохранения состояния. К тому же отпадает нужда в pandas на этом участке — это убирает лишнюю сложность, если требуется лишь выбрать колонки и построить точки в Plotly.

Выводы

Не рассчитывайте, что локальные переменные переживут границы запросов. Если нужна непрерывность между представлениями, храните небольшие данные в сессии или, как показано здесь, передавайте исходный SQL и переисполняйте его, чтобы построить фигуру Plotly из сырых строк. Так шаблоны остаются лаконичными, сессия — компактной, а поведение согласовано с тем, как на самом деле работают Flask и HTTP. Когда нужно всего лишь построить график по двум колонкам, отказ от pandas в пользу прямой работы со строками проще и вполне эффективно. Если пользователей несколько, помните: механизм привязки данных к конкретному пользователю — это сессия, она для этого и предназначена, а большие наборы данных лучше держать вне её.