Flask

前言

淡看人间三千事,闲来轻笑俩三声

1.Flask

Flask 是一个使用 Python 编写的轻量级 Web 框架(微框架)。

它的核心特点:

  • 小而精
  • 核心简单
  • 扩展能力强
  • 自由度高
  • 非常适合:
    • API 开发
    • 中小型项目
    • 微服务
    • 后端接口
    • AI Web 服务
    • Python Web 学习

官方:https://flask.palletsprojects.com/en/stable/?utm_source=chatgpt.com

Python 常见 Web 框架:

框架 特点 类型
Django 大而全 同步
Flask 小而精 同步
FastAPI 高性能 异步
Tornado 异步 IO 异步
Sanic 类 Flask 异步 异步
web.py 极简 同步

其它功能需要第三方扩展:

功能 Flask 扩展
ORM SQLAlchemy / Peewee
Admin Flask-Admin
缓存 Flask-Cache
登录认证 Flask-Login
JWT Flask-JWT-Extended
数据校验 Marshmallow

微框架

真正含义:

  • Flask 核心非常小
  • 不强制项目结构
  • 不强制数据库
  • 不强制 ORM
  • 不强制模板
  • 不强制缓存方案

开发者拥有完全控制权。

即:Flask 只提供 Web 最核心能力,其它全部自由选择。

Flask 核心组成

组件 作用
Werkzeug WSGI 工具包
Jinja 模板引擎
Click 命令行工具

Flask 请求处理流程

浏览器
   ↓
WSGI服务器
   ↓
Werkzeug
   ↓
Flask
   ↓
视图函数
   ↓
Jinja2模板渲染
   ↓
Response
   ↓
浏览器

Flask 本质理解

Flask 本质上:

  • 基于 WSGI 协议
  • 基于 Werkzeug 封装
  • 本质还是 Python Web Server

其核心是:

socket
HTTP协议
WSGI协议

2.Werkzeug(HTTP/WSGI核心)

Werkzeug

Werkzeug 是一个 WSGI Web 工具包

注意 Werkzeug:

  • 不是 Web 框架
  • 不是 Web 服务器
  • 是工具库

作用:为 Web 框架提供底层能力。

WSGI

Web Server Gateway Interface

即:Python Web 网关接口规范。

作用:统一 Web 服务器、Python Web 框架 之间的通信标准。

为什么需要 WSGI?

没有 WSGI 每个框架都得适配不同服务器

有了 WSGI:

  • 所有框架统一标准
  • 所有服务器统一协议

即:

Django
Flask
FastAPI
    ↓
  WSGI
    ↓
gunicorn/uWSGI

WSGI 常见实现

工具 作用
wsgiref Python内置WSGI服务器
gunicorn Linux生产WSGI服务器
uWSGI 高性能WSGI服务器
Werkzeug WSGI工具包

Werkzeug 功能

功能 说明
Request 请求对象
Response 响应对象
Routing 路由
URL解析 url处理
HTTP工具 HTTP处理
Debugger 调试器

所以:Flask 很多能力来自 Werkzeug。

Werkzeug 示例

from werkzeug.wrappers import Request, Response

@Request.application
def hello(request):
    return Response('Hello World!')

if __name__ == '__main__':
    from werkzeug.serving import run_simple
    run_simple('localhost', 4000, hello)

image-20260521235535352

Werkzeug 核心理解

本质:

Werkzeug
    ↓
封装WSGI
    ↓
提供Request/Response
    ↓
Flask继续封装

所以:

Flask 底层 ≈ Werkzeug

3.wsgiref(WSGI Web服务器)

wsgiref

  • Python 标准库
  • Python 官方 WSGI 实现

作用:快速启动一个 WSGI Web 服务。

wsgiref 本质

底层:

socket
HTTP解析
WSGI协议

它帮助开发者不用手写:

  • TCP
  • HTTP协议
  • socket

即可写 Web 服务。

WSGI 应用标准格式

WSGI 应用必须满足:

def app(environ, start_response):
    pass

参数:

参数 说明
environ 请求信息
start_response 设置响应

**environ **

本质:

dict

包含:

key 说明
PATH_INFO 请求路径
REQUEST_METHOD 请求方法
QUERY_STRING 查询参数

wsgiref 示例

import os
from wsgiref.simple_server import make_server

# 当前项目根目录
BASE_DIR = os.path.dirname(__file__)


# WSGI 应用函数
def mya(environ, start_response):
    # 设置响应状态码和响应头
    start_response(
        '200 OK',
        [('Content-Type', 'text/html;charset=utf-8')]
    )

    # 获取请求路径
    # 例如:/index  /login
    path = environ.get('PATH_INFO')

    # 首页
    if path == '/index':

        # 拼接 html 文件路径
        file_path = os.path.join(
            BASE_DIR,
            'templates/old',
            'index.html'
        )

        # 读取 html 文件
        with open(file_path, 'rb') as f:
            data = f.read()

    # 登录页
    elif path == '/login':

        file_path = os.path.join(
            BASE_DIR,
            'templates/old',
            'login.html'
        )

        with open(file_path, 'rb') as f:
            data = f.read()

    # 其他路径
    else:
        data = b'<h1>Hello, web!</h1>'

    # WSGI 必须返回可迭代对象
    return [data]


if __name__ == '__main__':
    # 创建 web 服务
    myserver = make_server('', 8011, mya)

    print('监听8011')

    # 启动服务
    myserver.serve_forever()

image-20260522002313831

Flask 和 wsgiref 关系

Flask 本质:

Flask
   ↓
Werkzeug
   ↓
WSGI

所以Flask 本质也是:

def app(environ,start_response)

的高级封装。

4.Jinja2(模板引擎)

Jinja2

Python 最流行模板引擎之一。

作用:

HTML + 数据
    ↓
渲染
    ↓
最终HTML

为什么需要模板引擎?

如果不用模板:

html = "<h1>" + name + "</h1>"

会非常难维护。

模板引擎让 HTML、Python 数据分离。

JJinja2 语法

变量

{{ name }}

for循环

{% for item in users %}
    {{ item }}
{% endfor %}

if判断

{% if age > 18 %}
    成人
{% endif %}

示例

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
    return render_template(
        'old/index.html',
        name='peng'
    )
    
# <h1>{{ name }}</h1>

image-20260522003255126

Jinja2 特点

特点 说明
快速 性能高
类Python语法 易学
自动转义 防XSS
可扩展 支持过滤器

5.Click(CLI命令系统)

Click

Python 命令行工具库。

Flask 命令系统:

flask run
flask shell
flask routes

底层就是 Click。

Click 核心作用

  • 命令行参数解析
  • 自动帮助文档
  • 命令嵌套
  • CLI开发

Click 示例

# 导入 click 库
# click 是 Python 中用于快速构建命令行工具(CLI)的第三方库
import click


# 使用 @click.command() 把 hello 函数变成一个命令行程序
@click.command()
# 定义命令行参数 --count
@click.option(
    '--count',  # 命令行参数名
    default=1,  # 默认值为 1
    help='Number of greetings.'  # 参数说明
)
# 定义命令行参数 --name
@click.option(
    '--name',  # 命令行参数名
    prompt='Your name',  # 如果用户没有输入 --name,则自动提示输入
    help='The person to greet.'  # 参数说明
)
# 命令执行函数
# count:问候次数
# name:用户名
def hello(count, name):
    # 循环 count 次
    for x in range(count):
        # 输出内容
        # click.echo 类似于 print
        # 推荐在 click 项目中使用 echo
        click.echo(f"Hello {name}!")


# 当前文件直接运行时执行
if __name__ == '__main__':
    # 启动命令行程序
    hello()

需要输入命令

python .\04Click示例.py --count 3 --name peng
# 或者 
.\.venv\Scripts\python.exe .\04Click示例.py --count 3 --name peng

image-20260522004146063

Click 核心装饰器

装饰器 作用
@click.command 定义命令
@click.option 定义参数
@click.argument 定义位置参数

6.Flask创建和运行

6.1基本使用

安装 Flask

pip install flask

image-20260522004656569

示例

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return 'hello flask'

if __name__ == '__main__':
    app.run()

image-20260522004744807

app.run() 本质

启动 Werkzeug 开发服务器

即:

from werkzeug.serving import run_simple

Flask 核心对象

作用:创建 Flask 应用对象。

app = Flask(__name__)

@app.route()

作用:路由映射

URL
  ↓
视图函数

视图函数

作用:处理请求。

def index():
    return 'hello'

Flask 开发模式

开启:

  • 自动重载
  • 调试模式
  • 错误页面
app.run(debug=True)

6.2.python-dotenv

python-dotenv用于读取 .env 配置文件并加载到环境变量

为什么需要 .env?

目的:配置分离,不能写死代码。

数据库密码
Redis地址
JWT密钥

安装 python-dotenv

pip install python-dotenv

image-20260522005751435

示例

import os
from dotenv import load_dotenv
from dotenv import dotenv_values
## 1 加载配置文件
# 必须在根路径下新建一个 .env 的文件,并写入配置才能返回True,会把.env下的配置文件设置进环境变量
res=load_dotenv()  # take environment variables from .env
print(res)
print(os.environ.get('DOMAIN'))
# You will probably want to add .env to your .gitignore, especially if it contains secrets like a password.

## 2 获取环境变量字典
config = dotenv_values(".env")
print(config)
print(config.get('DOMAIN'))



# .env
DOMAIN=www.baidu.com
SECRET_KEY=123456

image-20260522010917435

6.3watchdog

watchdog 是一个 Python 库,用来监听操作系统文件系统变化事件(File System Events)

它不是“扫描文件夹”,而是:基于操作系统底层通知机制的“事件驱动监听器”

watchdog 的核心能力

事件类型 说明
created 文件/目录创建
modified 文件内容修改
deleted 删除
moved 重命名 / 移动

watchdog 是一个基于操作系统文件事件通知机制的跨平台监听框架,用 Observer 管理线程,用 EventHandler 处理事件流,实现“文件变化驱动逻辑执行”。

安装

pip install watchdog

image-20260522013412993

示例

# pip3 install watchdog
# 用于文件系统监听(文件新增/修改/删除/移动)

import sys          # 获取命令行参数(监听路径)
import time         # 用于主线程 sleep,保持程序运行
import logging      # 用于打印事件日志
from watchdog.observers import Observer  # 核心:文件系统监控调度器
from watchdog.events import LoggingEventHandler  # 内置事件处理器:直接打印日志

if __name__ == "__main__":

    # 配置日志输出格式
    # level=logging.INFO:输出 INFO 及以上级别日志
    # format:定义日志结构
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )

    # 获取要监听的目录路径
    # sys.argv[1]:命令行第一个参数
    # 如果没有传参,默认监听当前目录 "."
    path = sys.argv[1] if len(sys.argv) > 1 else '.'

    # 创建事件处理器
    # LoggingEventHandler 是 watchdog 自带的处理器
    # 功能:当文件发生变化时,自动打印日志(无需自己实现逻辑)
    event_handler = LoggingEventHandler()

    # 创建 Observer(核心调度器)
    # Observer 负责:
    # 1. 监听文件系统变化(底层依赖 OS API)
    # 2. 将事件分发给 handler
    observer = Observer()

    # 注册监听任务
    # path:监听目录
    # recursive=True:递归监听子目录
    observer.schedule(event_handler, path, recursive=True)

    # 启动后台线程开始监听
    observer.start()

    try:
        # 主线程保持运行,否则程序会直接退出
        # watchdog 在后台线程工作
        while True:
            time.sleep(1)

    except KeyboardInterrupt:
        # Ctrl + C 时优雅退出
        observer.stop()

    # 等待 observer 线程完全结束
    observer.join()


# ==========================
# mysqldump -u ...
# ==========================
# 这里是 MySQL 数据导出命令的开头(未写完整)
# mysqldump:数据库备份工具
# -u:指定用户名

手动创建a.txt,然后随便添加字符串,发现文件被监听
image-20260522013642544

6.4虚拟环境

Python venv 是 Python 自带的一个模块,用来创建:“项目级隔离的 Python 运行环境”

它的本质不是“新装一个 Python”,而是: 在当前系统 Python 外面,复制一套“独立的依赖空间”

解决的核心问题

  • 依赖冲突问题

  • 生产环境不可控问题

  • Python版本冲突

虚拟环境的本质结构

创建 venv 后会生成:

project/
 ├── venv/
 │    ├── Scripts/ (Windows)
 │    ├── bin/ (Linux/mac)
 │    ├── lib/
 │    ├── pyvenv.cfg
 ├── app.py

核心机制

  • 解释器隔离:venv 指向一个 Python 解释器副本
  • pip 隔离:每个 venv 有自己的site-packages/

创建虚拟环境

Mac/linux

# 创建虚拟环境
mkdir myproject
cd myproject
python3 -m venv .venv
# 激活虚拟环境
. .venv/bin/activate

Win

# 创建虚拟环境
mkdir myproject
cd myproject
py -3 -m venv .venv
# 激活虚拟环境
.venv\Scripts\activate

安装依赖

只装在当前 venv,不污染系统

pip install flask

导出依赖(生产必备)

pip freeze > requirements.txt

生产环境恢复

pip install -r requirements.txt

venv vs 其他方案

工具 说明
venv 官方标准(推荐)
virtualenv 老工具(兼容旧Python)
conda 数据科学生态
poetry 现代依赖管理(更高级)

6.5Flask核心依赖

Flask 必装
│
├── Werkzeug → HTTP/WSGI核心
├── Jinja2 → 模板引擎
├── MarkupSafe → 安全转义
├── ItsDangerous → session/cookie签名
├── Click → CLI命令系统
└── Blinker → 事件信号系统(可选)

Flask选装
│
├── dotenv → 处理配置文件
└── watchdog → 监控文件变化

6.6示例

flask run 和 app.run 是两套启动方式,端口不共享配置

  • app.run(port=8888) → 只对 python app.py 生效

  • flask run → 默认 5000,必须用:

    flask run --port 8888
    

代码

from flask import Flask

app=Flask(__name__)
app.debug=True

# 注册路由
@app.route('/',methods=['GET'])
def index():
    return 'hello world111'


#运行方式一:写下面代码,右键运行
if __name__ == '__main__':
    app.run(port=8888)

image-20260522015640838

7.Flask配置文件

7.1简介

Flask 配置文件

在 Flask 中,所有配置都保存在:

app.config

它本质是:

flask.config.Config

继承自 Python 的 dict,所以:

app.config['DEBUG'] = True

本质就是字典操作。

Flask 配置的本质

from flask import Flask

app = Flask(__name__)

print(type(app.config))
print(app.config)

# <class 'flask.config.Config'>

image-20260522153229493

7.2配置项

