减少假设,让你的代码膨胀

Lobsters Hottest 新闻

摘要

文章对比了使用 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 类型则是另一种方法。这些方法的共同点是什么?失败是你语言中的一个正常值,应该与你语言的功能融合,成为其一部分。我打算再写更多关于这个主题的内容。

相似文章

测试用例简化器是被低估的调试工具

Lobsters Hottest

这篇博客文章解释了测试用例简化器在调试中的价值,详细介绍了它们如何自动化输入简化以隔离错误,并探讨了考虑错误频率或指令计数等高级技术。

@pidotdev:在此阅读完整博客文章

X AI KOLs Following

一篇博客文章,重点介绍 Pi——一个极简的编码代理框架,认为与更复杂的工具相比,其简洁性带来更好的性能和更低的成本,并得到 Databricks 基准测试结果和 Shopify 的 Pi Autoresearch 案例研究的支持。