术→技巧, 研发

Flask学习之上下文

钱魏Way · · 1 次浏览

在 flask 中,视图函数需要知道它执行情况的请求信息(请求的 url,参数,方法等)以及应用信息(应用中初始化的数据库等),才能够正确运行。最直观地做法是把这些信息封装成一个对象,作为参数传递给视图函数。但是这样的话,所有的视图函数都需要添加对应的参数,即使该函数内部并没有使用到它。

Flask上下文简介

在Flask中,上下文分为应用上下文和请求上下文,分别存储了不同的信息。

应用上下文

应用上下文是一个全局变量,用于存储当前应用程序的信息。在处理请求之前,Flask会将应用程序对象存储在应用上下文中,并在应用程序运行期间一直保持不变。应用上下文是线程局部的,即每个线程都有自己的应用上下文。

应用上下文包含了以下属性:

  • app:应用程序对象,表示当前应用程序的实例。
  • g:应用程序全局变量,用于在同一请求的不同函数之间共享数据。

在Flask中,我们可以通过current_app和g变量来访问应用上下文中的信息。例如:

from flask import Flask, current_app, g

app = Flask(__name__)
app.config['MY_CONFIG'] = 'my_config_value'

with app.app_context():
    print(current_app.config['MY_CONFIG'])
    g.my_data = 'my_data_value'

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

在上面的示例中,我们使用current_app.config属性获取了应用程序对象中的MY_CONFIG配置信息,并使用g变量共享了一个名为my_data的数据。

请求上下文

请求上下文是一个全局变量,用于存储当前请求的信息。在处理请求时,Flask会将请求信息存储在请求上下文中,并在请求处理完成后将其清除。请求上下文是线程局部的,即每个线程都有自己的请求上下文。

请求上下文包含了以下属性:

  • request:请求对象,包含了HTTP请求的所有信息,如请求方法、请求头、请求参数等。
  • session:会话对象,用于在多个请求之间存储和共享数据。
  • g:应用程序全局变量,用于在同一请求的不同函数之间共享数据。

在Flask中,我们可以通过request和session对象来访问当前请求的信息和会话数据。例如:

from flask import Flask, request, session

app = Flask(__name__)
app.secret_key = 'my_secret_key'

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']
    if username == 'admin' and password == '123456':
        session['logged_in'] = True
        return 'Login success!'
    else:
        return 'Login failed!'

@app.route('/profile')
def profile():
    if session.get('logged_in'):
        return f'Welcome, {session["username"]}!'
    else:
        return 'Please log in first!'

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

在上面的示例中,我们使用request.form属性获取了POST请求中的表单数据,使用session对象存储了登录状态信息。在/profile路由中,我们通过session.get()方法获取了logged_in键对应的值。

request属性及方法

在Flask中,请求对象request是一个全局变量,表示当前HTTP请求的所有信息。请求对象request提供了许多属性和方法,常见的属性和方法如下:

属性:

  • method:请求方法,如GET、POST等。
  • args:URL参数,以字典形式存储。
  • form:表单数据,以字典形式存储。
  • files:上传的文件,以字典形式存储。
  • cookies:请求中的Cookie,以字典形式存储。
  • headers:请求头,以字典形式存储。
  • json:请求的JSON数据,以字典形式存储。
  • data:请求的原始数据,以字节串形式存储。
  • url:请求的URL。
  • base_url:请求的基本URL。
  • script_root:脚本根路径。
  • path:请求的路径。
  • full_path:请求的完整路径。
  • host:请求的主机名。
  • remote_addr:请求的远程IP地址。