配置项 作用
DEBUG 是否开启调试模式
TESTING 是否测试模式
SECRET_KEY Session/CSRF 加密密钥
PERMANENT_SESSION_LIFETIME session 有效期
SERVER_NAME 域名配置
SESSION_COOKIE_NAME cookie 名称
SESSION_COOKIE_HTTPONLY JS 是否可访问 cookie
SESSION_COOKIE_SECURE 是否只允许 HTTPS
MAX_CONTENT_LENGTH 上传文件大小限制
JSON_AS_ASCII JSON 是否转 ASCII
TEMPLATES_AUTO_RELOAD 模板自动重载
PREFERRED_URL_SCHEME URL 协议
{
    'DEBUG': False,                         # 是否开启 Debug 模式
    'TESTING': False,                      # 是否开启测试模式
    'PROPAGATE_EXCEPTIONS': None,          # 是否向外抛出异常
    'SECRET_KEY': None,                    # Flask Session 加密密钥
    'SECRET_KEY_FALLBACKS': None,          # 备用密钥列表

    'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31),  # Session 有效期

    'USE_X_SENDFILE': False,               # 是否使用 X-Sendfile 优化文件发送
    'TRUSTED_HOSTS': None,                 # 允许访问的 Host 列表
    'SERVER_NAME': None,                   # 服务器域名和端口
    'APPLICATION_ROOT': '/',               # 应用根路径

    'SESSION_COOKIE_NAME': 'session',      # Session Cookie 名称
    'SESSION_COOKIE_DOMAIN': None,         # Session Cookie 域名
    'SESSION_COOKIE_PATH': None,           # Session Cookie 路径
    'SESSION_COOKIE_HTTPONLY': True,       # JS 是否禁止访问 Cookie
    'SESSION_COOKIE_SECURE': False,        # 是否仅 HTTPS 传输
    'SESSION_COOKIE_PARTITIONED': False,   # 是否启用分区 Cookie
    'SESSION_COOKIE_SAMESITE': None,       # SameSite 策略
    'SESSION_REFRESH_EACH_REQUEST': True,  # 每次请求是否刷新 Session

    'MAX_CONTENT_LENGTH': None,            # 请求体最大大小
    'MAX_FORM_MEMORY_SIZE': 500000,        # 表单内存最大值
    'MAX_FORM_PARTS': 1000,                # 表单字段最大数量

    'SEND_FILE_MAX_AGE_DEFAULT': None,     # 静态文件缓存时间

    'TRAP_BAD_REQUEST_ERRORS': None,       # 是否捕获 BadRequest 异常
    'TRAP_HTTP_EXCEPTIONS': False,         # 是否捕获 HTTP 异常

    'EXPLAIN_TEMPLATE_LOADING': False,     # 是否显示模板加载过程
    'PREFERRED_URL_SCHEME': 'http',        # URL 默认协议
    'TEMPLATES_AUTO_RELOAD': None,         # 模板是否自动重载

    'MAX_COOKIE_SIZE': 4093,               # Cookie 最大大小(字节)

    'PROVIDE_AUTOMATIC_OPTIONS': True      # 是否自动生成 OPTIONS 请求响应
}

image-20260522170423521

7.3Flask配置方式

7.3.1直接修改app属性

方式1:直接修改 app 属性

from flask import Flask
app = Flask(__name__)
app.debug = True
app.secret_key = '123456'

image-20260522172317120

7.3.2通过 app.config 修改

方式2:通过 app.config 修改(最常用)

from flask import Flask
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SECRET_KEY'] = '123456'
app.config['DB_HOST'] = '127.0.0.1'

image-20260522172433145

7.3.3from_pyfile

方式3:from_pyfile(类似 Django)

第一步:创建 settings.py

DEBUG = True
SECRET_KEY = '123456'
DB_HOST = '127.0.0.1'
DB_PORT = 3306
DB_USER = 'root'
DB_PASSWORD = 'root'

image-20260522172646967

第二步:加载配置

from flask import Flask
app = Flask(__name__)
app.config.from_pyfile('settings.py')

image-20260522172719999

7.3.4from_object

方式4:from_object(大型项目最推荐)

第一步:配置类

class Config:
    DEBUG = False
    TESTING = False
    DB_HOST = '127.0.0.1'
    DB_PORT = 3306
class DevelopmentConfig(Config):
    DEBUG = True
class ProductionConfig(Config):
    DB_HOST = '192.168.1.100'

第二步:加载配置

app.config.from_object(DevelopmentConfig)

或者

app.config.from_object('settings.DevelopmentConfig')

image-20260522173141611

7.3.5其它配置方式

方式5:其它配置方式

方式 示例 场景
from_envvar app.config.from_envvar() 环境变量
from_json app.config.from_json() JSON 配置
from_mapping app.config.from_mapping() 字典配置
dotenv .env 文件 现代项目
配置中心 Nacos/Apollo 微服务

7.3.6dotenv 配置

方式6:dotenv 配置

安装 python-dotenv

pip install python-dotenv

image-20260522173303747

创建 .env 文件

REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_DB=0
REDIS_PASSWORD=redis

image-20260522174237434

加载

from dotenv import load_dotenv
load_dotenv()
print(os.environ.get('REDIS_HOST'))
print(os.environ.get('REDIS_PORT'))
print(os.environ.get('REDIS_DB'))
print(os.environ.get('REDIS_PASSWORD'))

image-20260522174151277

8.路由

8.1Flask 路由本质

Flask 路由核心就是:

URL  --->  视图函数

例如:

@app.route('/index')
def index():
    return 'ok'

表示:访问 /index 时,执行 index 函数

本质上 Flask 内部维护了一张:

{
    '/index': index函数内存地址
}

的映射关系。

8.2Flask 路由注册方式

装饰器方式(最常用)

@app.route('/index')
def index():
    return 'ok'

add_url_rule方式(本质)

def index():
    return 'ok'

app.add_url_rule('/index', view_func=index)

实际上:@app.route('/index')最终内部也是调用:app.add_url_rule()

所以:route == add_url_rule 的语法糖

8.3app.route 常用方式

route

@app.route('/login', methods=['GET', 'POST'])
def login():
    pass

get

Flask2 新增:

@app.get('/index')
def index():
    pass

等价于:

@app.route('/index', methods=['GET'])

post

@app.post('/add')
def add():
    pass

等价于:

@app.route('/add', methods=['POST'])

其它

app.put
app.delete
app.patch

8.4route 参数详解

函数

@app.route(
    rule,
    methods=None,
    endpoint=None,
    defaults=None,
    strict_slashes=None,
    redirect_to=None,
    subdomain=None,
    host=None
)

底层本质

app.add_url_rule()

route 参数

参数 类型 作用
rule str 路由地址
methods list/tuple 请求方式
endpoint str 路由别名
defaults dict 默认参数
strict_slashes bool 是否严格匹配 /
redirect_to str/function 重定向
subdomain str 子域名匹配
host str 指定主机匹配

8.5路径转换器

基本语法

# app.add_url_rule('/index/<int:pk>')
@app.route('/路径/<转换器:参数名>')

例如:

@app.route('/user/<int:id>')
def user(id):
    return str(id)

用于:

  • 提取路径参数
  • 限制参数类型
  • 路由匹配校验

Flask 内置路径转换器

转换器 作用 示例
string 字符串(默认) <name>
int 整数 <int:id>
float 浮点数 <float:price>
path 包含 / 的字符串 <path:url>
uuid UUID <uuid:uid>
any 枚举值限制 <any(admin,user):role>

Flask 默认转换器

DEFAULT_CONVERTERS = {
    'default': UnicodeConverter,
    'string': UnicodeConverter,
    'any': AnyConverter,
    'path': PathConverter,
    'int': IntegerConverter,
    'float': FloatConverter,
    'uuid': UUIDConverter,
}

9.CBV(Class Based View)

9.1简介

Flask 默认主要是 FBV(Function Based View,函数视图):

@app.route('/index')
def index():
    return 'index'

但 Flask 也支持 CBV(Class Based View,类视图)。

Flask 中类视图核心是:

from flask.views import View
from flask.views import MethodView

通常开发中:

  • View:基础类视图
  • MethodView:最常用,支持 RESTful 风格

CBV:使用类处理请求。

优势:

  • 代码复用
  • 更适合 RESTful
  • 不同请求方式分离
  • 更容易继承
  • 逻辑更清晰

9.2示例

from flask import Flask
from flask.views import MethodView

app = Flask(__name__)

class UserView(MethodView):

    def get(self):
        return '查询用户'

    def post(self):
        return '新增用户'

app.add_url_rule(
    '/user',
    view_func=UserView.as_view('user')
)

if __name__ == '__main__':
    app.run()

image-20260522183849765

9.3源码分析

UserView.as_view('user')
            ↓
      返回view函数
            ↓
add_url_rule保存view函数
            ↓
        请求来了
            ↓
         执行view()
            ↓
   self = UserView()
            ↓
self.dispatch_request()
            ↓
 getattr(self,'get')
            ↓
       self.get()
            ↓
        返回响应

image-20260522184259236

10.模版

Django DTL / Flask Jinja2 都是模板引擎。本质:字符串渲染器(template renderer)

流程都是:

后端数据 + HTML模板 → 模板引擎 → 渲染成HTML字符串

DTL 与 Jinja2 共性

  • 都支持 if / for 控制结构

  • 都支持 include / extends 模板继承

  • 都支持过滤器(filter)

  • 都支持 safe 关闭转义

  • 都可以访问字典 / 对象 / 列表

Jinja2 比 DTL 多的能力

  • 支持函数调用(可加括号传参)
  • 支持更强表达式(类似 Python)
  • 语法更灵活(接近编程语言)

示例

from flask import Flask, session, redirect, request, render_template, url_for

from functools import wraps
from flask.views import MethodView, View

app = Flask(__name__)
app.secret_key = 'adfafa'
app.debug = True


def add(a, b):
    return a + b + 100


@app.route('/')
def index():
    a = '<a href="http://www.baidu.com">百度一下</a>'
    return render_template('moban.html', user={'name': 'peng', 'age': 19, 'hobby': '篮球'}, add=add, a=a)


if __name__ == '__main__':
    app.run(port=8888)

前端

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<div style="height: 300px;width: 100%;background-color: pink;display: flex;flex-direction:column;justify-content: center;align-items: center">
    <p>姓名:{{user.name}}</p>
    <p>年龄:{{user['age']}}</p>
    <p>爱好:{{user.get('hobby')}}</p>

</div>
<div style="height: 300px;width: 100%;background-color:darkslategray;display: flex;flex-direction:column;justify-content: center;align-items: center">
直接使用add:{{add(3,4)}}
</div>

<hr>
{%include 'top.html'%}
<br>
{{a|safe}}
<br>
{{a}}
<hr>
{{add(1,2)}}
</body>
</html>

image-20260522185348104

11.请求响应

11.1Request

request = 对当前线程/协程中 HTTP 请求的全量封装对象

它包含:

  • URL
  • 请求方法
  • 请求头
  • 请求参数
  • 请求体
  • Cookie
  • 文件上传
  • 客户端信息

request 生命周期

客户端发请求
    ↓
WSGI(server)
    ↓
Flask 创建 RequestContext
    ↓
push 到栈 (ctx.push)
    ↓
request 变成当前线程可访问对象
    ↓
视图函数中使用 request
    ↓
请求结束 ctx pop

核心属性

  • request.method
  • request.args
  • request.form
  • request.json / request.get_json()
  • request.headers
  • request.files
分类 属性 / 方法 作用 使用场景 示例
请求信息 request.method 请求方式 判断GET/POST "GET"
请求信息 request.url 完整URL 调试/日志 http://xx/user?id=1
请求信息 request.path 请求路径 路由分析 /user
请求信息 request.endpoint 路由别名 url_for / 调试 user_view
GET参数 request.args URL参数 GET请求 ?id=1
GET参数 request.args.get() 获取单个参数 安全取值 id=1
POST表单 request.form 表单数据 form提交 username=tom
JSON数据 request.json JSON请求体 API接口 {name:"tom"}
JSON数据 request.get_json() 获取JSON(推荐) 更安全 request.get_json()
原始数据 request.data 原始body(bytes) 二进制/特殊数据 b'xxx'
请求头 request.headers 所有请求头 token / UA dict-like
请求头 request.headers.get() 获取单个header Authorization Bearer xxx
Cookie request.cookies cookie集合 登录态 sessionid=xxx
文件上传 request.files 上传文件 上传图片/文件 avatar.png
客户端 request.remote_addr 客户端IP 安全/日志 192.168.x.x
客户端 request.user_agent 浏览器信息 统计分析 Chrome/Safari
JSON判断 request.is_json 是否JSON请求 API校验 True/False

示例

from flask import Flask,session,redirect,request,render_template,url_for
from flask.sessions import SessionMixin
app=Flask(__name__)
app.secret_key='adfafa'
app.debug=True

from flask import Request
@app.route('/',methods=['GET',"POST"])
def index():
    # 是一个代理对象
    print(type(request))  # 看类型打印出:<class 'werkzeug.local.LocalProxy'>,但request本质是flask import Request
    print(type(session))  # 看类型打印出:<class 'werkzeug.local.LocalProxy'>,但session本质是SessionMixin
    # Request对象中得属性和方法--》类比Django的request对象
    # request.method  提交的方法
    print(request.method)
    # request.args  get请求提及的数据
    print(request.args)
    # request.form   post请求提交的数据
    print(request.form)
    # request.values  post和get提交的数据总和
    print(request.values)
    # request.cookies  客户端所带的cookie
    print(request.cookies)
    # request.headers  请求头
    print(request.headers)
    # request.path     不带域名,请求路径
    print(request.path)
    # request.full_path  不带域名,带参数的请求路径
    print(request.full_path)
    # request.url           带域名带参数的请求路径
    print(request.url)
    # request.base_url     带域名请求路径
    print(request.base_url)
    # request.url_root      域名
    print(request.url_root)
    # request.host        127.0.0.1:500
    print(request.host)
    # request.files
    obj = request.files['myfile']
    obj.save('./' + obj.filename)

    return '请求对象'

if __name__ == '__main__':
    app.run(port=8888)

image-20260522190042104

11.2Response

Flask Response 类:

werkzeug.wrappers.response.Response

继承关系:

BaseResponse
   ↑
Response (Flask)

底层来自 Werkzeug(Flask 的 HTTP 工具库)

Response 的核心作用

功能 说明
响应内容 body(HTML / JSON / 字符串 / 文件)
状态码 200 / 404 / 500 等
响应头 Content-Type / Set-Cookie 等
cookie session、登录信息

示例

from flask import Flask, jsonify, make_response

# 创建 Flask 应用实例
app = Flask(__name__)

# 注册路由:访问根路径 "/"
@app.route("/")
def index():
    resp = "响应对象"
    resp = make_response(jsonify(name="peng", age=18), 201)
    resp.headers["X-Token"] = "abc123"
    resp.set_cookie("login", "true")
    return resp


# 程序入口:直接运行该文件时启动 Flask 服务
if __name__ == '__main__':
    # 监听 8888 端口
    app.run(port=8888)

image-20260522213748343

12.偏函数

在 Python 中,偏函数(partial function)指的是:把一个函数的部分参数固定住,生成一个新的“参数更少”的函数

来自标准库:

from functools import partial

核心作用:提前绑定参数,简化函数调用

关键特点总结

特性 说明
来自模块 functools
本质 生成新函数
参数机制 预先绑定部分参数
调用方式 少传参数
是否改变原函数 ❌ 不改变

示例

# partial 的本质:提前固定函数的部分参数,返回一个“参数更少的新函数”

from functools import partial


def add(a, b, c):
    return a + b + c


# 场景1:固定前两个参数,第三个以后再传
res = partial(add, 2000, 2000)
print(res(6000))  # 10000

# 场景2:只固定一个参数
add_2000 = partial(add, 2000)
print(add_2000(3000, 6000))  # 11000

image-20260522214337558

13.Flask-RESTful

Flask-RESTful 是 Flask 的一个扩展库,用于:在 Flask 基础上提供 RESTful API 开发的结构化封装

