重新回到 Python 的怀抱

标签:Python

在上一家公司时,我虽然是同时使用 Go 和 Python 进行开发,但 Go 的占比要远大于 Python。
作为一名 6 年的 Gopher 和 15 年的 Pythonista,我其实对这两门语言都很喜欢。虽然 Go 有很多的设计问题,我曾经也认为它设计得很敷衍,甚至现在也没多大进步,但它足够简单,我可以不用费很多心智就写出高性能的代码,且能原生地在各个平台运行。细想起来,似乎没有其他语言能做到。
而与之相对的,有三门语言是我无法提起兴趣的:C++、Ruby 和 Rust,我大概都用了不到半年就放弃了。我知道它们有不少很赞的设计,也不缺少众多的拥趸,可是我感觉在编码和阅读时,大半的精力可能都花在了和语言做斗争上,而不是去处理业务逻辑。这不得不让我想起了那句经典的 "Life is short (You need Python)"。

所以回到正题,既然 Go 足够好用,为啥我要回到 Python 的怀抱?
起因自然是新公司只用 Python。可是当我发现新公司用的还是陈旧的 Flask,各种初级程序员在上面堆砌了一层层屎山,我实在无法忍受。怀着重写一遍的想法,我调研起了替代方案。
其实大约 5 年前我就写过一篇近期对Python选型的一些思考与决策,那时的我还因为一点点的性能差距,选择了 Starlette。可当我用了这么久 Go 以后,我发现性能不是最重要的,可读性和开发的便捷性才是。既然我都可以接受从 Go 到 Python 的性能降级,那框架间不到 10% 的性能差距真的重要吗?
Go 的开发经验还给了我一个启示:类型的声明对可维护性有很大帮助。所以我开始依赖 Pylance 对 type hints 的支持,虽然它的体验还比不上静态语言,但是这种有限的静态检查和提示已经帮助我解决了动态语言专有的大部分问题。所以能在参数校验时通过 type hints 定义 schema,而不是传统地从 request 对象里手动解析并校验各个参数,成为了我最重要的考量因素。顺带一提,Python 3 的最近几个大更新对 type hints 的支持越来越好(例如直接用 dict 代替 typing.Dict,用 int | None 代替 typing.Union[int, None]typing.Optional[int]),所以尽量用最新的 Python 版本吧。
而异步是我的另一个关键考量,毕竟和同步框架的性能差距超过了一个数量级。

基于这些考量,BlackSheepFastAPI 是唯二达到如上要求的框架。它们都集成了 SwaggerUI,可以很方便地调试 API 和查看文档,也免去了我需要在路由中管理所有 handlers 的顾虑。
BlackSheep 的性能稍微高一点,但是它不能直接返回一个 Pydantic model 作为 JSON 响应(FastAPI 也存在不能直接返回 str 作为 PlainText 响应的问题),它的依赖注入需要手动注册为应用的服务(FastAPI 可以直接使用),authorization 也需要再定义一套机制(FastAPI 可以使用依赖注入)。另外,FastAPI 的文档更为详尽,提到了很多注意事项,并且有中文版本,特别是还有关于数据库的部分,作者还实现了一个把 Pydantic 和 SQLAlchemy 结合起来的 SQLModel
它们都是很好的框架,虽然我选择了 FastAPI,但建议两者的文档都看看,可以了解到不少知识。但是也需要鉴别是否是最佳实践,例如 FastAPI 的文档选择了使用 python-jose 来处理 JWT(而不是 PyJWT),这个库已经快 3 年没更新了,且存在安全漏洞

关于 ORM,那种老式的把 column 赋值到一个类属性的形式已经不合我胃口了,用 type hints 来简化大部分的属性定义才是未来,而目前似乎只有 SQLModel 做到了。而且在需要添加额外的参数时,它仍然能用老的形式。
至于在 model 类中区分 CHARVARCHAR,甚至操心长度也完全没必要,我都是用 SQL 来建表,ORM 中用 str 来表示即可。
最大的问题是 Pylance 会将这些类属性识别成它声明的类型,而不是 Field 类型,导致在把类属性作为参数时会提示类型不一致。

在搭建好框架开始编码后,我重新感受到了 Web 开发的流畅:REST 接口的参数可以直接用于创建 Model 保存到数据库,数据库里取出来的数据也能直接作为 JSON 响应输出,这一切还带有静态类型检查和参数校验。除了性能差一点,在类型安全方面不逊色于静态语言,开发效率却快了几倍。
我把大部分东西总结成了一个脚手架,照着这个最佳实践可以减少很多踩坑的几率。FastAPI 的依赖注入其实还可以附带参数校验的,建议大家自己试验下,因为涉及到用户校验逻辑就不方便开源了。

Go 还有一个很好的工具是 gofmt,它强制把所有代码格式化成同样的风格,避免团队内代码风格不一致。现在 Python 也有了 Black 和对应的 Black Formatter 插件。
不过 Go 的代码风格是官方完全定义好的,而 Python 有很多没有定义的风格,Black 选择了自行定义且没有给出使用者自定义的选择。例如我习惯用单引号,而 Black 默认会格式化成双引号
有人提议应该给出选项,并给出了理由,但它的作者固执地认为 Black 也是一种代码风格,所有用这个工具的代码应该长得一样,所以不愿意提供选项
好在该作者也提到它是 MIT 协议开源的,不爽可以把这个实现改掉。但是为了不和 Black 的风格冲突,我把它改名成了 White。考虑到想用这个库的一定是不喜欢双引号,否则就直接用原版了,我也就不提供选项来选择引号风格了;此外,原版还提供了大量的单元测试,我实在没精力去改成单引号的版本并持续更新,因此对正确性无法做保证。

最后顺带一提,我在查看 SQLAlchemy 源码时发现 Python 竟然支持重载了,然而官方语法里却没有提及这点。
最终我在 Python Cookbook 里找到了实现方法,简单来说就是用 inspect 库来区分方法签名,将不同的签名注册到不同的函数里,而这个注册过程则利用了元类。
初始化类的时候,会调用它的元类的 __prepare__ 类方法,这个方法可以返回一个定制的 dict,用来收集类定义中的各种方法和属性。而注册则可以在这个 dict 的 __setitem__ 方法中进行处理。
此外,标准库也提供了 functools.singledispatchfunctools.singledispatchmethod 这 2 个装饰器来支持重载,虽然用起来麻烦些,但是原理是相似的。
让我汗颜的是这并不是最近才发明出来的,只是编辑器支持提示不同的方法签名才让我注意到了而已。

0条评论 你不来一发么↓

    想说点什么呢?