方法:

  • get:获取指定键的值,如果键不存在,则返回默认值。
  • get_json:获取请求中的JSON数据,并转换为Python对象。
  • get_data:获取请求中的原始数据。
  • get_form:获取请求中的表单数据。
  • get_args:获取请求中的URL参数。
  • get_cookies:获取请求中的Cookie。
  • get_files:获取请求中的上传文件。
  • get_headers:获取请求头。
  • get_json:获取请求中的JSON数据。
  • get_json(force=False, silent=False, cache=True):获取请求中的JSON数据,并转换为Python对象。
  • stream:获取请求中的数据流。
  • is_json:判断请求是否是JSON类型。
  • is_xhr:判断请求是否是XMLHttpRequest类型。
  • is_secure:判断请求是否是安全的,即是否使用了HTTPS协议。
  • is_permanent_redirect:判断请求是否是永久性重定向。
  • is_redirect:判断请求是否是重定向。
  • is_fresh:判断请求是否是新的,即缓存是否有效。
  • is_multiprocess:判断请求是否是多进程。
  • is_multithread:判断请求是否是多线程。
  • is_run_once:判断请求是否只运行一次。
  • on_json_loading_failed:处理JSON数据加载失败的情况。

我们可以通过访问请求对象request的属性和方法来获取和设置请求相关的信息,如下所示:

 

from flask import Flask, request

app = Flask(__name__)

@app.route('/')
def index():
    method = request.method
    args = request.args
    form = request.form
    files = request.files
    cookies = request.cookies
    headers = request.headers
    json_data = request.json
    data = request.data
    url = request.url
    base_url = request.base_url
    script_root = request.script_root
    path = request.path
    full_path = request.full_path
    host = request.host
    remote_addr = request.remote_addr
    # do something with the request
    return 'Hello, World!'

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

在上面的示例中,我们使用了请求对象request的多个属性和方法获取和设置了请求相关的信息,包括获取请求方法、获取请求参数、获取请求头等等。在/路由中,我们返回了一个简单的欢迎消息。

Session属性及方法

在Flask中,会话对象session是一个全局变量,用于在多个请求之间存储和共享数据。会话对象session提供了许多属性和方法,常见的属性和方法如下:

属性:

  • modified:会话是否被修改过。
  • new:会话是否是新建的。
  • permanent:会话是否是永久性的,即是否在客户端保存为长期Cookie。
  • expires:会话过期时间。
  • timeout:会话超时时间。
  • path:Cookie的路径。
  • domain:Cookie的域名。
  • secure:Cookie是否只能通过HTTPS协议传输。
  • httponly:Cookie是否只能通过HTTP协议传输,不能被JavaScript访问。
  • keys:会话密钥列表,用于对会话数据进行加密。
  • get:获取指定键的值,如果键不存在,则返回默认值。
  • pop:弹出指定键的值,并从会话对象中删除该键值对。
  • setdefault:获取指定键的值,如果键不存在,则设置默认值并返回。
  • update:更新会话对象的键值对。
  • clear:清空会话对象的所有键值对。

方法:

  • pop:弹出并返回指定键的值,并从会话对象中删除该键值对。
  • setdefault:获取指定键的值,如果键不存在,则设置默认值并返回。
  • update:更新会话对象的键值对。
  • clear:清空会话对象的所有键值对。

我们可以通过访问会话对象session的属性和方法来获取和设置会话相关的信息,如下所示:

from flask import Flask, session

app = Flask(__name__)
app.secret_key = 'my_secret_key'

@app.route('/')
def index():
    session['username'] = 'admin'
    session.modified = True
    session.permanent = True
    session.timeout = 3600
    session.path = '/'
    session.domain = '.example.com'
    session.secure = True
    session.httponly = True
    session_key = session.keys()[0]
    session.pop('username')
    session.setdefault('username', 'admin')
    session.update({'username': 'admin', 'password': 'admin123'})
    session.clear()
    # do something with the session
    return 'Hello, World!'

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

在上面的示例中,我们使用了会话对象session的多个属性和方法设置和获取了会话相关的信息,包括设置会话变量、设置会话的过期时间、弹出会话变量、清空会话等。在/路由中,我们向session对象添加了一个名为username的键值对,并返回了一个简单的欢迎消息。

current_app的属性及方法

在Flask中,current_app是一个代理对象,表示当前应用程序的实例。current_app提供了许多属性和方法,常见的属性和方法如下:

属性:

  • name:应用程序的名称。
  • import_name:应用程序的导入名称。
  • config:应用程序的配置对象。
  • testing:是否处于测试模式。
  • debug:是否处于调试模式。
  • permanent_session_lifetime:会话的永久性存储时间。
  • jinja_env:Jinja2环境对象。
  • logger:日志记录器对象。
  • template_context_processors:模板上下文处理器列表。

