@delight
2017-01-13T10:28:29.000000Z
字数 3208
阅读 1789
python
web
写了几年Python web,实际框架源码基本没怎么看,这次打算把web.py
, django
和flask
这三个框架的源码学习一下。webpy是最简单的,就从它开始吧。
注意从github拉下来的代码,默认是master分支,最好切到最近的稳定分支(tag),不然可能有很多稀奇古怪的bug,目前看到的是v0.38.
程序的入口文件,可以从web.application
开始看。按着WSGI
协议,数据从web服务器发到应用,主要参数都在env
中,填充各种环境变量
框架不同于一般项目,要兼容各个Python版本,所以有一些奇奇怪怪的写法,平时不怎么常见。
__all__
这个用来限制导出范围。默认情况下,Python以命名来表示公有/私有,双下划线开头的不能导出(实际上是一个命名trick),单下划线的只能用from xxx import _ooo
。如果使用了__all__
就会覆盖这种默认的行为,即使以普通字母开头,也不能通过from xxx import *
导入。但是仍然可以通过from xxx import ooo
导入…总体来说,这个技术其实用处不大…但是可以更清晰(而非有效)的保证可见性。
sys.modules.values()
里面的文件的最后修改时间并存储,判断出模块过期后,使用reload
内置函数进行重载;ctx
)是利用线程本地存储生成的一个dict,在线程里面是一个单例。环境变量被存储在ctx.env
之中
def is_class(o):
return isinstance(o, (types.ClassType, type))
__slots__
用来限制动态注入的属性nonlocal
关键字,闭包内部不能修改外部变量。可以将普通值改为只含有一个元素的list从web.application
开始看起,这里定义了一个入口类application
,虽然很奇怪为啥没按着Python的命名规则来,不过webpy这个项目里面到处都是这种奇怪的写法,可能跟这个项目出现的时代有关吧。
__init__
里面定义了一些初始化的操作,几个参数中,mapping
是url映射,fvars
是模块元素的字典,一般传入globals()
,autoreload
一般通过调节debug
开关来修改。
init_mapping
将url参数改成格式[(url: classname)]
的pair序列;然后调用了两个很有意思的函数loadhook
和unloadhook
,前者需要一个函数作为参数并返回一个函数,这个函数接受一个函数作为参数,先执行参数中的函数,然后返回他自己的参数函数的执行结果。听起来非常绕,其实本质上是两个装饰器。用haskell解释一下:
loadhook h = p
where p x = do
h
return x
这东西具体干吗用的呢,其实我们定义了一个函数h(无参数),调用loadhook
给它包装一下,返回了一个新的函数p。这个新函数的参数也是一个函数,我们假设为x,那么如果我们调用p(x),这个函数会先执行h,再处理x,也就是说,这里注入了一个callback,强行在执行x之前执行h. 如果叠加调用loadhook,最先添加的先执行(队列)。
unloadhook
类似,不同的是,执行顺序正好相反,先调用传入的参数x,最后再调用h。如果x执行的结果不是一个数据,而是一个迭代器,当迭代器停止迭代(或者其他异常的时候)执行h。这里形成了一个堆栈,如果unloadhook(x)(y),执行顺序是y->x,那么再外围继续叠加的话:
unloadhook(unloadhook(x)(y))(z)的执行顺序就是z -> (y -> x),最后调用的最先执行。
在processeers最开始的位置,加上了hook和unhook后的_load
以及_unload
。前者只是将自身(即application的实例)放入web.ctx.app_stack
这个全局变量(县城本地存储)中;后者是将app_stack
里末尾帧弹出,并且检查是不是有需要恢复的断点。
如果打开了auto_reload
,就会将Reloader()
和reload_mapping
都加入到processors中.
request
感觉是用来做测试的,服务端测试请求。从这里可以看出服务器处理请求的流程。利用env参数,构建符合WSGI标准的环境变量,这里注意主要是数据编码的修改,最后把wsgi.input
转为StringIO对象。返回值更有趣,self.wsgifunc()
这个函数,参数是一系列中间件(函数/对象),返回的是一个高阶函数,最里面是wsgi
是真正的响应处理函数,它返回一个迭代器。真正的处理过程是:
1. 调用_cleanup
清理线程本地存储;
2. 调用load
加载环境变量到ctx;
3. 调用handle_with_processors
进行处理;这个函数也很有意思,我们前面可以看到,append
到self.processors
里面的函数都是被hook
或者unhook
后的,这里按着lfold
的思路,将一个[p1,p2,p3]
的processlist转为一个p1(p2(p3(self.handle())))
,类似于haskell中的p1 . p2 . p3 . handler
.
4. 最里层的handler
是用户写的处理逻辑。通过ctx.path
对应的url,找到对应函数/类/application(sub)/字符串等情况。这里的子应用可以无限递归,类似django
中的url派发机制。
倒过来看,就是真正的处理流程:先调用框架使用者的代码进行处理,完成以后将结果交给各个processor,从后道前一步一步处理,最后将结果交给中间件。这么看来有个问题,就是缺少前置的中间件,在用户处理请求之前,允许pre处理一下请求才对。其实没啥问题,这里非常绕,注意在processor中,最前面两个是loadhook(self._load)
和unloadhook(self._unload)
,所以最后调用的是loadhook(self._load)(unloadhook(self._unload)(result))
,用hs描述的话:(loadhook . self._load) $ (unloadhook self._unload) result
,按着loadhook
和unloadhook
的实现,这个函数等价于
self._load() # 将application放入栈
for r in result:
r()
self._unload() # 尾部出栈
由于使用了全局变量(严格来说是线程本地存储),所以所有的processor都可以使无参数的,直接改web.ctx
就可以了。
从这里可以看出,如果需要在代码中加入中间件,主要通过add_processor
的方法,利用两个hook
决定加载顺序。
web.template
里面是一个简单的模板解析器,里面使用python的语法。实际上一般使用jinjia2
这种专业的引擎更好。
session的数据被存放在一个threadeddict
实例里面,session作为一个processor被注入application,真正逻辑就是_load
和_save
这两个函数,如果session里面没有id,就生成一个id,否则就根据session_id取得self.store
中的上下文…不过这玩意儿会占用实际内存,所以必须有过期时间。某种意义上来讲,这玩意儿没啥意义…反正各种方法多得很。
webpy里面还有一个简易的db连接层,不是orm,只是方便写一些sql语句,在内部做了一些sql拼接。