核心目标:

  • 把 Flask 的“函数式路由” → 类视图资源(Resource)
  • 提供统一的 请求解析、序列化、响应封装
  • 提供基础的 REST 风格规范支持

安装

pip install flask-restful

image-20260522214855071

示例

Flask CLI 依赖 Python import 机制,而模块名必须符合 Python 标识符规范(不能以数字开头,也不能包含 -)

from flask import Flask
from flask_restful import Resource, Api, reqparse

app = Flask(__name__)
api = Api(app)

# 模拟数据库
USERS = {
    1: {"id": 1, "name": "Alice"},
    2: {"id": 2, "name": "Bob"}
}

# 请求参数解析器
parser = reqparse.RequestParser()
parser.add_argument("name", type=str, required=True, help="name is required")


# Resource:用户列表 / 创建用户
class UserList(Resource):
    def get(self):
        """获取所有用户"""
        return {"users": list(USERS.values())}

    def post(self):
        """创建用户"""
        args = parser.parse_args()
        new_id = max(USERS.keys()) + 1

        user = {
            "id": new_id,
            "name": args["name"]
        }

        USERS[new_id] = user
        return user, 201


# Resource:单个用户
class User(Resource):
    def get(self, user_id):
        """获取单个用户"""
        user = USERS.get(user_id)
        if not user:
            return {"msg": "User not found"}, 404
        return user

    def put(self, user_id):
        """更新用户"""
        args = parser.parse_args()
        if user_id not in USERS:
            return {"msg": "User not found"}, 404

        USERS[user_id]["name"] = args["name"]
        return USERS[user_id]

    def delete(self, user_id):
        """删除用户"""
        if user_id not in USERS:
            return {"msg": "User not found"}, 404

        del USERS[user_id]
        return {"msg": "deleted"}


# 注册路由
api.add_resource(UserList, "/users")
api.add_resource(User, "/users/<int:user_id>")


if __name__ == "__main__":
    app.run(debug=True)

image-20260522215435728

14.Session

Flask 默认 session = 签名 Cookie Session(客户端存储)

核心点:

  • 数据不在服务器
  • 存在浏览器 Cookie 中
  • 但内容是 加密签名后的 JSON
  • 防篡改,不防查看(默认)

Flask 中 session 的默认实现类:

SecureCookieSessionInterface

它负责两件事:

方法 生命周期 类比 Django
open_session 请求进入 process_request
save_session 响应返回 process_response

示例

from flask import Flask, session, request

app = Flask(__name__)

# session加密密钥
app.secret_key = "123456"


@app.route("/login")
def login():
    """
    模拟登录
    """
    # 登录成功后,把用户信息放入session
    session["user"] = "peng"

    return "登录成功"


@app.route("/index")
def index():
    """
    判断用户是否登录
    """
    user = session.get("user")

    if user:
        return f"当前登录用户:{user}"

    return "未登录"


@app.route("/logout")
def logout():
    """
    退出登录
    """
    session.clear()

    return "退出成功"


if __name__ == '__main__':
    app.run(debug=True)

image-20260522220758065

Flask 默认 session 基于 SecureCookieSessionInterface 实现,通过 itsdangerous 对 session 数据进行序列化和 HMAC 签名,并存储到浏览器 Cookie 中

  • 请求时通过 open_session 读取并验证 session
  • 响应时通过 save_session 重新签名并写回 Cookie。

image-20260522221134655

15.请求扩展

Flask 中的“请求扩展”本质上就是在请求处理的不同阶段,插入自定义逻辑。作用类似于 Django 的中间件。

Flask 请求扩展 vs 中间件

对比 Flask 请求扩展 Flask WSGI 中间件
使用频率 常用 较少
使用方式 装饰器 包装 app
操作阶段 请求生命周期 WSGI 层
开发难度 简单 偏复杂
常见用途 登录校验、日志、跨域、异常处理 请求转发、代理、底层处理

Flask 常用请求扩展

扩展 触发时机 是否必须返回值 常见用途
before_request 请求进入视图前 不必须 登录校验、权限认证
after_request 视图执行完成后 必须返回 response 修改响应头、跨域
teardown_request 请求结束时 不需要 释放资源、记录日志
errorhandler 出现指定错误时 需要 全局异常处理

示例

Noneteardown方法的exc输出

from flask import Flask, request, jsonify

app = Flask(__name__)


# 请求前执行
@app.before_request
def before():
    print("before")
    request.name = "peng"


# 请求后执行
@app.after_request
def after(response):
    print("after")
    response.headers["X-Token"] = "123"
    return response


# 请求结束执行
@app.teardown_request
def teardown(exc):
    print("teardown")
    print(exc)


# 全局异常
@app.errorhandler(Exception)
def error(e):
    return jsonify({
        "code": 500,
        "msg": str(e)
    })


@app.route("/")
def index():
    print(request.name)
    return "ok"


if __name__ == '__main__':
    app.run(debug=True)

image-20260522222204303

16.CBV+装饰器

请求流程

请求来了
   ↓
路由匹配
   ↓
as_view生成的view函数
   ↓
装饰器
   ↓
dispatch_request
   ↓
get/post
   ↓
返回响应

示例

from flask import Flask
from flask.views import MethodView
from flask import session, redirect, request

app = Flask(__name__)

# session 加密秘钥
app.secret_key = "123456"


# 登录认证装饰器
def login_auth(func):
    def inner(*args, **kwargs):
        # 判断是否登录
        if session.get("username"):
            print("用户已登录")
            res = func(*args, **kwargs)
            return res
        else:
            print("用户未登录")
            return redirect("/login")

    return inner


# 登录页面
@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "GET":
        return """
        <form method="post">
            用户名:<input type="text" name="username">
            <input type="submit">
        </form>
        """
    # 登录成功
    username = request.form.get("username")
    # 保存 session
    session["username"] = username
    return "登录成功"


# 用户管理 CBV
class UserView(MethodView):
    # 给整个类加装饰器
    decorators = [login_auth]

    def get(self):
        return "用户列表"

    def post(self):
        return "新增用户"


# 订单管理 CBV

class OrderView(MethodView):

    # decorators = [login_auth]
    @login_auth
    def get(self):
        return "订单列表"


# 注册路由
app.add_url_rule(
    "/user",
    view_func=UserView.as_view("user")
)

app.add_url_rule(
    "/order",
    view_func=OrderView.as_view("order")
)

if __name__ == '__main__':
    app.run(debug=True)

image-20260522231734377

17.闪现(Flask flash)

17.1Flask flash

Flask flash 的本质: 基于 session 的“单次消费队列”

也就是说:

  • 数据存入 session
  • 只在下一次请求有效
  • 一旦 get_flashed_messages() 读取 → 自动清空

生命周期流程

POST /login
   ↓
flash("用户名或密码错误")
   ↓
redirect("/login")
   ↓(新请求)
GET /login
   ↓
get_flashed_messages()
   ↓
数据被取出 + session清空
   ↓
页面展示错误信息

Flask flash 特点

特性 说明
依赖 session
生命周期 一次请求后消失
存储结构 list[tuple(category, msg)]
是否持久 不持久
使用场景 表单错误提示 / 登录提示

示例

02闪现.py

第一次发送登录验证,使用flash存储消息,服务器重定向到/login(这时已经算第二次请求了),前端显示错误信息

from flask import Flask, render_template, request, redirect, url_for, flash

app = Flask(__name__)

# flash 必须依赖 session,所以必须配置 secret_key
app.secret_key = "dev-secret-key"


@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form.get("username")
        password = request.form.get("password")

        # 模拟校验逻辑
        if username == "admin" and password == "123456":
            return redirect(url_for("index"))
        else:
            # ❗关键:写入 flash
            flash("用户名或密码错误", category="error")
            return redirect(url_for("login"))

    # GET 请求:只负责渲染页面
    return render_template("login.html")


@app.route("/")
def index():
    return "登录成功,进入首页"


if __name__ == "__main__":
    app.run(debug=True)

login.html

<!DOCTYPE html>
<html>
<head>
    <title>登录</title>
</head>
<body>

<h2>用户登录</h2>

<!-- ⭐ flash消息展示区 -->
{% with messages = get_flashed_messages(with_categories=true) %}
    {% if messages %}
        {% for category, message in messages %}
            <div style="color: red;">
                {{ message }}
            </div>
        {% endfor %}
    {% endif %}
{% endwith %}

<form method="post">
    <input type="text" name="username" placeholder="用户名" value="peng">
    <br>
    <input type="password" name="password" placeholder="密码" value="123456">
    <br>
    <button type="submit">登录</button>
</form>

</body>
</html>

image-20260522233258383

17.2django消息框架

################ 1 基础配置############
INSTALLED_APPS = [
    ...
    'django.contrib.messages',
    ...
]
# 在django setting.py 取消注释的message app
MIDDLEWARE = [
    ...
    'django.contrib.messages.middleware.MessageMiddleware',
    ...
]
# 在django setting.py 取消注释的message 的中间件
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                ...
                'django.contrib.messages.context_processors.messages',
            ],
        }
    }
]
################ 2 设置存放位置############
MESSAGE_STORAGE = "django.contrib.messages.storage.session.SessionStorage"


################ 3 放入值############
#添加message
from django.contrib import messages
def concel_order(request):
    messages.add_message(request, messages.SUCCESS, "删除成功1")
    messages.add_message(request, messages.SUCCESS, "删除成功2")
    
    return redirect("/order/control/")

################ 4 视图函数中取############
# 在视图函数中添加messages模块
# 再通过messages.add_message导入提示信息
# 在视图函数中import get_messages模块获取添加的提示信息
def control_order(request):
    if request.method == "GET":
        from django.contrib.messages.api import get_messages
        m1 = get_messages(request)
        print(m1)
        
################ 5 模板中取############
# 在html模板中添加for循环拿到message
<div>
    {% for obj in messages %}
        <ul>{{ obj.message }}</ul>
    {% endfor %}
</div>

18.g对象

Flask 中的 g 是一个请求上下文全局变量(global namespace per request)

它的全称不是 global,而是 Flask 内部定义的:flask.g = request-level global object

核心特点:

  • 每个请求独立
  • 请求结束自动销毁
  • 线程/协程隔离(基于 context local)

g / request / session

对象 作用 是否可写 生命周期 推荐用途
request 请求数据封装 基本只读 当前请求 获取参数、headers
g 请求共享存储 可写 当前请求 共享中间变量
session 会话级存储 可写 多请求 登录状态

g 的生命周期

请求进入
   ↓
创建 request context
   ↓
g 被创建(空对象)
   ↓
视图函数执行(可写 g)
   ↓
请求结束
   ↓
g 自动销毁

所以:g 只能用于“当前请求内的临时数据缓存”

g 和 Django 对比

框架 请求共享机制
Flask g + context local
Django request 对象 + middleware

Flask 的 g = 当前请求生命周期内的“临时共享变量容器”,用于解决函数链传参问题。

示例

from flask import Flask, g, request, jsonify

app = Flask(__name__)

# 模拟数据库
USERS = {
    1: {"id": 1, "name": "peng"},
    2: {"id": 2, "name": "tom"}
}

# 1. 模拟 token 解析
def decode_token(token):
    # 实际项目中:JWT / OAuth
    if token == "token_1":
        return 1
    elif token == "token_2":
        return 2
    return None


# 2. 请求前钩子:统一加载用户
@app.before_request
def load_user():
    token = request.headers.get("token")
    user_id = decode_token(token)

    if user_id:
        g.user = USERS.get(user_id)
    else:
        g.user = None


# 3. 业务函数 A
def check_permission():
    if not g.user:
        return False
    return True


# 4. 业务函数 B
def get_user_info():
    return g.user


# 5. 视图函数
@app.route("/profile")
def profile():
    if not check_permission():
        return jsonify({"msg": "未登录"}), 401

    user = get_user_info()
    return jsonify({
        "msg": "success",
        "data": user
    })


if __name__ == "__main__":
    app.run(debug=True)

image-20260522235732773

在请求进入时解析 token,得到用户信息,存入 g,后续业务直接使用,实现“登录态贯穿整个请求”。

所以请求必须带上token

image-20260522235803839

19.蓝图

Flask 的 Blueprint(蓝图) 本质是:用来“模块化拆分路由系统”的机制(类似 Django 的 app)

蓝图 = 把一个大 Flask 项目拆成多个“小路由模块”

比如:

  • 用户模块(user)
  • 订单模块(order)
  • 商品模块(product)

可以把蓝图理解成:

Flask App
    ↓
项目越来越大
    ↓
按功能拆分
    ↓
home模块
product模块
user模块
    ↓
每个模块对应一个Blueprint

19.1不使用蓝图

目录

image-20260531213652976

__init__.py

from flask import Flask
app = Flask(__name__)
from . import views

image-20260531213414887

views.py

from . import app
from .models import User

@app.route('/')
def index():  # put application's code here
    return 'index'

image-20260531213450852

app.py

from src import app

if __name__ == '__main__':
    app.run()

image-20260531213512456

启动项目

image-20260531213533430

19.2蓝图小项目

目录

image-20260531213716259

__init__.py

from flask import Flask

app = Flask(__name__, template_folder="../templates", static_folder="../static")
app.config.from_pyfile('settings.py')

from .views import main_blue

app.register_blueprint(main_blue, url_prefix="/api/v1/main")
# app.register_blueprint(main_bl,url_prefix="/api/v1/user")

image-20260531213801984

views.py

from flask import Blueprint, render_template

main_blue = Blueprint('main', __name__)


@main_blue.before_request
def before():
    print("before...")


@main_blue.get('/')
def index():
    return "首页"


@main_blue.get('/home')
def home():
    return render_template('home.html', name="peng")

image-20260531213829399

app.py

image-20260531213854128

启动项目

http://127.0.0.1:5000/api/v1/main/home

image-20260531213955715

19.3蓝图大项目

目录结构

image-20260531214049317

每个项目的目录的基本结构都是一致的:

  • src/apps/home/static:静态文件目录
  • src/apps/home/templates:模版文件目录
  • src/apps/home/__init__.py:应用初始化
  • src/apps/home/models.py:数据模型
  • src/apps/home/serialzier.py:序列化与反序列化
  • src/apps/home/views.py:视图函数

home模版

src/apps/home/__init__.py

from flask import Blueprint

home_blue = Blueprint('home', __name__, template_folder='templates', static_folder='static')

from . import views

image-20260531214649704

src/apps/home/views.py

from flask import render_template
from . import home_blue

# 请求扩展
@home_blue.before_request
def before():
    print('home before...')

@home_blue.get('/')
def index():
    return render_template("home.html")

image-20260531214709193

product模版

src/apps/product/__init__.py

from flask import Blueprint

product_blue = Blueprint('product', __name__, template_folder='templates', static_folder='static')

from . import views

image-20260531214820379

src/apps/product/views.py

from flask import render_template
from . import product_blue


# 请求扩展
@product_blue.before_request
def before():
    print('product before...')


@product_blue.get('/')
def index():
    return render_template("product.html")

image-20260531214855853

user模版

__init__.py

from flask import Blueprint

user_blue = Blueprint('user', __name__, template_folder='templates', static_folder='static')

from . import views

views.py

from flask import render_template
from . import user_blue


# 请求扩展
@user_blue.before_request
def before():
    print('user before...')


@user_blue.get('/')
def index():
    return render_template("user.html")

src/__init__.py