方法:

  • run:运行应用程序。
  • test_client:创建测试客户端对象。
  • test_cli_runner:创建测试CLI运行器对象。
  • add_template_filter:添加模板过滤器。
  • add_template_global:添加模板全局变量。
  • before_request:注册before_request钩子函数。
  • before_first_request:注册before_first_request钩子函数。
  • after_request:注册after_request钩子函数。
  • teardown_request:注册teardown_request钩子函数。
  • teardown_appcontext:注册teardown_appcontext钩子函数。
  • template_filter:注册模板过滤器。
  • template_global:注册模板全局变量。

我们可以通过访问current_app的属性和方法来获取或设置应用程序相关的信息,如下所示:

from flask import Flask, current_app

app = Flask(__name__)
app.config['MY_CONFIG'] = 'my_config_value'

with app.app_context():
    app.logger.info('Logging from current_app.')
    print(current_app.name)
    print(current_app.config['MY_CONFIG'])

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

在上面的示例中,我们使用了current_app的多个属性和方法,包括访问应用程序配置信息、注册日志记录器、访问应用程序名称等。在应用程序上下文中,我们可以通过current_app访问应用程序相关的信息。

g的属性

在Flask中,g是一个全局变量,用于在同一请求中共享数据。

属性:

  • name:g对象的名称。
  • request:当前请求对象。
  • session:当前会话对象。
  • app:当前应用程序对象。
  • logger:日志记录器对象。

我们可以通过访问g的属性来获取或设置应用程序相关的信息,如下所示:

from flask import Flask, g

app = Flask(__name__)

@app.route('/')
def index():
    g.username = 'admin'
    g.age = 25
    g.logger.info('Logging from g object.')
    # do something with the g object
    return 'Hello, World!'

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

在上面的示例中,我们使用了g的多个属性设置了应用程序相关的信息,包括设置用户名和年龄、记录日志等等。在/路由中,我们返回了一个简单的欢迎消息。

请求钩子

在客户端和服务器交互的过程中,有些准备工作或扫尾工作需要处理,比如:在请求开始时,建立数据库连接;在请求结束时,指定数据的交互格式。为了让每个视图函数避免编写重复功能的代码,Flask提供了通用设施的功能,即请求钩子。

在Flask中,请求钩子是一系列的函数,用于在处理请求之前或之后执行特定的操作。Flask提供了四种类型的请求钩子:before_request、before_first_request、after_request和teardown_request。

  • before_first_request : 在第一次请求之前运行,只需执行一次,如链接数据库
  • before_request : 在每一次请求都会执行,可以在这里做权限校验操作,比如说某用户是黑名单用户,黑名单用户登录系统将遭到拒绝访问,可以使用before_request进行权限校验。
  • after_request :在请求之后运行,会接收一个参数,这个参数就是前面的请求处理完毕之后, 返回的响应数据,如果需要对响应做额外处理,可以再这里进行。
  • teardown_request :每一次请求之后都会调用,会接受一个参数,参数是服务器出现的错误信息

before_request

before_request函数会在每个请求之前被执行,可以用于在请求处理之前进行一些初始化工作,如打开数据库连接、验证用户身份等。如果before_request函数返回一个响应对象,那么该响应对象将被立即返回,不会继续执行后续的请求处理函数。

from flask import Flask, g

app = Flask(__name__)

@app.before_request
def before_request():
    g.user = 'admin'

@app.route('/')
def index():
    return f'Hello, {g.user}!'

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

在上面的示例中,我们使用before_request函数将admin赋值给了g.user变量。在/路由中,我们使用g.user变量返回了欢迎消息。

before_first_request

before_first_request函数会在第一个请求之前被执行,可以用于初始化应用程序,如创建数据库表、加载配置文件等。如果before_first_request函数返回一个响应对象,那么该响应对象将被立即返回,不会继续执行后续的请求处理函数。

from flask import Flask

app = Flask(__name__)

@app.before_first_request
def before_first_request():
    print('Initializing the application...')

