首页 module导入示例
文章
取消

module导入示例

有一个类似的示例:

有两个文件app.py、test1.py.

app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 省略import

# jwt
app = Flask(__name__)

app.config["JWT_SECRET_KEY"] = "super-secret"  # Change this!
jwt = JWTManager(app)

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

@app.route('/protected', methods=['GET'])
@jwt_required()
def protected():
    return 'hello protected'

import test1

from werkzeug.serving import run_simple

if __name__ == '__main__':
    # 启动参数
    run_simple('0.0.0.0', 7070, app, threaded=True)

test1.py

1
2
3
4
5
6
7
8
9
from app import app

from flask_jwt_extended import jwt_required, verify_jwt_in_request

@app.route('/test1')
@jwt_required()
def test1():
    print("enter test1")
    return "test1"

执行python app.py,”/test1”的url能注册进去吗,答案是不能。

为什么呢。

我们加上调试信息:

app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from flask import Flask
from flask_jwt_extended import JWTManager
from flask_jwt_extended import jwt_required

# jwt
print("create app")
app = Flask(__name__)

app.config["JWT_SECRET_KEY"] = "super-secret"  # Change this!
jwt = JWTManager(app)

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

@app.route('/protected', methods=['GET'])
@jwt_required()
def protected():
    return 'hello protected'

print("before import test1")

import sys

if "app" in sys.modules:
    print("app get app")
    if hasattr(sys.modules["app"], "app"):
        print("app get app")
        print(sys.modules["app"].app)

import test1

print("after import test1")

from werkzeug.serving import run_simple

if __name__ == '__main__':
    # 启动参数
    run_simple('0.0.0.0', 7070, app, threaded=True)

test1.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
print("start init test1")

import sys

if "app" in sys.modules:
    print("test1 get app")
    if hasattr(sys.modules["app"], "app"):
        print("test1 get app")
        print(sys.modules["app"].app)

from app import app

from flask_jwt_extended import jwt_required, verify_jwt_in_request

@app.route('/test1')
@jwt_required()
def test1():
    print("enter test1")
    return "test1"

@app.route('/test2')
def protected1():
    print("add url protected1")
    verify_jwt_in_request()
    return "protected1"

print("end init test1")

同时修改app.route的源码:

1
2
3
4
5
6
7
8
9
def route(self, rule: str, **options: t.Any) -> t.Callable:

        def decorator(f: t.Callable) -> t.Callable:
            endpoint = options.pop("endpoint", None)
            print(f"add url into app { f.__name__}")
            self.add_url_rule(rule, endpoint, f, **options)
            return f

        return decorator

调试信息如下:(add url)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PS C:\Users\nfuser\workspace\test_flask> python .\app.py
**create app**
add url into app index
add url into app protected
before import test1
start init test1
**create app**
add url into app index
add url into app protected
before import test1
app get app
app get app
<Flask 'app'>
after import test1
add url into app test1
add url into app protected1
end init test1
after import test1

分析调用过程:

  1. 首先进入app.py,初始化app,可以看到第一条语句”create app”,然后是注册app中url,然后导入test1,这个时候在app中是不包含app的module的,这是当然的,app.py里也没有import app。
  2. 进入test1,这个时候碰到第一句from app import app ,按照Python执行流程和Module理解 总结的module导入规则,就是在sys.modules里面去找这个模块,如果找不到,就实例化这个module模块的对象,然后从新的这个对象中,取出import进来的对象。这里可以看到,test1中判断是没有app的module的。因此,在start init test1 以后,继续进入了app.py文件,又输出了一个create app !相当于是把app.py又执行了一次。可以看到后面的add url又出现了一次,import test1 也是。但这里不会循环引用,因为app中也没有引用test1的内容,只是import,发现已经import 过了(import会先添加到sys.module里面,然后再执行,可以看到导入app里面,已经出现了app get app)就会跳过。这个时候算是完成了from app import app 这句话。
  3. 前面重点是,test1中取得的app,是test1中重新初始化一次的app!和app.py里的app不是一个。那之后的add url,也是加到这个新的app中。而在main里面,我们启动的是app.py里的py,因此,显然我们无法从浏览器访问到test1这个url。(但是如果在test1中,app.run,这个是能访问到所有url的)

怎么办呢?

从app.py中拆分一个run.py出来。

1
2
3
4
5
6
7
8
from app import app

# run web server
from werkzeug.serving import run_simple

if __name__ == '__main__':
    # 启动参数
    run_simple('0.0.0.0', 7070, app, threaded=True)

这是因为为什么不对,就是test1中重新实例化一个app,而为什么重新实例化一个app,是因为app不存在sys.module里面,因此只需要在调用test1时,app已经被人导入过就好办了。因此,应该将main拆分到一个单独的文件中。

可以看看新的调试信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PS C:\Users\nfuser\workspace\test_flask> python .\run.py
**create app**
add url into app index
add url into app protected
before import test1
app get app
app get app
<Flask 'app'>
start init test1
test1 get app
test1 get app
<Flask 'app'>
add url into app test1
add url into app protected1
end init test1
after import test1

这个时候只有一个create app 了,同时这次app里直接就输出了app get app .

启示:

这个示例有好几个重要的作用:

  • 一是深入理解python的执行过程,以及module导入的本质概念。
  • 不要导入作为main的包,会重复执行。作为替代,分解到单独的、简单的一个py中。
  • 所有module都导入到sys.module中,重复被后续所使用。即一个包只有被运行一次。
  • 根据上一点,这就能够借助monkey patch,先导入包的地方,对包进行动态变更,则后续再导入这个包的地方都会使用到变更后的函数。这就是动态语言的魔性。

扩展:

  • 虽然用的是同一份包的内容,但要注意的是,导入到当前module时,会包含在当前module的命名空间中
  • 因此如果使用monkey patch在这里直接修改导入后的对象是没用的,该对象仅作用于当前空间,需要使用导入包.对象去访问唯一的对象。
1
2
3
4
5
6
7
8
9
oidc_auth = False

def optional_jwt_required(fresh=False, refresh=False, locations=None):
    return original_jwt_required(optional=True, fresh=fresh, refresh=refresh, locations=locations)

if not oidc_auth:
    logger.warning("unauth now")
    jwt_required = optional_jwt_required
    flask_jwt_extended.jwt_required = optional_jwt_required

这里对于jwt_required这个对象,只有当前文件使用,修改这个对外界是没有影响的,除非其他地方import该文件的jwt_required,相反,后面对于flask_jwt_extended.jwt_required,是全局通用的。后续的所有地方去import flask_jwt_extended.jwt_required都会使用变化后的函数。

本文由作者按照 CC BY 4.0 进行授权

认证和授权

重装系统