创建Flask应用,加载配置,注册各模块蓝图,并为所有请求添加全局前置处理逻辑。

from flask import Flask

app = Flask(__name__)
app.config.from_pyfile('settings.py')

from src.apps.home import home_blue
from src.apps.product import product_blue
from src.apps.user import user_blue

app.register_blueprint(home_blue, url_prefix='/api/v1/home')
app.register_blueprint(product_blue, url_prefix='/api/v1/product')
app.register_blueprint(user_blue, url_prefix='/api/v1/user')


@app.before_request
def before():
    print("总路由...")

image-20260531215115804

app.py

启动项目

from src import app

if __name__ == '__main__':
    app.run()

image-20260531215546723

测试

http://127.0.0.1:5000/api/v1/home/
http://127.0.0.1:5000/api/v1/user/
http://127.0.0.1:5000/api/v1/product/

image-20260531215616752

19.4总结

创建蓝图对象

from flask import Blueprint
# 蓝图名,当前模块名
order_blue = Blueprint('order', __name__)

在蓝图中编写路由

@order_blue.route('/order')
def order():
    return 'order-order-order'

给蓝图添加请求扩展

@order_blue.before_request
def before():
    print('来了')

@order_blue.after_request
def after(response):
    print('走了')
    return response    

@order_blue.errorhandler(404)
def error(e):
    return '蓝图404'

在 app 中注册蓝图

from order import order_blue
app.register_blueprint(
    order_blue,
    url_prefix='/api/v1'
)

典型项目结构

project/
│
├── app.py
│
├── apps/
│   ├── user/
│   │   ├── __init__.py
│   │   └── views.py
│   │
│   ├── order/
│   │   ├── __init__.py
│   │   └── views.py
│   │
│   └── product/
│       ├── __init__.py
│       └── views.py

20.flask-session

Flask 默认的 Session 是客户端 Session

数据保存在浏览器 Cookie 中,只不过经过了签名(不是加密)。

安装flask-session

 pip install flask-session

image-20260531220901515

如果没有redis需要安装

pip install redis

image-20260531223755491

20.1方式一:手动(RedisSessionInterface)

from flask import Flask, session, request
from flask_session.redis import RedisSessionInterface
import redis

# 连接Redis
conn = redis.Redis(
    host='192.168.188.180',
    port=6379,
    password='redis',
    # decode_responses=True
)

# 替换Session实现
app.session_interface = RedisSessionInterface(
    app,
    client=conn,
    key_prefix='session:',
    use_signer=True,
    permanent=True,
    sid_length=32
)


@app.route('/login')
def login():
    username = request.args.get('username')

    # 登录成功
    session['user'] = username
    session['age'] = 18

    return f'{username} 登录成功'


@app.route('/index')
def index():
    user = session.get('user')

    if not user:
        return '请先登录'

    return f'当前登录用户:{user}'


@app.route('/logout')
def logout():
    session.clear()

    return '退出成功'


if __name__ == '__main__':
    app.run(port=5000)

image-20260531225202722

20.2方式二:配置(app.config)

from flask_session import Session
from redis import Redis

app = Flask(__name__)
# 必须配置
app.secret_key = "peng"

# Session存Redis
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = Redis(
    host='192.168.188.180',
    port=6379,
    password='redis'
)

Session(app)

image-20260531225358336

20.3源码分析

open_session:
    1. 从Cookie取session值
    2. 验证签名
    3. 反序列化
    4. 创建Session对象

save_session:
    1. Session转字典
    2. 序列化
    3. 使用secret_key签名
    4. 写入Cookie

特点:
    Session数据直接存浏览器Cookie
    服务端不存储Session

Cookie:
    session=Session数据+签名

默认过期时间:
    PERMANENT_SESSION_LIFETIME=31天

image-20260531225619820

21.WTForms

WTForms 是 Flask 中最常用的表单验证库,主要解决两个问题:

  • 1.表单数据接收

  • 2.表单数据校验

WTForms 是 Flask 中的表单验证框架,通过「Form + Field + Validator」的方式统一完成请求参数接收、数据校验和错误管理;Flask-WTF 则是在 WTForms 基础上增加了 Flask 集成和 CSRF 防护功能。

核心功能

功能 说明
字段(Field) 接收数据
验证器(Validator) 验证数据
Form 表单类
FlaskForm Flask集成版Form
validate() 执行验证
validate_on_submit() POST并验证
errors 错误信息
data 验证后的数据
CSRF 防止跨站请求伪造

安装

pip install wtforms

image-20260531234527407

示例

06wtforms.py

from flask import Flask, render_template, request
from wtforms import Form
from wtforms.fields import simple
from wtforms import validators
from wtforms import widgets

# 创建Flask应用
app = Flask(__name__, template_folder='templates')

# 开启调试模式
app.debug = True


# ==========================================
# 登录表单类
# 作用:
# 1. 定义表单字段
# 2. 定义字段验证规则
# 3. 定义页面渲染方式
# ==========================================
class LoginForm(Form):
    # 用户名输入框
    name = simple.StringField(
        label='用户名',  # 页面显示的标签

        validators=[
            # 不能为空
            validators.DataRequired(
                message='用户名不能为空'
            ),

            # 长度限制
            validators.Length(
                min=6,
                max=18,
                message='用户名长度必须大于%(min)d且小于%(max)d'
            )
        ],

        # 指定生成<input type="text">
        widget=widgets.TextInput(),

        # 给HTML标签添加属性
        render_kw={
            'class': 'form-control'
        },
        default='peng'
    )

    # 密码输入框
    pwd = simple.PasswordField(
        label='密码',

        validators=[
            # 不能为空
            validators.DataRequired(
                message='密码不能为空'
            ),

            # 最少8位
            validators.Length(
                min=8,
                message='密码长度必须大于%(min)d'
            ),

            # 正则验证密码强度
            validators.Regexp(
                regex=r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&])[A-Za-z\d$@$!%*?&]{8,}$",

                message='密码至少8位,且包含大写字母、小写字母、数字和特殊字符'
            )
        ],

        # 指定生成<input type="password">
        widget=widgets.PasswordInput(hide_value=False),

        render_kw={
            'class': 'form-control',
            "value": "peng123456"
        }
    )


# ==========================================
# 登录页面
# GET:
#     显示页面
#
# POST:
#     接收用户提交的数据
#     执行WTForms验证
# ==========================================
@app.route('/login', methods=['GET', 'POST'])
def login():
    # 第一次访问页面
    if request.method == 'GET':

        # 创建空表单对象
        form = LoginForm(data={
            "name": "peng",
            "pwd": "peng123456"
        })

        return render_template(
            'login_new.html',
            form=form
        )

    # 用户提交表单
    else:

        # request.form中的数据交给WTForms处理
        form = LoginForm(
            formdata=request.form
        )

        # 执行所有验证器
        if form.validate():

            print(
                '用户提交数据通过格式验证,提交的值为:',
                form.data
            )

            """
            form.data格式:

            {
                'name':'peng123',
                'pwd':'Abc123@#'
            }
            """

        else:

            print('验证失败')
            print(form.errors)

            """
            form.errors格式:

            {
                'name': [
                    '用户名不能为空'
                ],
                'pwd': [
                    '密码长度必须大于8'
                ]
            }
            """

        return render_template(
            'login_new.html',
            form=form
        )


if __name__ == '__main__':
    app.run()

login_new.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>登录</h1>
<form method="post">
    <p>{{form.name.label}} {{form.name}} {{form.name.errors[0] }}</p>
    <p>{{form.pwd.label}} {{form.pwd(value="peng123456")}} <span style="color: red">{{form.pwd.errors[0] }}</span></p>
    <input type="submit" value="提交">
</form>
</body>
</html>

image-20260601000222728

22.mysql

数据库三大范式:

  • 字段原子化
  • 非主键完全依赖主键
  • 非主键只依赖主键

安装pymysql

pip install pymysql

image-20260601000649780

22.1全局连接(错误)

from flask import Flask, render_template, request, redirect
import pymysql

app = Flask(__name__, template_folder='templates')

app.debug = True


# 多个请求共用:conn cursor
#       请求A执行SQL
#       请求B执行SQL
#       请求A获取结果
# 结果被覆盖。

# =========================
# 创建 MySQL 连接对象
# =========================
conn = pymysql.connect(
    host='192.168.188.180',  # MySQL服务器地址
    port=3306,  # MySQL端口
    user='root',  # 用户名
    password='root',  # 密码
    database='rbac_manager'  # 数据库名称
)

# =========================
# 创建游标对象
# DictCursor:查询结果返回字典格式
# 默认返回元组格式
# =========================
cursor = conn.cursor(pymysql.cursors.DictCursor)


@app.route('/dept')
def dept():
    """
    查询部门表前3条数据
    访问:
    http://127.0.0.1:5000/dept
    """

    # 执行SQL语句
    cursor.execute("select * from lqz_dept limit 3")
    # 获取所有查询结果
    data = cursor.fetchall()
    # 转成字符串返回给浏览器
    return str(data)


@app.route('/user')
def user():
    """
    查询用户表前3条数据
    访问:
    http://127.0.0.1:5000/user
    """

    # 执行SQL语句
    
    cursor.execute("select * from lqz_user limit 3")
    # 获取所有查询结果
    data = cursor.fetchall()
    # 转成字符串返回给浏览器
    return str(data)


if __name__ == '__main__':
    app.run(debug=True)

image-20260601002207901

22.2每个请求独立连接

from flask import Flask, render_template, request, redirect
import pymysql

app = Flask(__name__, template_folder='templates')

app.debug = True


@app.route('/menu')
def menu():
    conn = pymysql.connect(
        host='192.168.188.180',  # MySQL服务器地址
        port=3306,  # MySQL端口
        user='root',  # 用户名
        password='root',  # 密码
        database='rbac_manager'  # 数据库名称
    )

    cursor = conn.cursor(pymysql.cursors.DictCursor)

    cursor.execute("select * from lqz_menu limit 3")
    data = cursor.fetchall()

    cursor.close()
    conn.close()

    return str(data)


@app.route('/role')
def role():
    conn = pymysql.connect(
        host='192.168.188.180',  # MySQL服务器地址
        port=3306,  # MySQL端口
        user='root',  # 用户名
        password='root',  # 密码
        database='rbac_manager'  # 数据库名称
    )

    cursor = conn.cursor(pymysql.cursors.DictCursor)

    cursor.execute("select * from lqz_role")
    data = cursor.fetchall()

    cursor.close()
    conn.close()

    return str(data)

if __name__ == '__main__':
    app.run(debug=True)

image-20260601002346717

22.3面试题

22.3.1脏读、不可重复读、幻读

脏读:读到其他事务未提交的数据。

不可重复读:同一事务中,多次读取同一条记录,读到了其他事务已提交的更新或删除数据,结果不一致。

幻读:同一事务中,多次查询同一范围数据,读到了其他事务已提交的新增或删除数据,结果集数量不一致。

22.3.2MySQL事务隔离级别

隔离级别 脏读 不可重复读 幻读
Read Uncommitted(读未提交)
Read Committed(读已提交)
Repeatable Read(可重复读) ❌*
Serializable(串行化)

MySQL 5.7 / 8.0 默认隔离级别:Repeatable Read(可重复读)

22.3.3qpstps、并发量、pvuv

指标 全称 含义
QPS Queries Per Second 每秒查询请求数
TPS Transactions Per Second 每秒事务处理数
并发量 Concurrent Users/Requests 同一时刻系统正在处理的请求数
PV Page View 页面访问量
UV Unique Visitor 独立访客数

22.3.4接口幂等性

幂等性(Idempotent):同一个请求执行一次和执行多次,对系统产生的结果是一样的,不会因为重复请求导致数据错误。

唯一索引

数据库增加唯一约束:
ALTER TABLE orders
ADD UNIQUE(order_no);
相同订单号只能插入一次。

Token机制

1. 请求前获取唯一Token
2. 提交时携带Token
3. 服务端校验并删除Token
4. 重复提交直接拒绝

Redis分布式锁

SETNX(key,value)
同一时间只允许一个请求执行。
适用于:抢购/秒杀/库存扣减

状态机控制

例如订单:
待支付 -> 已支付
如果已经是:
已支付
再次支付直接返回成功。

防重表(幂等记录表)

记录业务唯一号:
request_id
order_no
message_id
处理前先查询是否存在。
存在则不再执行。

22.3.5gil

GIL(Global Interpreter Lock,全局解释器锁) 是 Python 解释器(CPython)中的一种机制。

作用:同一时刻只允许一个线程执行 Python 字节码。

即使创建了多个线程,在执行 Python 代码时也需要先获取 GIL。

主要是为了保证:

  • Python对象的线程安全
  • 简化内存管理
  • 提高解释器实现效率

它简化了内存管理并保证对象线程安全,但导致CPU密集型任务无法通过多线程充分利用多核CPU。

因此计算密集型任务通常使用多进程,IO密集型任务由于线程等待IO时会释放GIL,所以适合使用多线程。

22.3.6python垃圾回收机制

Python 主要采用 引用计数(Reference Counting),并结合 标记清除(Mark and Sweep)分代回收(Generational GC) 来管理内存。

当对象引用计数为0时会立即释放内存。为了处理循环引用问题,Python又引入了标记清除算法。同时采用分代回收思想,将对象划分为0代、1代、2代,优先回收年轻对象,提高垃圾回收效率。

22.3.7多进程和多线程

计算密集型任务主要消耗CPU资源,由于Python存在GIL锁,多线程无法真正并行执行CPU计算,因此通常使用多进程。每个进程都有独立的GIL,可以充分利用多核CPU。

IO密集型任务大部分时间都在等待网络、磁盘或数据库响应,等待期间会释放GIL,因此多个线程可以并发执行。相比多进程,多线程创建和切换开销更小,所以IO密集型任务通常使用多线程。

22.4数据库连接池

数据库连接池是一种池化技术,用于复用数据库连接。程序启动时会预先创建一定数量的连接放入池中,请求访问数据库时直接从连接池获取连接,使用完后归还连接,而不是频繁创建和销毁连接。这样可以减少数据库连接开销,提高系统性能和并发能力。常见连接池有 HikariCP、Druid、C3P0,其中 Spring Boot 默认使用 HikariCP。对于 Python 项目,常用 DBUtils、SQLAlchemy Pool 等实现连接池。

安装dbutils

pip install dbutils

image-20260601221615400

安装pymysql

pip install pymysql

image-20260601221716326

22.4.1配置

pool.py

from dbutils.pooled_db import PooledDB
import pymysql

# 创建数据库连接池对象
POOL = PooledDB(
    creator=pymysql,          # 使用 pymysql 作为数据库驱动

    maxconnections=6,         # 连接池允许的最大连接数
                              # 0 或 None 表示不限制

    mincached=2,              # 初始化连接池时,创建的最小空闲连接数

    maxcached=5,              # 连接池中最多保留的空闲连接数
                              # 超过该数量的空闲连接会被关闭

    maxshared=3,              # 最大共享连接数
                              # 对 pymysql 基本无效,因为 pymysql 不支持共享连接

    blocking=True,            # 连接池耗尽时是否阻塞等待
                              # True:等待连接释放
                              # False:立即抛出异常

    maxusage=None,            # 单个连接最大复用次数
                              # None 表示无限次复用

    setsession=[],            # 获取连接后执行的 SQL
                              # 例如:
                              # ["SET NAMES utf8mb4"]

    ping=0,                   # 检测数据库连接是否可用
                              # 0:从不检测
                              # 1:获取连接时检测
                              # 2:创建游标时检测
                              # 4:执行 SQL 时检测
                              # 7:每次都检测

    # ========================
    # MySQL 数据库连接参数
    # ========================
    host='192.168.188.180',   # MySQL服务器IP
    port=3306,                # MySQL端口
    user='root',              # 用户名
    password='root',         # 密码
    database='rbac_manager',  # 数据库名称
    charset='utf8mb4'         # 字符编码,支持emoji
)

