又聊几件我在 Django 里用得很开心的事

查看原文 HN 讨论

文章摘要

Julia Evans 这篇短文延续了她最近的一段「奇怪旅程」:试着用一种 2010 年前后的方式做网站——有一个 SQL 数据库,在后端渲染 HTML。她坦白这条路对她并不「容易」:她没在 2000 或 2010 年代学过这种做法,要补的东西很多。

她先解释了动机。此前她有信心的工具箱是:静态站点生成器(比如她的博客)、带一点 JavaScript 的静态站(比如她的 SQL playground)、以及用 Lambda 或 Go 后端配简单 Vue.js 单页应用(比如 mess with dns)。这套前端为主的方案对极简应用很好用,但当她开始想做一个有很多不同页面(而不只是一个页面)的东西时,那些需要写大量前端代码的选项就没那么吸引人了,于是她转向后端。她还给出一个有意思的观察:写一个尽量少用 JS 的后端站点,和写一个尽量少做后端的单页应用,对她来说感觉是一样的——尽管两者看起来是对立的——因为两种情况下她都只是想把尽可能多的逻辑集中在一个地方。

她喜欢的第一件事是 query builder。 Django 里可以定义一个 QuerySet 类,把各种可能用到的 WHERE 条件写成方法。她的视图代码调用起来是这样的:Events.objects.approved().for_tab(tab).with_festivals(...).is_free(...).is_outdoors(...);定义端则是一个继承自 models.QuerySet 的类,每个方法返回一个 self.filter(...)。她说定义过滤器的语法不是她的最爱,但她大部分时间只是在使用这些方法,读起来非常舒服,这让她想去看看别的 query builder 库——「以前我想的是『我会 SQL,谁需要 query builder』,但这种结构确实让代码读起来很好。」

第二件是模板过滤器。 她列举了自己用到的几个:urlize 把纯文本 URL 转成链接、linebreaksbr 把换行转成 <br>date 格式化日期、以及 json_script——它把一个 Python 字典自动转成 JSON 并以安全的方式插进 HTML 的 <script> 标签里。「这些单独看都是小事,但它们能直接拿来用,不知怎的就带来了很大的差别。」她最喜欢的是 querystring:它能生成一个「只改动一个参数」的当前查询串链接,比如链到前一天、或者把某个参数移除。

第三件是自动数据库迁移。 她说自己仍然非常喜欢 Django 的自动迁移:改一下 model 加个字段,Django 就自动生成迁移。项目至今做了 19 次迁移,而且大概还会更多。「随着我对问题的理解发生变化,能轻松改数据库,这对我来说是巨大的差别。」

然后是两个她遇到的问题。 其一是继承:Django 文档有时会建议用类视图和继承来组织代码,她有四个视图共享很多代码,试了一下,「我不喜欢用继承在视图间共享代码的体验」,于是换成了函数,直观得多。「我在 Python 里从来没有过好的继承体验,我想我不会再试了。」不过她不介意用继承来使用 Django 自己提供的接口。这一段她还加了一句元评论:她最近在练习用「THING 让我感觉不好,我更喜欢 OTHER THING」的方式表达编程观点——她链接的那篇文章说函数式视图是「正确方式」,而她并不关心是否「正确」,只是很高兴知道别人对继承有类似感受。

其二是性能,她坦承「我不知道该怎么思考 Django 的性能」。某个时刻 LLM 爬虫发现了她的站点,开始以每秒约 10 个请求的速度打过来;她把它们屏蔽了,但这让她开始想站点的容量到底是多少。用 ab -n 1000 -c 1 简单压测,一台约每月 10 美元的 VM 上大约只能扛 2-3 QPS。她列了一串还没想明白的问题:偶发流量高峰要不要能扩容?要不要把站点设计得更容易缓存(缓存真的很难做对)?Django 性能文档说 Jinja 模板更快,要不要换?文档还说 {% block %}{% include %} 快,差别大吗、为什么?

她由此得出一个关于框架的教训:因为 Django 是个「框架(tm)」,很容易不小心配错。她在读性能文档时看到一句「启用 cached template loader 通常能极大改善性能」,而她做 CPU profiling 时正好注意到大量时间花在渲染模板上;点进去才发现这个 loader 本来就是默认开启的,是她之前折腾别的东西时不小心关掉了。打开模板缓存之后,站点轻松就能处理约 12 QPS 而不跑满 CPU。

最后她提炼了一条与常见建议相左的经验:人们总说「有性能问题就去查数据库查询,也许加个索引」,但她碰到的一系列性能问题(比如这次的模板缓存)都不是慢查询导致的,所以对她来说先跑 CPU profile 更有用——何况她用的是 SQLite,慢查询的问题本来也会出现在 CPU profile 里。

HN 评论精华

那个 2-3 QPS 的数字引爆了讨论区,这是整条帖子最大的争议点。

Django ORM 与迁移:最强的护城河,也是最大的槽点

模板系统的老账

seanwilson(11 条子回复)系统地列出了 Django 默认模板语言的限制并追问是否仍然存在:不允许用括号辅助布尔表达式;不能做基本算术(可以 add 但不能乘,要靠 widthratio 之类的 hack);不能把表达式赋给变量,只能重复自己;不能把模板代码生成的 HTML 捕获进变量以实现 slot 式组件;不能给 model 方法传参。「我理解『模板不该包含复杂逻辑』这套哲学,但上面这些相当随意,而且导致更难维护的代码。加法可以但乘法不行?布尔逻辑可以但不能加括号?」leetrout 回答说 Jinja2 是可以直接换的模板后端,甚至可以两者同时跑(当然不能混用);mcrk 提醒迁移本身简单且文档完善,问题在于第三方 Django app(多为遗留的)可能不支持 Jinja,先确认兼容性。

跑题但精彩的 asyncio 讨论

CraigJPerry(21 条子回复)说:async Python 有太多走火装置,但它确实偷走了现代 Python Web 的时代精神;同步 Django 有很多可取之处——只要部署得当(worker、放在带缓存的反向代理后面),即使在压力下也很容易运维。它不适合长连接 WebSocket 之类,但对 CRUD 型或企业应用往往非常高效。他列举的 asyncio 陷阱包括:无界的 asyncio.Queue 导致缓冲膨胀、无限并发的 asyncio.gather 把出站 socket 甚至文件描述符耗尽、在事件循环里误用同步的 requests 阻塞住整个循环;以及最惊悚的一个——如果你忘了持有 asyncio.create_task() 的引用,那套机制里全是弱引用,你的 task 可能在生产环境里还没运行或完成就被垃圾回收掉了。loevborg 深有同感:「我也觉得 asyncio 是个大 hack……我读到你需要把 task 存进一个 set 才能防止它被意外取消时简直不敢相信。这东西怎么就成了 AI 后端的主力技术栈?它像 node,但更慢,而且因为同步 IO 更容易搞砸。」codethief 直接求他写篇博客。

「你该换个技术栈」派

其他