@app.route('/')
def index():
    return 'Hello, World!'

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

在上面的示例中,我们使用before_first_request函数打印了一条初始化消息。在/路由中,我们返回了一个简单的欢迎消息。

after_request

after_request函数会在每个请求之后被执行,可以用于在请求处理之后进行一些处理工作,如关闭数据库连接、设置响应头等。after_request函数必须接收一个响应对象作为参数,并返回一个响应对象。

from flask import Flask

app = Flask(__name__)

@app.after_request
def after_request(response):
    response.headers['Server'] = 'My Server'
    return response

@app.route('/')
def index():
    return 'Hello, World!'

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

在上面的示例中,我们使用after_request函数设置了响应头中的Server字段。在/路由中,我们返回了一个简单的欢迎消息。

teardown_request

teardown_request函数会在每个请求处理完成之后被执行,无论请求是否成功。可以用于清理工作,如关闭数据库连接、释放资源等。teardown_request函数不需要返回值。

from flask import Flask

app = Flask(__name__)

@app.teardown_request
def teardown_request(exception):
    print('Cleaning up...')

@app.route('/')
def index():
    return 'Hello, World!'

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

在上面的示例中,我们使用teardown_request函数打印了一条清理消息。在/路由中,我们返回了一个简单的欢迎消息。

除了before_request、before_first_request、after_request和teardown_request之外,Flask还提供了其他一些钩子函数,如下:

teardown_appcontext

teardown_appcontext函数会在应用程序上下文被弹出栈之后执行,可以用于执行一些清理工作。teardown_appcontext函数不需要返回值。

from flask import Flask

app = Flask(__name__)

@app.teardown_appcontext
def teardown_appcontext(exception):
    print('Cleaning up...')

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

在上面的示例中,我们使用teardown_appcontext函数打印了一条清理消息。

template_rendered

template_rendered函数会在模板被渲染之后执行,可以用于执行某些操作,如记录模板渲染时间、修改渲染结果等。template_rendered函数必须接收一个Template对象和一个Context对象作为参数。

from flask import Flask
from flask import render_template_string
from flask.signals import template_rendered

app = Flask(__name__)

@app.route('/')
def index():
    return render_template_string('<h1>Hello, {{ name }}!</h1>', name='World')

def record_render_time(sender, template, context, **extra):
    print(f'Template {template.name} rendered in {context.get("render_time")} seconds.')

template_rendered.connect(record_render_time, app)

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

在上面的示例中,我们使用render_template_string函数渲染了一个模板,并在template_rendered信号中记录了渲染时间。

appcontext_pushed

appcontext_pushed函数会在应用程序上下文压入栈中之后执行,可以用于执行一些初始化工作。appcontext_pushed函数必须接收一个AppContext对象和一个任意的**kwargs参数。

from flask import Flask
from flask import current_app

app = Flask(__name__)

@app.appcontext_pushed
def appcontext_pushed(sender, **kwargs):
    ctx = sender.app_context()
    ctx.push()
    print(f'Current app: {current_app.name}')

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

Flask中的信号和钩子(Hooks)都是用于处理应用程序事件的机制,但它们有一些区别:

  • 触发时机:
    • 信号: 信号是在特定的事件发生时手动触发的,通过send方法。您可以在应用程序中的任何地方触发信号,从而允许更大的灵活性。
    • 钩子: 钩子是在应用程序生命周期中的特定阶段自动触发的。例如,before_request钩子在每次请求开始前触发,after_request钩子在每次请求结束后触发。
  • 用途:
    • 信号: 信号通常用于自定义事件,允许您在应用程序的不同部分注册和触发自定义事件。信号提供了一种松耦合的方式,让您的代码更具模块化和可维护性。
    • 钩子: 钩子主要用于在特定的应用程序生命周期阶段执行操作。例如,在请求处理前或后执行代码,或在应用程序启动前执行一些初始化操作。
  • 触发方式:
    • 信号: 通过手动调用send方法触发信号,可以向信号传递额外的参数。这使得信号更加通用,适用于各种场景。
    • 钩子: 钩子是自动触发的,与特定的生命周期事件相关。您只需定义钩子函数,框架会在适当的时候调用它们。