image-20260601235909824

22.4.2使用

数据库连接都从连接池中获取

"""
数据库连接池本质上是提前创建并维护一批数据库连接,程序需要访问数据库时直接从池中获取连接,用完后归还,而不是每次都重新创建和销毁连接。
"""
import pymysql
from flask import Flask

from pool import POOL

app = Flask(__name__, template_folder="templates", static_folder="static")
app.debug = True


@app.route('/menu')
def menu():
    # 从连接池获取连接
    conn = POOL.connection()
    # 创建游标
    cursor = conn.cursor(pymysql.cursors.DictCursor)
    cursor.execute("select * from lqz_menu limit 3")
    data = cursor.fetchall()
    cursor.close()
    conn.close()
    return str(data)


@app.route('/role')
def role():
    # 从连接池获取连接
    conn = POOL.connection()
    # 创建游标
    cursor = conn.cursor(pymysql.cursors.DictCursor)
    cursor.execute("select * from lqz_role")
    data = cursor.fetchall()
    cursor.close()
    conn.close()
    return str(data)

if __name__ == '__main__':
    app.run()

image-20260602000020543

测试

image-20260602000049797

22.5压力测试

22.5.1接口

一个接口使用数据库连接池连接,一个接口使用独立的数据库连接

"""
数据库连接池本质上是提前创建并维护一批数据库连接,程序需要访问数据库时直接从池中获取连接,用完后归还,而不是每次都重新创建和销毁连接。
"""
import pymysql
from flask import Flask,jsonify
import time
from pool import POOL

app = Flask(__name__, template_folder="templates", static_folder="static")
app.debug = True


@app.route('/menu')
def menu():
    # 从连接池获取连接
    conn = POOL.connection()
    # 创建游标
    cursor = conn.cursor(pymysql.cursors.DictCursor)
    cursor.execute("select * from lqz_menu limit 3")
    time.sleep(2)
    data = cursor.fetchall()
    print(data)
    cursor.close()
    conn.close()
    return jsonify(data)

@app.route('/role')
def role():
    conn = pymysql.connect(
        host='192.168.188.180',  # MySQL服务器地址
        port=3306,  # MySQL端口
        user='root',  # 用户名
        password='root',  # 密码
        database='rbac_manager'  # 数据库名称
    )

    cursor = conn.cursor(pymysql.cursors.DictCursor)

    cursor.execute("select * from lqz_role")
    time.sleep(1)
    data = cursor.fetchall()
    print(data)
    cursor.close()
    conn.close()

    return jsonify(data)

if __name__ == '__main__':
    app.run()

image-20260602000142971

22.5.2requests和线程压测

安装requests

pip  install requests

image-20260602000439075

开启100个线程请求接口

import requests
from threading import Thread


def task():
    # 有连接池接口(复用连接)
    # url = "http://127.0.0.1:5000/menu"

    # 无连接池接口(每次新建连接)
    url = "http://127.0.0.1:5000/role"

    res = requests.get(url)
    print(res.json())


if __name__ == '__main__':
    threads = []

    # 模拟100并发请求
    for _ in range(100):
        t = Thread(target=task)
        t.start()
        threads.append(t)

    for t in threads:
        t.join()

"""
==================================================
核心观察
==================================================

1. 使用连接池:
    - 复用数据库连接
    - Threads_connected较稳定
    - 避免频繁创建/销毁连接

2. 不使用连接池:
    - 每次请求创建新连接
    - Threads_connected暴涨
    - MySQL压力大

==================================================
连接池本质
==================================================

提前创建连接 → 需要时取 → 用完归还

==================================================
核心结论(面试重点)
==================================================

连接池:
    提升性能 + 控制连接数 + 提高稳定性

缺点:
    连接池过小 → 请求排队 → 变慢

image-20260602000612356

22.5.3测试结果

用于查看 MySQL 当前线程和连接状态

show status like '%Threads%';

我们这里留意Threads_connected即可

参数

参数 说明
Delayed_insert_threads 执行 INSERT DELAYED 的线程数(已废弃,基本为0)
Mysqlx_worker_threads X Protocol(MySQL X Plugin)工作线程总数
Mysqlx_worker_threads_active X Protocol 当前活跃工作线程数
Slow_launch_threads 创建线程耗时超过 slow_launch_time 的线程数
Threads_cached 线程缓存中的空闲线程数
Threads_connected 当前客户端连接数(包括 Sleep)
Threads_created MySQL 启动后累计创建的线程数
Threads_running 当前正在执行的线程数(不包括 Sleep)

image-20260602001320085

使用线程池连接每次都稳定在8个连接

image-20260602000839827

独立连接请求数据库并发连接暴涨,但是程序执行的比较快

程序执行的比较快,命令需要快速执行,要不看不出效果

image-20260602000931639

结论

连接池太小 → 并发请求排队 → 响应变慢
不用连接池 → 并发连接暴涨 → 数据库压力大
连接池的本质是:用“限制并发数”换“系统稳定性”

23.定制命令

23.1老版本(Flask-Script)

安装命令

pip install Flask-Script==2.0.3
pip install flask==1.1.4
pip install markupsafe==1.1.1

Flask-Script 状态(重点),已经废弃(Deprecated),原因:

  • 不再维护
  • 与 Flask 新版本不兼容
  • 被官方 CLI 替代

要用低版本 python 3.8/3.9 环境,我这里使用 3.11 还是不行

本来就是淘汰的技术,这里就不再演示了

from flask import Flask
from flask_script import Manager

app = Flask(__name__)

manager = Manager(app)


# =========================
# 自定义命令
# =========================
@manager.command
def initdb(arg):
    '''
    # 自定义命令:
    # python manage.py initdb 123
    '''
    print("参数:", arg)


# =========================
# 启动入口
# =========================
if __name__ == "__main__":
    manager.run()


image-20260602001651872

23.2flask定制命令

Flask 2.x/3.x 已经内置了命令系统,底层基于 click,不再需要老版本的 Flask-Script

Flask 定制命令基于 Click 实现,

  • 通过 @app.cli.command() 定义命令
  • 通过 @click.argument() 接收位置参数
  • 通过 @click.option() 接收可选参数。

常用于数据库初始化、创建管理员、数据导入导出、缓存清理等运维场景。Flask 2.x 以后已经内置命令系统,不再需要 Flask-Script。

创建表

CREATE TABLE test_user (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    password VARCHAR(100) NOT NULL
);

自定义命令

# =========================
# Flask 自定义命令
# =========================

@app.cli.command("create-user")
@click.argument("name")
@click.argument("password")
def create_user(name, password):
    """
    命令:
    flask create-user peng 123456
    """

    from pool import POOL

    # 1. 获取连接
    conn = POOL.connection()
    cursor = conn.cursor()

    # 2. 执行 SQL
    sql = "INSERT INTO test_user(name, password) VALUES (%s, %s)"
    cursor.execute(sql, (name, password))

    # 3. 提交
    conn.commit()

    # 4. 关闭连接
    cursor.close()
    conn.close()

    print(f"用户创建成功: {name}")


# =========================
# 普通接口
# =========================
@app.route("/")
def index():
    return "index"


if __name__ == "__main__":
    app.run(debug=True)

"""

命令:
flask --app <应用模块名> <命令名> [参数...]   
flask --app "06flask定制命令" create-user peng 123456

