减少假设,让你的代码膨胀
摘要
文章对比了使用 Python 和 Rye 脚本下载 PDF 的方式,强调了最初优雅的代码对成功做了许多假设。随后展示了添加验证的过程,虽然降低了可读性,但提高了健壮性。
<p><a href="https://lobste.rs/s/be22hc/reducing_assumptions_exploding_your">评论</a></p>
查看缓存全文
缓存时间: 2026/07/04 12:41
# 减少假设,代码膨胀
来源:https://ryelang.org/blog/posts/reducing_assumptions_but_exploding/
## 优雅的脚本
我们都写过这样的脚本:它们完美地组合在一起,可读性强,但只假设了顺风顺水的路径。世界可以是快乐的,但它也充满了缺陷和不完美。你的代码要在这样的世界里运行,甚至创造价值……就必须处理这些不完美,否则就会成为问题的一部分。
---
## 我们那个美好的世俗示例
我们的脚本接受一个 ID 作为参数。它会在 setup.json 中查找 API 令牌,然后向远程服务器发起请求,下载一个 PDF。下载文件的名称由服务器决定。这很简单,但稍微有些现实的混乱——不过,嘿,我们是程序员,这就是我们做的事,这就是我们擅长的,对吧 :)**……对吧 :I**(想到所有那些极简主义……)
### Python 版本
Python 是编程界的通用语言。来吧!
```python
import sys, json, requests, re
from requests.auth import HTTPBasicAuth
id = int(sys.argv[1])
with open('setup.json') as f:
setup = json.load(f)
url = f"https://www.example.com/pdf-api?id={id}"
resp = requests.get(url, auth=HTTPBasicAuth(setup['token'], 'x'))
pattern = re.compile(r"filename\*?=[f']?(.*?)[']?(?:;?$)")
content_disp = resp.headers['Content-Disposition']
filename = pattern.search(content_disp).group(1)
with open(filename, 'wb') as f:
f.write(resp.content)
```
这是一个完美的小脚本。每个代码块只做一件事,每个块只有几行,**没有不必要的结构**或样板代码——我喜欢。
### Rye 版本
既然这是 Rye 语言的博客,我们也会用 Rye 来写:)
```rye
rye .Args? .load .first :id
Load %setup.rye |context :setup
re: regexp "filename\*?=[f']?(.*?)[']?(?:;?$)"
format id https://www.example.com/pdf-api?id=%d
|Request 'GET "" |Basic-auth! setup/token "x"
|Call :resp
|Header? "Content-Disposition"
|Submatch?* re
|file .Create
|Copy* Reader resp
```
好吧,有点不同,但类似。
### 我们假设了什么?
- 脚本总是得到一个整数参数
- setup 文件存在并且内容正确
- HTTP 请求永远不会失败
- Content-Disposition 头部总是存在并带有文件名
- 我们总是能创建新文件
正如 *Eugene Lewis Fordsworthe* 所说……那真是太多操蛋事了 :(
---
## 添加基本验证(第 2 步)
我可能会像 Eugene 的弱化版:*“用户输入是许多问题的根源”*。没有用户输入,就没有问题——但我们就是需要用户。所以让我们验证输入。
### Python 版本
我们现在要:
- 检查参数个数
- 检查 ID 是否为整数
- 检查 setup 中是否定义了 token 值
```python
import sys, json, requests, re
from requests.auth import HTTPBasicAuth
if len(sys.argv) != 2:
raise ValueError("script argument id - expected exactly one integer")
try:
id = int(sys.argv[1])
except ValueError:
raise ValueError("script argument id - must be an integer")
with open('setup.json') as f:
setup = json.load(f)
if 'token' not in setup or not isinstance(setup['token'], str):
raise ValueError("loading setup - token field required as string")
url = f"https://www.example.com/pdf-api?id={id}"
resp = requests.get(url, auth=HTTPBasicAuth(setup['token'], 'x'))
pattern = re.compile(r"filename\*?=[f']?(.*?)[']?(?:;?$)")
content_disp = resp.headers['Content-Disposition']
filename = pattern.search(content_disp).group(1)
with open(filename, 'wb') as f:
f.write(resp.content)
```
我们添加了这几个检查,如果你问我(我有点偏心),优雅可读的脚本已经消失了。这就是我讨厌 try/catch 方法的原因之一。它增加了破坏代码流程的结构。
### Rye 版本
```rye
rye .Args? .validate { integer }
|check "script argument id"
|first :id
Load %setup.rye |context
|validate { token: required string }
|check "setup file" :setup
re: regexp "filename\*?=[f']?(.*?)[']?(?:;?$)"
format id https://www.example.com/pdf-api?id=%d
|Request 'GET "" |Basic-auth! setup/token "x"
|Call :resp
|Header? "Content-Disposition"
|Submatch?* re
|file .Create .defer\ 'Close
|Copy* resp .Reader .defer\ 'Close
```
我们对参数和配置使用了验证方言。并且用了 `.defer\ 'Close` 来确保资源(文件写入器和 HTTP 流读取器——顺便说一下,没有复制到内存)被清理。脚本变得稍微复杂了一些,但结构和流程没有改变。
---
## 完整错误处理(第 3 步)
现在让我们处理所有失败情况,并在失败时向用户提供有用的反馈。我们原本优雅的脚本,膨胀成了这样……:o
### Python 版本
我们现在还检查:
- setup.json 是否存在
- 能否解析 setup.json 的 JSON
- HTTP 请求是否成功
- 如果没有 Content-Disposition,我们提供默认文件名
- 能否创建新文件
- 能否向其写入 PDF
```python
import sys, json, requests, re
from requests.auth import HTTPBasicAuth
# Validate arguments
if len(sys.argv) != 2:
print("Error: script argument id - expected exactly one integer")
sys.exit(1)
try:
id = int(sys.argv[1])
except ValueError:
print("Error: script argument id - must be an integer")
sys.exit(1)
# Load and validate config
try:
with open('setup.json') as f:
setup = json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"Error: couldn't open config - {e}")
sys.exit(1)
if 'token' not in setup or not isinstance(setup['token'], str):
print("Error: loading setup - token field required as string")
sys.exit(1)
pattern = re.compile(r"filename\*?=[f']?(.*?)[']?(?:;?$)")
url = f"https://www.example.com/pdf-api?id={id}"
try:
resp = requests.get(url, auth=HTTPBasicAuth(setup['token'], 'x'))
resp.raise_for_status()
except requests.RequestException as e:
print(f"Error: Http request failed - {e}")
sys.exit(1)
# Extract filename with default fallback
content_disp = resp.headers.get('Content-Disposition', '')
match = pattern.search(content_disp) if content_disp else None
filename = match.group(1) if match else "default.pdf"
try:
with open(filename, 'wb') as f:
f.write(resp.content)
except IOError as e:
print(f"Error: couldn't create local pdf - {e}")
sys.exit(1)
except Exception as e:
print(f"Error: couldn't save contents - {e}")
sys.exit(1)
```
最初只有 15 行的脚本,现在变成了 45 行并且有很多结构。工作代码隐藏在所有安全代码之中,几乎无处可寻。Python 程序员自然会用一些辅助函数进行重构,使用额外的库如 argparse 和验证库,但这仍然为之前清晰的快乐路径增加了“结构”。它隐藏得更好,但也增加了依赖。
### Rye 版本
我们为 Rye 版本添加了所有相同的检查。
```rye
rye .Args? .validate { integer }
|^check "script argument id"
|first :id
Load %setup.rye
|check "couldn't open setup file"
|context
|validate { token: required string }
|^check "loading setup" :setup
re: regexp "filename\*?=[f']?(.*?)[']?(?:;?$)"
format id https://www.example.com/pdf-api?id=%d
|Request 'GET "" |Basic-auth! setup/token "x"
|Call
|^check "Http request failed" :resp
|Header? "Content-Disposition"
|Submatch?* re
|fix { "default.pdf" }
|file .Create
|^check "couldn't create local pdf"
|defer\ 'Close
|Copy* resp .Reader .defer\ 'Close
|^check "couldn't save contents"
```
代码变得稍微紧凑了一些,但行数几乎没有增加,最重要的是,程序结构保持不变!:O
### 但是……魔法
上面代码中没有魔法。你看到的每个单词都是普通的 Rye 函数。上面的代码只是 Rye 前身、我们以及一些运气经过许多精心设计决策的结果。事实上,我们只用了 Rye 故障和验证处理能力的一小部分。如果不先阅读更多关于 Rye 的内容(https://ryelang.org/#principles)和认识 Rye(https://ryelang.org/meet_rye/),很难直接上手,但上面关键的函数做了以下事情:
- `check`——返回一个值,如果不是失败;否则将失败包装在更高层次的失败中并返回
- `fix`——再次返回一个值,如果不是失败;否则计算一个块并返回该块的结果
我们上面使用的是一个相关的函数。Rye 有一个返回函数的概念(在 Rye 中,return 也是一个返回函数——每个活动词都是 Rye 中的函数)。返回函数的命名约定是前缀 `^`。
- `^check`——类似于 check,但在失败时也会返回/退出到调用者(一个更高层次的失败)
## 重点
上面就是我不喜欢 try/catch 模型的原因之一,try/catch 是许多语言(当然不仅仅是 Python)的主要故障处理模型。还有更好的选择,我们上面展示了一种,但这只是 Go 所做的一种改进版本,而使用类型系统和 Option 类型则是另一种方法。这些方法的共同点是什么?失败是你语言中的一个正常值,应该与你语言的功能融合,成为其一部分。我打算再写更多关于这个主题的内容。
相似文章
测试用例简化器是被低估的调试工具
这篇博客文章解释了测试用例简化器在调试中的价值,详细介绍了它们如何自动化输入简化以隔离错误,并探讨了考虑错误频率或指令计数等高级技术。
@pidotdev:在此阅读完整博客文章
一篇博客文章,重点介绍 Pi——一个极简的编码代理框架,认为与更复杂的工具相比,其简洁性带来更好的性能和更低的成本,并得到 Databricks 基准测试结果和 Shopify 的 Pi Autoresearch 案例研究的支持。
@h100envy:这篇论文彻底改变了我对 RAG 中信任检索的看法:获取文档 -> 评估质量 -> 得…
本文提出了一种5步蓝图,通过使用轻量级检索评估器来提高 RAG 中的信任度。该评估器对文档质量进行评分,并触发(正确、错误、模糊)三种动作来处理检索失败,具有即插即用的集成特性。
通过实际代码了解 RAG 的一切
作者宣布为开源 RAG 框架 RAG Me Up 提供以教育为先的文档,通过代码和可运行示例解释 RAG 概念。
@freeCodeCamp: 很多RAG教程在本地运行没问题,但一旦尝试部署就会出问题。在这本手册中,@dannwaneri 教你…
这本手册教开发者如何构建一个生产级RAG系统,使用Cloudflare Workers、Vectorize和Workers AI,专注于成本效益和可靠性。