Flask信号通常用于应用程序中的事件处理,特别是在以下场景下:

初始化操作: 当应用程序初始化时,您可能希望执行一些特定的操作,例如设置默认配置或者连接到数据库。您可以使用before_first_request信号:

from flask import Flask, before_first_request

app = Flask(__name__)

@before_first_request
def initialize():
    # 执行初始化操作
    print("Initialization code here")

请求生命周期中: 在请求的处理过程中,您可能需要在请求开始或结束时执行某些操作。以下是一个在请求开始时记录请求信息的例子:

from flask import Flask, request_started

app = Flask(__name__)

@request_started.connect
def log_request_info(sender, **extra):
    # 记录请求信息
    print("Request started. Path:", request.path, "Method:", request.method)

自定义事件: 您可以在应用程序中定义自己的信号,以便触发自定义事件。例如,您可能希望在用户注册时触发一个信号:

from flask import Flask, signal

app = Flask(__name__)
user_registered = signal('user-registered')

@user_registered.connect
def send_welcome_email(sender, user, **extra):
    # 发送欢迎邮件
    print("Welcome email sent to", user.username)

在用户注册的地方,您可以触发这个信号:

with app.app_context():
    user_registered.send(app._get_current_object(), user=current_user)

Flask上下文的实现

在处理每个请求时,都会调用 Flask.wsgi_app() 方法。它在请求中控制上下文。具体来说,存储请求上下文和应用上下文的数据结构为栈,分别为 _request_ctx_stack 以及 _app_ctx_stack。当上下文被推入栈,依赖栈的代理对象变得可用,指向在栈顶的上下文。

当请求开始,将创建并推入 RequestContext 对象。如果此时应用上下文不在上下文栈的顶部,将先创建 AppContext。当这些上下文被推入,代理对象 current_app、g、request 以及 session 在处理请求的线程变得可用。

因为上下文对象存放在栈中,在请求中,其他上下文对象推入时可能会改变代理对象的指向。尽管这不是常见的设计模式,但它可以让应用能够内部重定向或者将不同应用串联在一起。

当请求被分配到视图函数,生成并发送响应,请求上下文先被弹出,然后应用上下文也被弹出。在上下文被弹出前,函数 teardown_request() 以及 teardown_appcontext() 会被执行。即使在请求被分配过程中有未处理的请求抛出,这些函数也会被执行。

在Flask中,由于同一个线程可能会处理多个请求,因此需要一种机制来确保每个请求的环境是独立的。为此,Flask提供了三个类:Local、LocalStack和LocalProxy,用于实现线程局部变量的管理。

Local

在flask项目中某一个功能中会有多个视图,怎么保证某次请求的上下文不会被别的视图拿走呢?很简单,用来隔离线程之间数据的,类似一个字典,众所周知,线程的数据都是共享的,那么我现在就是想有一些线程内的私有数据该咋办?

local = {thread_id: {上下文数据key: 上下文数据value }}

那就使用如下在Python的标准库中提供几个对象来完成某个线程内的全局变量的设置。thread local用于存储thread-safe和thread-specific的数据,通过这种方式存储的数据只在本线程中有效,而对于其它线程则不可见。正是基于这样的特性,我们可以把针对线程全局的数据存储进thread local对象。

from threading import local # 线程模块内导包
thread_local_data = local() # 初始化local对象
thread_local_data.name="NB" # 存储一个变量
thread_local_data.name # 输出看看
'NB'

使用thread local对象虽然可以基于线程存储全局变量,但是在Web应用中可能会存在如下问题:

  • 有些应用使用的是greenlet协程,这种情况下无法保证协程之间数据的隔离,因为不同的协程可以在同一个线程当中
  • 即使使用的是线程,WSGI应用也无法保证每个http请求使用的都是不同的线程,因为后一个http请求可能使用的是之前的http请求的线程,这样的话存储于thread local中的数据可能是之前残留的数据。

为了解决上述问题,Werkzeug开发了自己的local对象。

Local类是一个线程局部变量的容器,用于存储线程局部变量。每个线程都有自己的Local实例,它可以存储和访问线程局部变量,不同线程的Local实例互不干扰。Local类常用于存储应用程序上下文、请求上下文等线程局部变量。