"""

image-20260602002525816

执行命令

# flask --app <应用模块名> <命令名> [参数...]   
flask --app "06flask定制命令" create-user peng 123456

image-20260602002601855

23.3django定制命令

创建一个django项目,创建user模块,创建目录user/management/commands/sayhello.py

startapp user

image-20260602002754309

在该 app 下创建命令目录和命令 user/management/commands/sayhello.py

from django.core.management.base import BaseCommand

class Command(BaseCommand):
    help = "测试命令"

    def add_arguments(self, parser):
        parser.add_argument('name', type=str)
        parser.add_argument('--times', type=int, default=1)

    def handle(self, *args, **options):
        name = options['name']
        times = options['times']

        for i in range(times):
            print(f"hello {name} - {i+1}")

image-20260602002911576

执行命令,要保证自己django环境正常,不需要启动项目

python manage.py sayhello tom --times 3
python manage.py sayhello peng --times 3

image-20260602003019527

步骤

"""
1.创建 django 项目 demo0333
2.随便创建一个 app ---> user   命令: startapp user
3.在该 app 下创建命令目录和命令 user/management/commands/sayhello.py
4.命令代码
5.执行命令
python manage.py sayhello peng --times 3
"""

24.缓存(Flask-Caching)

Flask-Caching 是 Flask 官方推荐的缓存扩展,用于将数据库查询结果、接口返回结果、函数执行结果缓存起来,从而提高系统性能、降低数据库压力。

Flask-Caching支持的缓存方式:

  • SimpleCache(开发测试):缓存放在内存中
  • RedisCache(生产最常用):缓存放在 Redis 中
  • FileSystemCache:缓存到文件
  • MemcachedCache:也是在内存中,使用较少

安装Flask-Caching

pip install Flask-Caching

image-20260602003846149

安装redis

pip install redis

image-20260602003901956

redis配置

DEBUG=True # some Flask specific configs

CACHE_TYPE="RedisCache" # Flask-Caching related configs ,可以缓存到 redis
CACHE_DEFAULT_TIMEOUT=300
CACHE_KEY_PREFIX='peng'
CACHE_REDIS_HOST='192.168.188.180'
CACHE_REDIS_PORT=6379
CACHE_REDIS_PASSWORD='redis'
CACHE_REDIS_DB=0

image-20260602003956295

代码


from flask import Flask
from flask_caching import Cache,SimpleCache
app = Flask(__name__)
app.config.from_pyfile('./settings.py')

cache = Cache(app)

@app.route('/')
def index():
    cache.set('name', 'peng')
    return 'index'

@app.route('/get')
def get():
    res=cache.get('name')
    return res

if __name__ == '__main__':
    app.run()

image-20260602004025459

测试

image-20260602004104866

25.信号

25.1简介

Flask 信号基于 Blinker 实现,本质上是一种观察者模式(Observer Pattern)

简单理解:

  • 某个事件发生(发送信号)
  • 多个函数监听这个事件(接收信号)
  • 事件触发时,所有监听函数自动执行

例如:

用户注册成功
    ↓
发送 create_user 信号
    ↓
日志记录
发送短信
发送邮件
刷新缓存

优点:

  • 解耦
  • 易扩展
  • 符合开闭原则

AOP思想

信号其实体现了 AOP(面向切面编程)思想。

即:不修改原有业务代码,在特定位置插入额外逻辑。

  • 装饰器

    @login_required
    def index():
        pass
    
  • 钩子函数

    # Flask 请求扩展:
    @app.before_request
    def before():
        pass
    
  • 信号

    signals.before_render_template.connect(func)
    

信号

日志

记录用户访问
记录异常
记录操作日志

缓存

轮播图更新
    ↓
删除Redis缓存

消息通知

用户注册
    ↓
发送短信

数据同步

用户创建成功
    ↓
同步ES
同步MQ
同步其他系统

25.2Flask内置信号

Flask 基于 Blinker 提供了一系列内置信号,用于监听请求生命周期中的关键事件。

导入方式:

from flask import (
    request_started,
    request_finished,
    request_tearing_down,
    got_request_exception,
    template_rendered,
    before_render_template,
    appcontext_pushed,
    appcontext_popped,
    appcontext_tearing_down
)

Flask内置信号

信号名称 触发时机 常见用途
request_started 请求开始,视图函数执行前 请求日志、访问统计
request_finished 响应生成后,请求结束时 响应时间统计、状态码记录
request_tearing_down 请求上下文销毁时(无论是否异常) 资源释放、关闭连接
got_request_exception 请求发生异常时 异常日志、告警通知
before_render_template 模板渲染前 修改模板上下文、调试
template_rendered 模板渲染后 模板统计、性能分析
appcontext_pushed 应用上下文创建时 初始化资源
appcontext_tearing_down 应用上下文销毁前 清理资源、提交数据
appcontext_popped 应用上下文销毁后 收尾处理

请求生命周期中的信号顺序

顺序 阶段
1 appcontext_pushed
2 request_started
3 视图函数执行
4 before_render_template
5 template_rendered
6 request_finished
7 request_tearing_down
8 appcontext_tearing_down
9 appcontext_popped

示例

from flask import (
    Flask,
    render_template,
    request,
    g,
    request_started,
    request_finished,
    request_tearing_down,
    got_request_exception,
    before_render_template,
    template_rendered,
    appcontext_pushed,
    appcontext_tearing_down,
    appcontext_popped
)

import time

app = Flask(__name__)


# ==========================
# 应用上下文创建
# ==========================
@appcontext_pushed.connect_via(app)
def app_ctx_push(sender, **kwargs):
    print("① appcontext_pushed -> 应用上下文创建")


# ==========================
# 请求开始
# ==========================
@request_started.connect_via(app)
def req_start(sender, **kwargs):
    print("② request_started -> 请求开始")

    g.start_time = time.time()

    print("   请求路径:", request.path)
    print("   请求方式:", request.method)


# ==========================
# 模板渲染前
# ==========================
@before_render_template.connect_via(app)
def before_render(sender, template, context, **kwargs):
    print("③ before_render_template -> 模板即将渲染")
    print("   模板名称:", template.name)


# ==========================
# 模板渲染后
# ==========================
@template_rendered.connect_via(app)
def after_render(sender, template, context, **kwargs):
    print("④ template_rendered -> 模板渲染完成")
    print("   模板名称:", template.name)


# ==========================
# 请求结束
# ==========================
@request_finished.connect_via(app)
def req_finished(sender, response, **kwargs):
    cost = time.time() - g.start_time

    print("⑤ request_finished -> 请求结束")
    print("   状态码:", response.status_code)
    print(f"   耗时:{cost:.3f}s")


# ==========================
# 请求销毁
# ==========================
@request_tearing_down.connect_via(app)
def req_teardown(sender, **kwargs):
    print("⑥ request_tearing_down -> 请求上下文销毁")


# ==========================
# 应用上下文销毁前
# ==========================
@appcontext_tearing_down.connect_via(app)
def app_teardown(sender, **kwargs):
    print("⑦ appcontext_tearing_down -> 应用上下文销毁前")


# ==========================
# 应用上下文销毁后
# ==========================
@appcontext_popped.connect_via(app)
def app_popped(sender, **kwargs):
    print("⑧ appcontext_popped -> 应用上下文销毁后")


# ==========================
# 异常监听
# ==========================
@got_request_exception.connect_via(app)
def handle_exception(sender, exception, **kwargs):
    print("★ got_request_exception -> 捕获异常")
    print("   异常信息:", exception)


# ==========================
# 正常请求
# ==========================
@app.route("/")
def index():

    time.sleep(1)

    return render_template("index.html")


# ==========================
# 异常请求
# ==========================
@app.route("/error")
def error():

    a = 1 / 0

    return "ok"


if __name__ == "__main__":
    app.run(debug=True)

index.html

<!DOCTYPE html>
<html>
<head>
    <title>首页</title>
</head>
<body>
<h1>首页</h1>
</body>
</html>

image-20260602012139194

25.3自定义信号

安装mysql相关包

pip install pymysql
pip install dbutils

image-20260602014236991

创建信号,然后给信号绑定函数,当信号触发(send)时,当前函数自动执行

image-20260602013902511

注册用户的时候发送信号

image-20260602014007166

完整代码

import datetime

from flask import Flask
from flask.signals import Namespace
import pymysql

from pool import POOL

app = Flask(__name__)

# ==================================================
# 1. 创建信号空间(Namespace)
# ==================================================
# 一个 Namespace 可以管理多个自定义信号
# 类似于一个信号容器
my_signal = Namespace()

# 创建一个名为 create_user 的信号
# 当用户注册成功后触发
create_user_signal = my_signal.signal("create_user")


# ==================================================
# 2. 监听器1:记录日志
# ==================================================
# connect装饰器表示:
# 将当前函数绑定到 create_user_signal 信号
# 当信号触发(send)时,当前函数自动执行
@create_user_signal.connect
def write_log(sender, **kwargs):
    """
    记录日志
    """

    # 获取发送信号时携带的数据
    user = kwargs.get("user")

    print("==========日志==========")
    print("新增用户成功")
    print(user)


# ==================================================
# 3. 监听器2:发送欢迎邮件
# ==================================================
@create_user_signal.connect
def send_email(sender, **kwargs):
    """
    发送邮件
    """

    user = kwargs.get("user")

    print("==========邮件==========")
    print(f"给 {user['name']} 发送欢迎邮件")


# ==================================================
# 4. 监听器3:发送欢迎短信
# ==================================================
@create_user_signal.connect
def send_sms(sender, **kwargs):
    """
    发送短信
    """

    user = kwargs.get("user")

    print("==========短信==========")
    print(f"给 {user['mobile']} 发送欢迎短信")


# ==================================================
# 公共数据库操作方法
# ==================================================
def execute(sql, *args):
    """
    执行增删改SQL
    """

    conn = None
    cursor = None

    try:
        # 从连接池获取连接
        conn = POOL.connection()

        # 返回字典格式结果
        cursor = conn.cursor(
            pymysql.cursors.DictCursor
        )

        # 执行SQL
        row = cursor.execute(sql, args)

        # 提交事务
        conn.commit()

        return row

    except Exception as e:

        # 回滚事务
        conn.rollback()

        raise e

    finally:

        # 关闭游标
        if cursor:
            cursor.close()

        # 归还连接到连接池
        if conn:
            conn.close()


# ==================================================
# 用户注册接口
# ==================================================
@app.route("/register")
def register():
    """
    模拟用户注册
    """

    # 注册用户信息
    user = {
        "name": "peng",
        "mobile": "18888888888",
        "score": 100,
        "create_time": datetime.datetime.now()
    }

    # 插入数据库
    sql = """
          INSERT INTO test_user
          (
              name,
              mobile,
              score,
              create_time
          )
          VALUES
          (
              %s,
              %s,
              %s,
              %s
          )
          """

    execute(
        sql,
        user["name"],
        user["mobile"],
        user["score"],
        user["create_time"]
    )

    # ==================================================
    # 发送信号
    # ==================================================
    # sender:信号发送者,一般写app或当前对象
    # user=user: 额外传递的数据
    # 触发后:
    #   write_log()
    #   send_email()
    #   send_sms()
    # 都会自动执行
    create_user_signal.send(
        app,
        user=user
    )

    return "注册成功"


if __name__ == '__main__':
    app.run(debug=True)

"""
DROP TABLE IF EXISTS test_user;
CREATE TABLE test_user (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(32) NOT NULL COMMENT '用户名',
    mobile VARCHAR(20) NOT NULL COMMENT '手机号',
    score INT DEFAULT 0 COMMENT '积分',
    create_time DATETIME COMMENT '创建时间'
);
"""

数据库连接池

from dbutils.pooled_db import PooledDB
import pymysql

# 创建数据库连接池对象
POOL = PooledDB(
    creator=pymysql,          # 使用 pymysql 作为数据库驱动

    maxconnections=6,         # 连接池允许的最大连接数
                              # 0 或 None 表示不限制

    mincached=2,              # 初始化连接池时,创建的最小空闲连接数

    maxcached=5,              # 连接池中最多保留的空闲连接数
                              # 超过该数量的空闲连接会被关闭

    maxshared=3,              # 最大共享连接数
                              # 对 pymysql 基本无效,因为 pymysql 不支持共享连接

    blocking=True,            # 连接池耗尽时是否阻塞等待
                              # True:等待连接释放
                              # False:立即抛出异常

    maxusage=None,            # 单个连接最大复用次数
                              # None 表示无限次复用

    setsession=[],            # 获取连接后执行的 SQL
                              # 例如:
                              # ["SET NAMES utf8mb4"]

    ping=0,                   # 检测数据库连接是否可用
                              # 0:从不检测
                              # 1:获取连接时检测
                              # 2:创建游标时检测
                              # 4:执行 SQL 时检测
                              # 7:每次都检测

    # ========================
    # MySQL 数据库连接参数
    # ========================
    host='192.168.188.180',   # MySQL服务器IP
    port=3306,                # MySQL端口
    user='root',              # 用户名
    password='root',         # 密码
    database='rbac_manager',  # 数据库名称
    charset='utf8mb4'         # 字符编码,支持emoji
)

测试

image-20260602014130153

26.SQLAlchemy

26.1简介

SQLAlchemy 是 Python 中最流行的 ORM(Object Relational Mapping,对象关系映射)框架,同时也是功能最强大的数据库工具包之一。

它的作用是:

Python对象  <------>  数据库表

让开发者用 Python 代码操作数据库,而不是直接写 SQL。

SQLAlchemy 分为两层:

SQLAlchemy
│
├── Core(核心层)
│      类似 JDBC
│      操作SQL
│
└── ORM(对象映射层)
       类似 MyBatis、Hibernate
       操作对象

核心组件

SQLAlchemy 主要由 5 个核心对象组成:

  • Engine(引擎层):SQLAlchemy 与数据库之间的总入口,管理连接池(pool)、DBAPI 驱动,负责生成 Connection

  • Connection(连接层):表示一次真实数据库连接,负责执行 SQL(execute)

  • Session(会话层 / ORM核心):ORM 的核心入口,管理对象生命周期(Unit of Work),管理事务(commit/rollback)

  • Model(模型层):数据库表的Python对象映射,定义字段、约束、关系

  • Query(查询层):描述“我要查什么数据”,生成 SQL(但不立即执行)

Python ORM 生态全景

ORM 定位 特点 适用场景
Django ORM Django内置 强绑定Django Django项目
Peewee 轻量ORM 简单、小巧 小型项目
SQLAlchemy 企业级ORM 灵活、强大、解耦 中大型系统(主流)
Tortoise ORM 异步ORM 类似Django ORM风格 FastAPI生态

四大架构组件(底层架构)

  • Engine(引擎)
  • Connection Pooling(连接池)
  • Dialect(方言)
  • SQL Expression Language(SQL表达语言)

支持不同数据库

PostgreSQL

# 默认(不推荐)
create_engine("postgresql://scott:tiger@localhost/mydatabase")
# 推荐驱动(psycopg2)
create_engine("postgresql+psycopg2://scott:tiger@localhost/mydatabase")
# 轻量驱动(pg8000)
create_engine("postgresql+pg8000://scott:tiger@localhost/mydatabase")

MySQL

# 默认(旧写法)
mysql://scott:tiger@localhost/foo
# pymysql(纯 Python)
create_engine("mysql+pymysql://scott:tiger@localhost/foo")
# mysqlclient(性能更好)
create_engine("mysql+mysqldb://scott:tiger@localhost/foo")

Oracle

create_engine("oracle://scott:tiger@127.0.0.1:1521/sidname")
create_engine("oracle+cx_oracle://scott:tiger@tnsname")

SQL Server

# pyodbc(推荐)
create_engine("mssql+pyodbc://scott:tiger@mydsn")
# pymssql(旧方案)
create_engine("mssql+pymssql://scott:tiger@hostname:port/dbname")

SQLite

# Linux/Mac
sqlite:////absolute/path/to/foo.db
# Windows
sqlite:///C:\path\to\foo.db
# 推荐写法(raw string)
create_engine(r"sqlite:///C:\path\to\foo.db")

安装

pip install sqlalchemy
pip install pymysql   

image-20260602212554998

26.2原生操作

import threading
import time

from sqlalchemy import create_engine

# mysql                 mysql    数据库类型
# pymysql               pymysql      驱动程序
# root                  用户名
# root                  密码
# 192.168.188.180       数据库IP
# 3308                  数据库端口
# rbac_manager          数据库名
# charset=utf8mb4       字符集
# 创建 Engine(自带连接池)
engine = create_engine(
    "mysql+pymysql://root:root@192.168.188.180:3306/rbac_manager?charset=utf8mb4",

    # 连接池参数
    pool_size=5,         # 连接池大小
    max_overflow=0,      # 不允许创建额外连接
    pool_timeout=30,     # 获取连接最长等待时间
    pool_recycle=3600,   # 1小时回收一次连接
    echo=False           # 是否打印SQL
)


def task():
    """
    模拟一个查询业务
    """
    conn = None
    cursor = None

    try:
        thread_name = threading.current_thread().name

        print(f"{thread_name} 开始获取连接")

        # 从连接池获取连接
        conn = engine.raw_connection()

        print(f"{thread_name} 获取连接成功")

        cursor = conn.cursor()

        sql = "select * from lqz_user"
        cursor.execute(sql)

        result = cursor.fetchall()

        print(f"{thread_name} 查询到 {len(result)} 条数据")

        # 模拟业务耗时
        time.sleep(3)

    except Exception as e:
        print(f"{thread_name} 执行异常:{e}")

    finally:
        # 关闭游标
        if cursor:
            cursor.close()

        # 归还连接到连接池
        if conn:
            conn.close()

        print(f"{thread_name} 归还连接")


if __name__ == '__main__':

    thread_list = []

    # 启动20个线程
    for i in range(20):
        t = threading.Thread(
            target=task,
            name=f"Thread-{i + 1}"
        )

        thread_list.append(t)
        t.start()

    # 等待所有线程执行结束
    for t in thread_list:
        t.join()

    print("所有线程执行完成")

image-20260602214519981

26.3CRUD

这部分比较简单,自己对照代码敲一遍基本都懂了

26.3.1数据库连接池(common.py)

注意自己的数据库连接

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, scoped_session

# =========================
# 1 Engine:数据库连接入口 + 连接池
# =========================
engine = create_engine(
    "mysql+pymysql://root:root@192.168.188.180:3306/flask_demo?charset=utf8mb4",

    pool_size=5,        # 连接池常驻连接数(复用连接)
    max_overflow=0,     # 不允许额外连接(严格控制并发)
    pool_timeout=30,    # 获取连接超时时间
    pool_recycle=3600,  # 1小时重建连接(防断开)
    echo=False          # 是否打印SQL(调试用)
)

# =========================
# 2 Session工厂:创建ORM会话
# =========================
Session = sessionmaker(bind=engine)

# =========================
# 3 scoped_session:线程/请求隔离(Web必备)
# =========================
session = scoped_session(Session)

# 核心作用:
# - 每个线程/请求一个独立Session
# - 防止数据串用
# - 常用于 Flask / Web项目

# =========================
# 4 关键点
# =========================
# session不是普通对象,是“线程代理对象”
# 用完必须:
# session.remove()

image-20260602223030121

26.3.2数据库模型(models.py)

记得运行生成数据库表

import datetime
from sqlalchemy import String, Text, ForeignKey, DateTime
from sqlalchemy.orm import (
    DeclarativeBase,
    Mapped,
    mapped_column,
    relationship
)
"""
-- 创建数据库
CREATE DATABASE IF NOT EXISTS flask_demo
CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;

-- 查看数据库
SHOW DATABASES;

