July 12, 2018
不想造轮子的程序员不是一个好码农。 —— 鲁迅
我不知道点进来看这篇文章的人,有多少是知道我在写一个Python的异步框架的。
不过无所谓,这篇文章适合于任何人看,适当的时候跳过代码部分即可。实际上更多的是对 Python 的一些见解。
那时候写项目,特别是在跟前端对接的时候,我就在想「写 API 文档好麻烦啊」,「难道就不能自动生成这些文档吗?」。
是啊,为啥不能自动生成呢?Parameters 的读入和处理都在代码里面写好了。返回的内容也是在代码里面的。难道我就不能改一下代码就可以轻松生成 API 文档了吗?
那一段时间,我整天都在想这个事情。当时我用 Flask 写一个 API 的流程基本是这样的:下文用用户注册来做演示:
POST /user
Flask 代码大概长这样:
@app.route('/user', methods=['POST'])
def register():
email = request.form.get('email')
password = request.form.get('password')
valid_email(email)
valid_password(email)
# register logic here
UserService.register(email, password)
return {
'token': token_generator()
}
上述的五个步骤对于一份API文档来说,除了第四部逻辑部分是不需要的,其他部分都应该展示在文档中。
那么如果传统的开发流程包括了两个步骤:写代码 和 写文档。可是这是一项十分重复的工作,而且存在实效性的问题:代码更新之后文档还没来得及更新导致对接时出现问题。
细看每一步,我们又可以采用自动化的方式来生成这些重复的信息:
@app.route('/user', methods=['POST'])
完全可以解析到 POST /user
email = request.form.get('email')
和 valid_email(email)
则可以合并成一步,通过一个函数来实现这项自动化的工作 email = Parameter('email', from='form', type=email)
return
部分在大型的系统中都需要在返回之前检查输出的内容是否符合我们预期的值这样细想之后,代码就可以写出以下的形式:
@app.route('/user', methods=['POST'])
@param('email', email, location='form')
@param('password', password, location='form')
@marshal({
'token': str
})
def register():
UserService.register(params.email, params.password)
return {
'token': token_generator()
}
两份代码,就个人而言,我更喜欢第二种,因为后者的层次感更加分明:
根据这些特点,我实现了这样的Router:https://github.com/Riparo/nougat-router
由于在 flask 上面的造诣不深,对「如何编写一个 Flask 的插件」不熟悉,所以我决定自己去写一个 Web 框架。是的,一切从头来,从零开始。到目前为止已经断断续续维护了1年多了,提交了差不多300个commit。
决定写这个框架的时候,心里想着要把这样的一个 Router 实现,同时要深入了解一下 Python 的异步。所以就变成了写一个异步的 Web 框架。
在最开始的那段时间,我在抄袭 Sanic,对,抄袭,对着 Sanic 的第一次提交记录抄了一个可运行的框架。此后我就对这个抄来的东西改了无数次,每一次修改都是一次不同库的选择和思考:
Future
来执行框架的代码。在HTTP make respone 的时候需要在 Future
中添加 call_back
来实现。相比之下 Stream 模式下的代码表现形式会更加直接,可以直接在函数中 await logic()
,因此,我选择了 Stream 的模式。curio
是民间的一个良好实现。
curio
更加好,更加优雅asyncio
由于是官方实现,所以生态做得最好。curio
甚至还在修修补补阶段,生态近乎很惨,不过后来推出了curio-to-asyncio bridge
使得 curio
可以分享 asyncio
的部分生态asyncio
可以使用 uvloop
调度库来加速 Python 的异步处理,让速度上升一个量级。而 curio
是纯粹的 Python 实现,所以相比之下 curio
的速度会慢一些。http_tools
是一个用 C 写的 HTTP 解析器,只能解析报文层面的内容。 h11
库同时提供了一个服务器和客户端为主体的DFA,用状态来表示当前HTTP进行到了哪一个环境上述的每一点,每一个选择的碰撞都可以在我的框架代码中找到,每一次的修改都让我对 Web 框架的理解深入几分。
一个框架即便再优秀,没有一个完善的生态圈都难以让它成为一个优秀的框架。每一位程序员都是高傲和懒惰的,他们期望框架可以完成他们所需要的一切内容。
我写的这个 Nougat 框架完成后是一个长得非常像 Koa 这样基于 Middleware 的框架,同时我也注入了一些 Flask 的思想进去。即便 Nougat 的设计再好,没有配套使用的库都注定让它成为一个不优秀的框架。
我很长一段时间就是在做这么一件事:为 Nougat 编写各色必备的中间件。但是实际上我并不清楚我需要写哪些,于是乎我便尝试用 Nougat 来写一个 API 服务器,尝试去做一个真正的项目。我在想,在尝试把 Nougat 放进项目中,我需要做什么:
于是乎,这段时间,项目逻辑并没有写过太多,更多的是在重构 Nougat 的架构和这些组件的组织模式。框架内每个理所当然的功能都是维护人员辛苦的劳动成果
即便是 Python 3.5 便推出的官方支持 Asyncio,到目前来说都属于混乱状态。
目前为止,文件 I/O 没有纳入 asyncio 库是我感到很纳闷的事情,而且 aiofiles 还是通过多线程来实现异步的,不说对不对,感觉起来就是怪怪的。
在网络 I/O 方面,也没有发展出能让人眼前一亮的 ORM。这点让大部分想升级至异步框架的使用者停下了脚步。
我现在甚至有点羡慕 JavaScript 那种全异步的处理了,羡慕的原因恰恰是为什么不能发展出优秀 ORM 的原因。
我们先来看看 Python 里面 ORM 是怎么处理 Relationship 的。
假设我们定义了两个模型: 用户模型,和用户 Token 模型,采用一对一关系:
class User:
username = Text()
class Token:
access = Text()
refresh = Text()
user = ForeignKey(User, back_ref='token')
比如我们在用户调用API时验证权限时需要判断 Token 的用户:
token = Token.query.filter(access="...").first() # whatever
user = token.user
如第二行这样读取用户信息是很理所当然的,毕竟这也是 ORM 的一大特色。那么我们再深入一点看看 ORM 是怎么处理第二行的代码的。
实际上 Token 在数据库中储存的 user
只是 user_id
,是 User 的主键。第一行代码相当于:
SELECT * FROM token WHERE access="..."
注意,我们这个时候并没有查询用户相关的任何信息,当我们执行第二行代码时,相对于在执行:
SELECT * FROM user WHERE id="{token.user_id}"
这个时候才查询用户信息,简单来说就是按需查询,同时也是 lazy load。这有时候也是部分用户不喜欢使用 ORM 的原因,因为上述代码可以用一句 SQL 查询完成。直接写 SQL 可以降低数据库的压力。当然了,这不在这篇文章的讨论范围。
换成异步的场景,这些代码会发生什么事情。别忘了,只要是异步调用都需要在方法前面加上 await
来表示异步方法。
首先,Python Property 不支持异步调用,所以 user = token.user
是不能用的了,即便是你想这样用也不行:user = await token.user
如果我们退而求其次,用一个函数来调用 user = await token.relationship('user')
。看似表现很棒,但是如果需要调用几层,那么代码看起来将会是灾难性的难受。
# address = token.user.address
address = (await (await token.relationship('user')).relationship('address'))
还记得在 Nougat 刚创立起初,有人发 ISSUE 问我有没有写 ORM 的计划,我当时还说 peewee-async 已经很棒了,现在想想都觉得丢人。
不过高兴的是,asyncio 得到了 uvloop 的支援,让 Python 可以在跑起来有可以跟 NodeJS 一拼的机会。
我想,我做的事情已经很多了,也为 Nougat 付出了很多,同时 Nougat 也存在很多问题。但是我想我应该先缓一缓了。
我接下来会继续为 Nougat 把文档继续写好,写一份比较完善的文档。
同时我很期待有人尝试去用 Nougat 写一个网站,给我一些反馈。