以下是一个简单的使用Local类的示例:

from werkzeug.local import Local

my_local = Local()
my_local.my_var = 'foobar'

def my_func():
    print(my_local.my_var)

my_func() # 输出:'foobar'

在上面的示例中,我们创建了一个Local实例,并向其中添加了一个名为my_var的变量。在my_func函数中,我们可以访问my_local中的my_var变量,并输出其值。

Werkzeug的local对象重写了关于上下文存储对象方法。主要是通用在greenlet或者线程中使用local对象时,自动获取对应的greenlet id(或者线程id),从而获取到对应的dict存储空间,再通过name key就可以获取到真正的存储的对象,这样有效区分开了在线程或是协程中

这个技巧实际上在编写线程安全或协程安全的代码时是非常有用的,即通过线程id(或协程id)来分别存储数据。

LocalStack

LocalStack与Local对象类似,都是可以基于Greenlet协程或者线程进行全局存储的存储空间,实际LocalStack就是对Local进行了二次封装,区别在于其数据结构是栈的形式

LocalStack有两个特性:

  • 作为栈的特性,先进后出
  • 作为线程隔离对象的特性

LocalStack是基于栈,所以可以通过push和pop来存储和弹出数据

>>> ls = LocalStack()
>>> ls.push(42)
>>> ls.top
42
>>> ls.push(23)
>>> ls.top
23
>>> ls.pop()
23

LocalStack在Flask框架中会频繁的出现,其Request Context和App Context的实现都是基于LocalStack。

LocalStack类是一个Local对象的栈,用于存储线程局部变量的堆栈。在Flask中,LocalStack类常用于存储请求上下文栈,即用于存储当前请求的上下文信息。

以下是一个简单的使用LocalStack类的示例:

from werkzeug.local import Local, LocalStack

my_local = Local()
my_stack = LocalStack()

def my_func():
    my_local.my_var = 'foobar'
    my_stack.push(my_local)

def my_other_func():
    print(my_stack.top().my_var)

my_func()
my_other_func() # 输出:'foobar'

在上面的示例中,我们创建了一个Local实例和一个LocalStack实例。在my_func函数中,我们向my_local中添加了一个名为my_var的变量,并将其压入了my_stack中。在my_other_func函数中,我们通过my_stack.top()访问了my_local中的my_var变量。

flask提供了两个LocalStack,分别存储Request context、App Context。通过 LocalStack 实现栈结构而不直接使用 Local 的目的是为了在多应用情景下让一个请求可以很简单的知道当前上下文是哪个。每个请求,其相关的上下文就在栈顶,直接将栈顶上下文出栈就可以获得当前请求对应上下文中的信息了。

LocalProxy

LocalProxy 类是 Local 类的代理对象,它的作用就是将操作都转发到 Local 对象上

@implements_bool
class LocalProxy(object):
    ...

LocalProxy类是一个代理对象,用于访问线程局部变量。它可以作为全局变量的载体,当访问该代理对象时,它会自动获取当前线程的局部变量并返回其值。在Flask中,LocalProxy类常用于存储应用程序上下文,即用于存储当前应用程序的上下文信息。

以下是一个简单的使用LocalProxy类的示例:

from werkzeug.local import Local, LocalProxy

my_local = Local()
my_local.my_var = 'foobar'
my_proxy = LocalProxy(lambda: my_local)

def my_func():
    print(my_proxy.my_var)

my_func() # 输出:'foobar'

在上面的示例中,我们创建了一个Local实例和一个LocalProxy实例。在my_local中,我们向其中添加了一个名为my_var的变量。在my_proxy中,我们使用了LocalProxy,并将其初始化为一个lambda函数,该函数返回当前线程的my_local对象。在my_func函数中,我们通过my_proxy访问了my_var变量。

总之,在Flask中,Local、LocalStack和LocalProxy是用于实现线程局部变量的管理的重要类。它们可以帮助我们完成线程局部变量的存储和访问,保证每个请求的环境是独立的,从而实现线程安全。

参考链接:

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注