-- 使用数据库
USE flask_demo;
"""

# =========================================================
# 1 Base类(所有ORM模型的根)
# =========================================================
# ✔ 所有表模型必须继承 Base
# ✔ Base 负责收集所有模型 -> 用于 create_all / drop_all
class Base(DeclarativeBase):
    pass


# =========================================================
# 2 用户表(基础单表模型)
# =========================================================
class User(Base):
    __tablename__ = "user"  # ✔ 映射数据库表名

    # === 主键 ===
    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)

    # === 普通字段 ===
    name: Mapped[str] = mapped_column(
        String(32),
        nullable=False,   # 不允许为空
        index=True        # 创建索引(提升查询速度)
    )

    email: Mapped[str] = mapped_column(
        String(32),
        unique=True       # 唯一约束(不能重复)
    )

    # === 时间字段 ===
    # 注意:不能写 datetime.now()
    # default = 函数引用(每次插入自动执行)
    ctime: Mapped[datetime.datetime] = mapped_column(
        DateTime,
        default=datetime.datetime.now
    )

    # === 可选字段 ===
    extra: Mapped[str | None] = mapped_column(Text, nullable=True)

    def __repr__(self):
        return self.name


# =========================================================
# 3 Address表(简单映射示例)
# =========================================================
class Address(Base):
    __tablename__ = "address"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)

    email_address: Mapped[str] = mapped_column(
        String(30),
        index=True   # 索引:加速查询
    )


# =========================================================
# 4 Hobby(一对多:主表)
# =========================================================
class Hobby(Base):
    __tablename__ = "hobby"

    id: Mapped[int] = mapped_column(primary_key=True)

    caption: Mapped[str] = mapped_column(
        String(50),
        default="篮球"
    )

    # =====================================================
    # relationship(ORM层关系,不是数据库字段)
    # =====================================================
    # ✔ 作用:
    #    - 不生成数据库字段
    #    - 用于Python对象之间的关联查询
    #
    # ✔ 逻辑:
    #    Hobby 1 ---> N Person
    #
    # ✔ back_populates:
    #    双向绑定(必须和 Person.hobby 对应)
    persons = relationship(
        "Person",
        back_populates="hobby"
    )

    def __repr__(self):
        return self.caption


# =========================================================
# 5 Person(一对多:从表)
# =========================================================
class Person(Base):
    __tablename__ = "person"

    id: Mapped[int] = mapped_column(primary_key=True)

    name: Mapped[str | None] = mapped_column(
        String(32),
        index=True
    )

    # =====================================================
    # 外键(数据库层真正的关系)
    # =====================================================
    # ✔ 作用:
    #    - 约束数据合法性
    #    - 建立表关系
    hobby_id: Mapped[int] = mapped_column(
        ForeignKey("hobby.id")  # 指向 Hobby 表主键
    )

    # =====================================================
    # relationship(ORM层快捷访问)
    # =====================================================
    # ✔ hobby_id 是数据库字段(存ID)
    # ✔ hobby 是 Python对象(直接拿 Hobby 对象)
    hobby = relationship(
        "Hobby",
        back_populates="persons"
    )

    def __repr__(self):
        return self.name


# =========================================================
# 6 Boy2Girl(多对多:中间表)
# =========================================================
class Boy2Girl(Base):
    __tablename__ = "boy2girl"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)

    # =====================================================
    # 两个外键:连接 Boy 和 Girl
    # =====================================================
    boy_id: Mapped[int] = mapped_column(ForeignKey("boy.id"))
    girl_id: Mapped[int] = mapped_column(ForeignKey("girl.id"))

    ctime: Mapped[datetime.datetime] = mapped_column(
        DateTime,
        default=datetime.datetime.now
    )


# =========================================================
# 7 Girl(多对多的一方)
# =========================================================
class Girl(Base):
    __tablename__ = "girl"

    id: Mapped[int] = mapped_column(primary_key=True)

    name: Mapped[str] = mapped_column(
        String(64),
        unique=True,
        nullable=False
    )

    # =====================================================
    # 多对多 relationship
    # =====================================================
    # secondary = 中间表
    # back_populates = 双向映射 Boy.girls
    #
    # ✔ 作用:
    #    Girl -> Boy(通过 boy2girl 自动桥接)
    boys = relationship(
        "Boy",
        secondary="boy2girl",
        back_populates="girls"
    )

    def __repr__(self):
        return self.name


# =========================================================
# 8 Boy(多对多另一方)
# =========================================================
class Boy(Base):
    __tablename__ = "boy"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)

    name: Mapped[str] = mapped_column(
        String(64),
        unique=True,
        nullable=False
    )

    # =====================================================
    # 多对多 ORM 映射
    # =====================================================
    girls = relationship(
        "Girl",
        secondary="boy2girl",
        back_populates="boys"
    )

    def __repr__(self):
        return self.name

# =========================================================
# 9 建表 / 删表入口
# =========================================================
if __name__ == "__main__":

    from common import engine

    # =====================================================
    # drop_all:
    #   - 删除所有表(慎用)
    # =====================================================
    Base.metadata.drop_all(engine)

    # =====================================================
    # create_all:
    #   - 根据 Base 注册的所有模型生成表
    #   - 只会“新增表”,不会修改字段
    # =====================================================
    Base.metadata.create_all(engine)

image-20260602223131903

26.3.3增删改查

import random

from sqlalchemy import select, update, delete
from sqlalchemy.orm import Session

from common import engine
from models import User


# =========================
# 1 Session 上下文管理
# =========================
def get_session():
    return Session(engine)


# =========================
# 2 查询(新版本)
# =========================
def query_user_by_name(name: str):
    with get_session() as session:
        stmt = select(User).where(User.name == name)
        result = session.execute(stmt)
        return result.scalars().all()


# =========================
# 3 新增(单个)
# =========================
def add_user(name: str, email: str, extra: str | None = None):
    with get_session() as session:
        try:
            user = User(
                name=name,
                email=email,
                extra=extra  # None 就写入 NULL
            )
            session.add(user)
            session.commit()
        except Exception:
            session.rollback()
            raise


# =========================
# 4 新增(多个)
# =========================
def add_users(users: list[User]):
    with get_session() as session:
        try:
            session.add_all(users)
            session.commit()
        except Exception:
            session.rollback()
            raise


# =========================
# 5 更新(推荐写法)
# =========================
def update_user(old_name: str, new_name: str, new_email: str):
    with get_session() as session:
        try:
            stmt = (
                update(User)
                .where(User.name == old_name)
                .values(name=new_name, email=new_email)
            )
            session.execute(stmt)
            session.commit()
        except Exception:
            session.rollback()
            raise


# =========================
# 6 删除(推荐写法)
# =========================
def delete_user(name: str):
    with get_session() as session:
        try:
            stmt = delete(User).where(User.name == name)
            session.execute(stmt)
            session.commit()
        except Exception:
            session.rollback()
            raise

def seed_users():
    with Session(engine) as session:
        try:
            users = []

            for i in range(1, 101):
                user = User(
                    name=f"user_{i}",
                    email=f"user_{i}@test.com",
                    extra=f"test_data_{random.randint(1000, 9999)}"
                )
                users.append(user)
            session.add_all(users)
            session.commit()

            print("✅ 成功插入100条测试数据")

        except Exception as e:
            session.rollback()
            print("❌ 插入失败:", e)
            raise

def query_user_by_name_like(keyword: str):
    with Session(engine) as session:
        stmt = select(User).where(User.name.like(f"%{keyword}%"))
        result = session.scalars(stmt).all()
        return result

def to_dict(user):
    return {
        "id": user.id,
        "name": user.name,
        "email": user.email,
        "ctime": user.ctime,
        "extra": user.extra,
    }

if __name__ == "__main__":
    # # 1.seed_users()
    res = query_user_by_name("user_1")
    print([to_dict(u) for u in res])

    # # 2.add
    # add_user("tom","tom@123,com")
    # res = query_user_by_name("tom")
    # print([to_dict(u) for u in res])

    # # 3.add
    # add_user("jack", "jack@123,com","普通用户")
    # res = query_user_by_name("jack")
    # print([to_dict(u) for u in res])

    # # 4.add_users
    # users = []
    # for i in range(1, 10):
    #     user = User(
    #         name=f"peng_{i}",
    #         email=f"peng_{i}@test.com",
    #         extra=f"test_data_{random.randint(1000, 9999)}"
    #     )
    #     users.append(user)
    # add_users(users)
    # res = query_user_by_name_like("peng")
    # print([to_dict(u) for u in res])

    # # 5.update_user
    # update_user(
    #     old_name="peng",
    #     new_name="pengshuai",
    #     new_email="peng@888.comm"
    # )
    # res = query_user_by_name("pengshuai")
    # print([to_dict(u) for u in res])

    # 6.delete_user
    delete_user("peng_9")
    res = query_user_by_name_like("peng")
    print([to_dict(u) for u in res])

image-20260602223246452

26.4一对多

26.4.1一对多配置

Hobby(1的一方)

  • 一个 Hobby 对应多个 Person

  • ORM自动帮你做“列表关系”

persons = relationship(
    "Person",
    back_populates="hobby"
)

image-20260602234202766

Person(N的一方)

  • 每个 Person 只属于一个 Hobby

  • ORM直接返回对象,而不是 id

hobby = relationship(
    "Hobby",
    back_populates="persons"
)

image-20260602234259913

back_populates 的作用

双向绑定

Hobby.persons  <---->  Person.hobby

26.4.2一对多操作

import random

from sqlalchemy import select, update, delete
from sqlalchemy.orm import Session

from common import engine
from models import User, Hobby, Person


# =========================
# 1 Session 上下文管理
# =========================
def get_session():
    return Session(engine)


def add_hobby(caption: str):
    with get_session() as session:
        try:
            h = Hobby(
                caption=caption,
            )
            session.add(h)
            session.commit()
        except Exception:
            session.rollback()
            raise


def get_hobbys():
    with get_session() as session:
        hs = select(Hobby)
        result = session.execute(hs)
        return result.scalars().all()


def to_dict(obj):
    return {
        c.name: getattr(obj, c.name)
        for c in obj.__table__.columns
    }


def add_person(name: str, hobby_id: int):
    with get_session() as session:
        try:
            person = Person(
                name=name,
                hobby_id=hobby_id,
            )
            session.add(person)
            session.commit()
        except Exception:
            session.rollback()
            raise


def get_person():
    with get_session() as session:
        hs = select(Person)
        result = session.execute(hs)
        return result.scalars().all()


if __name__ == "__main__":
    # 1.先 hobby 再添加 person,笨办法
    # add_hobby("唱")
    # add_hobby("跳")
    # add_hobby("rap")
    # hobbys = get_hobbys()
    # h_ids = [h.id for h in hobbys ]
    # add_person(name="peng",hobby_id= random.choice(h_ids))
    res = get_person()
    print([to_dict(u) for u in res])

    # 2.一对多添加:通过表关系
    # with get_session() as session:
    #     try:
    #         person = Person(name='tom', hobby=Hobby(caption="跳"))
    #         session.add(person)
    #         session.commit()
    #     except Exception:
    #         session.rollback()
    #         raise
    # res = get_person()
    # print([to_dict(u) for u in res])

    # 3.一对多查询
    with get_session() as session:
        person = session.query(Person).filter_by(id=2).first()
        print(person.id)
        print(person.name)
        print(person.hobby_id)
        print(person.hobby)

    # 4.夸表查询
    with get_session() as session:
        try:
            # hobby = session.execute(
            #         select(Hobby).where(Hobby.name == "唱")
            #     ).scalar_one()
            h = session.query(Hobby).filter_by(caption="唱").first()
            person1 = Person(name='qwe', hobby=h)
            person2 = Person(name='asd', hobby=h)
            person3 = Person(name='zxc', hobby=h)
            session.add_all([person1,person2,person3])
            session.commit()
        except Exception:
            session.rollback()
            raise
    res = get_person()
    print([to_dict(u) for u in res])

    with get_session() as session:
        hobby = session.query(Hobby).filter_by(id=1).first()
        print(hobby.id)
        print(hobby.caption)
        print(hobby.persons)
        print(hobby.persons[0].id)

image-20260602234535761

26.5threading.local()

local() 的意义是:每个线程拥有“独立的属性空间”,互不干扰

也就是说:

  • 线程 A:l.session = "aaa"

  • 线程 B:l.session = "bbb"

  • 两者互相看不见

示例

import uuid
from threading import local, Thread
import time

# threading.local() 创建一个“线程隔离的对象”
# 它的特点是:不同线程访问同一个变量名,但实际存储是独立的
# 类似:每个线程都有自己的“字典空间”
l = local()

def worker(name, value):
    """
    线程执行函数
    name: 线程名称(用于打印)
    value: 当前线程要设置的 session 值
    """

    # 给当前线程绑定一个属性 session
    # 注意:这个 session 只属于当前线程,不会影响其他线程
    # 这里的 session 和 tokne 可以随机命名
    l.session = value
    l.token = uuid.uuid4()

    # 模拟线程执行耗时操作
    time.sleep(1)

    # 读取当前线程自己的 session
    # 不同线程这里取到的值是互不干扰的
    print(f"{name}: {l.session}")
    print(f"{l.token }")


# 创建线程 t1,传入参数 ("thread-1", "aaa")
t1 = Thread(target=worker, args=("thread-1", "aaa"))

# 创建线程 t2,传入参数 ("thread-2", "bbb")
t2 = Thread(target=worker, args=("thread-2", "bbb"))

# 启动线程1
t1.start()

# 启动线程2
t2.start()

# 等待线程1执行完成
t1.join()

# 等待线程2执行完成
t2.join()

image-20260602235406837

26.6scoped线程安全

这里提供2中线程安全写法

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, scoped_session

from models import User

engine = create_engine("mysql+pymysql://root:root@192.168.188.180:3306/flask_demo?charset=utf8mb4")

# 1️.创建 session 工厂(注意:不是 Session 实例)
SessionFactory = sessionmaker(bind=engine)

# 2️.创建线程安全 session
session = scoped_session(SessionFactory)

# 方式一:scoped_session(经典线程安全方案)
db = session()
result = db.query(User).all()
print(result)

# 方式二:Session(engine) + 手动管理(2.x 推荐)
with SessionFactory() as session:
   result= session.query(User).all()
   print(result)

# 之前:不是线程安全的---》多个线程公用这一个
# from sqlalchemy.orm import Session
# session=Session(engine)

image-20260603000108030

26.7flask使用scoped_session

26.7.1配置(common.py)

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, scoped_session

# =========================
# 1 Engine:数据库连接入口 + 连接池
# =========================
engine = create_engine(
    "mysql+pymysql://root:root@192.168.188.180:3306/flask_demo?charset=utf8mb4",

    pool_size=5,        # 连接池常驻连接数(复用连接)
    max_overflow=0,     # 不允许额外连接(严格控制并发)
    pool_timeout=30,    # 获取连接超时时间
    pool_recycle=3600,  # 1小时重建连接(防断开)
    echo=False          # 是否打印SQL(调试用)
)

# =========================
# 2 Session工厂:创建ORM会话
# =========================
SessionFactory  = sessionmaker(bind=engine)

# =========================
# 3 scoped_session:线程/请求隔离(Web必备)
# =========================
Session = scoped_session(SessionFactory )

# 核心作用:
# - 每个线程/请求一个独立Session
# - 防止数据串用
# - 常用于 Flask / Web项目

# =========================
# 4 关键点
# =========================
# session不是普通对象,是“线程代理对象”
# 用完必须:
# session.remove()

# # 提供获取 session 方法(可选)
# def get_session():
#     return Session()

# # 请求结束后必须清理
# def remove_session(exception=None):
#     Session.remove()

image-20260603001626717

26.7.2flask使用

from flask import Flask, jsonify

from common import Session
from models import User

app = Flask(__name__)


def to_dict(obj):
    return {
        c.name: getattr(obj, c.name)
        for c in obj.__table__.columns
    }

@app.get("/user/<int:user_id>")
def index(user_id):

    try:
        session = Session()
        user = session.query(User).filter(User.id == user_id).first()

        if not user:
            return jsonify({
                "code": 404,
                "message": "user not found"
            }), 404

        return jsonify({
            "code": 200,
            "data":  to_dict(user)
        })

    finally:
        Session.close()


if __name__ == "__main__":
    app.run(threaded=True)

image-20260603001731705

26.8多对多

26.8.1配置

中间表

作用 说明
关系拆解 把 N↔N 拆成 1↔N + N↔1
存关系 只存 ID,不存对象
无业务字段 通常只做“连接桥梁”
# =========================================================
# 6 Boy2Girl(多对多:中间表)
# =========================================================
class Boy2Girl(Base):
    __tablename__ = "boy2girl"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)

    # =====================================================
    # 两个外键:连接 Boy 和 Girl
    # =====================================================
    boy_id: Mapped[int] = mapped_column(ForeignKey("boy.id"))
    girl_id: Mapped[int] = mapped_column(ForeignKey("girl.id"))

    ctime: Mapped[datetime.datetime] = mapped_column(
        DateTime,
        default=datetime.datetime.now
    )

image-20260603005029555

Girl

  • Girl 不直接连 Boy

  • 通过 boy2girl 中间表自动关联

# =========================================================
# 7 Girl(多对多的一方)
# =========================================================
class Girl(Base):
    __tablename__ = "girl"

    id: Mapped[int] = mapped_column(primary_key=True)

    name: Mapped[str] = mapped_column(
        String(64),
        unique=True,
        nullable=False
    )

    # =====================================================
    # 多对多 relationship
    # =====================================================
    # secondary = 中间表
    # back_populates = 双向映射 Boy.girls
    #
    # ✔ 作用:
    #    Girl -> Boy(通过 boy2girl 自动桥接)
    boys = relationship(
        "Boy",
        secondary="boy2girl",
        back_populates="girls"
    )

    def __repr__(self):
        return self.name

image-20260603005040086

Boy

# =========================================================
# 8 Boy(多对多另一方)
# =========================================================
class Boy(Base):
    __tablename__ = "boy"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)

    name: Mapped[str] = mapped_column(
        String(64),
        unique=True,
        nullable=False
    )

    # =====================================================
    # 多对多 ORM 映射
    # =====================================================
    girls = relationship(
        "Girl",
        secondary="boy2girl",
        back_populates="boys"
    )

    def __repr__(self):
        return self.name

image-20260603005100956

26.8.2操作

方式一不通过关系表,手动创建

方式二通过关系表自动创建

from common import Session
from models import Boy, Girl, Boy2Girl

session = Session()

# =========================
# 方式一:手动中间表
# =========================、
# 1.添加 Boy Girl
# boy = Boy(name="胡歌")
# girl1 = Girl(name="赵露思")
# girl2 = Girl(name="田曦薇")

# session.add_all([boy, girl1, girl2])
# session.commit()

# 2.添加映射关系
# rel1 = Boy2Girl(boy_id=boy.id, girl_id=girl1.id)
# rel2 = Boy2Girl(boy_id=boy.id, girl_id=girl2.id)
# session.add(rel1)
# session.add(rel2)
# session.commit()

# 3.测试查询
# boy = session.query(Boy).filter_by(name="古天乐").first()
# print(boy)
# print(boy.girls)


# =========================
# 方式二:ORM关系写法(推荐)
# =========================
boy = Boy(name="peng")
girl1 = Girl(name="赵今麦")
girl2 = Girl(name="张子枫")
girl3 = Girl(name="宋祖儿")
boy.girls = [girl1, girl2, girl3]
session.add(boy)
session.commit()

boy = session.query(Boy).filter_by(name="peng").first()
print(boy)
print(boy.girls)

girl = session.query(Girl).filter_by(name="赵今麦").first()
print(girl)
print(girl.boys)

image-20260603005309305

26.9更多查询操作

from sqlalchemy import and_, or_, func

from common import Session
from models import User, Person, Hobby, Girl, Boy, Boy2Girl

session = Session()


def print_res(title, res):
    print(f"\n===== {title} =====")

    if res is None:
        print(None)
        return

    if not isinstance(res, list):
        res = [res]

    for item in res:
        if hasattr(item, "__table__"):
            data = {c.name: getattr(item, c.name) for c in item.__table__.columns}
            print(data)
        else:
            print(item)


# =====================================================
# filter_by(简单等值查询)
# =====================================================
res = session.query(User).filter_by(name="张三").first()
print_res("filter_by", res)

# =====================================================
# filter / where(推荐)
# =====================================================
res = session.query(User).filter(User.id >= 2).all()
print_res("filter", res)

res = session.query(User).where(User.id >= 2).all()
print_res("where", res)

# =====================================================
# between
# =====================================================
res = session.query(User).filter(User.id.between(1, 10)).all()
print_res("between", res)

# =====================================================
# in / not in
# =====================================================
res = session.query(User).filter(User.id.in_([1, 3, 5])).all()
print_res("in", res)

res = session.query(User).filter(~User.id.in_([1, 3, 5])).all()
print_res("not in", res)

# =====================================================
# 子查询 in
# =====================================================
# 先查出 name=刘备 的所有 id,再用这些 id 去过滤 User 表
sub = session.query(User.id).filter_by(name="刘备")
res = session.query(User).filter(User.id.in_(sub)).all()
print_res("subquery:", res)

# =====================================================
# and / or
# =====================================================
res = session.query(User).filter(
    and_(User.id > 1, User.name == "诸葛亮")
).all()
print_res("and:", res)

res = session.query(User).filter(
    or_(User.id <= 2, User.name == "李世民")
).all()
print_res("or:", res)

# =====================================================
# like 模糊查询
# =====================================================
res = session.query(User).filter(User.name.like("%李%")).all()
print_res("like:", res)

# =====================================================
# 分页
# =====================================================
page = 2
size = 4
res = session.query(User)[(page - 1) * size: page * size]
print_res("page:", res)

# =====================================================
# 排序
# =====================================================
res = session.query(User).order_by(User.name.asc()).all()
print_res("asc:", res)

res = session.query(User).order_by(User.name.desc(), User.id.asc()).all()
print_res("desc multi:", res)

# =====================================================
# 🔟 分组 + 聚合
# =====================================================
# 这个示例不好,没有分组条件
res = session.query(
    User.name,
    func.max(User.id),
    func.sum(User.id),
    func.min(User.id),
    func.count(User.id)
).group_by(User.name).all()

print_res("group by:", res)

# =====================================================
# having
# =====================================================
res = session.query(
    User.name,
    func.sum(User.id).label("total")
).group_by(User.name).having(func.sum(User.id) > 10).all()

print_res("having:", res)

# =====================================================
# 连表查询(笛卡尔积方式)
# =====================================================
res = session.query(Person, Hobby).filter(
    Person.hobby_id == Hobby.id
).all()

print_res("cartesian join:", res)

# =====================================================
# join(推荐)
# =====================================================
res = session.query(Person).join(Hobby).all()
print_res("inner join:", res)

res = session.query(Person).join(Hobby, isouter=True).all()
print_res("left join:", res)

res = session.query(Hobby).join(Person, isouter=True).all()
print_res("right join:", res)

# =====================================================
# 多对多查询(Boy ↔ Girl)
# =====================================================
g = session.query(Girl).first()
if g:
    print_res("girl -> boys:", g.boys)

b = session.query(Boy).first()
print(b)
if b:
    print_res("boy -> girls:", b.girls)

# =====================================================
# UNION
# =====================================================
# 并集
q1 = session.query(Person.name).filter(Person.id > 10)
q2 = session.query(Person.name).filter(Person.id < 5)

res = q1.union(q2).all()
print("union:", res)

res = q1.union_all(q2).all()
print("union_all:", res)

# =====================================================
# 提交(如果有增删改)
# =====================================================
session.commit()
session.close()

image-20260603015847944

26.10集成flask

安装相关包

pip install flask sqlalchemy pymysql
pip install redis  
pip install Flask-Caching
pip install flask-session

26.10.1数据库(common.py)

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, scoped_session

# =========================
# 1 Engine:数据库连接入口 + 连接池
# =========================
engine = create_engine(
    "mysql+pymysql://root:root@192.168.188.180:3306/flask_demo?charset=utf8mb4",

    pool_size=5,        # 连接池常驻连接数(复用连接)
    max_overflow=0,     # 不允许额外连接(严格控制并发)
    pool_timeout=30,    # 获取连接超时时间
    pool_recycle=3600,  # 1小时重建连接(防断开)
    echo=False          # 是否打印SQL(调试用)
)

# =========================
# 2 Session工厂:创建ORM会话
# =========================
SessionFactory  = sessionmaker(bind=engine)

# =========================
# 3 scoped_session:线程/请求隔离(Web必备)
# =========================
session = scoped_session(SessionFactory )

image-20260603204814529

26.10.2数据库模型(models.py)

import datetime
from sqlalchemy import String, Text, ForeignKey, DateTime
from sqlalchemy.orm import (
    DeclarativeBase,
    Mapped,
    mapped_column,
    relationship
)
"""
-- 创建数据库
CREATE DATABASE IF NOT EXISTS flask_demo
CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;

-- 查看数据库
SHOW DATABASES;

-- 使用数据库
USE flask_demo;
"""

# =========================================================
# 1 Base类(所有ORM模型的根)
# =========================================================
# ✔ 所有表模型必须继承 Base
# ✔ Base 负责收集所有模型 -> 用于 create_all / drop_all
class Base(DeclarativeBase):
    pass


# =========================================================
# 2 用户表(基础单表模型)
# =========================================================
class User(Base):
    __tablename__ = "user"  # ✔ 映射数据库表名

    # === 主键 ===
    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)

    # === 普通字段 ===
    name: Mapped[str] = mapped_column(
        String(32),
        nullable=False,   # 不允许为空
        index=True        # 创建索引(提升查询速度)
    )

    email: Mapped[str] = mapped_column(
        String(32),
        unique=True       # 唯一约束(不能重复)
    )

    # === 时间字段 ===
    # 注意:不能写 datetime.now()
    # default = 函数引用(每次插入自动执行)
    ctime: Mapped[datetime.datetime] = mapped_column(
        DateTime,
        default=datetime.datetime.now
    )

    # === 可选字段 ===
    extra: Mapped[str | None] = mapped_column(Text, nullable=True)

    def __repr__(self):
        return self.name


# =========================================================
# 3 Address表(简单映射示例)
# =========================================================
class Address(Base):
    __tablename__ = "address"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)

    email_address: Mapped[str] = mapped_column(
        String(30),
        index=True   # 索引:加速查询
    )

image-20260603204824871

26.10.3设置(settings.py)

from redis import Redis

DEBUG = True
DEBUG = True  # some Flask specific configs
CACHE_TYPE = "RedisCache"  # Flask-Caching related configs ,可以缓存到redis
CACHE_DEFAULT_TIMEOUT = 300

CACHE_KEY_PREFIX = 'peng'
CACHE_REDIS_HOST = '192.168.188.180'
CACHE_REDIS_PORT = 6379
CACHE_REDIS_PASSWORD = 'redis'
CACHE_REDIS_DB = 0

SESSION_TYPE = 'redis'
SESSION_REDIS = Redis(host='192.168.188.180', port='6379', password="redis")
SESSION_KEY_PREFIX = 'peng'

image-20260603204900697

26.10.4视图(views.py)

from flask import Blueprint, render_template,request

from src.common import session
from src.models import User

main_bl = Blueprint('main', __name__)

@main_bl.route("/")
def index():
    return render_template('index.html')


@main_bl.route("/users")
def users():

    try:
        users = session.query(User).all()

        return {
            "data": [
                {
                    "id": u.id,
                    "name": u.name
                }
                for u in users
            ]
        }

    finally:
        session.close()

image-20260603204926826

26.10.5初始化(__init__.py)

from flask import Flask

app=Flask(__name__,template_folder='../templates',static_folder='../statics')
# 1 配置文件
app.config.from_pyfile('./settings.py')

# 2 使用缓存
from flask_caching import Cache,SimpleCache
cache = Cache(app)

# 3 使用flask-session
from flask_session import Session
Session(app)

# # 4 自定制命令
# import click
# @app.cli.command("create-user")
# @click.argument("name")
# @click.argument("password")
# def create_user(name,password):
#     print(name)

# 5 蓝图
from . import views
app.register_blueprint(views.main_bl)


# 6 使用sqlalchemy
# common.py ---> session = Session()

image-20260603205058427

26.10.6测试

image-20260603205132507

26.11数据库迁移(flask-migrate)

26.11.1简介

Flask-Migrate 是 Flask 项目中最常用的数据库版本管理工具,本质上是对 SQLAlchemy 官方迁移工具 Alembic 的封装。

作用:当你的 ORM 模型发生变化时,不需要手工写 SQL 去修改表结构,而是自动生成数据库变更脚本。

安装

# 迁移核心包
pip install flask-sqlalchemy
pip install flask-migrate

# 其他需要的包
pip install pymysql
pip install flask_caching
pip install flask_session

image-20260603215312972

迁移命令

命令 作用
flask db init 初始化迁移系统
flask db migrate 根据模型变化生成“迁移脚本”
flask db upgrade 执行迁移脚本,真正改数据库

26.11.2数据库配置(databse.py)

提前创建好数据库

from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from . import app

db = SQLAlchemy(app)
migrate = Migrate(app, db) # flask 就会多出好几个命令---》

"""
-- 创建数据库
CREATE DATABASE IF NOT EXISTS flask_demo
CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;

-- 查看数据库
SHOW DATABASES;

-- 使用数据库
USE flask_demo;
"""

image-20260603215548300

26.11.3数据库实体(models.py)

这里留2个字段一会再迁移

import datetime
from .databse import db

class TestUser(db.Model):
    __tablename__ = 'user'  # 表名
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.String(32), index=True, nullable=False)  # name列varchar32,索引,不可为空
    email = db.Column(db.String(32), unique=True)  # email 列,varchar32,唯一

    # 新加字段
    # password = db.Column(db.String(32), nullable=False)
    # extra = db.Column(db.Text, nullable=True)

    # datetime.datetime.now不能加括号,加了括号,以后永远是当前时间
    ctime = db.Column(db.DateTime, default=datetime.datetime.now)



    # def __str__(self):
    #     return self.name

    def __repr__(self):
        return self.name

image-20260603215632721

26.11.4配置(settings.py)

from redis import Redis

DEBUG = True
DEBUG = True  # some Flask specific configs
CACHE_TYPE = "RedisCache"  # Flask-Caching related configs ,可以缓存到redis
CACHE_DEFAULT_TIMEOUT = 300

CACHE_KEY_PREFIX = 'peng'
CACHE_REDIS_HOST = '192.168.188.180'
CACHE_REDIS_PORT = 6379
CACHE_REDIS_PASSWORD = 'redis'
CACHE_REDIS_DB = 0

SESSION_TYPE = 'redis'
SESSION_REDIS = Redis(host='192.168.188.180', port='6379', password="redis")
SESSION_KEY_PREFIX = 'peng'

SQLALCHEMY_DATABASE_URI = "mysql+pymysql://root:root@192.168.188.180:3306/flask_demo?charset=utf8mb4"
SQLALCHEMY_POOL_SIZE = 5
SQLALCHEMY_POOL_TIMEOUT = 30
SQLALCHEMY_POOL_RECYCLE = -1
# 追踪对象的修改并且发送信号
SQLALCHEMY_TRACK_MODIFICATIONS = False

image-20260603215713032

25.11.5初始化(__init__.py)

from flask import Flask

app=Flask(__name__,template_folder='../templates',static_folder='../statics')
# 1 配置文件
app.config.from_pyfile('./settings.py')

# 2 使用缓存
from flask_caching import Cache,SimpleCache
cache = Cache(app)

# 3 使用flask-session
from flask_session import Session
Session(app)

# # 4 自定制命令
# import click
# @app.cli.command("create-user")
# @click.argument("name")
# @click.argument("password")
# def create_user(name,password):
#     print(name)

# 5 蓝图
from . import views
app.register_blueprint(views.main_bl)

image-20260603215801209

25.11.6视图层(views.py)

主要实现几个接口测试

image-20260603215827645

25.11.7迁移

先创建好数据库,然后执行迁移命令

flask db init  
flask db migrate  
flask db upgrade

image-20260603220111681

数据库表也成功创建

image-20260603220134127

25.11.8再次迁移

这次增加2个字段,再次迁移

image-20260603220257424

表新增字段成功

image-20260603220325664

25.11.9测试

测试addusers接口可以正常新增和查询

image-20260603220807462

25.12开源项目

项目地址

https://gitee.com/lunarlanding/pear-admin-flask

image-20260603231116094

创建数据库

-- 创建数据库
CREATE DATABASE IF NOT EXISTS pear_admin_flask
CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;

-- 查看数据库
SHOW DATABASES;

-- 使用数据库
USE pear_admin_flask

image-20260603231125973

虚拟环境配置

# 1. 查看当前系统已安装的 Python 版本及路径
py -0p

# 2. 使用 Python 3.11 创建虚拟环境(推荐用于 Flask 老项目)
py -3.11 -m venv venv

# 3. 激活虚拟环境(进入项目专属 Python 环境)
venv\Scripts\activate 

# 4. 安装项目依赖(只安装到当前 venv,不影响系统 Python)
pip install -r requirements.txt

# 5.退出命令 deactivate

image-20260603230846913

迁移命令

flask db init
flask db migrate
flask db upgrade
flask admin init

image-20260603231338131

命令启动

flask --app app.py run -h 0.0.0.0 -p 8000 --debug
# 或者
python app.py

image-20260603232055735

解释器启动

image-20260603232017708

📌 创作不易,感谢支持!

每一篇内容都凝聚了心血与热情,如果我的内容对您有帮助,欢迎请我喝杯咖啡☕,您的支持是我持续分享的最大动力!

💬 加入交流群(QQ群):576434538

微信打赏

posted @ 2026-06-09 01:29  peng_boke  阅读(48)  评论(0)    收藏  举报