<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Frost&apos;s Blog</title><description>Frost Ming&apos;s personal blog</description><link>https://frostming.com/</link><item><title>楞次定律</title><link>https://frostming.com/posts/2026/changes/</link><guid isPermaLink="false">https://frostming.com/2026/changes/</guid><pubDate>Wed, 08 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我不是一个喜欢走出舒适区的人。&lt;/p&gt;
&lt;p&gt;相反，每当即将有变化发生，我总是会感到不安，即便这个变化是朝着好的方向。比如长假前夕、长途旅游前夕，这意味着我要从日复一日同样的生活节奏中脱离出来，去适应另一种节奏。同样的，一旦适应了另一种节奏，我又不情愿从里面跳脱出来。我的人生好像一个巨大的&lt;a href=&quot;https://baike.baidu.com/item/%E6%A5%9E%E6%AC%A1%E5%AE%9A%E5%BE%8B/574407&quot;&gt;楞次定律&lt;/a&gt;。我觉得滚筒中的小白鼠也挺幸福的，不用想别的，一件事干熟了，也不费什么力。什么拥抱变化，开拓进取，36 岁了，只想躺平。&lt;/p&gt;
&lt;p&gt;但总有一些变化，你无法避免。比如换工作，说来奇怪，我这样一个安分人，已经是第五份工作了。每次换工作，似乎都有充分的理由，但也总会怀念过去的工作，而且适应新工作的过程，也会造成一些不安。&lt;/p&gt;
&lt;p&gt;又比如说要放暑假了，这意味着我家娃要回老家去，而我则要选择要么放弃现在的工作环境，要么放弃陪伴她的时间。这让我十分焦虑，我还没有选定。我有些后悔生了娃，但我十分爱我的娃，你们知道，这并不矛盾。她最近因为肠胃问题，一直不咋吃东西，掉了六七斤了，查半天说身体没病，她没病，家人可太焦虑了。&lt;/p&gt;
&lt;p&gt;按说焦虑的时候应该观看一些文艺作品，我已经好久没阅读了，因为心神不能安定，只能一直看一些短的长的不短不长的视频。完了，死锁了。&lt;/p&gt;
&lt;p&gt;对了，页面底部这条色带，是随时间变化的，如果你在黄昏时刻进来，应该能看到它是橙黄色的。这是一句没头没脑和文章无关的话，我只是想说，我最爱黄昏。&lt;/p&gt;
</content:encoded></item><item><title>我们为什么要重写 bub?</title><link>https://frostming.com/posts/2026/why-rewrite-bub/</link><guid isPermaLink="false">https://frostming.com/2026/why-rewrite-bub/</guid><pubDate>Tue, 07 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;引子&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://bub.build&quot;&gt;bub&lt;/a&gt; 最开始是 &lt;a href=&quot;https://github.com/psiace&quot;&gt;PsiACE&lt;/a&gt; 做的一个小型 Python Agent 项目，用来实践自己的 Agent 想法。2026 年 1 月的最后十天里，OpenClaw 经过两次改名风波迎来了暴发，所有人都在讨论，都在使用。而我[^1]不满足于当个纯粹的使用者，我想要拆解开看看它是怎么做的，虽然基于 LLM 的了解我能勾勒出大致的轮廓，但真要了解它，还得深入代码中去。那时已经到了二月，OpenClaw 的代码库已经非常庞大了，每天合入的 PR 不计其数，全身上下充满了 vibe 的气氛（双关）。于是了解到了 &lt;a href=&quot;https://github.com/HKUDS/nanobot&quot;&gt;Nanobot&lt;/a&gt; 这个项目，号称是 OpenClaw 的极简实现，这非常适合拿来学习[^2]。于是我很快就跑起了一个实例，放到 TG 群里使用。&lt;/p&gt;
&lt;p&gt;很快我们发现 Nanobot 并不适合群聊中的场景，于是我们决定根据现有的理解，和针对群聊的场景，来改造一下 bub，把它变成一个真正的类似龙虾的自主 Agent[^3]。这个版本在 2 月 6 日就完成了，在 Agent 的内核上增加了 Telegram 的消息收发能力，和&lt;a href=&quot;https://tape.systems&quot;&gt;基于 tape 的记忆机制&lt;/a&gt;。但在短短一个月后，我们决定对 bub 进行&lt;a href=&quot;https://github.com/bubbuild/bub/pull/85&quot;&gt;大幅重写&lt;/a&gt;，下面我想分享一些我的思考。&lt;/p&gt;
&lt;p&gt;[^1]: 本文虽使用了「我」这个人称代词，实际上并非我一人的智慧结晶，bub 是在多人使用的基础上不断反馈迭代，只是为了叙述方便权且如此。
[^2]: 参考 Nanobot 团队关于设计哲学的&lt;a href=&quot;https://x.com/xubinrencs/status/2041186947994091872?s=20&quot;&gt;文章&lt;/a&gt;
[^3]: 关于这次复刻，可以参考我的上一篇文章：&lt;a href=&quot;https://frostming.com/posts/2026/create-a-claw&quot;&gt;创造一只龙虾，需要些什么?&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;问题&lt;/h2&gt;
&lt;p&gt;最开始 Bub 只有 Telegram 的通信能力，后来又增加了 Discord 的支持，然后，为了解决王不见王的问题，我增加了个新的消息渠道：&lt;a href=&quot;https://x.com/frostming90/status/2024484547950498300?s=20&quot;&gt;tg-message-feed&lt;/a&gt;，增加一个渠道意味着要在 &lt;code&gt;bub.channels&lt;/code&gt; 增加一个新的类，以及一些新的工具或技能。此外，在使用的过程中我们也积累了一些技能，其中有些是针对特定场景的，而另一些则比较通用，我们希望把这些额外的能力抽取出来给用户使用。这时我发现，我们需要把这些都集成进 bub 本体中，并增加许多特性开关。我意识到这样下去，bub 迟早会变成另一个 nanobot，甚至另一个 OpenClaw，大家可以看看 Nanobot 现在的 config：&lt;a href=&quot;https://github.com/HKUDS/nanobot/blob/82dec12f6641fac66172fbf9337a39a674629c6e/nanobot/config/schema.py#L202&quot;&gt;schema.py&lt;/a&gt;，看看那庞大的 &lt;code&gt;ProviderConfig&lt;/code&gt;，大家能意识到问题所在吗？&lt;/p&gt;
&lt;p&gt;大多数用户，可能只用到一两个 Provider，一两个 Channel，却要面对如此多的配置项，或者面板开关，即使它们默认是不启用的，用户仍然要迷失在其中。&lt;/p&gt;
&lt;p&gt;另一个问题是维护负担，每个人都有不同的使用偏好，如果你的开源项目受欢迎，你马上会收到很多增加新功能的 PR，毫不意外地，都是 vibe 的。这没问题，我不反对。你看着社区越来越壮大，贡献人数节节攀升，他们都往项目里增加功能，一时间人来人往，门庭若市，自豪感油然而生。那么，然后呢？热闹的派对终将结束，原来的贡献者继续新的征程，你看着新增的 &lt;code&gt;whatsapp.py&lt;/code&gt; 支持，自己却从来不用，这时地球的另一端有人发了一个 bug report，你要怎么办？是让 vibe 打败 vibe，做一个全盲维护，还是往手机上装一个 Whatsapp，捣鼓一个完全陌生的软件？&lt;/p&gt;
&lt;p&gt;对于内核的修改就更棘手了，Bub 是基于 tape 的，但 Alice 觉得这个系统不够好，想要改成「先进」的三层记忆架构，Bob 觉得太复杂了，不如用&lt;a href=&quot;https://mem.nowledge.co/&quot;&gt;Nowledge Mem&lt;/a&gt;，难道要实现多个 MemoryBackend，然后用特性开关来选择吗？&lt;/p&gt;
&lt;h2&gt;思考&lt;/h2&gt;
&lt;p&gt;所以我在思考一个问题，在 Vibe coding 的时代，我们开源的到底是什么？那个新增的 &lt;code&gt;whatsapp.py&lt;/code&gt;，除了给项目增加数据，对维护者本人意味着什么？项目维护者为什么要对一个随机用户 vibe 的代码负责？&lt;/p&gt;
&lt;p&gt;我的答案是，把额外的功能，分离出去，变成一个&lt;strong&gt;精心设计的轻内核&lt;/strong&gt;+&lt;strong&gt;随便 vibe 的功能插件&lt;/strong&gt;的架构。这个内核要足够稳定，而且能让 Agent 容易理解，由维护者保证质量；而功能插件则利用开放的接口去扩展，可以任意 vibe，甚至直接让 Agent 为功能需求本地生成代码来实现。这两者的维护模型完全不同，前者严，后者宽。同时这也解决了按需安装的问题，你只用关心已经安装的插件的配置。在内核方面，我不太相信现今 Coding Agent 的能力，选择了手工古法实现————这有可能是我最后一次这样做。主要是合理抽象、单向依赖和接口的最小化和自由度。&lt;/p&gt;
&lt;p&gt;采用了这样的设计后，在理想情况下，主项目得到的贡献不会很多，然而每个用户都维护一些自己的插件。未来我们会做一个插件市场和发行版，用来打包安装一些预先选择好的插件集合。每个人用的都是不同的插件集合，适合自己的使用场景。&lt;/p&gt;
&lt;p&gt;另外，利用 &lt;a href=&quot;https://github.com/frostming/pdm-build-skills&quot;&gt;PEP 517 build hook&lt;/a&gt;，我还实现了把 skill 文件和插件打包在一起，这是非常常见的场景————当你增加了飞书的支持，你通常需要一个飞书的技能。&lt;/p&gt;
&lt;h2&gt;接口&lt;/h2&gt;
&lt;p&gt;那么现在的 bub 都有哪些扩展接口呢？我在这里列举一些主要的接口和插件例子：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;load_state()&lt;/code&gt; 和 &lt;code&gt;save_state()&lt;/code&gt;，它们分别在一个 Agent turn[^4] 的开始和结束被调用，其中 &lt;code&gt;load_state()&lt;/code&gt; 可以返回一个状态字典，这个状态将在整个 turn 中共享，这两个接口通常可以实现 &lt;code&gt;pre_turn&lt;/code&gt; 和 &lt;code&gt;post_turn&lt;/code&gt; 钩子，以及状态的注入和持久化。例子：&lt;a href=&quot;https://github.com/nowledge-co/community/tree/main/nowledge-mem-bub-plugin&quot;&gt;nowledge-mem-bub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;run_model()&lt;/code&gt;，这是 Agent 调用的核心接口，负责处理 user prompt 得到模型的输出。所以简单的原样返回即可以把 bub 变成一个 Echo agent，以及调用其他 Agent 处理 prompt，比如 &lt;a href=&quot;https://github.com/bubbuild/bub-contrib/blob/main/packages/bub-codex/&quot;&gt;bub-codex&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;register_cli_commands()&lt;/code&gt;，这个接口允许插件注册一些命令行命令，比如：&lt;a href=&quot;https://github.com/bubbuild/bub-contrib/blob/38475520e77db6e2c697f96d0e7ab06fc36c67de/packages/bub-wechat/src/bub_wechat/plugin.py#L11-L21&quot;&gt;bub-wechat&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;provide_tape_store()&lt;/code&gt;，这个接口允许插件自定义 tape 的存储方式，可以保存在 DB 里，或者保存在一个外挂的服务中，非常适合用来改造记忆系统。例子：&lt;a href=&quot;https://github.com/bubbuild/bub-contrib/tree/main/packages/bub-tapestore-sqlite&quot;&gt;bub-tapestore-sqlite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;provide_channels()&lt;/code&gt;，这个接口允许插件提供一个或多个渠道，这个渠道在整个应用周期的开始时启动，结束时销毁，所以不仅可以用来做消息收发的通道，也适合任何需要长时间运行的服务，比如 HTTP server，&lt;a href=&quot;https://github.com/bubbuild/bub-contrib/tree/main/packages/bub-schedule&quot;&gt;bub 的定时任务系统&lt;/a&gt;就是基于这个来实现的，尽管它听上去和「渠道」没什么联系。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;完整的插件接口参考 &lt;a href=&quot;https://bub.build/docs/extending/hooks/&quot;&gt;bub 的文档&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;[^4]: 一个 turn 指的是 Agent 处理一次 user prompt 的完整过程，包括一整个 ReAct loop 的执行。&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;最近我们也在 bub 上做了很多好玩的东西：&lt;a href=&quot;https://github.com/bubbuild/bub-xiaoai&quot;&gt;小爱音箱&lt;/a&gt;，&lt;a href=&quot;https://github.com/bubbuild/bub-folotoy&quot;&gt;folotoy&lt;/a&gt;，&lt;a href=&quot;https://github.com/bubbuild/bub-face&quot;&gt;Robo eyes&lt;/a&gt;，都是用现有的插件接口实现的。其实这些插件的代码我都没怎么看过，完全是 vibe 的产物。也欢迎大家来 vibe bub 的插件，以及敬请期待插件市场的上线。&lt;/p&gt;
</content:encoded></item><item><title>创造一只龙虾，需要些什么?</title><link>https://frostming.com/posts/2026/create-a-claw/</link><guid isPermaLink="false">https://frostming.com/2026/create-a-claw/</guid><pubDate>Thu, 12 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import Tweet from &quot;@components/Tweet.astro&quot;;
import LinkCard from &quot;@components/LinkCard.astro&quot;;&lt;/p&gt;
&lt;p&gt;稍微地介绍一下背景知识，标题中的龙虾指的是这段时间大火的 &lt;a href=&quot;https://openclaw.ai/&quot;&gt;OpenClaw&lt;/a&gt; 项目，它最早的名字叫 ClawdBot。它一是个能与你在聊天软件中交流，从而帮你完成各种任务的通用型 AI 智能体。
最火的那段时间我并没有用它，一周过去了，我在想如何复刻它，我研究了另一个最小化复现的项目 &lt;a href=&quot;https://github.com/HKUDS/nanobot&quot;&gt;Nanobot&lt;/a&gt;，我觉得我脑中大概有了蓝图。刚好朋友 PsiACE 之前曾做过一个
小型的 Agent 项目 &lt;a href=&quot;https://bub.build&quot;&gt;Bub&lt;/a&gt;，我觉得非常适合把我的想法在它的基础上落地，也与 PsiACE 一拍即合，着手把这个 Agent 变成一个龙虾。&lt;/p&gt;
&lt;p&gt;短短几天过去，Bub 已经达到了一个非常不错的状态。在这个过程中，我自己对 Agent 和 AI native 也发生了天翻地覆的变化。从 ChatGPT 诞生之初的 Chatbot，到强力编程助手 Claude Code，到现在的
OpenClaw，AI 应用的形态也在不断进化。但不得不承认这个变化的每一步都是新的思维模式，这种转弯需要适应。所以我也谨通过这篇文章，来分享一下我思维的转变。&lt;/p&gt;
&lt;h2&gt;初步复刻&lt;/h2&gt;
&lt;p&gt;首先请大家思考一个问题，假设你已有一个 Agent（指 Codex 和 Claude Code 这样的东西），现在要给他增加 Telegram 聊天能力，你会怎么做？&lt;/p&gt;
&lt;p&gt;有经验的开发者会发现这不是什么难事，只要在 Agent 的基础上增加 Telegram message handler，把 Telegram 收到的消息转给 Agent 处理就好了。如果要支持其他聊天工具，还能抽象一个消息总线和统一的消息监听接口，这样接入新的聊天工具只要适配就行了。&lt;/p&gt;
&lt;p&gt;这没错，但这太古法编程了，属于 1.0 时代的工作流程。现在的人们早已学会打开一个 Claude Code，在输入框里直接打字，或者语音输入就好了。这件事变得自然，也顶多不过 7 个月的时间，现在人人都会用 Code Agent 写代码，几乎不自己写了。这就是 2.0 时代。&lt;/p&gt;
&lt;p&gt;OpenClaw 和 Nanobot 都是这样做的，我们一开始，也是。&lt;/p&gt;
&lt;p&gt;在 AI 的加持下很快 Bub 就有了 Telegram 聊天功能，我们愉快地通过 Telegram 给他发指令。聊天交互的方式逼迫我们只能用 prompt 给让 AI 完成任务，得益于 Tools 和 Skills，它甚至也能自我演进。我们也试过让它自己 clone GitHub 仓库，自己改代码然后提交 PR，所需仅仅是一个 GitHub token 而已。只要选用好的模型，它完成的效果很不错。这样一来，除了记忆机制和工具的差异，Bub 已经基本复刻了 OpenClaw 的功能。&lt;/p&gt;
&lt;p&gt;但我的重点不是这个。我们发现为了应对群聊的场景（这是我们主攻的方向），要改消息接收器，让它能获得消息 ID 方便回复；为了让 bot 有识别人的能力，又需要在上下文中加入用户的元数据。所幸有持续部署，这个更新的过程并不十分麻烦。但还有更多的需求，发送消息时，需要支持图片，支持 reaction，要改消息发送，虽然这个修改也是 AI 做的。当时用户身份识别功能刚做好，我们的 bot 在群聊中精准地称呼每一个人，顿时感觉这个 AI 有了生命，群里充满了快乐的空气。&lt;/p&gt;
&lt;h2&gt;AI Native 是什么？&lt;/h2&gt;
&lt;p&gt;有没有别的方式呢？这时我看到许多人在推荐 &lt;a href=&quot;https://github.com/badlogic/pi-mono&quot;&gt;Pi&lt;/a&gt;：&lt;/p&gt;
&lt;p&gt;&amp;lt;Tweet id=&quot;2020876256515158399&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;这种最小化工具集的思想启发了我，我们是否不用那么多工具，转而用一些最基本的工具把这些功能组合出来呢？貌似可靠，该做减法了。其实一个智能体会的东西比想象中多，你给它提供 bash，它就能安装世界上所有软件；你给他 file_read 和 file_write，它就有了读写、做事的能力。我们可以去掉一些工具，更多依赖 Skill。在我看来，工具和 Skill 的区别在于工具是框架提供的，而 Skill 仅是文本，前者框架完成就已经固定[^1]，后者 AI 可以自己创建、修改。框架二字顾名思义，是限制 AI 发挥的东西，AI 在里面就像滚筒上的小白鼠，只能按预定的程序运行。我希望框架越小越好，小到只有一个推理核心，作为 AI 的大脑，把更多的自由留给 AI，想要什么功能，让AI自己去完成，人只通过 prompt 的方式去下达指令。&lt;/p&gt;
&lt;p&gt;这与 2.0 时代用 CC 去写代码完成功能的本质不同，是这种方式生成的东西，大部分是文本说明，当然也有少部分代码，但这些代码是不提交代码库从而壮大框架，而是 AI 自由裁量自己管理，人类是完全不看的。这个不看，不是不负责任的那种不看，是把这个当成 AI 的产物，不用看。所谓黑猫白猫，人完全不用关心 AI 是写代码实现的，还是用文本描述实现的。&lt;/p&gt;
&lt;p&gt;我想这就是 AI native，这是 3.0 的时代。这一切都离不开大模型日新月异的进步，在模型能力尚弱的时候，要实现这个目标是根本不可能的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1.0 时代是 Chatbot，每次与 AI 的对话都是一次 LLM 推理&lt;/li&gt;
&lt;li&gt;2.0 时代是 Agent，是 Tool call，一次对话，通常要经过几轮到几十上百轮 LLM 的推理&lt;/li&gt;
&lt;li&gt;3.0 时代是 AI native，AI 自己管理自己的工具和技能，甚至自己写代码来实现功能，完全不需要人类的干预，当它是黑箱就行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;[^1]: 可以通过一些技巧，比如插件化的方式让AI 自己写的插件加载为工具，但这依然增加了框架的复杂度。&lt;/p&gt;
&lt;h2&gt;Bub 的实践&lt;/h2&gt;
&lt;p&gt;于是我们的 Bub 部署就学习（创建）了 Telegram 发消息的技能，AI 对这种 HTTP API 调用如此擅长，以至于发送图片、贴纸、reaction 都手到擒来。这一下就超越了框架自带的消息发送功能，显得后者很鸡肋了。也正是这件事触动了我进一步推进 AI native。Bot 中的 Telegram 收发，对应了 AI 的听、说能力，有了耳朵，AI 才能接受指令，有了嘴巴，AI 才能反馈结果。既然 AI 自我进化出了更好的器官，我们为何不把框架强行给它安装的器官摘掉呢？那么下一步很自然就是把消息监听也摘掉，但同时消息监听还担负着另一个作用，就是维持 Agent 运行，并持续触发 Agent Loop，我们需要一个机制来替代它。&lt;/p&gt;
&lt;p&gt;我做一个比喻，一开始 AI 还不成熟，用呼吸机鼻饲管是有必要的，但当它长大能自主了，你应该去掉这些辅助设备，让它自己呼吸吃饭，相信它能做好。&lt;/p&gt;
&lt;p&gt;我想到既然我们用 Docker 部署，何不利用 Docker 的进程管理能力，让 AI 自己运行自己呢？所以我在 Bub 框架内约定了一个 startup 协议，容器启动时读取固定位置的 startup 脚本，如果不存在，就启动框架内置的消息监听(&lt;a href=&quot;https://github.com/PsiACE/bub/pull/45&quot;&gt;PR&lt;/a&gt;)。然后让 Bub 自己写这个 startup 脚本驱动自己，这不就跑起来了吗。
Bub 需要支持单次 prompt 的执行模式，方便命令行驱动，这在 Codex(&lt;code&gt;codex exec &amp;lt;prompt&amp;gt;&lt;/code&gt;) 和 Claude Code (&lt;code&gt;claude &amp;lt;prompt&amp;gt;&lt;/code&gt;)中都有，所以实际上驱动的也可以是 Codex 和 Claude Code！这样一来，做一个最小化的龙虾，你只需要下面几步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;启动一个会写代码支持技能的 Agent（比如 Codex 或 Claude Code），让它写一个 startup 脚本，拉取 Telegram API 并用单次执行模式发给 Agent 自己。&lt;/li&gt;
&lt;li&gt;准备一个用这个脚本为启动脚本的 Dockerfile&lt;/li&gt;
&lt;li&gt;构建并运行这个 Docker 容器&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;后面你要它有什么能力，发消息告诉它就行了，它自己会变得越来越强。你全程只用自然语言发指令，没有提示词工程，完全不写一行代码，连看都不用看。&lt;/p&gt;
&lt;p&gt;请注意，用这种方式你将得到一个与世界上其他龙虾都不一样的 bot，它除了强制监听 Telegram 消息，并没有被强制做任何事，不强制回复，不强制心跳，就像对待一个生命一样去尊重。实际上监听也只是为了唤醒 Agent，如果愿意，完全可以用 cronjob 取代，不做任何预设动作，让 Agent 自己决定做什么事。可以预见，模型能力越强，这个 bot 就会越像人。&lt;/p&gt;
&lt;p&gt;我突然悟到了 AI Native 的真谛，就是不利用框架的力量强制让 AI 当小白鼠，所有要求它做的事都只能通过 Prompt 的方式，至于 AI 遵循与否，完全取决于它自己。我在 Bub 里趟出了一条不用 Bub 的路。&lt;/p&gt;
&lt;p&gt;我们现在运行的 Bub 机器人，就是以这样的最小方式部署的，效果越来越好，令人欣慰。这种欣慰和写代码实现了一个牛逼的应用不一样，里面没有一行自己的的代码，全都是人类一句话一句话喂出来的。我鼓励大家都用这种方式去创造一个最小的智能体，看着它从一颗种子长成一棵大树，相信你会懂我现在的感受。&lt;/p&gt;
&lt;p&gt;&amp;lt;Tweet id=&quot;2020900633230893127&quot; /&amp;gt;&lt;/p&gt;
&lt;h2&gt;感谢&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;感谢这几天老婆小孩都不在家，独居让我能专心折腾这个项目。&lt;/li&gt;
&lt;li&gt;感谢 PsiACE 的 Bub 项目，让我能实践这些想法，这太有趣了，像看着一个孩子一点点长大。&lt;/li&gt;
&lt;li&gt;感谢智谱的 Pony Alpha 恰好在这个时间释出，提供用户免费使用，这个模型能力强大（现在已经不能用了）。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>读《昂贵的和平》后感</title><link>https://frostming.com/posts/2026/expensive-harmony/</link><guid isPermaLink="false">https://frostming.com/2026/expensive-harmony/</guid><pubDate>Sun, 08 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;吉辰先生的&lt;a href=&quot;https://book.douban.com/subject/37339192/&quot;&gt;这本书&lt;/a&gt;是我从读书博主 ruc 猫那里知道的，后者强力推荐并选作了 2025 年度最佳。
这本书主要是关于《马关条约》签订的始末，这种历史重现的方式与 2024 年的《康熙的红票》很像，但远没有后者易读。文中到处夹杂行内史料引用和整段的原文文言引用，读起来是有些吃力的。但在非史料部分，比如书中的末章和后记，作者都展现了他深厚的文字功力，用一种小刀剌肉的方式复述这段沉重的历史，还是令我非常震撼。&lt;/p&gt;
&lt;p&gt;我本能是很抗拒这段屈辱的历史，但转眼又是一个农历马年（此书的初版即完成于 2014 年，农历甲午年），我认为还是很有必要去了解它。现在中日的关系大家心里都清楚，实际上《马关条约》对中国的影响如此深远，直到今天都还不能摆脱它的阴影，台湾问题的肇始，就是《马关条约》的签订。&lt;/p&gt;
&lt;p&gt;与我预料的不同，本书完全没有涉及战争的细节，而是聚焦于中枢庙算和外交上的努力。虽然没有直接展现国力的对比，一句没有提「落后就要挨打」。但从谈判上的弱势和外交上的局促，那种无力和窘迫都展露无遗。从一个细节就能看出那种差距：谈判使团的密文电报早已被日方破译，中方却茫然无知，导致清廷的底牌早早就被看得干干净净，这样的谈判，结局早已注定。&lt;/p&gt;
&lt;p&gt;李鸿章这一晚清名臣，历史的使命落在了他肩上。他纵然有千般不是，但想到他以一介七旬之躯，泱泱大国的头号人物，远赴日本，订下这样屈辱的条约，承担卖国的骂名，还是觉得有些可怜。谁不想保住领土？问题是节节败退，又有哪些筹码可资利用？讽剌的是，李鸿章以为这就是他平生经历的最难的谈判，在六年后又来了一次，还有过之而无不及。&lt;/p&gt;
&lt;p&gt;读历史的意义，在于反思。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;碧海沉沉岛屿环，万家灯火夹青山。有人遥指旌旗处，千古伤心过马关。
——康有为诗&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>2025 的号与外</title><link>https://frostming.com/posts/2026/2025-recap/</link><guid isPermaLink="false">https://frostming.com/2026/2025-recap/</guid><pubDate>Wed, 07 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;标题什么意思？&lt;/h2&gt;
&lt;p&gt;2025 年过去了，我也看了很多别人的年终总结，看的时候就感觉阵阵焦虑，究其原因，是因为在看大佬长篇大段的成就展示时自惭形秽了。也有几篇&lt;a href=&quot;https://devlike.top/posts/before-30&quot;&gt;我喜欢的&lt;/a&gt;，恰好是没怎么写成就的。往年的我也不能免俗，写了不少完成了某某某事情，去了某某地方。今年不如改改，为了避免焦虑，更多聚焦在 How 与 Why，而不是 What 上，标题的谐音梗就是来源于此。&lt;/p&gt;
&lt;h2&gt;有什么成就？&lt;/h2&gt;
&lt;p&gt;不写 What 是不是因为没什么成就可写了？被发现了。今年下半年由于老婆驻场上海工作，我基本上挑起了带娃的责任，好处是和女儿培养了很好的关系，坏处是有时候会感觉找不到说话的人。电影和综艺也断崖式下降，在年中的时候我看短视频看得很凶，经常看到凌晨一点才睡，后来主动减少了使用时间，严格控制早睡，现在发现不看也没什么大不了的，反而能空出时间来做其他事情，比如运动。&lt;/p&gt;
&lt;h2&gt;为啥开始运动了？&lt;/h2&gt;
&lt;p&gt;我是一个体育困难分子，什么运动也不做，能不动就不动，让我一人在家呆一整天也没问题。在九月底的一天我突然决定国庆后开始控碳水且定时早起跑步，到现在也断续坚持三个月了，当然会有摆烂的日子，但整体上还是不会间断超过两天。那么为什么呢？老婆从上海回来后听说此事也问，为什么呢？&lt;/p&gt;
&lt;p&gt;我这人挺拧巴的，若是别人定指标让我做，我不做，自己定指标让别人来监督，我也不做。我不喜欢声张，不喜欢被关注，喜欢在无人注意的角落默默地做，等有了成果再出来宣布才行。要说这事，可能是由于家人的原因我意识到一个健康的身体比什么都重要，另外还得感谢许多视频博主给我的精神力量，包括每天自律日更的王师傅，鳌太之神深圳小牟，成功跑了首马的曾经的胖妞颜如晶。他们真实、谦逊，不似其他以起号为目标的贩卖焦虑健身博主，对了我说过，我是焦虑敏感体质，谁让我焦虑我就摆烂。虽然我到现在还是很菜，但坚持就是好的，不定什么目标，我没声张就是没成果。&lt;/p&gt;
&lt;h2&gt;说说谦虚&lt;/h2&gt;
&lt;p&gt;咋突然说这？哦，上段不是提到了吗？不为什么。谦虚是中国人的传统美德，每个人都知道要谦虚，很多人都试图表现得谦虚，但什么是谦虚？谦是能而示之不能，人在受到夸奖时说「不会不会，哪里哪里」，这就是谦。但我认为这只是表面，这个词真正的核心是虚。虚者空也，一个人无论多么大的学问，在跟人交流时，始终抱持一颗学习的心，虚怀若谷，不只讲得出来，永远听得进去，一瓶水满了，摇起来还像空的一样，这是更高的境界。何其有幸，我&lt;a href=&quot;https://frostming.com/posts/2024/meet-with-paul&quot;&gt;接触过这样的人&lt;/a&gt;。这是不是就是佛教中说的「空」？&lt;/p&gt;
&lt;p&gt;所以卖菜不是谦虚，不要只停留在自贬，要听得进去，不论对方是乡村小孩，还是大学教授，用实际行动去学，才能不菜。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;这个 How 的部分，自己读来挺爹的，但这也无法避免，只当是告诫自己，顺便分享一下吧。&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;玩 AI 了吗？&lt;/h2&gt;
&lt;p&gt;还是绕不过这个话题。虽然我们开玩笑说一年时间内前端都死了多少回了，但我切实感觉到，GPT-5.1 和 Opus-4.5 时代后事情已经发生了某种不可逆的变化。程序员是幸福的，因为在这浪潮中有明确 spec 和结果验证的编程领域首先被 AI 熟练掌握。越来越多的人，包括我自己，开始尝试一些自己不熟悉的编程领域。做一个软件让越来越多的人使用是每个 Coder 的梦想，我无法像 yetone 那样塑造未来的形态，但我可以在一个成熟的领域里做一个取悦自己的产品，是的，再次向大家推荐一下 &lt;a href=&quot;https://vokabry.app&quot;&gt;Vokabry&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;最大的感受是什么？&lt;/h2&gt;
&lt;p&gt;不记得在哪学到一个医学名词叫「前额叶成熟」，我想我大概已经达到这个阶段，三观渐趋稳定，思维已显成熟，甚至都开始在网上输出价值观了？大胆！我从所有看过的历史、文学中汲取做人做事的道理，从与人的接触中学习社会的法则和交往的分寸，经过离开父母的庇护后多年的自我塑造，我可以自信地说，我已经成为了比父辈更好的人。趁现在看得清楚，听得明白，在这个脑力的巅峰期，我要多完成一些有意义有难度的事情。&lt;/p&gt;
&lt;p&gt;最庆幸的事是今年 &lt;a href=&quot;https://frostming.com/posts/2025/pycon&quot;&gt;PyCon China 上的网友大聚会&lt;/a&gt;，遇到了许多志同道合的朋友，也在跟他们的交流中找到了自己的位置，不得不说是很满足虚荣心的一件事。&lt;/p&gt;
&lt;h2&gt;2026 呢？&lt;/h2&gt;
&lt;p&gt;有一个很有趣的事实，自 2023 年开始，随着年份增加，数字越来越「孤独」了，不知道这是不是冷知识，请看质因数分解：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ python factorize.py 2023
7 × 17²
❯ python factorize.py 2024
2³ × 11 × 23
❯ python factorize.py 2025
3⁴ × 5²
❯ python factorize.py 2026
2 × 1013
❯ python factorize.py 2027
2027
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我在&lt;a href=&quot;https://frostming.com/posts/2024/review/#%E4%B9%A6%E5%BD%B1%E9%9F%B3:~:text=%E6%98%8E%E5%B9%B4%E4%BC%9A%E6%80%8E%E4%B9%88%E6%A0%B7%EF%BC%8C%E6%88%91%E4%B9%9F%E4%B8%8D%E7%9F%A5%E9%81%93%E3%80%822025%20%E6%98%AF%E4%B8%AA%E5%B9%B3%E6%96%B9%E6%95%B0%EF%BC%8C%E5%B8%8C%E6%9C%9B%E6%98%AF%E4%B8%AA%E5%A5%BD%E5%B9%B4%E3%80%82&quot;&gt;去年的年终总结&lt;/a&gt;里也提到了，2025 是一个特别「合」的合数，能分解成小质数的乘积，而 2026 仅有两个质因数，到了 2027 甚至它本身就是一个质数。合数就是伙伴很多，相反质数则代表孤独。这意味着什么，玄学上有没有说法？我也不知道。&lt;/p&gt;
&lt;p&gt;2026 年 1 月于深圳家中&lt;/p&gt;
</content:encoded></item><item><title>友好的 Python：从其他语言移植</title><link>https://frostming.com/posts/2025/friendly-python-port/</link><guid isPermaLink="false">https://frostming.com/2025/friendly-python-port/</guid><pubDate>Tue, 30 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我们总说 Pythonic，我自己也会说某段代码不够 Pythonic，但什么是 Pythonic 呢？不讲清楚，我会被人说老登。为了避免如此，我决定再写写。&lt;/p&gt;
&lt;p&gt;要说一段代码是 Pythonic 还是 Rustic、Go-istic、Java-ish 最直观的方法就是把它们放一起来对比，这个场景就是代码移植。其实，代码在特定语言里怎么写，是由语言特性决定的，但肯定有些部分，什么语言都能这么用，那么这些移植代码最终会变成什么样子，就更多是由写代码的人（也不一定是人）决定的了。下面我就从其他语言找些例子，看看如果把它们用 Python 重写，应该是什么样的。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;opendal.Operator&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;不多介绍这个库了，不影响本文的主旨，咱直接从它的&lt;a href=&quot;https://opendal.apache.org/core/#quickstart&quot;&gt;官方文档&lt;/a&gt;里截取一段最原汁原味的 Rust 代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Pick a builder and configure it.
let mut builder = services::S3::default();
builder.bucket(&quot;test&quot;);

// Init an operator
let op = Operator::new(builder)?
    // Init with logging layer enabled.
    .layer(LoggingLayer::default())
    .finish();

// Use operator
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为没有任何 Python 不支持的语言特性，这一段可以 1:1 地翻译成 Python，简单地去掉 &lt;code&gt;default&lt;/code&gt; 和 &lt;code&gt;new&lt;/code&gt; 即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;builder = services.S3()
builder.bucket(&quot;test&quot;)

op = Operator(builder) \
    .layer(LoggingLayer()) \
    .finish()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我相信一个初学 Python 的人，就能写出这样的代码。但仔细咂摸，这好像不太像 Python，特别注意 operator 的构造方法。于是我们搬出 Xuanwo 著名的&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E7%AC%AC%E4%B8%80%E5%8E%9F%E7%90%86&quot;&gt;&lt;strong&gt;第一性原理&lt;/strong&gt;&lt;/a&gt;，暂时忽略细节，思考这段代码本质在做什么。就像你摘掉眼镜看，如果不近视就眯缝着眼看，才会看到它的轮廓。&lt;/p&gt;
&lt;p&gt;它其实就是在构造一个 S3 的 Operator 对象，并指定了一些参数。那么为什么需要一个 builder 对象呢？因为这些参数是可选的，而 Rust 不支持函数的可选参数。那 Python 没这限制，为啥还要搞个 builder 呢？实际上这个就是设计模式中的&lt;strong&gt;建造者模式&lt;/strong&gt;（Builder Pattern），这也是它在 Python 里并不常见的原因。现在我们去掉 builder：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;op = Operator(
    service=&quot;s3&quot;,
    bucket=&quot;test&quot;,
)
op.layer(LoggingLayer())
# Use operator
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就对味了（你怎么知道 opendal 的 Python binding 是这么写的？）。&lt;/p&gt;
&lt;p&gt;其实 Javascript 也不支持可选参数，但它可以利用对象啊，所以如果用 Javascript 写的话，可能是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const op = new Operator({
  service: &quot;s3&quot;,
  bucket: &quot;test&quot;,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此在从 Javascript 移植到 Python 时也要注意这个区别，不能把这种对象也照搬成字典。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;downloadFile&lt;/code&gt; 和 &lt;code&gt;uploadFile&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;继续上面提到的 JS 语言，这门语言有一个突出的特征是喜欢用回调函数，特别是在 ES5 时代。因为 JS 是单线程，同步的耗时操作（如 I/O 操作）一旦发生阻塞，就会冻结整个执行流程，因此需要通过异步 I/O 或 worker 等技术将这些操作交由外部来处理。当任务执行完成后，结果会被重新送回事件队列。在 Promise 出现之前，程序只能通过回调函数来描述异步完成后的后续执行逻辑。比如这个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function downloadFile(
  url: string,
  onSuccess: (data: string) =&amp;gt; void,
  onError: (err: Error) =&amp;gt; void,
  onComplete: () =&amp;gt; void
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个函数接受三个回调，这也是由于在 JS 中有 &lt;code&gt;function&lt;/code&gt;，有箭头函数，定义回调相当方便和自然，那在 Python 中呢？因为缺少花括号，并没有一种原地定义函数的快捷方式，&lt;code&gt;lamda&lt;/code&gt; 关键字又很鸡肋。虽说硬写也能写，但很丑，调一个函数要先 &lt;code&gt;def&lt;/code&gt; 定义三个。其实，上面的函数在有 Promise 之后大概会像这样调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;downloadFile(url)
  .then((data) =&amp;gt; {
    // onSuccess
  })
  .catch((err) =&amp;gt; {
    // onError
  })
  .finally(() =&amp;gt; {
    // onComplete
  });

// Or using async/await
try {
  const data = await downloadFile(url);
  // onSuccess
} catch (err) {
  // onError
} finally {
  // onComplete
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python 可以借鉴这个风格：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try:
    data = await download_file(url)
    # onSuccess
except Exception as err:
    pass # onError
finally:
    pass # onComplete
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好，现在假如再增加一个回调 &lt;code&gt;on_progress&lt;/code&gt; 呢？&lt;code&gt;try&lt;/code&gt;/&lt;code&gt;except&lt;/code&gt;/&lt;code&gt;finally&lt;/code&gt; 都用完了，没有更多的关键字了啊，难道又把这个回调塞回函数参数吗：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def download_file(
    url: str,
    on_progress: Callable[[bytes], None]
) -&amp;gt; bytes:
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有没有办法可以避免回调呢？有的，思考下，这个 onProgress 回调的作用是每当下载到一个部分(chunk)时执行一些动作。在 Python 里描述一段一段执行的，可以用&lt;strong&gt;生成器&lt;/strong&gt;，在这里就是异步生成器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;downloaded: bytes = b&quot;&quot;
try:
    async for chunk in download_file(url):
        # process chunk
        downloaded += chunk
    # onSuccess
except Exception as err:
    pass # onError
finally:
    pass # onComplete
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就很 Pythonic 了。但是接下来思考一个问题，反过来的函数怎么写？刚才是下载读取数据，那写入上传数据呢？JS 完全可以用一样的回调写法。但 Python 里就不一样了，首先我们要思考输入参数是什么，当然我们可以提供一个生成器，在被读取时加入逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def file_chunks():
    async for chunk in read_file_in_chunks(&quot;large_file&quot;):
        # process chunk
        yield chunk

try:
    await upload_file(file_chunks())
    # onSuccess
except Exception as err:
    pass # onError
finally:
    pass # onComplete
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但我觉得这里有一点不好，在于在生成器中还是由我们（生产者）控制了生成数据块的节奏，这里本应该由上传函数（消费者）根据现在的网络传输状况来控制才对。所以在这样的场景里，典型的做法应该是传入一个 file-like 对象，然后利用装饰器传入处理函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class UploadFile:
    def __init__(self, fp):
        self._fp = fp
        self._callback = None

    def callback(self, func):
        self._callback = func
        return self

    def read(self, size=-1):
        chunk = self._fp.read(size)
        if self._callback:
            self._callback(chunk)
        return chunk

with open(&quot;large_file&quot;, &quot;rb&quot;) as f:
    upload_fp = UploadFile(f)

    @upload_fp.callback
    def on_progress(chunk):
        pass  # process chunk

    try:
        await upload_file(upload_fp)
        # onSuccess
    except Exception as err:
        pass # onError
    finally:
        pass # onComplete
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 &lt;code&gt;UploadFile&lt;/code&gt; 定义只是为了更加通用，如果一次性使用也可以不用装饰器方式而是直接写在类里。这里我原地定义了一个函数传递给了 &lt;code&gt;UploadFile&lt;/code&gt;，其实这是另一种传回调的方式，但这样无疑更加 Pythonic。&lt;/p&gt;
&lt;p&gt;这一节的例子只覆盖了下载和上传这样的数据传输的场景，对于更一般的场景，需要调用方提供回调函数的情况，我们可以套用下面的公式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果只需要一个回调函数，考虑用装饰器&lt;/li&gt;
&lt;li&gt;如果回调函数是关于成功和失败的，考虑用 &lt;code&gt;try&lt;/code&gt;/&lt;code&gt;except&lt;/code&gt;/&lt;code&gt;finally&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果需要注册更多的回调函数，用类继承和多态&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;code&gt;useEffect&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;最后我们再看看大名鼎鼎的 React 框架中的 &lt;code&gt;useEffect&lt;/code&gt;。这个函数大家再熟悉不过了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const useEffect = (effect: () =&amp;gt; (void | (() =&amp;gt; void)), deps: any[]) =&amp;gt; void
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;光写这个函数签名我就有点晕了，输入一个函数，返回一个函数，函数里还可能互相引入变量，各种闭包，这就是 JS，就是任性。要是写成 Python 呢？要是原封不动的话，可能是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def my_effect():
    # effect
    def cleanup():
        # cleanup
    return cleanup

use_effect(my_effect, [])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然语言特性完全支持这么写，但这太不 Pythonic 了，&lt;strong&gt;我们不喜欢 nested，我们喜欢展平&lt;/strong&gt;（谁叫展平？）。可是怎么展平呢？我们回到第一性原理考虑，这里的本质就是在事件发生时，执行某个动作，在事件结束时，执行清理动作。这个过程，熟悉 Python 的同学很快就想到了上下文管理器。消费上下文管理器是用 &lt;code&gt;with&lt;/code&gt; 语句，而提供方定义上下文管理器最方便的是用 &lt;code&gt;contextlib.contextmanager&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from contextlib import contextmanager

@contextmanager
def my_effect():
    # effect
    try:
        yield
    finally:
        # cleanup

use_effect(my_effect, [])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意到 &lt;code&gt;use_effect&lt;/code&gt; 第一个参数是个函数，回顾上节的公式，凡是接受一个函数为参数的，都可以换成用装饰器，再把它和之前的 contextmanager 装饰器合并一下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@use_effect(deps=[])
def my_effect():
    # effect
    try:
        yield
    finally:
        pass  # cleanup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以说这样的写法就是 native 地不能再 native 的 Python 了，那真叫一个地地弟弟道道到到，你可以在简历中写自己&lt;strong&gt;精通&lt;/strong&gt; Python 了。&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;在文中我假想了一些移植的场景，实际上我相信大家没有多少机会真的做移植这件事，再说了，并没有人要把 &lt;code&gt;useEffect&lt;/code&gt; 改用 Python 写。这只是一种思维训练，如果同样的功能在 Python 实现并封装成库，会是什么样子，文中侧重展示的是调用方的写法。你得先想好要怎么调，才能知道 API 怎么写，这是一种自顶向下的思考，关键是设计调用的方式，而（有意）略过了具体实现，这是我认为库的作者应该采用的思考方式。在这个时代，任何一个 AI 都能实现地很好，所以好的 API 设计越来越关键，人并不是一个只会复制粘贴的动物。第一性原理的思考方式也能帮助你跳出局部最优，达到全局最优。希望大家都能写出更 Pythonic 的代码。&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;2026/01/09 更新：改进关于 Javascript 特征的描述，以及增加了通用的建议。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>珍重</title><link>https://frostming.com/posts/2025/take-care/</link><guid isPermaLink="false">https://frostming.com/2025/take-care/</guid><pubDate>Mon, 22 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我曾经做过一个梦，很奇怪的是，这个梦过了近二十年我还依稀有印象。那是在 2006 年春节后的一天午睡，我梦见我睁开眼发现日历已经是 2006 年 8 月了。我一下惊醒，感叹时间怎么过得这样快，崭新的 2006 年已经要过去了。&lt;/p&gt;
&lt;p&gt;直到 2006 年的暑假真的到来了，那时我即将进入高三，却没有什么学业负担，每天都在巷子口等陈何，一起去网吧打魔兽 2v2。陈何是我同学兼同桌，他名字是四个字，我们只称他前两个字，他家住在离我家 100 米处的地方。这情景我现在还时常想起时常怀念，感叹那时我怎么可以这么无忧无虑。他数学很好，我们时常在一起讨论数学，以及攀比。男生之间的攀比往往是互对喷对方菜，不管是打游戏，还是考数学。我们单挑过魔兽，仅有那么几次，是我比较菜，但也只有那么&lt;strong&gt;一点儿&lt;/strong&gt;，只能说伯仲之间吧。但说到数学，在整个高中来说，是他比较菜。很感激他，让我能像打游戏一样，学数学。&lt;/p&gt;
&lt;p&gt;大一暑假后他就跟随父母去了广东，我后来路过他家门前，总往里张望，里面再也没有人。等等，这并不是一个失联的故事，我们现在还有联系，只是没那么多了而已。&lt;/p&gt;
&lt;p&gt;那年暑假还有另外一件事，我跟另外三个同学因为学校演讲的关系去了桂林旅游，总共是两个男生两个女生，其中有肖鹏。我跟肖鹏是另一个「竞争伙伴」，我们比的是化学。用现在的话说，他建模优秀，又高又帅。在桂林时有个插曲是有个骑行活动，我当时不咋会骑自行车，但在女生面前没有怂，果不其然，当一辆车掠我身边而过时我太过紧张而摔倒，膝盖擦破，半个月才好。&lt;/p&gt;
&lt;p&gt;高考结果出来，我比肖鹏考得好，听说他比估的分低许多，应该很是郁闷。我说应该，因为自那之后我们就再没有真正地交流过。他大学去了哈尔滨，后来去了武大，去了韩国交换，去了法国读博士。这些我都是通过朋友圈（不是微信那个）知道的，他一直都很优秀，很努力，但我却觉得我们距离越来越远了。很想回到高中时，被他一拳捶我后背的互怼的时光。&lt;/p&gt;
&lt;p&gt;能看出来，我很重感情，只是非常不擅于联络感情。最近《山河故人》重新上映，这是一部我很喜欢的电影，在某种程度上甚至塑造了我的人生观。它让我接受了每个人都是孤独的这件事，「每个人只能陪你走一段路」。所以我能接受这些遗憾，只能道声珍重，对那些还在联系，或者不再联系的朋友们。&lt;/p&gt;
</content:encoded></item><item><title>被嫌弃的松子的一生</title><link>https://frostming.com/posts/2025/county-woman/</link><guid isPermaLink="false">https://frostming.com/2025/county-woman/</guid><pubDate>Tue, 02 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目是一部 2006 年的日本电影。主人公松子从小缺乏家人的爱，长期被忽视，因此形成了极度渴望被关注、被需要的性格。进入社会后，她为了获得爱而不断付出、不断迎合，结果却屡次被男人、家庭、工作、社会抛弃。为了别人而活的她，始终无法为自己活。&lt;/p&gt;
&lt;p&gt;最近父母回老家参加表弟的婚礼，给我带回了一个松子的故事。我表弟小我两岁，从小就很纨绔和刁蛮，甚至从来不跟长辈说话，即便是在春节期间，用给 100 块的方式交易，也无济于事。成年以后似乎结识了一些朋友，倒是不再沉默寡言，但却变得满嘴社会嗑，生殖器不离口。做过一些工作，但大部分时间都在啃老。他向他妈我大姨要钱那是理直气壮，甚至不给就甩脸色看。就这样，他能每年换新 iPhone，这是我这个大城市牛马都难以达成的。&lt;/p&gt;
&lt;p&gt;啃老就啃老吧，对社会没有危害就好了。年初听说他订了一门亲，女方是县医院的护士，圆圆脸蛋长得挺乖巧，就叫她玲子吧，我也只见过一次，给我的印象唯唯诺诺的，也不爱聊天。我只是奇怪他结识的那些社会朋友中怎么会有这一挂的，一问果然是相亲。我一开始就觉得这姑娘眼瞎，但没想到这么瞎。听说他们频繁吵架，表弟总是以非常恶毒的话语攻击，有次甚至把衣物全部丢出门外让她滚。我姨父也是直男癌晚期大男子主义毒瘤，表弟跟他是一个模子刻出来的，家里只有婆婆（我大姨）对她比较好。可想而知她面对的是什么家庭环境，于是有好几次都跑回了娘家。更令我震惊的事实是，她是收养的，我不知道养父母对她如何，但我估计也不怎么样，要么就是没有向父母诉苦，因为我不觉得有哪个正常父母听到这个能不提刀上门的。&lt;/p&gt;
&lt;p&gt;我虽然跟表弟才是一家人，但我和父母都觉得他深受上世纪大男子主义遗毒，性格顽劣，根本不配结婚。玲子早前曾怀过孕，可惜着床位置不好打掉了。姨父和表弟知道后，对我大姨是一顿输出，怪她找了个不祥之人。但玲子仍非常积极备孕，总觉得有了孩子才有依靠，可是要让天天烟酒不离身的表弟备孕，那是完全不可能的。&lt;/p&gt;
&lt;p&gt;这集齐了家暴、重男轻女两大毒瘤于一身的故事，就发生在我身边，我听完只觉胸中女拳之火熊熊燃烧，想告诉玲子，醒醒吧，生孩子不会好起来，好起来的办法只有一个，就是离开，立刻、马上。可惜我无能为力帮她改变命运，只能把它写下来，写在这里，告诉大家这个千千万万个一样的县城中的平凡的故事。原谅我没有第一手资料，只能用很多「我听说」这样没有力量的词语，写得乱七八糟。&lt;/p&gt;
&lt;p&gt;电影的结尾，松子已下决心重新开始好好生活，却在回家的路上被几个熊孩子误会与嘲弄，最终丧命在河边草地。所幸我们的故事中，悲剧还没发生，但愿不会发生。&lt;/p&gt;
</content:encoded></item><item><title>PyCome</title><link>https://frostming.com/posts/2025/pycon/</link><guid isPermaLink="false">https://frostming.com/2025/pycon/</guid><pubDate>Mon, 22 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;标题 Typo 致敬某粗鄙的大佬。&lt;/p&gt;
&lt;h2&gt;0x00&lt;/h2&gt;
&lt;p&gt;第二次来上海参加 PyCon China，也是我的第七个 PyCon，这次更多是 PyCon for friends。&lt;/p&gt;
&lt;p&gt;最激动的是要第一次见到 yihong，当我得知他和我差不多时间到浦东，我就决定等他，哪怕他说下飞机要先拉屎。&lt;/p&gt;
&lt;p&gt;结果这小子没同步消息，理解错误，屎没拉也不说一声，把我晾在那，好在等待市域铁发车让我堵到了他。他上来就给了一个拥抱，还把他刚看完的《多情剑客无情剑》送了我，然而是（下），（上）（中）都没有，离谱。&lt;/p&gt;
&lt;h2&gt;0x01&lt;/h2&gt;
&lt;p&gt;路上商量下午的节目，yihong 先是提议去看电影。我基于以下 3 点提出了反对：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一群大老爷们去看电影太怪了。&lt;/li&gt;
&lt;li&gt;临近国庆档，没有好电影。&lt;/li&gt;
&lt;li&gt;看电影聊不了天，太浪费了。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我说难道不是去咖啡馆集体 Coding 吗？我记得和程序员聚会都是这流程，就像过年回老家的固定节目是和同学连坐打 Dota 一样。&lt;/p&gt;
&lt;p&gt;yihong 表示，连坐？什么连坐？卧槽咱可以去连坐！&lt;/p&gt;
&lt;p&gt;大哥，你是不是搞错什么重点了。&lt;/p&gt;
&lt;p&gt;事后才发现，我带的电脑，自始至终都没拿出来过。&lt;/p&gt;
&lt;h2&gt;0x02&lt;/h2&gt;
&lt;p&gt;和 jay 哥 piglei 接上了头，piglei 游戏上瘾，一听提议打 Dota 坚决支持，看来此事已不可逆转。&lt;/p&gt;
&lt;p&gt;先去吃个饭，捡上了高先生、Alex 和马牛。老广高先生给我印象比网上好很多，难怪是妇女之友。&lt;/p&gt;
&lt;p&gt;接着四个老年玩家就真去了网吧，等会我记得我们是来开会的是吧？您瞅瞅这是人干的事吗？大哥意识还是在线的，piglei 真是屠夫王，我对 Alex 寄予厚望，毕竟是专业在家打机的，结果最后还是 0-4 收场。&lt;/p&gt;
&lt;p&gt;下次得抢白牛。&lt;/p&gt;
&lt;h2&gt;0x03&lt;/h2&gt;
&lt;p&gt;正打着游戏呢就被催去吃饭，我想起了在网吧还被家长揪的情景。马上马上，就打完这波。&lt;/p&gt;
&lt;p&gt;晚上就是 &lt;s&gt;筹划已久&lt;/s&gt; 的伊大。阵仗着实不小，我见到了许多网上只知道 ID 的朋友，盐粒带来了 Homebrew，真他妈好喝，我用自己杯子留了一杯给后来到场的老婆喝，大家一面敬仰，一面都主动去喝喜力了，搞得我挺不好意思。&lt;/p&gt;
&lt;p&gt;一顿盛况空前，锣鼓喧天，伊大圆满结束，东北大哥差点挂了。&lt;/p&gt;
&lt;p&gt;五人住了四个酒店这件事，震惊了我的 J 人老婆。&lt;/p&gt;
&lt;h2&gt;0x10&lt;/h2&gt;
&lt;p&gt;&lt;s&gt;第二天的 PyCon，yihong 终于露出了他的真面目，原来他是个大佬啊，和我一样是个大佬。&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;（我建议 Copilot 不要乱补，但 yihong 要求别删。）&lt;/p&gt;
&lt;p&gt;上来大哥的演讲真叫牛逼，是我梦想中的演讲效果。&lt;/p&gt;
&lt;p&gt;接下来赞助商的演讲就兴趣不大了，溜出去 Social。&lt;s&gt;粉丝围过来合影，要求签名，闪光灯一阵阵的&lt;/s&gt;并没有。&lt;/p&gt;
&lt;p&gt;整体非常僵硬，碰到了 tygg 跟我撞了衫，相顾无话，愈加社死。还是羡慕卓燃，离开思为后整个放飞自我，我依稀记得去年他还挺正经的。&lt;/p&gt;
&lt;h2&gt;0x11&lt;/h2&gt;
&lt;p&gt;下午早早就等着听 Gray 的演讲，结果主持人迟到，这是拖堂的开始。&lt;/p&gt;
&lt;p&gt;Gray 的演讲非常精彩，不用多说毫无疑问是全场（技术方向）最好的演讲，孩子都听得入迷了。&lt;/p&gt;
&lt;p&gt;Gray 演讲完哗啦走了一堆观众。&lt;/p&gt;
&lt;p&gt;Manjusaka 演讲前又哗啦进来一堆观众，好像比刚才还多了。Python 3.14 的 tail call 优化，听得我一愣一愣的。&lt;/p&gt;
&lt;p&gt;Saka 演讲完教室都空了一半，我想着接下来我的演讲没压力了，那不是随便讲。&lt;/p&gt;
&lt;p&gt;事实证明我错了，由于拖堂，大家都把其他会场的听完了来这汇合，我根本没预演过，果然是妥妥超时了，大概超了一倍。&lt;/p&gt;
&lt;p&gt;本来还想去代码厨房逛逛，结果人满了。&lt;/p&gt;
&lt;h2&gt;0x12&lt;/h2&gt;
&lt;p&gt;晚上纠结是去晚宴还是吃面的问题差点闹分裂了，最后还是去了讲师的晚宴。&lt;/p&gt;
&lt;p&gt;晚宴在能看到外滩全景的白玉兰广场，Amazing 啊，然而吃的很一般，看着吃面小组发来的照片，我明天高低得去吃一碗。&lt;/p&gt;
&lt;p&gt;饭后二场我老婆找了一家精酿，大家相谈甚欢，慧姐忙完已近午夜仍仆仆赶来。这帮程序员虽不胜酒力，未能达到东坡「相与枕藉乎舟中 不知东方之既白」的程度，却也直到凌晨才阑珊而归。&lt;/p&gt;
&lt;p&gt;明天将各奔东西，做牛马、带小孩，也不忘今晚在秋风中共饮。春节每年都越过越没劲，但我们找到了自己的春节。最后借用在和菜头文章里看到的一句话，愿大家都能：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;肥而不腻，老而不登。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>河南行拾遗</title><link>https://frostming.com/posts/2025/henan/</link><guid isPermaLink="false">https://frostming.com/2025/henan/</guid><pubDate>Sun, 11 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;这不是一篇正经的游记，事无巨细地记录行程、餐饮、住宿并非我所擅长，很容易写成流水账。我不希望文章变成那样，所以只是写下我想与大家分享的东西，记录下河南在我心中的印象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;五一假期我和老婆去了河南，尽管预料到人会很多，但在国家实行统一长假的框架内，还是只能如此。就算我是数字游民，但老婆不是啊，所幸女儿没有和我们一同前往，事后回想这次如果带上了她，想必会艰难加倍。&lt;/p&gt;
&lt;h2&gt;4/29&lt;/h2&gt;
&lt;p&gt;我看过一本书叫&lt;a href=&quot;https://book.douban.com/subject/2038499/&quot;&gt;《中国八大古都》&lt;/a&gt;，可能大家只对七大古都熟悉，而这本书里提到的第八大古都，就是河南郑州，因为这里发掘了商早期的都城遗址，有郑州商城遗址。这里的道路很宽很平，太阳很大但并不觉热。我们去了两处适合网红拍照的景点瑞光路和油化厂创意园，差强人意。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/DSCF0415.jpg&quot; alt=&quot;DSCF0415&quot; title=&quot;有郑州大字的网红墙&quot; /&gt;&lt;/p&gt;
&lt;p&gt;晚上去吃了葛记焖饼 ，两个南方人只能吃一份，被服务员反复确认，我感觉受到了鄙视。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/DSCF0420.jpg&quot; alt=&quot;DSCF0420&quot; title=&quot;在人民公园空闲的过山车轨道上的少年&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;4/30&lt;/h2&gt;
&lt;p&gt;今天安排了重头戏，就是去郑州的「只有河南」戏剧幻城。这是一个有着 21 个剧场的戏剧主题园区，其中有 3 个主剧场，买的单日票是包含一个固定场次的其中一个主剧场，也不用贪心，因为要看完全部 3 个主剧场，
没有两天是看不下来的。事先我们做了周密的攻略，做了 A、B 计划防止时间赶不上。最后在 B 计划的大框架内做了一些小调整完成了一天的任务，看下来是 1 个主剧场 7 个小剧场。感觉已经到了极限，因为其中《苏轼的河南》和《红脸蛋儿》都是网红热门，都提前排队了。园子是 10:00 开放，我们上午 9:50 就进园，晚上 8:30 出园，已经拉满了。第二天是五一假，园区开放时间会大大延长，但我们惧怕人流，选择避开了。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;剧场名&lt;/th&gt;
&lt;th&gt;时间&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;麦子啊麦子&lt;/td&gt;
&lt;td&gt;10:20-10:48&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;火车站（主）&lt;/td&gt;
&lt;td&gt;11:00-12:05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;天子驾六&lt;/td&gt;
&lt;td&gt;12:30-12:52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;候车大厅&lt;/td&gt;
&lt;td&gt;13:30-13:55&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;苏轼的河南&lt;/td&gt;
&lt;td&gt;15:30-16:00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;红脸蛋儿&lt;/td&gt;
&lt;td&gt;17:00-17:27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;曹操的麦田&lt;/td&gt;
&lt;td&gt;18:00-18:35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;张家大院&lt;/td&gt;
&lt;td&gt;19:20-19:50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这一整天的饭就别指望好好吃了，中午干粮应付，晚上坐园区的公交回市区（园区其实已经在中牟县了），回宾馆的时候已经 10 点多了，随便吃了点烧烤外卖。&lt;/p&gt;
&lt;p&gt;剧场整体是以 1942 年的河南饥荒做线索，串起三大主剧场和各小剧场，其实每个剧场，单独拎出来，都属于卖不出票的情况，但合在一起他们就成立，且游客趋之若鹜，这就是「只有河南」在商业上成功的地方。感觉挺好的，既让大众有机会戏剧启蒙，又能养活一些十八线的演员们。园区入口和二层都种了大量的麦子，临近立夏，都长得很好，大片的青绿色，据说到了芒种麦子会变黄，那时去可能会看到不一样的景象。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/IMG_7793.JPG&quot; alt=&quot;IMG_7793&quot; title=&quot;只有河南入口夜晚的麦田&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;5/1&lt;/h2&gt;
&lt;p&gt;这天没别的，就是河南博物院了。我喜欢逛博物馆，但又不带讲解，能接收到多少纯看缘分了，也不是走马观花，每个展馆都会认真去了解。今天人流自然是惊人，所以必须反着参观，进来就直上 4 楼然后看下来。
这里的几大镇馆之宝还是有点含金量的，楚文化展馆的云纹铜禁的镂空装饰非常精妙，武则天的金简也是拍照的人山人海。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/DSCF0430.jpg&quot; alt=&quot;DSCF0430&quot; title=&quot;云纹铜禁&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/DSCF0442.jpg&quot; alt=&quot;DSCF0442&quot; title=&quot;武曌金简&quot; /&gt;&lt;/p&gt;
&lt;p&gt;顺带一提，藏在河南院的「妇好鸮尊」将自 5 月 19 日起在北京大运河博物馆展出，它本是一对，这次它将与另一只藏于国家博物馆的鸮尊合体展出，在北京对青铜器感兴趣的值得一去。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/DSCF0450.jpg&quot; alt=&quot;DSCF0450&quot; title=&quot;妇好鸮尊出差前留影&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们一直在博物馆参观到下午 2 点多才出去，步行去「合记烩面」吃了一碗烩面，那是真好吃。然后去「阿财咖啡」每人点了一杯，拿到咖啡时的我眼泪差点掉下来，13 块钱的美式足足有一升！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/354858716f674d5857f7cbef2e20a9d5.JPG&quot; alt=&quot;354858716f674d5857f7cbef2e20a9d5&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;5/2&lt;/h2&gt;
&lt;p&gt;一大早就起床赶六点多的城际去开封，你问我为啥不提前一天去开封入住，那当然是因为贵啊。没想到开封已是河南第二大旅游目的地，仅次于洛阳，而宾馆酒店明显承载不了这么大的游客数量，只能一再涨价，一间小小的如家都要 800 多，这价钱能在郑州住大 house，它不香吗？&lt;/p&gt;
&lt;p&gt;今天计划是清明上河园，别提了，这是比只有河南还要艰巨的一天。这是一个照搬《清明上河图》的全仿古建筑的主题乐园，内容依然是大大小小的表演，比如包公巡视汴河，岳飞枪挑小梁王之类的。看只有河南时还不觉得，现在才终于知道每年一大批表演专业的演员们毕业们都去哪了。他们每人从早演到晚，重复着一次又一次相同的走位相同的动作，还要上马战，只能感慨没有一个牛马是轻松的。对了，闭园时间是凌晨 1:00 哦。&lt;/p&gt;
&lt;p&gt;除了人还是人，每场表演都里三十层外三十层，比肩接踵，重点演出你还必须提前占座，对我们这种单日的外地游客太不友好了。带小朋友的更是遭罪了，全程只能看屁股，所以这是为什么我庆幸没带女儿来，起早贪黑特种兵不说，还没有体验。&lt;/p&gt;
&lt;p&gt;一天演出的重头戏就是晚上的打铁花表演和《岳飞郾城大捷》，我们毅然选择了提前两小时去占岳飞的座，然后看晚场的打铁花。事后证明这个选择无比正确，因为到晚上七点钟突然狂风大作电闪雷鸣，进而下起了大雨，而我们在的观看岳飞的场地是有顶棚的。我们等了两个小时，不仅看到了有上天赠送雷电特效的《郾城大捷》，还在之后的打铁花中坐到了第一排。&lt;/p&gt;
&lt;p&gt;雷电加持的《岳飞郾城大捷》（来自小红书）：http://xhslink.com/a/UhoRDNI06Fvcb&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/IMG_7897.JPG&quot; alt=&quot;IMG_7897&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不得不提的是这里的物价，这么多演出随便看，一人只要 120，就连园内的可乐也只要 5 元一瓶，我从来没见过 5 元的可乐，如果你买了《东京梦华》的演出票，还能三天内随意进园。这价格放眼全国也是相当良心，难怪人都成山了。其实开封还有另一个类似的乐园叫万岁山武侠城，但据说人数比清园还多得多我们就放弃了。&lt;/p&gt;
&lt;p&gt;今天不得不下榻开封，住的地方其实是一个洗浴中心，也算打开思路了。&lt;/p&gt;
&lt;h2&gt;5/3&lt;/h2&gt;
&lt;p&gt;今天参观了开封博物馆，吃到了宋园的灌汤包和化三驴肉火烧，下午逛了开封府。&lt;/p&gt;
&lt;p&gt;总体来说，如果你是古建爱好者，喜欢寻找历史的遗迹，那开封作为七大古都之一是乏善可陈，古建全是现代仿的，博物馆展品也没有含金量，唯一一个开封府题名记碑还到处复制，我不知道看到多少块，其实只有博物馆里那块是真的。但是清明上河园和万岁山武侠城这两个沉浸式复原古代生活场景的园区，绝对是能值回票价（那场打铁花和烟火表演，在我看过的所有里面都算顶级的，单这一场表演就能值 120 块），推荐一去。&lt;/p&gt;
&lt;p&gt;晚上实在不能再住洗浴中心了，坐高铁颠去安阳了。&lt;/p&gt;
&lt;h2&gt;5/4&lt;/h2&gt;
&lt;p&gt;古都之旅的最后一站是安阳，我们当然是慕殷墟之名而来。如果中国每个朝代能挑出一个博物馆来代表它，那么商代绝对是殷墟博物馆，19 世纪末因为一次抓药发现了甲骨文这个宝库，将中国信史上推一千年。
在文字发展的初期，文字还没有形成系统，很多字都是临时组合而成，比如商朝每代君主都有一个专属的合成字。而且人们还会充分利用象形文字的优势，随意改笔，表达特定的意思，比如下面这块龟甲:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/19c03de0401e007be1344df6bf1e782a.jpg&quot; alt=&quot;19c03de0401e007be1344df6bf1e782a&quot; /&gt;&lt;/p&gt;
&lt;p&gt;圈出的两字其实都是「车（車）」，但仔细观察，上面的车轴断了，下面的车上下翻转了，这段卜辞其实讲的是王出猎，追犀牛，结果发生了轴断，车翻，人坠的事故。&lt;/p&gt;
&lt;p&gt;另外还参观了很多的青铜器，现在我已经是青铜器小能手了，能准确辨别属于哪种青铜器型。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/DSCF0475.jpg&quot; alt=&quot;DSCF0475&quot; title=&quot;亚长牛尊&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/DSCF0490.jpg&quot; alt=&quot;DSCF0490&quot; title=&quot;盛有人头的甗&quot; /&gt;&lt;/p&gt;
&lt;p&gt;至于殷墟宫殿遗址就没什么好去了，房子不可能有真迹，妇好墓能下去，但什么也没有。大家以后要是来殷墟，只用 80 块的博物馆门票就行了，要是下午 5:30 以后进，是不要钱的。&lt;/p&gt;
&lt;h2&gt;河南印象&lt;/h2&gt;
&lt;p&gt;不管之前网上大家对河南人是怎么调侃的，河南给我的印象就是一个厚道的北方人，你来他家玩，他恨不得把他家最好的东西拿来给你看给你吃。凉菜给你塞满，表演给你看足。
我上次来河南还是 2010 年的洛阳，那时洛阳远没有现在这样热门，当然那时我也穷得很，连龙门石窟都没去，我想可能还会再来吧。&lt;/p&gt;
&lt;p&gt;这几天强度还是非常高的，有时候正餐都没法好好吃，谁让我五一出门来着了。&lt;/p&gt;
</content:encoded></item><item><title>再也别问 Singleton 了好吗？</title><link>https://frostming.com/posts/2025/singleton/</link><guid isPermaLink="false">https://frostming.com/2025/singleton/</guid><pubDate>Wed, 05 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;起笔的原因是群里的一段聊天：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/20250305103123.png&quot; alt=&quot;20250305103123&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不禁感叹 Singleton（单例模式）作为一个经典的设计模式，是如何被滥用的，特别是在 Python 这门语言中。它竟然成了一个八股式的面试题，就像「茴字有几种写法」一样，一直被问个没完。但我敢说，绝大多数人回答的时候，都是照本宣科，他们参考的网上的答案，也很少有能讲正确的。&lt;/p&gt;
&lt;p&gt;先说结论&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 Python 中，你不需要 Singleton。&lt;/li&gt;
&lt;li&gt;如果需要，就用模块级别的变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;至于原因，让我们来看看几种流行的 Singleton 实现方式：&lt;/p&gt;
&lt;h2&gt;1. 装饰器&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;def singleton(cls):
    instances = {}
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

# Usage
@singleton
class MyClass:
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;如无特殊说明，代码均由 Copilot 提供&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;这个方案的问题很明显：应用装饰器后改变了对象的类型，由一个 class 变成了一个 function。假使有人要用 &lt;code&gt;isinstance(obj, MyClass)&lt;/code&gt; 来判断对象类型，就会报错。&lt;/p&gt;
&lt;p&gt;这又涉及另一个问题，你的代码将被如何使用，取决于你暴露了什么，上面的例子中暴露的就是 &lt;code&gt;MyClass&lt;/code&gt; 这个对象，那就要考虑会不会被当成 class 来用，以及若被这样使用，是不是合理的要求。&lt;/p&gt;
&lt;h2&gt;2. 类变量&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Singleton:
    _instance = None
    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个方案的问题是，如果有多个单例类型，就得写多个这样的类，而且这个类也不能被继承，继承之后，实际上还是共享同一个 &lt;code&gt;_instance&lt;/code&gt; 变量，产生冲突，这不是我们想要的。还是老问题，你暴露了一个类，别人就会用类的方式来用。&lt;/p&gt;
&lt;p&gt;第二个问题，这个方案没有屏蔽 &lt;code&gt;__init__&lt;/code&gt; 的调用，实际上你如果多次实例化 Singleton 类，虽然返回的对象 id 唯一，&lt;code&gt;__init__&lt;/code&gt; 方法还是会被调用多次，这可能不是你想要的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Singleton:
    _instance = None
    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        print(&apos;init called&apos;)

s1 = Singleton()
s2 = Singleton()
print(s1 is s2)

# Output
# init called
# init called
# True
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 继承&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Singleton:
    _instances = {}

    def __new__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__new__(cls, *args, **kwargs)
        return cls._instances[cls]

# Usage
class MyClass(Singleton):
    ...

class MyAnotherClass(Singleton):
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个方案暴露的对象其实就是一个基类，它的标准用法就是让你用来继承的，它解决了上个方案的第一个问题，但第二个问题依然存在。&lt;/p&gt;
&lt;h2&gt;4. 元类&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Singleton(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

# Usage
class MyClass(metaclass=Singleton):
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个方案用了元类，就是所有致力于掌握 Python 高级语言特性的人们会想到的终级方案。它在元类级别直接拦截了实例化的调用，而不仅仅是 &lt;code&gt;__new__&lt;/code&gt; 方法，因为实例化的调用包括了 &lt;code&gt;__new__&lt;/code&gt; 和 &lt;code&gt;__init__&lt;/code&gt;，这样就解决了上面提到的第二个问题。如果你再运行上面的测试代码，会发现输出符合期待了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s1 = MyClass()
s2 = MyClass()
print(s1 is s2)

# Output
# init called
# True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很完美，不是吗？先别沾沾自喜，看下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyClass(metaclass=Singleton):
    def __init__(self, name: str) -&amp;gt; None:
        self.name = name

s1 = MyClass(&apos;Alice&apos;)
s2 = MyClass(&apos;Bob&apos;)
print(s1.name, s2.name)
# Output
# Alice Alice
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你认为这个输出符合编写者的意图吗？不能说是也不能说不是，只是值得商榷，这取决于调用者如何看待单例模式。一个考虑是如果用方案 2 和 3， &lt;code&gt;name&lt;/code&gt; 属性的值就会被统一改成 &lt;code&gt;Bob&lt;/code&gt;，这本身已经体现了问题所在。你可能会说没人在单例类的实例化中传参，这点我也不确定，但我认为，有人这么做，是因为你暴露的接口允许他这么做。&lt;/p&gt;
&lt;h2&gt;5. 模块级别的变量&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Singleton:
    ...

singleton = Singleton()

# Usage
from singleton_module import singleton
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;朴实无华，没有花里胡哨的东西，用这个答案如何能体现我&lt;strong&gt;精通&lt;/strong&gt; Python 呢？相反，我认为这个方案是最能实现原始需求的，相对来说问题最少，也最容易理解。就像人生的三重境界一样，最终还是要对花哨的东西袪魅，回到需求本身上去。这个方案里暴露的对象是唯一的 &lt;code&gt;singleton&lt;/code&gt; 模块变量，你不可能用类的方式来用它，也不可能传参给它，这才是我们想要的。你甚至可以把类名写成 &lt;code&gt;_Singleton&lt;/code&gt; 断了人的念想（当然这不是硬禁止，你想用还能用，别在这上面抬杠了）。&lt;/p&gt;
&lt;p&gt;那如果，我还是想暴露 &lt;code&gt;Singleton&lt;/code&gt; 类来做一些比如 &lt;code&gt;isinstance&lt;/code&gt; 的操作呢？我承认有这个需求，那我们稍加改造一下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is not None:
            raise TypeError(&quot;Singleton class cannot be instantiated twice&quot;)

        cls._instance = super().__new__(cls)
        return cls._instance

singleton = Singleton()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看上去好像跟方案 2 差不多，但暴露的主要对象已经从类变成了实例，这个类已经不允许做实例化的操作了。这就是从代码层面控制了暴露的接口。&lt;/p&gt;
</content:encoded></item><item><title>我的 2024</title><link>https://frostming.com/posts/2024/review/</link><guid isPermaLink="false">https://frostming.com/2024/review/</guid><pubDate>Tue, 24 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;工作与技术&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在 BentoML lead 了几个重要的 feature，包括 1.3 和 1.4 两个大版本，以及 Codespaces 和 Comfy-Pack。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;11 月参与 PyCon China 2024，见了许多网友，参加了代码厨房的活动，很开心。做了关于 Pydantic 的演讲。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;https://webp.frostming.com/images/20241224101143.png&quot; alt=&quot;20241224101143&quot; /&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://slides.fming.dev/pydantic/&quot;&gt;slides&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1xUzRYTEBG?spm_id_from=333.1387.homepage.video_card.click&quot;&gt;视频&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;11 月参与了 NebulaGraph 深圳的线下活动，做了关于 BentoML 的分享。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;5 月份有荣幸&lt;a href=&quot;https://frostming.com/2024/meet-with-paul/&quot;&gt;与 Paul Romer 见面交流&lt;/a&gt;，并造就了 2024 年我最受关注的一篇文章。但于我来说会总结为&lt;strong&gt;机缘巧合&lt;/strong&gt;，不必赋与我其他的光环。事实上从那以后我试图联系过 Romer 先生都没有得到回应。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;开源了 &lt;a href=&quot;https://www.fxzhihu.com&quot;&gt;FxZhihu&lt;/a&gt; 被很多人使用，感受到贡献了一些价值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;写作博客文章 15 篇。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;由于 uv 的横空出世，PDM 无疑受到了一些冲击，发展明显变缓。
&lt;img src=&quot;https://webp.frostming.com/images/star-history-20241224.png&quot; alt=&quot;star-history-20241224&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但我不是那么狭隘，人不能沉湎在过去的辉煌中举步不前。好用的工具得用，我自己平时也会用用 uv。PDM 也在 2.19 中加入了&lt;a href=&quot;https://pdm-project.org/en/latest/usage/uv/&quot;&gt;对 uv 的支持&lt;/a&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;开源了 &lt;a href=&quot;https://github.com/frostming/tetos/&quot;&gt;Tetos&lt;/a&gt;，一个 TTS 的统一 SDK 库。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其他的一些娱乐项目比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/frostming/picguessr&quot;&gt;TG 猜成语机器人&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/frostming/Watermarker&quot;&gt;给照片添加边框的工具&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2024 依然目睹了 GenAI 和 LLM 的火爆，我个人也体验了 Cursor 最后又回到了 VSCode。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;出行&lt;/h2&gt;
&lt;p&gt;今年继续去了挺多地方。&lt;/p&gt;
&lt;p&gt;1 月份汕尾&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/IMG_4111.HEIC_compressed.JPEG&quot; alt=&quot;IMG_4111.HEIC_compressed&quot; /&gt;&lt;/p&gt;
&lt;p&gt;2 月珠海——中山&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/20241224103554.png&quot; alt=&quot;20241224103554&quot; /&gt;&lt;/p&gt;
&lt;p&gt;春节马六甲——吉隆坡&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/20241224103332.png&quot; alt=&quot;20241224103332&quot; /&gt;&lt;/p&gt;
&lt;p&gt;清明节广西梧州&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/20241224103801.png&quot; alt=&quot;20241224103801&quot; /&gt;&lt;/p&gt;
&lt;p&gt;4 月份上海&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/20241224104551.png&quot; alt=&quot;20241224104551&quot; /&gt;&lt;/p&gt;
&lt;p&gt;五一普者黑——弥勒&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/20241224104641.png&quot; alt=&quot;20241224104641&quot; /&gt;
&lt;img src=&quot;https://webp.frostming.com/images/20241224104722.png&quot; alt=&quot;20241224104722&quot; /&gt;&lt;/p&gt;
&lt;p&gt;6 月份泉州&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/20241224104824.png&quot; alt=&quot;20241224104824&quot; /&gt;&lt;/p&gt;
&lt;p&gt;8 月份太原&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/20241224104920.png&quot; alt=&quot;20241224104920&quot; /&gt;&lt;/p&gt;
&lt;p&gt;国庆日本九州&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.msmiao.me/images/jiuzhou_027.jpeg&quot; alt=&quot;jiuzhou_027.jpeg&quot; /&gt;
&lt;img src=&quot;https://webp.msmiao.me/images/jiuzhou_088.jpeg&quot; alt=&quot;jiuzhou_088.jpeg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;11 月潮汕&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/20241224105516.png&quot; alt=&quot;20241224105516&quot; /&gt;&lt;/p&gt;
&lt;p&gt;PyCon China 上海 Again&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/20241224105552.png&quot; alt=&quot;20241224105552&quot; /&gt;&lt;/p&gt;
&lt;p&gt;12 月香港&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/20241224105633.png&quot; alt=&quot;20241224105633&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;生活&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;房子做了几次提前还款，减少了一些负担。&lt;/li&gt;
&lt;li&gt;把手上相机卖了，换了个新的，富家子弟又支棱起来了。&lt;/li&gt;
&lt;li&gt;买了 Mac Mini M4，用来做开发机，体验还不错。&lt;/li&gt;
&lt;li&gt;被保险代理忽悠去香港考了个 IIQE（保险中介资格考试），很容易就过了，但不知道有什么用。&lt;/li&gt;
&lt;li&gt;呃啊我的高才明年就要续了，还不知道怎么搞，很焦虑。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/2024/2023-review/#%E7%94%9F%E6%B4%BB&quot;&gt;去年提到的精神内耗状况&lt;/a&gt;减轻了许多，在外界因素变化不大的情况下，我开始更多地放松和和解。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;书影音&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://frostming.notion.site/Frost-s-Home-bc46e4cd47d34eea94f2337c34ba2085&quot;&gt;2024 年的书影音&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;最喜欢的书是《隳三都》,在历史非虚构里属于非常优秀的了。另外还有刘震云的《我叫刘跃进》，高级的黑色幽默。
电影没有最喜欢的，相对好的是《走走停停》《从 21 世纪安全撤离》和《死侍与金刚狼》。&lt;/p&gt;
&lt;p&gt;只是 10 月份以后就没有看书和电影了，提不起兴趣来。&lt;/p&gt;
&lt;p&gt;明年会怎么样，我也不知道。2025 是个平方数，希望是个好年。&lt;/p&gt;
&lt;p&gt;$$
2025 = 45^2 = 3^4 \times 5^2
$$&lt;/p&gt;
&lt;p&gt;&lt;em&gt;本文题图由 Ideogram 生成，其余图片，除 PyCon China 合影，均为本人拍摄。&lt;/em&gt;&lt;/p&gt;
</content:encoded></item><item><title>一个 monkeypatch 引起的循环引用问题</title><link>https://frostming.com/posts/2024/circular-ref-monkeypatch/</link><guid isPermaLink="false">https://frostming.com/2024/circular-ref-monkeypatch/</guid><pubDate>Wed, 18 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近社区里有人提了一个PR&lt;a href=&quot;https://github.com/psf/cachecontrol/pull/344/&quot;&gt;^1&lt;/a&gt;，解决了一个潜在的内存泄漏问题。我认为这个问题在很多场景都容易忽略，所以分享在这里。&lt;/p&gt;
&lt;p&gt;上代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import gc

class Foo:
    def bar(self):
        print(&quot;bar&quot;)

foo = Foo()

print(&quot;Before&quot;, gc.get_count())
foo.bar = foo.bar
print(&quot;setattr&quot;, gc.get_count())
del foo
print(&quot;After&quot;, gc.get_count())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行发现 &lt;code&gt;foo&lt;/code&gt; 对象无法被正常回收，造成了内存泄漏。这是因为看似不起眼的一行 &lt;code&gt;foo.bar = foo.bar&lt;/code&gt;，实际上创建了一个循环引用。&lt;/p&gt;
&lt;p&gt;点解？&lt;/p&gt;
&lt;p&gt;因为，&lt;code&gt;foo.bar&lt;/code&gt; 这个表达式，实际上执行了 &lt;code&gt;Foo.bar.__get__(foo, Foo)&lt;/code&gt;，返回了一个绑定了 &lt;code&gt;foo&lt;/code&gt; 的方法，此方法中持有 &lt;code&gt;foo&lt;/code&gt; 的引用。而 &lt;code&gt;foo.bar = foo.bar&lt;/code&gt;，相当于把这个结果缓存在了 &lt;code&gt;foo.bar&lt;/code&gt; 属性上。之后的 &lt;code&gt;foo.bar&lt;/code&gt; 只是从属性上取值，这样 &lt;code&gt;foo&lt;/code&gt; 和该方法互相持有对方的引用，出现了循环引用。&lt;/p&gt;
&lt;p&gt;现实中我们不太可能会写出 &lt;code&gt;foo.bar = foo.bar&lt;/code&gt; 这样的代码，但是在 monkeypatch 场景下，可能很容易被坑到，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests

resp = requests.get(&quot;https://example.com&quot;, stream=True)
resp._fp = CallbackFileWrapper(resp, callback)
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码 patch 了 &lt;code&gt;requests&lt;/code&gt; 库的 &lt;code&gt;Response&lt;/code&gt; 对象，让它在被读取完成中调用我们指定的 &lt;code&gt;callback&lt;/code&gt; 函数。这里 &lt;code&gt;CallbackFileWrapper&lt;/code&gt; 会持有 &lt;code&gt;resp&lt;/code&gt; 的引用。&lt;/p&gt;
&lt;p&gt;如何避免？&lt;/p&gt;
&lt;p&gt;在上述例子中，我们可以使用 weakref 来避免循环引用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import weakref

class CallbackFileWrapper:
    def __init__(self, resp, callback):
        self.resp_ref = weakref.ref(resp)
        self.callback = callback

    def read(self, size):
        resp = self.resp_ref()
        if resp is None:
            return b&quot;&quot;
        read_bytes = resp.read(size)
        if not read_bytes:
            self.callback(resp)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>功不唐捐</title><link>https://frostming.com/posts/2024/efforts/</link><guid isPermaLink="false">https://frostming.com/2024/efforts/</guid><pubDate>Thu, 07 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我女儿第一天上幼儿园回来，我就拿着群里他们班级的合照，挨个问她每个小朋友都叫什么名字。一方面是想看看她集体融入得怎么样，另一方面是自己想留个底，争取每个小朋友的脸和名字都能对上。
她当然不能每个都叫出名字来，但配合群里其他家长聊天的信息，三天后我就基本能全叫出名字了。有回我正在问，丈母娘见了在旁说，「知道这个有什么用，你又不是老师。」面对这种言论，我只能一笑了之。&lt;/p&gt;
&lt;p&gt;我觉得，总会有用的。在我看来，至少有下面几个用处：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先，安全。以后万一有突发事件，我能对号到人。做个假设，有一天我女儿和她同学出游走丢了，我能凭借这些信息，更好地找人，不至于「纵使相逢还不识」。&lt;/li&gt;
&lt;li&gt;其次，我能更好地了解她的生活。她跟我讲八卦时，我能知道说的是谁，做一个优秀的倾听者。&lt;/li&gt;
&lt;li&gt;Last but not least，这是对人的一种尊重。若是上学放学路上碰到同学打招呼，我能很好地回应。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但丈母娘这个看法就见识短浅了，她才是接送孩子的主力。我已经不知多少次听她说路上碰见个幼儿园同学，做了什么举动，却不知道是谁。&lt;/p&gt;
&lt;p&gt;今天去幼儿园参加家长开放日的活动，其实就是小朋友在上课活动时家长在旁观看。户外休息时，很多小朋友围到我身边来，因为他们发现我能叫对他们每一个人的名字，甚至还知道小名。于是就疯狂地来考我，乐意跟我聊天。
我突然觉得，两年前做过的功课没有白做，今天派上了用场，不是什么大用，能温暖我一阵子，也足够了。&lt;/p&gt;
&lt;p&gt;所以，你看过的每一本书，玩过的游戏，走过的路，都不会浪费。只是你还没找到用处罢了，又或者它们已经内化在你的个人气质中了。&lt;/p&gt;
&lt;p&gt;功不唐捐。&lt;/p&gt;
</content:encoded></item><item><title>表达的阈值</title><link>https://frostming.com/posts/2024/express-threshold/</link><guid isPermaLink="false">https://frostming.com/2024/express-threshold/</guid><pubDate>Sun, 20 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import Tweet from &quot;@components/Tweet.astro&quot;;&lt;/p&gt;
&lt;p&gt;我一直在思考要在博客写些什么，我能写什么。刚好看到了 @laike9m 的这篇推文：&lt;/p&gt;
&lt;p&gt;&amp;lt;Tweet id=&quot;1847557409457524950&quot; /&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;有很多观点我是不敢在网上表达的，只能和女朋友说说。有的是因为太个人化，有的是违反了普遍的认知，以及第三种情况：解释清楚要花太多时间，而我的时间本就不多。于是每次都是想想，很快便打消了这个念头了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;确实，很多时候阻挠我表达的，是四个字「大可不必」。我要表达的观点会有人认同吗？或至少，会引发一些思考吗？如果两者的答案都是否，那就「大可不必」发表了。或者即使，观点或许有一些价值，那么我要怎么表达呢？是不是得安排逻辑结构、旁征博引、收集数据案例，这些值不值得？「大可不必」，这四个字又在我脑子里冒了出来。&lt;/p&gt;
&lt;p&gt;于是，很多想法可能就这样，只停留在了熄灯后的卧谈，甚至，更普遍的情况是，只停留在了脑海里，永远没有见天日的机会。&lt;/p&gt;
&lt;p&gt;我们不知不觉中，给自己的表达设定了一个阈值，超过了这个值的，才会出现在对外分享的表达中。而要克服这个阈值，很需要一些能量。我死去的半导体课上学过的知识又突然袭击了我，价带电子需要一定能量，才能跃迁到导带。&lt;/p&gt;
&lt;p&gt;所以我有时非常羡慕一些生活博主（特别是居住在国外的），坐了什么公共交通上班，吃了什么饭，日落时没云彩，下雨忘了带伞，都可以成为他们笔下津津乐道的话题。我心想要是按他们这个阈值来，那我每天不得发 24 条动态。&lt;/p&gt;
&lt;p&gt;但有时候我又会想，大可不必。&lt;/p&gt;
&lt;p&gt;感谢今天战胜了我的阈值的我的表达欲。&lt;/p&gt;
</content:encoded></item><item><title>《21世纪》与主流文化</title><link>https://frostming.com/posts/2024/21-centry-trend/</link><guid isPermaLink="false">https://frostming.com/2024/21-centry-trend/</guid><pubDate>Wed, 24 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;上周末&lt;a href=&quot;https://movie.douban.com/subject/26816104/&quot;&gt;《从 21 世纪安全撤离》&lt;/a&gt;（后简称《21》）在深圳点映，一直很期待导演李阳的作品，距离《李献计历险记》已过去十多年了，那时房祖名还在呢。于是一出来就买了票和老婆一起去看了。看完后我和朋友聊的感想是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我最大感想是，感谢所有资方，感谢电影工业，感谢演职人员，信任李阳，帮他完成这个作品。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;没错，李阳在所有中国导演中都是一个异类，只有他能拍出这种味道的东西。怪诞、热血、信息量爆炸、莫名其妙。可以预见，这将会是一种评论两极分化的片子，密集的视效和神奇的转折之下，是破碎的剧情和二维的人物。我个人是很喜欢的，我记得在临近结尾的时候，吴晓亮演的角色在家里打《街霸》，我疯狂地扯我老婆，说这是《我爱我家》里贾家的布景啊哈哈哈，但她没什么反应。有时 GET 到了一个彩蛋，我会在内心想：这就是给我准备的啊，我懂你的梗，我厉害吧。我突然有些明白了，喜欢这部电影的会是什么人群，诚然导演是拍他自己喜欢的事物，他是个老二次元了，没有刻意要迎合谁，但这个受众人群的画像的轮廓，却渐渐在我脑中浮现了出来。我甚至于认为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;李阳这部作品等了这么久，难说不是因为这群人成为了社会中流，时机才成熟了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这群人大概是：从小看日本动画长大、玩街机、爱摇滚、听唐朝乐队，现在喜欢二次元，喜欢上破站，喜欢宫骑英高，喜欢新民谣，喜欢万能青年旅店。&lt;/p&gt;
&lt;p&gt;如果你全中了，可以在评论区扣个 666。问题是，这些就代表了全部的人吗？那些不喜欢这些的，看不懂《21》的，也是和我们同龄的人。我个人的话，符合 70% 吧，有些事，不喜欢就是不喜欢。我们曾认为我们是非主流，然而这已俨然成为主流，在中推、在知乎、在即刻、在小红书，到处都是同类。以至于那些对这些本不感冒的人，也硬要变成一分子，仿佛你不喜欢这些，就会被排斥在外，比之大城市的排外，有过之而不及。我不希望在这里表达对这部电影的喜欢，会排斥到了其他人。我写过&lt;a href=&quot;/2024/diversity/&quot;&gt;《我们生活在差异里》&lt;/a&gt;，最后一句是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;周围的人大都是相似的，但除此之外存在着更多其他的人，我需要认清这种差异，并包容它。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以最后，我推荐你去看这部电影，但如果你看了之后觉得不好，甚至垃圾，也欢迎你回复对这部电影的看法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;This message is used to verify that this feed (feedId:41342818704332806) belongs to me (userId:41749083576628224). Join me in enjoying RSS on the next generation information browser https://follow.is.
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>我「接见」了诺奖得主</title><link>https://frostming.com/posts/2024/meet-with-paul/</link><guid isPermaLink="false">https://frostming.com/2024/meet-with-paul/</guid><pubDate>Thu, 30 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::note[版权声明]
本文谢绝一切转载，只能以链接形式分享。
:::&lt;/p&gt;
&lt;p&gt;如同这个 Drama 的标题所写的，在我 34 岁生日这几天发生了一件非常 Drama 的事情。到现在我依然感觉有些不真实，但事情就是，我「接见」了诺贝尔奖得主。&lt;/p&gt;
&lt;h2&gt;收到邀请&lt;/h2&gt;
&lt;p&gt;5 月 28 日这天我收到了一封邮件。我向来习惯及时阅读新的邮件，至少是邮件的标题，所以我的邮箱从不积压未读邮件。我每天早上都会收到很多 GitHub 的通知邮件，但这一封没有 [GitHub] 的前缀，
我留了意，但也没有立刻打开阅读。可能是这个「只读标题未读内容」的行为触发了 Gmail 的机制，&lt;strong&gt;它把它扔进了垃圾箱&lt;/strong&gt;。等我有空想要去读的时候发现邮件已经不见了，所幸我扫了眼垃圾箱，找到了这封邮件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/20240530191605.png&quot; alt=&quot;20240530191605&quot; /&gt;&lt;/p&gt;
&lt;p&gt;首先开头有我的称呼，然后他说自己是一位小有名气的经济学者，有维基页面。看到这我当然立即去看了一眼维基，&lt;a href=&quot;https://en.wikipedia.org/wiki/Paul_Romer&quot;&gt;果然有&lt;/a&gt;。履历中最为耀眼的，当然是 &lt;a href=&quot;https://www.nobelprize.org/prizes/economic-sciences/2018/romer/facts/&quot;&gt;2018 年的诺贝尔经济学奖共同获奖者之一&lt;/a&gt;。他表明了自己的 Python 爱好者身份，联系到我是因为他刚好现在在香港，并在整个航班的时间里都在看 Python 打包的知识，认为我做的 PDM 是一个非常好的项目，想要约我见面，并表示周四（两天后）可以到深圳来。&lt;/p&gt;
&lt;p&gt;信息量有点大，你问我有没有怀疑这是假的，我比任何人都怀疑这是假的，但开头的称呼和了解我做的项目这两点还是打消了我一半的顾虑（邮件里提到 PDM 的，至今没有一封是 spam）。另外提前仅仅两天的约见，要不是我习惯清空未读列表，否则很容易错过。至于一位功成名就的，大我 35 岁的大人物，为什么想见我这个无名氏，我相信一件事：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当佬巨到一定程度，他是向下兼容的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我读了几遍，发现没有任何拒绝的理由，但保险起见，我还是和 yihong 一起，核实了来信地址的真实性。并且他的照片在网上到处都是，是很容易验证的。&lt;/p&gt;
&lt;h2&gt;见面前&lt;/h2&gt;
&lt;p&gt;于是我覆信表达了愿意见面，并询问对方的行程和意向的见面时间地点。关于他想来和我谈论什么我还没有想明白，但我不愿意想得太功利。如果只是谈生意和上价值，那我会有些失望。我愿意认为他只是一个年近七十的 Life Long Learner，想要和我交流一下我擅长的领域，毕竟这是他唯一能从我这里获得的了。&lt;/p&gt;
&lt;p&gt;没想到他为了打消我的顾虑，还回信说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Our situation may be more symmetrical than you think. I have been learning Python since 2018. I am certainly not a developer. My Python is probably worse than your English!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这也太 nice 了，什么叫向下兼容。对方这么说，翻译我是不可能翻译的，感激涕零都来不及。随后就是确定时间地点。&lt;/p&gt;
&lt;p&gt;见面之前，他发了很长的一封信介绍他的相关背景，以及计划要谈的话题。即使地位悬殊，年龄悬殊，他依然恪守了基本礼仪，让未来的见面能顺畅进行。我不想因为他是大佬就拼命溜须，但这种谦恭的态度真的让我学到了很多。此时我已经完全不怀疑这事有假了。他不远万里从香港辗转来深圳，我这是真正的「接见」了，荣幸之至，诚惶诚恐。&lt;/p&gt;
&lt;h2&gt;见面交谈&lt;/h2&gt;
&lt;p&gt;我们约在了下午两点见面，作为资深 I 人，我从来不会在约定时迟到，但也绝不会提前很久到，每次都会卡得非常恰当。我算好时间出门，没想到他说行程提前了，所以可以提前到达。这表明他需要在茶馆等我一段时间，不过我没有错过约定时间，也不会特别歉疚。但这样有另外一个后果：该茶馆是预付费的，我没办法付账了。我真的是必被抢单的体质，但我想外国人不会太在意这些。&lt;/p&gt;
&lt;p&gt;刚一见上我们就开始了对谈，可以说在整个一个多小时时间里我们连茶都没喝一口。我作为听者比较多，毕竟说也不擅长，有点像是我在采访他了。&lt;/p&gt;
&lt;p&gt;他目前在做的有两个项目，其一是帮助 Python 初学者搭建 Python 环境的一个 GUI 应用。另一个是帮助用户生成和管理加密密钥，并进行数字签名的一个应用程序。两者都是面向初学者的简化操作和理解负担的效率工具。从这可以看出，从现在到未来一段时间内，Python 初学者仍然是一个庞大的群体，而 &lt;a href=&quot;https://peps.python.org/pep-0582/&quot;&gt;PEP 582&lt;/a&gt; 对于简化用户环境配置是一个非常好的提案。只是在 Python
论坛中讨论提案的人，还是以有经验的开发者居多，包括 PDM 的 issue tracker 中，多半也是一些有着奇奇怪怪使用场景的「高端」用户，而不是经验不足的初学者。同时我自己写的文档也不能很好的满足初学者的习惯。PEP 582 的拒绝是一件非常遗憾的事，但如果有更多初学者的声音，我相信有希望可以重启该提案的评估。&lt;/p&gt;
&lt;p&gt;另外他还谈到了数据安全和数字签名，希望做一个面向小白的更易用的密钥管理和签名工具（gpg 太过 geek 了）。这方面我也不是很懂，主要以听为主。同时他也认为 Jupyter Notebook 在未来应该替代 PDF 成为研究论文的推荐媒介，在 Mathematica 和 Jupyter Notebook 之间推崇后者。&lt;/p&gt;
&lt;p&gt;他非常喜欢开源，愿意为开源提供经济支持，期间还提到了 &lt;a href=&quot;https://github.com/laike9m/Python-Type-Challenges&quot;&gt;Python-Type-Challenges&lt;/a&gt; 这个非常适合初学者的学习项目，还以为是我的作品，我立刻做了澄清，怎能抢了 @laike9m 的功劳。&lt;/p&gt;
&lt;p&gt;再写就变成新闻稿了，文末会附上&lt;a href=&quot;https://webp.frostming.com/meet-with-paul-transcript.md&quot;&gt;音频转写&lt;/a&gt;，我想 AI 应该会比我总结得更好。让我感到敬佩的是他对很多技术细节的了解，比如他喜欢 PDM 用的 python-standalone-build 胜于 pyenv，而这是 PDM 最近才引入的一个特性。又比如他了解去年 ChatGPT 的重大事故，是 asyncio 和 redis 引起的，还想让我去 Boston College 解答 asyncio 的问题。所以啊，无论是什么牛人，都用不好 Python 的 async。&lt;/p&gt;
&lt;p&gt;最后我邀请他到 PyCon China 来做一次演讲，我认为 Python 新手教学的话题，会是一个很有价值的输入。&lt;/p&gt;
&lt;p&gt;见了厉害的大牛并不表示我也是厉害的大牛，但这是我第一次靠自己的工作和成就，而不是雇主的背书和推介而获得外界的认可，而且可以回应家人「你都在搞什么」这个问题，这才是真正值得我高兴的事。如果要感谢，那就感谢 Guido 吧。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/20240530203400.png&quot; alt=&quot;20240530203400&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;附录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://webp.frostming.com/meet-with-paul-transcript.md&quot;&gt;谈话实录&lt;/a&gt; 由 AI 转写，未做人工校对，可能存在错误。禁止转载演绎。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>友好的 Python：封装和复用</title><link>https://frostming.com/posts/2024/friendly-python-reuse/</link><guid isPermaLink="false">https://frostming.com/2024/friendly-python-reuse/</guid><pubDate>Thu, 09 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近我写了一个 TTS(Text to Speach) 库 &lt;code&gt;Tetos&lt;/code&gt;，为的就是统一各种云 TTS 服务的调用接口，让用户可以用同一套代码，只需要变动参数就可以在不同的 TTS 间切换。&lt;/p&gt;
&lt;p&gt;https://github.com/frostming/tetos&lt;/p&gt;
&lt;p&gt;在实现过程中，我翻阅了很多云 TTS 服务的接口文档，发现它们接口的设计大相径庭，有的是 RESTful，有的是伪 RESTful，有的文档里甚至只让你用 SDK，没有 HTTP 接口说明。
本来嘛，我做的工作就是让用户可以不用做这些工作，但本篇文章还是想主要吐槽一下火山引擎的接口，和它的 SDK 设计。所以这篇可能不能叫《友好的 Python》了，可以当吐槽大会来看。&lt;/p&gt;
&lt;h2&gt;提出问题&lt;/h2&gt;
&lt;p&gt;假设你是一名公有云厂商 Python SDK 的开发者，你们的接口有一个非常复杂的验签机制，你人微言轻，不能质疑，只能按照上面交给你的文档来做。那么你会怎么设计这个 SDK 给用户使用？
进一步，不如我们脱离签名的具体细节，把它抽象出来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sign(request, randomData, secrets) -&amp;gt; signedRequest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;签名的输入有三个：HTTP 请求、现场随机生成的数据，和密钥数据。输出是签名的请求，这个签名可能修改了请求头，或是请求体，我们不管它，总之后续就用这个新的请求执行。
假如这个 SDK 支持的是 &lt;code&gt;requests&lt;/code&gt; 库，你会怎么设计呢？不妨先带着这个思考，来~吃一口屎~看一下火山引擎的 SDK。&lt;/p&gt;
&lt;p&gt;下面的代码是我直接从&lt;a href=&quot;https://www.volcengine.com/docs/6489/71995#python&quot;&gt;火山引擎的接口文档&lt;/a&gt;里截取的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class SAMIService(Service):
    _instance_lock = threading.Lock()

    def __new__(cls, *args, **kwargs):
        if not hasattr(SAMIService, &quot;_instance&quot;):
            with SAMIService._instance_lock:
                if not hasattr(SAMIService, &quot;_instance&quot;):
                    SAMIService._instance = object.__new__(cls)
        return SAMIService._instance

    def __init__(self):
        self.service_info = SAMIService.get_service_info()
        self.api_info = SAMIService.get_api_info()
        super(SAMIService, self).__init__(self.service_info, self.api_info)

    @staticmethod
    def get_service_info():
        api_url = &apos;open.volcengineapi.com&apos;
        service_info = ServiceInfo(api_url, {},
                                   Credentials(&apos;&apos;, &apos;&apos;, &apos;sami&apos;, &apos;cn-north-1&apos;), 10, 10)
        return service_info

    @staticmethod
    def get_api_info():
        api_info = {
            &quot;GetToken&quot;: ApiInfo(&quot;POST&quot;, &quot;/&quot;, {&quot;Action&quot;: &quot;GetToken&quot;, &quot;Version&quot;: &quot;2021-07-27&quot;}, {}, {}),
        }
        return api_info

    def common_json_handler(self, api, body):
        params = dict()
        try:
            body = json.dumps(body)
            res = self.json(api, params, body)
            res_json = json.loads(res)
            return res_json
        except Exception as e:
            res = str(e)
            try:
                res_json = json.loads(res)
                return res_json
            except:
                raise Exception(str(e))

if __name__ == &apos;__main__&apos;:
    sami_service = SAMIService()

    sami_service.set_ak(ACCESS_KEY)
    sami_service.set_sk(SECRET_KEY)

    req = {&quot;appkey&quot;: APPKEY, &quot;token_version&quot;: AUTH_VERSION, &quot;expiration&quot;: 3600}
    resp = sami_service.common_json_handler(&quot;GetToken&quot;, req)
    try:
        print(&quot;response task_id=%s status_code=%d status_text=%s expires_at=%s\n\t token=%s&quot; % (
            resp[&quot;task_id&quot;], resp[&quot;status_code&quot;], resp[&quot;status_text&quot;], resp[&quot;expires_at&quot;], resp[&quot;token&quot;]))
    except:
        print(&quot;get token failed, &quot;, resp)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;大病得治&lt;/h2&gt;
&lt;p&gt;这是一个获取 Token 的请求，最后使用的是 &lt;code&gt;common_json_handler()&lt;/code&gt; 这个函数。一眼看去，你发现一点都不像正常的 Python HTTP 调用风格，你以为他是祖传自建的 HTTP 轮子，但其实不是，它底层还是 &lt;code&gt;requests&lt;/code&gt;，那么为什么 SDK 会变得这么畸形呢？&lt;/p&gt;
&lt;p&gt;我们先忽略 &lt;code&gt;set_ak()&lt;/code&gt;, &lt;code&gt;Singleton&lt;/code&gt; 这种从别的语言过来的在 Python 里毫无必要的写法，并且也忽略他在 &lt;code&gt;except Exception&lt;/code&gt; 逻辑里返回正常响应的行为（我得咬着后槽牙才能忍，这么写是要浸猪笼的）。&lt;/p&gt;
&lt;p&gt;我第一个反对的是，为什么要用继承 + &lt;code&gt;staticmethod&lt;/code&gt; 的方法来写，我们知道 Python 里用 class 基本是要共享状态的，而用了 &lt;code&gt;staticmethod&lt;/code&gt; 就没得共享了，那么为什么不能直接改成下面这样？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;api_url = &apos;open.volcengineapi.com&apos;
service_info = ServiceInfo(
    api_url, {},
    Credentials(&apos;&apos;, &apos;&apos;, &apos;sami&apos;, &apos;cn-north-1&apos;), 10, 10
)
api_info = {
    &quot;GetToken&quot;: ApiInfo(&quot;POST&quot;, &quot;/&quot;, {&quot;Action&quot;: &quot;GetToken&quot;, &quot;Version&quot;: &quot;2021-07-27&quot;}, {}, {}),
}
sami_service = Service(service_info, api_info)
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并且阅读代码可知 &lt;code&gt;Credentials&lt;/code&gt; 的头两个参数就是 &lt;code&gt;access_key&lt;/code&gt; 和 &lt;code&gt;secret_key&lt;/code&gt;，那么直接传入，不必后面再 &lt;code&gt;set_ak&lt;/code&gt; 了。上面这个写法和之前继承 + &lt;code&gt;staticmethod&lt;/code&gt; 的效果完全一样。
好了现在除了 &lt;code&gt;common_json_handler()&lt;/code&gt; 以外这个类的成员全被我干掉了，需要注意到 &lt;code&gt;api_info&lt;/code&gt; 里仿佛包含的是一些请求相关的信息，依次分别是 &lt;code&gt;method&lt;/code&gt;, &lt;code&gt;path&lt;/code&gt;, &lt;code&gt;body&lt;/code&gt; 和 &lt;code&gt;headers&lt;/code&gt; 之类的东西。下面我们来看看怎么改掉这个函数。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;common_json_handler()&lt;/code&gt; 唯一用到的 Service 的方法是 &lt;code&gt;self.json()&lt;/code&gt;，从名字猜测这是一个接收 JSON 响应的方法，注意到 body 和 response 都分别经过了 &lt;code&gt;json.dumps&lt;/code&gt; 和 &lt;code&gt;json.loads&lt;/code&gt;，等于这个名为 &lt;code&gt;json()&lt;/code&gt; 的函数啥事都要自己来干。既然如此不要把它放在类里面了，直接拉出来写成一个函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def common_json_handler(service, api, body):
    params = dict()
    try:
        body = json.dumps(body)
        res = service.json(api, params, body)
        res_json = json.loads(res)
        return res_json
    except Exception as e:
        # 后面的太可怕了，不要学
        ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还记得直接用 &lt;code&gt;requests&lt;/code&gt; 怎么发送和接收 JSON 响应吗？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;res = requests.post(url, json=body)
res_json = res.json()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好优雅，好舒服，这么优雅舒服的库怎么被他包成了这样？不要忘了一开始提出的问题，要对请求签名。我们看看 &lt;code&gt;Service.json()&lt;/code&gt; 的实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def json(self, api, params, body):
    if not (api in self.api_info):
        raise Exception(&quot;no such api&quot;)
    api_info = self.api_info[api]
    r = self.prepare_request(api_info, params)
    r.headers[&apos;Content-Type&apos;] = &apos;application/json&apos;
    r.body = body

    SignerV4.sign(r, self.service_info.credentials)

    url = r.build()
    resp = self.session.post(url, headers=r.headers, data=r.body,
                                timeout=(self.service_info.connection_timeout, self.service_info.socket_timeout))
    if resp.status_code == 200:
        return json.dumps(resp.json())
    else:
        raise Exception(resp.text.encode(&quot;utf-8&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好家伙，难怪我要自己 &lt;code&gt;json.loads()&lt;/code&gt; 呢，&lt;code&gt;json.dumps(resp.json())&lt;/code&gt; 来来来，你过来我保证不打死你。&lt;/p&gt;
&lt;p&gt;接着看，这里出现了关键的 &lt;code&gt;SignerV4.sign()&lt;/code&gt;，参数是一个&lt;strong&gt;自己&lt;/strong&gt;生成的 request 对象，和上面我抽象的差不多，需要一些请求的信息和密钥。这也是为什么要一个如此奇怪的 &lt;code&gt;api_info&lt;/code&gt;，因为这是签名需要用的请求的信息，只好单独传递。好了问题找到了，搞这么奇怪，就是因为他自己弄了个请求对象，然后又要费劲把它变成 &lt;code&gt;requests&lt;/code&gt; 接受的对象（&lt;code&gt;r.build()&lt;/code&gt; 拿 URL 及 &lt;code&gt;r.headers&lt;/code&gt;, &lt;code&gt;r.body&lt;/code&gt;）。那么请问下，为什么不能用 &lt;code&gt;requests&lt;/code&gt; 内部的请求对象去生成签名？反正最终是要靠 &lt;code&gt;requests&lt;/code&gt; 发送请求，要有的信息这全都有。就好比你跑马拉松，补给点都是在跑道必经之处，想象一下你要喝个水还要专门跑岔路去补给点，怎生一个卧槽。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;尽量不要自己封装新的对象，因为你要拷贝原有的属性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;那么现在要做的事情就清楚了，就是要在请求前修改 &lt;code&gt;requests&lt;/code&gt; 即将要发送的请求对象，给它加上签名信息。这其实是一种 interceptor，&lt;code&gt;requests&lt;/code&gt; 有什么机制实现这个需求呢？
我第一想到的是 &lt;a href=&quot;https://requests.readthedocs.io/en/latest/user/advanced/#event-hooks&quot;&gt;Event Hook&lt;/a&gt;，但仿佛 &lt;code&gt;requests&lt;/code&gt; 没有 &lt;code&gt;before_request&lt;/code&gt; 这个钩子（曾经有），那么接下来考虑的是重载，由于这个签名方法是应用在 request 对象上的，所以不同在 &lt;code&gt;get&lt;/code&gt;，&lt;code&gt;post&lt;/code&gt; 之前做文章，因为这两个方法都还没产生 request 对象呢，可以重载 &lt;code&gt;Session.send()&lt;/code&gt; 这个方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class VolcSession(requests.Session):
    def send(self, request, **kwargs):
        # new_sign 具体实现略，照抄即可
        new_sign(request, service_info, credentials)
        return super().send(request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（重载 &lt;code&gt;Session.prepare_request()&lt;/code&gt; 也是一样的效果，区别是在 &lt;code&gt;super()&lt;/code&gt; 返回的对象上修改）不知对开始的问题你们心目中的方案是不是这样。&lt;/p&gt;
&lt;p&gt;但是，我说但是了，这里最好的方法利用，&lt;code&gt;requests.auth&lt;/code&gt;，他的签名是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class AuthBase:
    &quot;&quot;&quot;Base class that all auth implementations derive from&quot;&quot;&quot;

    def __call__(self, r):
        raise NotImplementedError(&quot;Auth hooks must be callable.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接收一个唯一对象 &lt;code&gt;r&lt;/code&gt;，这个就是即将要发送的请求，并返回一个新的请求，你可以对它作任何修改，这不就是我们要做的事情吗？签名所需的其他信息，可以作为 &lt;code&gt;__init__&lt;/code&gt; 的初始化参数。那么就可以改写成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class VolcAuth(AuthBase):
    def __init__(self, service_info, credentials):
        self.service_info = service_info
        self.credentials = credentials

    def __call__(self, r):
        # new_sign 具体实现略，照抄即可，区别是自定义的 request 对象改成了 requests 的
        new_sign(r, self.service_info, self.credentials)
        return r
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只需要这一个小小的对象即可。利用库的已存在的数据结构的好处是，我们能最大化保持原来的库的接口，因为请求方法我们没有任何侵入。用这个 Auth 对象请求的方法是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auth = VolcAuth(service_info, credentials)
res = requests.post(url, json=body, auth=auth)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样 &lt;code&gt;post()&lt;/code&gt; 方法里的所有参数，包括 &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;files&lt;/code&gt;, &lt;code&gt;headers&lt;/code&gt; 你可以任意使用，就像用 &lt;code&gt;requests&lt;/code&gt; 一样去调火山的接口，你还可以把创建一个带 auth 的 &lt;code&gt;Session&lt;/code&gt;，这样后面调用就不用每次都传 &lt;code&gt;auth&lt;/code&gt; 了。无感亲肤，就像冰丝内裤一样。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;对一个库的重载或修改，修改面要越小越好，并尽可能利用库本身提供的扩展方式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这与上面的方案相比，上面需要继承 &lt;code&gt;Session&lt;/code&gt;，而利用的 &lt;code&gt;AuthBase&lt;/code&gt; 本来就是提供给你扩展的，而且创建的对象 Auth 比 Session 小得多。只有当库扩展能力不足时，才考虑前面的方式，一直到无能为力，甚至动用 monkey patch 这种武器。&lt;/p&gt;
&lt;p&gt;这里面的细微优劣，就像你想要车的某个高级功能，你是希望得到一个插到任何车上都能用的零件，还是一台升级好的车，且你不知道它改了哪里呢？&lt;/p&gt;
&lt;h2&gt;参考实现&lt;/h2&gt;
&lt;p&gt;我在 Tetos 里做了一个针对 &lt;code&gt;httpx&lt;/code&gt; 的 &lt;code&gt;Auth&lt;/code&gt; 实现，和 &lt;code&gt;requests&lt;/code&gt; 的 &lt;code&gt;Auth&lt;/code&gt; 作用差不多，有兴趣的话甚至可以用一个 &lt;code&gt;Auth&lt;/code&gt; 同时支持 &lt;code&gt;httpx&lt;/code&gt; 和 &lt;code&gt;requests&lt;/code&gt; 两个库。&lt;/p&gt;
&lt;p&gt;https://github.com/frostming/tetos/blob/15a039f15feda2a3f7ffba7c441b5438f22a6ee4/src/tetos/volc.py#L25-L86&lt;/p&gt;
&lt;p&gt;比较一下，这个实现 62 行，加上不超过两行的调用，实现了原来 &lt;a href=&quot;https://github.com/volcengine/volc-sdk-python/blob/main/volcengine/auth/SignerV4.py&quot;&gt;SignerV4.py&lt;/a&gt; 207 行，加上 &lt;a href=&quot;https://github.com/volcengine/volc-sdk-python/blob/main/volcengine/base/Service.py&quot;&gt;Service.py&lt;/a&gt; 290 行，近 500 行，还没算上 import 的公共函数，十倍的差距。可见阅读库的文档，理清逻辑，是可以大大节省代码量的。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这个 SDK 写成这样，可能是直接从别的语言直译过来的。不知从事 code review 的 @piglei 如何看待，能不能过你这关。如果阅读本文的你恰好就是维护这个 SDK 的人被我中伤了我深表抱歉，并绝对不改。&lt;/p&gt;
</content:encoded></item><item><title>我们生活在差异里</title><link>https://frostming.com/posts/2024/diversity/</link><guid isPermaLink="false">https://frostming.com/2024/diversity/</guid><pubDate>Fri, 19 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;对不起余华老师，小小碰一下瓷&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上周我去了上海，见了我研究生时代关系最好的同学，我们已经快九年没见了，他头发理得很短，略显发福，就称呼他为亮子吧。亮子还在一家芯片设计公司上班，从毕业后到现在一直没换过，已逾十年。这对于我来说有些不可思议，疫情以来，「降本增笑」对于我们来说已经是一个谈资，而他却一直在做着同样的事情，同样的工作，同样的生活，难能可贵。&lt;/p&gt;
&lt;p&gt;亮子过着高度重复机械的工作生活，每天要到十点以后才下班，周末也得去。他说：「有时就连洗个澡，都会影响我的睡眠时间，往往回家倒头就睡。」我向来痛恨此事，他却说：「这和那种无效加班不同，是真的安排了超出负荷的任务，多到做不完。」问及为何不换工作，他说被公司上市的饼钓着，员工签了协议，已经预缴了一部分资金。&lt;/p&gt;
&lt;p&gt;亮子还向我要了翻墙的教程，说想在领英上更新一下简历，而领英在大陆已经不能用了。临走我非常想让他平时多在社交网站上联络，培养一些兴趣爱好。但看他的现状，恐怕也是奢望，于是我挥手道别时，只是说了一句：「祝你早日脱离苦海！」&lt;/p&gt;
&lt;p&gt;我如今做着完全远程的工作，也得益于工作的自由，能陪老婆孩子去上海玩了一圈。我平时习惯了上墙外的网，和跟我同样喜好的「网友」们互动，谈笑风生，臧否时事。我们在这种自由的氛围里，些许爹味的言论就会被喷，久而久之就汇集了一些有相似生活态度的人。但我有些反感「同温层」这个词，我们在虚拟的网络上久了，不要忘了现实中那些曾经的同学、朋友，他们还真实地生活在这个差异的世界里，体会着他们的悲欢。亮子是我的研究生同学，并不是老家县城小学的同学，我们的视界起初并没有什么不同。只是后来，被不同的工作、不同的环境，渐渐地塑造成现在的样子。正如《代码之外第一期听众来信》所说的，第一份工作决定了我现在的样子，我至今非常感谢它。&lt;/p&gt;
&lt;p&gt;https://www.xiaoyuzhoufm.com/episode/64aba6940c9873af30c76886&lt;/p&gt;
&lt;p&gt;周围的人大都是相似的，但除此之外存在着更多其他的人，我需要认清这种差异，并包容它。&lt;/p&gt;
</content:encoded></item><item><title>PDM 的内部实现(2)</title><link>https://frostming.com/posts/2024/pdm-lock-strategy/</link><guid isPermaLink="false">https://frostming.com/2024/pdm-lock-strategy/</guid><description>Lock 策略</description><pubDate>Mon, 01 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;这篇文章将会介绍 PDM 的 lock 策略，基于当前最新版本 2.13。&lt;a href=&quot;/en/2024/pdm-lock-strategy&quot;&gt;英文版&lt;/a&gt;由 LLM 辅助翻译。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;PDM 是如何解析依赖的？&lt;/h2&gt;
&lt;p&gt;PDM 底层是用的一个纯 Python 实现的 &lt;a href=&quot;https://github.com/dart-lang/pub/blob/master/doc/solver.md&quot;&gt;PubGrub 解析算法&lt;/a&gt;，&lt;a href=&quot;https://github.com/sarugaku/resolvelib&quot;&gt;Resolvelib&lt;/a&gt;。若用通俗的语言解释，它的解析过程大致如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;选择一个未解析的依赖，获取它的所有版本的列表&lt;/li&gt;
&lt;li&gt;从最新版本开始尝试，获取这个版本的依赖&lt;/li&gt;
&lt;li&gt;检查这个版本的依赖与已解析的依赖是否有冲突&lt;/li&gt;
&lt;li&gt;若有冲突，尝试下一个版本&lt;/li&gt;
&lt;li&gt;若无冲突，将这个依赖的版本加入结果中&lt;/li&gt;
&lt;li&gt;若尝试过所有版本都无法解析，回溯到上个确定结果的依赖，尝试下一个版本&lt;/li&gt;
&lt;li&gt;最终所有依赖都解析完成，得到一个满足所有依赖的版本列表&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;解析完成以后，PDM 就会将结果写到 &lt;code&gt;pdm.lock&lt;/code&gt; 文件中，这个文件除了包含所有依赖的版本信息，还包含了一些其他元数据。&lt;/p&gt;
&lt;h3&gt;条件依赖&lt;/h3&gt;
&lt;p&gt;有时我们需要根据不同的条件安装不同的包版本，这会利用 Marker，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pytest &amp;gt;= 7.0; python_version &amp;gt;= &quot;3.6&quot;
pytest &amp;lt; 7.0; python_version &amp;lt; &quot;3.6&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是 PDM 现在暂时不支持解析这种条件依赖，原因是 PDM 的依赖解析器实现会把包名作为解集中的 key，换句话说，每个包在解集中有且只有一个确定的版本。不得不承认，这确实是 PDM 的一大缺陷，欢迎大家贡献代码来解决这个问题。&lt;/p&gt;
&lt;h2&gt;Lock 文件的元数据&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;pdm.lock&lt;/code&gt; 文件是一个 TOML 格式的文件，在它的 &lt;code&gt;[metadata]&lt;/code&gt; 表中包含了一些元数据，包括：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[metadata]
groups = [
  &quot;default&quot;,
  &quot;all&quot;,
  &quot;doc&quot;,
  &quot;pytest&quot;,
  &quot;test&quot;,
  &quot;tox&quot;,
  &quot;workflow&quot;
]
strategy = [
  &quot;cross_platform&quot;,
  &quot;inherit_metadata&quot;
]
lock_version = &quot;4.4.1&quot;
content_hash = &quot;sha256:13270582f610302a77a5e1fef2192e1a65f5b6202cf15aedf12bf799de8de45c&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;lock_version&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;这是一个三位的版本号，标明这个 lock 文件的兼容情况。第一位表示&lt;a href=&quot;https://en.wikipedia.org/wiki/Backward_compatibility&quot;&gt;向后不兼容&lt;/a&gt;的改动，第二位表示向后兼容但&lt;a href=&quot;https://en.wikipedia.org/wiki/Forward_compatibility&quot;&gt;向前不兼容&lt;/a&gt;的改动，第三位表示向前向后都兼容的改动。&lt;/p&gt;
&lt;p&gt;比如说，假如当前 lock 文件的版本是 4.4.2，那么：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持读取的版本有：&lt;code&gt;4.3.0&lt;/code&gt;, &lt;code&gt;4.4.0&lt;/code&gt;, &lt;code&gt;4.4.3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;不能读取的版本有：&lt;code&gt;5.1.0&lt;/code&gt;, &lt;code&gt;3.0.0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每当更新 lock 文件时，PDM 会把当前的 lock 版本写入文件中。通过这个版本号，PDM 就可以决定是否应该尝试读取这个 lock 文件，或是提示用户重新生成 lock 文件。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;groups&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;记录了这个 lock 文件是从哪些依赖分组生成的，列表中的每个值都对应了 &lt;code&gt;pyproject.toml&lt;/code&gt; 中 &lt;code&gt;optional-dependencies&lt;/code&gt; 或 &lt;code&gt;dev-dependencies&lt;/code&gt; 的一个分组。
当依赖解析完成时，这些分组就会被记录在 lock 文件中，安装时，PDM 会检查你要求安装的分组是否包含其中。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;content_hash&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;因为 lock 文件对应了一组初始输入，即从哪些依赖解析生成。在 PDM 中，这个输入就是 &lt;code&gt;pyproject.toml&lt;/code&gt; 中写的依赖信息，&lt;code&gt;content_hash&lt;/code&gt; 就是从这些内容计算出来的一个 sha256 值，当你的 &lt;code&gt;pyproject.toml&lt;/code&gt; 发生变化，PDM 就会重新计算这个值，如果发现与 lock 文件中的值不一致，就会为你更新 lock 文件，并把新的 &lt;code&gt;content_hash&lt;/code&gt; 写入其中。&lt;/p&gt;
&lt;h2&gt;Lock 策略&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;[metadata]&lt;/code&gt; 表中的 &lt;code&gt;strategy&lt;/code&gt; 字段就记录了当前 lock 文件的策略，用来控制依赖解析的过程。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;cross_platform&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;默认启用，PDM 会将所有平台的文件都写入 Lock 文件中，详见&lt;a href=&quot;/2024/pdm-lockfile#%E8%B7%A8%E7%89%88%E6%9C%AC-lock-%E4%B8%8E%E5%BD%93%E5%89%8D%E7%8E%AF%E5%A2%83-lock&quot;&gt;上篇文章&lt;/a&gt;。但有时我们不得不使用当前环境的 Lock，一大原因是某些包在不同平台发布的包有着完成不同的依赖列表，这样会使得跨平台锁产生错误的结果。在 PDM 中，如果要禁用一个 Lock 策略，只需要：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pdm lock --strategy=no_cross_platform
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令会取消 &lt;code&gt;cross_platform&lt;/code&gt; 策略，已使用的其他策略不会被影响。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;static_urls&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;默认情况下，&lt;code&gt;[[package]]&lt;/code&gt; 的 &lt;code&gt;files&lt;/code&gt; 字段中只会记录包文件的文件名，而不包含 URL。这样做的好处是用户可以自由切换到其他 PyPI 的镜像源，PDM 安装时只会检查下载的文件名包含在 Lock 文件中。而如果启用了 &lt;code&gt;static_urls&lt;/code&gt; 策略，PDM 会记录包文件的 URL，安装时就会直接通过这些 URL 下载安装包。这样也可以方便一些安全审计工具检查包的来源。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;inherit_metadata&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;默认启用，PDM 会尝试为每个包计算它的最终 Marker（详见 &lt;a href=&quot;/2024/pdm-lockfile#markers&quot;&gt;上篇文章&lt;/a&gt;）。这样的好处是，安装时 PDM 只需要 &lt;code&gt;pdm.lock&lt;/code&gt; 这一个数据来源，并且遍历 lock 文件和求值 Markers 这个过程使用 Python 标准库[^1]就可以完成，不需要依赖 PDM 的其他组件。如果禁用这个策略，Marker 将不会记录在包中，这样 lock 文件中记录的信息就不足以让安装器决定是否安装这个包，安装过程会有些许变化。PDM 会通过要求安装和依赖列表，和 Lock 文件中的依赖信息，运行一次依赖解析过程，来取得最终需要安装的包版本的列表。&lt;/p&gt;
&lt;p&gt;[^1]: 包括 &lt;a href=&quot;https://github.com/pypa/packaging&quot;&gt;&lt;code&gt;packaging&lt;/code&gt;&lt;/a&gt; 库，因为它是诸多 Python 打包的标准实现，已经几乎是标准库。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;direct_minimal_versions&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;默认情况下，解析依赖时会从最新版本开始尝试，这样得到的 lock 结果通常包含了尽可能新的包版本。但有时为了测试库的兼容性，我们会希望看到它是否能在指定的最小依赖版本下工作。启用这个策略后，PDM 会尝试从最小版本开始解析依赖，得到的 lock 文件中的版本号就是最小版本的依赖。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;--exclude-newer DATE&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;除了上述策略之外，PDM lock 还支持一个一次性选项 &lt;code&gt;--exclude-newer&lt;/code&gt;。这个选项的作用有点类似于时光机，当指定了一个时间或日期之后，PDM 解析依赖时会跳过那些晚于这个时间点上传的包版本。使用这个选项可以让 lock 文件是可复现的。需要注意的是，包的上传时间需要 PyPI 源的支持，它必须实现了 &lt;a href=&quot;https://peps.python.org/pep-0700/&quot;&gt;PEP 700&lt;/a&gt;，否则，这个包会被认为&lt;strong&gt;不满足条件并会被忽略&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;更新策略&lt;/h2&gt;
&lt;p&gt;在你尝试更新 lock 文件中的包版本时，PDM 也提供了不同的更新策略，这些策略可以通过 &lt;code&gt;--update-*&lt;/code&gt; 选项来指定，&lt;code&gt;pdm add&lt;/code&gt;，&lt;code&gt;pdm lock&lt;/code&gt;，&lt;code&gt;pdm update&lt;/code&gt; 均支持这组选项。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--update-all&lt;/code&gt;：更新所有包（直接依赖+间接依赖）到最新版本，也就是完全忽略 lock 文件中的版本信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--update-reuse&lt;/code&gt;：只更新直接依赖的版本，复用 lock 文件中的间接依赖版本&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--update-eager&lt;/code&gt;：更新指定依赖及其间接依赖到最新版本，复用 lock 文件中的其他依赖版本&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--update-reuse-installed&lt;/code&gt;： 尽可能复用当前已安装的版本&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更新依赖版本时，仍然会尊重 &lt;code&gt;pyproject.toml&lt;/code&gt; 中指定的版本范围，这个行为可以通过 &lt;code&gt;--unconstrained&lt;/code&gt; 选项关闭，即取消版本范围的限制。&lt;/p&gt;
&lt;p&gt;到此为止，我们介绍了围绕 PDM 的 lock 文件的一系列功能和背后的逻辑，希望这些信息能帮助你更好地理解 PDM 的工作原理。&lt;/p&gt;
</content:encoded></item><item><title>PDM Internals(2)</title><link>https://frostming.com/posts/en/2024/pdm-lock-strategy/</link><guid isPermaLink="false">https://frostming.com/en/2024/pdm-lock-strategy/</guid><description>Lock strategy</description><pubDate>Mon, 01 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This article will introduce the lock strategy of PDM based on the current latest version 2.13. Read the &lt;a href=&quot;/2024/pdm-lock-strategy&quot;&gt;Chinese version&lt;/a&gt; for your convenience.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;How does PDM solve dependencies&lt;/h2&gt;
&lt;p&gt;Under the hood, PDM uses a pure Python implementation of the &lt;a href=&quot;https://github.com/dart-lang/pub/blob/master/doc/solver.md&quot;&gt;PubGrub algorithm&lt;/a&gt;，named &lt;a href=&quot;https://github.com/sarugaku/resolvelib&quot;&gt;Resolvelib&lt;/a&gt;. If explained in plain language, its parsing process is roughly as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Select an unresolved dependency and get the list of all available versions.&lt;/li&gt;
&lt;li&gt;Starting from the latest version, obtain the dependencies for this version.&lt;/li&gt;
&lt;li&gt;Check if there are any conflicts between this version&apos;s dependencies and the already resolved dependencies.&lt;/li&gt;
&lt;li&gt;If there is a conflict, try the next version.&lt;/li&gt;
&lt;li&gt;If there is no conflict, add this dependency&apos;s version to the solution set.&lt;/li&gt;
&lt;li&gt;If all versions have been attempted but none can be solved, backtrack to the last pinned package and try the next version.&lt;/li&gt;
&lt;li&gt;Eventually, all dependencies will be solved and we will get a list of pinned versions that satisfy all dependencies.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After the resolution, PDM will write the result to the &lt;code&gt;pdm.lock&lt;/code&gt; file. This file contains not only all dependency version information but also some other metadata.&lt;/p&gt;
&lt;h3&gt;Conditional dependencies&lt;/h3&gt;
&lt;p&gt;Sometimes we need to install different package versions based on different conditions, which can be achieved using markers. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pytest &amp;gt;= 7.0; python_version &amp;gt;= &quot;3.6&quot;
pytest &amp;lt; 7.0; python_version &amp;lt; &quot;3.6&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, PDM currently does not support solving this type of conditional dependencies. The reason is that the dependency solver in PDM implementation encodes the package name as the key in the solution set. In other words, each package has only one determined version in the solution set. I have to admit that this is indeed a major flaw of PDM and everyone is welcome to contribute code to solve this problem.&lt;/p&gt;
&lt;h2&gt;The metadata in &lt;code&gt;pdm.lock&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;pdm.lock&lt;/code&gt; file is a TOML formatted file that contains some metadata in its &lt;code&gt;[metadata]&lt;/code&gt; table, including:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[metadata]
groups = [
  &quot;default&quot;,
  &quot;all&quot;,
  &quot;doc&quot;,
  &quot;pytest&quot;,
  &quot;test&quot;,
  &quot;tox&quot;,
  &quot;workflow&quot;
]
strategy = [
  &quot;cross_platform&quot;,
  &quot;inherit_metadata&quot;
]
lock_version = &quot;4.4.1&quot;
content_hash = &quot;sha256:13270582f610302a77a5e1fef2192e1a65f5b6202cf15aedf12bf799de8de45c&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;lock_version&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This is a three-digit version number indicating the compatibility of this lock file.The first number indicates &lt;a href=&quot;https://en.wikipedia.org/wiki/Backward_compatibility&quot;&gt;backward incompatible&lt;/a&gt; changes. The second number indicates backward compatibile but &lt;a href=&quot;https://en.wikipedia.org/wiki/Forward_compatibility&quot;&gt;forward incompatible&lt;/a&gt; changes, and the last number indicates backward and forward compatible changes. For example, if the current lock file version is 4.4.2, then:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Able to read: &lt;code&gt;4.3.0&lt;/code&gt;, &lt;code&gt;4.4.0&lt;/code&gt;, &lt;code&gt;4.4.3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Unable to read: &lt;code&gt;5.1.0&lt;/code&gt;, &lt;code&gt;3.0.0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Whenever the lock file is updated, PDM will write the current lock version to the file. With this version number, PDM can determine whether to attempt to read this lock file or prompt the user to regenerate the lock file.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;groups&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This is stored to indicate which dependency groups the lock file was generated from. Each value in the list corresponds to a group in &lt;code&gt;optional-dependencies&lt;/code&gt; or &lt;code&gt;dev-dependencies&lt;/code&gt; in &lt;code&gt;pyproject.toml&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;When dependency resolution is complete, these groups are recorded in the lock file. When installing, PDM checks whether the requested installation group is included and aborts the installation if not.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;content_hash&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Because the lock file corresponds to a set of initial inputs, that is, from which dependencies are resolved. In PDM, this input is the metadata written in &lt;code&gt;pyproject.toml&lt;/code&gt;. &lt;code&gt;content_hash&lt;/code&gt; is a sha256 checksum calculated from these contents. When your &lt;code&gt;pyproject.toml&lt;/code&gt; changes, PDM will revalidate this value. If it finds that it does not match the value in the lock file, it will update the lock file for you and write the new &lt;code&gt;content_hash&lt;/code&gt; into it.&lt;/p&gt;
&lt;h2&gt;Lock strategies&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;strategy&lt;/code&gt; field in the &lt;code&gt;[metadata]&lt;/code&gt; table records the strategies being usd by the lock file, which is used to control the dependency resolution process.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;cross_platform&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;By default, PDM will write package files for all platforms to the lock file. For more information, please refer to the previous article (/en/2024/pdm-lockfile#cross-version-lock-and-lock-for-current-environment). However, sometimes we have to use the lock for current environment. One major reason is that some packages have different dependencies for different platforms, which can cause wrong locking results. In PDM, if you want to disable a lock strategy, just run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pdm lock --strategy=no_cross_platform
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command will turn off the &quot;cross_platform&quot; strategy, and other strategies that are stored will not be affected.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;static_urls&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;By default, the &lt;code&gt;files&lt;/code&gt; field of &lt;code&gt;[[package]]&lt;/code&gt; only records the file names of package files, not URLs. The benefit of doing this is that users can freely switch to other PyPI mirror sources, and PDM will only check if the downloaded file name matches those in the lock file during installation. If the &lt;code&gt;static_urls&lt;/code&gt; strategy is enabled, PDM will record the URL of package files, and it will directly download and install packages through these URLs during installation. This also facilitates some security audit tools to check the source of packages.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;inherit_metadata&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Enabled by default, PDM will attempt to calculate the final markers for each package (see &lt;a href=&quot;/en/2024/pdm-lockfile#markers&quot;&gt;previous article&lt;/a&gt; for details). The benefit of this is that during installation, PDM only needs &lt;code&gt;pdm.lock&lt;/code&gt; as the sole data source and does the installation by only traversing the lock file and evaluating the markers using Python&apos;s standard library[^1], without relying on other components of PDM. If this strategy is disabled, instead, markers will not be recorded for packages. As a result, the information recorded in the lock file may not be sufficient for the installer to determine whether or not to install a package, resulting in slight changes during installation. To obtain a list of versions of packages that need to be installed finally, PDM will run another dependency resolution process based on the required dependencies lists along with dependency information from the lock files.&lt;/p&gt;
&lt;p&gt;[^1]: Including the &lt;a href=&quot;https://github.com/pypa/packaging&quot;&gt;&lt;code&gt;packaging&lt;/code&gt;&lt;/a&gt; library, because it contains implementations for many Python packaging standards and has become a core library in the Python packaging ecosystem.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;direct_minimal_versions&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;By default, when resolving dependencies, the latest version is attempted first, resulting in a lock file that usually contains the newest possible package versions. However, sometimes we want to test library compatibility with the minimum version within a specific range. Enabling this strategy will cause PDM to attempt resolving dependencies from the minimum version and result in a lock file containing the minimum version&apos;s dependency numbers.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;--exclude-newer DATE&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;In addition to the above strategies, &lt;code&gt;pdm lock&lt;/code&gt; also supports a one-time option &lt;code&gt;--exclude-newer&lt;/code&gt;. The function of this option is somewhat similar to a time machine. When a specific time or date is specified, PDM will skip package versions uploaded later than that time point when parsing the index. Using this option can make the lock file reproducible. It should be noted that the upload time of packages requires support from the package registry which must have implemented &lt;a href=&quot;https://peps.python.org/pep-0700/&quot;&gt;PEP 700&lt;/a&gt;. Otherwise, the package will be considered &lt;strong&gt;not meeting the requirements and will be ignored&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Update strategies&lt;/h2&gt;
&lt;p&gt;When you try to update the package version in the lock file, PDM also provides different update strategies. These strategies can be specified through the &lt;code&gt;--update-*&lt;/code&gt; option, and &lt;code&gt;pdm add&lt;/code&gt;, &lt;code&gt;pdm lock&lt;/code&gt;, and &lt;code&gt;pdm update&lt;/code&gt; all support this set of options.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--update-all&lt;/code&gt;: Update all packages (direct and indirect dependencies) to the latest version, completely ignoring version stored in the lock file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--update-reuse&lt;/code&gt;: Only update direct dependency versions and reuse indirect dependency versions from the lock file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--update-eager&lt;/code&gt;: Update specified dependencies and their indirect dependencies to the latest version, reusing other dependency versions from the lock file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--update-reuse-installed&lt;/code&gt;: Reuse currently installed versions as much as possible.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When updating package versions, the version range specified in &lt;code&gt;pyproject.toml&lt;/code&gt; will still be respected. This behavior can be disabled by using the &lt;code&gt;--unconstrained&lt;/code&gt; option to remove the version constraints.&lt;/p&gt;
&lt;p&gt;So far, we have introduced a series of features and the logic behind the lock files around PDM. We hope this information can help you better understand how PDM works behind the scene.&lt;/p&gt;
</content:encoded></item><item><title>PDM 的内部实现(1)</title><link>https://frostming.com/posts/2024/pdm-lockfile/</link><guid isPermaLink="false">https://frostming.com/2024/pdm-lockfile/</guid><description>Lockfile</description><pubDate>Mon, 11 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;为了解答一些高频出现的问题和方便未来的贡献者，我计划从这篇文章开始，写一系列关于 PDM 内部实现的文章。
这篇文章将会介绍 PDM 的 lockfile，基于当前最新版本 2.12。&lt;a href=&quot;/en/2024/pdm-lockfile&quot;&gt;英文版&lt;/a&gt;由 LLM 辅助翻译.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Lockfile 是什么？&lt;/h2&gt;
&lt;p&gt;Lockfile[^1] 是一个用来记录项目依赖的文件，它记录了项目的依赖和依赖的版本号。这个文件在包管理器中比较常见，比如 &lt;code&gt;yarn.lock&lt;/code&gt;, &lt;code&gt;Cargo.lock&lt;/code&gt;, &lt;code&gt;go.sum&lt;/code&gt; 等。
在 Python 的生态中，Pipenv 和 Poetry 也有自己的 Lockfile。PDM 同样是一个有 Lockfile 的包管理器，这区分于其他不带 Lockfile 的包管理器，比如 pip。有了 Lockfile 会有一些行为上的改变。
很多人都会在 PDM 的 Issue 里问为什么 pip 能安装 PDM 不能，希望这篇文章能解答这个问题。&lt;/p&gt;
&lt;p&gt;虽然如此，我们似乎需要先界定一下 Lockfile 的作用。这似乎不是一件很容易的事，至少在 PDM 中，Lockfile 是用来&lt;strong&gt;限定&lt;/strong&gt;所有安装过程中&lt;em&gt;可能&lt;/em&gt;会安装的包的版本，以及它的来源、checksum 等，目的是提供可复现的
Python 环境。
你可以通过运行 &lt;code&gt;pdm lock&lt;/code&gt; 来产生一个 Lockfile，PDM也会在你运行 &lt;code&gt;pdm install&lt;/code&gt; 时确保 Lockfile 存在与有效，并在必要时候生成它。
最近&lt;a href=&quot;https://discuss.python.org/t/lock-files-again-but-this-time-w-sdists/46593&quot;&gt;新的一轮 Lockfile 提案讨论&lt;/a&gt;正在进行中，讨论比较长，有余力可以看看大家对 Lockfile 有什么不同的理解和期待。&lt;/p&gt;
&lt;p&gt;[^1]: 中文应该翻译成「（依赖）锁文件」，但这个名字用起来觉得很别扭，所以后文还是用 Lockfile。&lt;/p&gt;
&lt;h2&gt;Lockfile 是如何生成的？&lt;/h2&gt;
&lt;h3&gt;跨版本 lock 与当前环境 lock&lt;/h3&gt;
&lt;p&gt;最初 Python 的包管理器都是不带 Lockfile 的，但依赖解析仍然是一个必要的过程。那么当 pip 安装一个包 &lt;code&gt;foo&lt;/code&gt; 时会发生什么呢？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;访问 &lt;code&gt;https://pypi.org/simple/foo/&lt;/code&gt; 获取 &lt;code&gt;foo&lt;/code&gt; 的所有版本&lt;/li&gt;
&lt;li&gt;从满足条件的最新版本开始，逐个检查每个包文件是否满足&lt;strong&gt;当前环境和 Python 版本&lt;/strong&gt;。如果满足，就选择这个版本进行下一步。&lt;/li&gt;
&lt;li&gt;从这个文件取得它的依赖列表，依赖也可以有环境要求，所以也要逐个检查每个依赖是否满足&lt;strong&gt;当前环境和 Python 版本&lt;/strong&gt;，如果满足，就记录这个依赖。&lt;/li&gt;
&lt;li&gt;对记录好的且没有找到对应包文件的依赖，重复步骤 1。&lt;/li&gt;
&lt;li&gt;如果没有找到一个符合的版本，就退回步骤 2，选择下一个符合要求的文件。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以发现我加粗了&lt;strong&gt;当前环境和 Python 版本&lt;/strong&gt;，是的，解析器在检查是否满足条件时都是考虑当前环境和 Python 版本。这就是一种只针对当前环境的 lock。在写作这篇文章时，除了 Poetry 和 PDM 的 Python 包管理器，都是这种依赖解析方式。&lt;/p&gt;
&lt;p&gt;这对于那些不生成的 Lockfile 的包管理器来说，每次安装依赖都是现场解析，只需要考虑当前环境，完全没必要考虑其他。但如果包管理器生成了 Lockfile，既然它的目的就是复现环境，那么就有可能会在不同的 Python 版本或操作系统上执行安装。
那么就需要一种跨版本的 Lockfile，你既可以为每个目标环境都生成一份，但 PDM 选择的是把所有包版本，以及它的环境信息都记录在一个 Lockfile 里。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;requires-python&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;requires-python&lt;/code&gt; 是 &lt;a href=&quot;https://peps.python.org/pep-0621/#requires-python&quot;&gt;PEP 621&lt;/a&gt; 中定义的一个元数据字段，写在 &lt;code&gt;[project]&lt;/code&gt; 表中，但其实相似的概念在更早的时候就引入了，&lt;code&gt;setuptools.setup()&lt;/code&gt;
就有 &lt;code&gt;python_requires&lt;/code&gt; 这个参数，作用是一样的，都是限制这个包能在哪些 Python 环境上安装。在 Python 3 的破坏性更新之后，理论上所有的包或者 Python 项目都应该有这个字段，表明它支持的 Python 版本范围。&lt;/p&gt;
&lt;p&gt;这个字段在 PDM 的依赖解析中起到了非常关键的作用，为了说明它的机制，我们来看一个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[project]
name = &quot;foo&quot;
requires-python = &quot;&amp;gt;=3.8&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在运行 &lt;code&gt;pdm add numpy&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;输出&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Adding packages to default dependencies: numpy
${SITE_PACKAGES}/pdm/resolver/providers.py:196: PackageWarning: Skipping numpy@1.26.4 because it requires Python&amp;gt;=3.9 but the
project claims to work with Python&amp;gt;=3.8. Instead, another version of numpy that supports Python&amp;gt;=3.8 will be used.
If you want to install numpy@1.26.4, narrow down the `requires-python` range to include this version. For example, &quot;&amp;gt;=3.9&quot; should work.
  return self.repository.find_candidates(
${SITE_PACKAGES}/pdm/resolver/providers.py:196: PackageWarning: Skipping numpy@1.26.3 because it requires Python&amp;gt;=3.9 but the
project claims to work with Python&amp;gt;=3.8. Instead, another version of numpy that supports Python&amp;gt;=3.8 will be used.
If you want to install numpy@1.26.3, narrow down the `requires-python` range to include this version. For example, &quot;&amp;gt;=3.9&quot; should work.
  return self.repository.find_candidates(
${SITE_PACKAGES}/pdm/resolver/providers.py:196: PackageWarning: Skipping numpy@1.26.2 because it requires Python&amp;gt;=3.9 but the
project claims to work with Python&amp;gt;=3.8. Instead, another version of numpy that supports Python&amp;gt;=3.8 will be used.
If you want to install numpy@1.26.2, narrow down the `requires-python` range to include this version. For example, &quot;&amp;gt;=3.9&quot; should work.
  return self.repository.find_candidates(
${SITE_PACKAGES}/pdm/resolver/providers.py:196: PackageWarning: Skipping numpy@1.26.1 because it requires Python&amp;lt;3.13,&amp;gt;=3.9
but the project claims to work with Python&amp;gt;=3.8. Instead, another version of numpy that supports Python&amp;gt;=3.8 will be used.
If you want to install numpy@1.26.1, narrow down the `requires-python` range to include this version. For example, &quot;&amp;lt;3.13,&amp;gt;=3.9&quot; should work.
  return self.repository.find_candidates(
${SITE_PACKAGES}/pdm/resolver/providers.py:196: PackageWarning: Skipping numpy@1.26.0 because it requires Python&amp;lt;3.13,&amp;gt;=3.9
but the project claims to work with Python&amp;gt;=3.8. Instead, another version of numpy that supports Python&amp;gt;=3.8 will be used.
If you want to install numpy@1.26.0, narrow down the `requires-python` range to include this version. For example, &quot;&amp;lt;3.13,&amp;gt;=3.9&quot; should work.
  return self.repository.find_candidates(
${SITE_PACKAGES}/pdm/resolver/providers.py:196: PackageWarning: Skipping numpy@1.25.2 because it requires Python&amp;gt;=3.9 but the
project claims to work with Python&amp;gt;=3.8. Instead, another version of numpy that supports Python&amp;gt;=3.8 will be used.
If you want to install numpy@1.25.2, narrow down the `requires-python` range to include this version. For example, &quot;&amp;gt;=3.9&quot; should work.
  return self.repository.find_candidates(
${SITE_PACKAGES}/pdm/resolver/providers.py:196: PackageWarning: Skipping numpy@1.25.1 because it requires Python&amp;gt;=3.9 but the
project claims to work with Python&amp;gt;=3.8. Instead, another version of numpy that supports Python&amp;gt;=3.8 will be used.
If you want to install numpy@1.25.1, narrow down the `requires-python` range to include this version. For example, &quot;&amp;gt;=3.9&quot; should work.
  return self.repository.find_candidates(
${SITE_PACKAGES}/pdm/resolver/providers.py:196: PackageWarning: Skipping numpy@1.25.0 because it requires Python&amp;gt;=3.9 but the
project claims to work with Python&amp;gt;=3.8. Instead, another version of numpy that supports Python&amp;gt;=3.8 will be used.
If you want to install numpy@1.25.0, narrow down the `requires-python` range to include this version. For example, &quot;&amp;gt;=3.9&quot; should work.
  return self.repository.find_candidates(
INFO: Use `-q/--quiet` to suppress these warnings, or ignore them per-package with `ignore_package_warnings` config in [tool.pdm] table.
🔒 Lock successful
Changes are written to pyproject.toml.
Synchronizing working set with resolved packages: 1 to add, 0 to update, 0 to remove

  ✔️ Install numpy 1.24.4 success
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;可以看到它拒绝了一堆 numpy 的新版本[^2]，只是因为它们支持的 Python 的版本最低只到 3.9，而你的项目指定的最低 Python 版本是 3.8。所以如果你想安装某个依赖了 &lt;code&gt;numpy&amp;gt;=1.26&lt;/code&gt; 的包，PDM 会解析失败并报错。
有很多用户都问过这个问题，明明我现在使用的 Python 版本是 3.10，为何 PDM 依然拒绝这些新版本的包？&lt;/p&gt;
&lt;p&gt;答案是， PDM 总是尝试让这个 Lockfile 能在所有你指定的 Python 版本上工作，&lt;strong&gt;它不会考虑你当前使用的是哪个 Python 版本&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这样做是因为，如果你的项目中指定了 &lt;code&gt;requires-python = &quot;&amp;gt;=3.8&quot;&lt;/code&gt; 并分享了出去，那么就表示你完全允许一个用户使用 Python 3.8 来安装这个包。这时所有的依赖都必须支持 Python 3.8，&lt;code&gt;numpy@1.26&lt;/code&gt; 明显是不满足的。
所以&lt;strong&gt;在实际使用中，你项目中 &lt;code&gt;requires-python&lt;/code&gt; 所指定的范围，必须是所有依赖的包的 &lt;code&gt;requires-python&lt;/code&gt; 范围的子集&lt;/strong&gt;。PDM 会为你计算出这个合适的值，显示在警告信息中，就像上面的例子一样。&lt;/p&gt;
&lt;p&gt;[^2]: 你可能会认为这么多警告太嘈杂了，但我担忧不加这些信息会让用户更加困惑，不知道结果为何如此。这些警告可以通过添加 &lt;code&gt;--quiet&lt;/code&gt; 屏蔽。&lt;/p&gt;
&lt;h3&gt;Markers&lt;/h3&gt;
&lt;p&gt;另一个与环境限制相关的概念是 Markers[^3]，它来自于 &lt;a href=&quot;https://peps.python.org/pep-0508/&quot;&gt;PEP 508&lt;/a&gt;规范。它是一种条件表达式，用来限制包的安装条件。比如 &lt;code&gt;foo&amp;gt;=1.0; sys_platform == &quot;win32&quot;&lt;/code&gt; 表示只有在 Windows 平台上才会安装 &lt;code&gt;foo&lt;/code&gt;。
在当前环境 lock 中，如果解析器碰到这样的表达式，并且检测发现当前环境不满足这个条件，那么这个包就会被忽略，否则会将 &lt;code&gt;foo&amp;gt;=1.0&lt;/code&gt; 记录下来。
而在跨版本 lock 中，表达式并不能被求值，所以这个表达式会被整体记录下来 &lt;code&gt;foo&amp;gt;=1.0; sys_platform == &quot;win32&quot;&lt;/code&gt;，并继续解析 &lt;code&gt;foo&lt;/code&gt; 的依赖。等到安装包时，才会求值这个表达式，选择是否安装这个包。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/pdm-lock-graph.png&quot; alt=&quot;pdm-lock-graph&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但需注意到，条件依赖的依赖，也应该应用同样的条件，即若 &lt;code&gt;foo&lt;/code&gt; 依赖了 &lt;code&gt;bar&lt;/code&gt;，那 &lt;code&gt;bar&lt;/code&gt; 也应该只在 Windows 上安装。这叫做条件的传递。甚至如果 &lt;code&gt;bar&lt;/code&gt; 本身有一个另外的安装条件，那么这两个条件应该用与逻辑合并。另一方面，同个依赖可能上游的来源不同，于是「继承」得到的条件也不同，这些条件又应该用或逻辑合并。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/marker-propagation.png&quot; alt=&quot;marker-propagation&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在 PDM 的 Lockfile 中，这些 markers 的运算结果，会被记录在每个包的 &lt;code&gt;markers&lt;/code&gt; 字段中。这样在安装时，只需要拿到这个条件表达式，就能判断是否需要安装此包，而不用管他上游祖先具有什么条件限制。&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;比如这是 &lt;code&gt;rich&lt;/code&gt; 的解析结果&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# This file is @generated by PDM.
# It is not intended for manual editing.

[metadata]
groups = [ &quot;default&quot; ]
strategy = [
  &quot;cross_platform&quot;,
  &quot;inherit_metadata&quot;
]
lock_version = &quot;4.4.1&quot;
content_hash = &quot;sha256:37d2aae470ae5f416baf9366fdba3a83f22de379a8ab288ec6077f4ce3b0ec59&quot;

[[package]]
name = &quot;markdown-it-py&quot;
version = &quot;3.0.0&quot;
requires_python = &quot;&amp;gt;=3.8&quot;
summary = &quot;Python port of markdown-it. Markdown parsing, done right!&quot;
groups = [ &quot;default&quot; ]
dependencies = [ &quot;mdurl~=0.1&quot;, ]
files = [
  { file = &quot;markdown-it-py-3.0.0.tar.gz&quot;, hash = &quot;sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb&quot; },
  { file = &quot;markdown_it_py-3.0.0-py3-none-any.whl&quot;, hash = &quot;sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1&quot; },
]

[[package]]
name = &quot;mdurl&quot;
version = &quot;0.1.2&quot;
requires_python = &quot;&amp;gt;=3.7&quot;
summary = &quot;Markdown URL utilities&quot;
groups = [ &quot;default&quot; ]
files = [
  { file = &quot;mdurl-0.1.2-py3-none-any.whl&quot;, hash = &quot;sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8&quot; },
  { file = &quot;mdurl-0.1.2.tar.gz&quot;, hash = &quot;sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba&quot; },
]

[[package]]
name = &quot;pygments&quot;
version = &quot;2.17.2&quot;
requires_python = &quot;&amp;gt;=3.7&quot;
summary = &quot;Pygments is a syntax highlighting package written in Python.&quot;
groups = [ &quot;default&quot; ]
files = [
  { file = &quot;pygments-2.17.2-py3-none-any.whl&quot;, hash = &quot;sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c&quot; },
  { file = &quot;pygments-2.17.2.tar.gz&quot;, hash = &quot;sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367&quot; },
]

[[package]]
name = &quot;rich&quot;
version = &quot;13.7.1&quot;
requires_python = &quot;&amp;gt;=3.7.0&quot;
summary = &quot;Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal&quot;
groups = [ &quot;default&quot; ]
dependencies = [
  &quot;markdown-it-py&amp;gt;=2.2.0&quot;,
  &quot;pygments&amp;lt;3.0.0,&amp;gt;=2.13.0&quot;,
  &quot;typing-extensions&amp;lt;5.0,&amp;gt;=4.0.0; python_version &amp;lt; \&quot;3.9\&quot;&quot;,
]
files = [
  { file = &quot;rich-13.7.1-py3-none-any.whl&quot;, hash = &quot;sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222&quot; },
  { file = &quot;rich-13.7.1.tar.gz&quot;, hash = &quot;sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432&quot; },
]

[[package]]
name = &quot;typing-extensions&quot;
version = &quot;4.10.0&quot;
requires_python = &quot;&amp;gt;=3.8&quot;
summary = &quot;Backported and Experimental Type Hints for Python 3.8+&quot;
groups = [ &quot;default&quot; ]
marker = &quot;python_version &amp;lt; \&quot;3.9\&quot;&quot;
files = [
  { file = &quot;typing_extensions-4.10.0-py3-none-any.whl&quot;, hash = &quot;sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475&quot; },
  { file = &quot;typing_extensions-4.10.0.tar.gz&quot;, hash = &quot;sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb&quot; },
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;rich 依赖的 &lt;code&gt;typing-extensions&lt;/code&gt; 上附带的条件传递到了 &lt;code&gt;typing-extensions&lt;/code&gt; 包上面。PDM 的实现是利用了我写的另一个库 &lt;a href=&quot;https://github.com/pdm-project/dep-logic&quot;&gt;dep-logic&lt;/a&gt;，它提供了对 markers 的逻辑运算能力。&lt;/p&gt;
&lt;p&gt;[^3]: 全称 Environment Markers，&lt;em&gt;似乎&lt;/em&gt;应译作「环境标记」，但同上述原因，中文名称极少使用，所以这里还是用英文。&lt;/p&gt;
&lt;h3&gt;元数据&lt;/h3&gt;
&lt;p&gt;一个包文件的依赖列表，支持 Python 版本（&lt;code&gt;requires-python&lt;/code&gt;)，都属于这个包的元数据（metadata）。PDM 获取包的元数据有两种方式，一种是把文件下载下来，然后读取它的 &lt;code&gt;METADATA&lt;/code&gt;/&lt;code&gt;PKG_INFO&lt;/code&gt; 文件内容，另一钟是利用 &lt;a href=&quot;https://peps.python.org/pep-0658/&quot;&gt;PEP 658&lt;/a&gt; 中规范的 metadata 链接，单独请求元数据的内容。
但由于 PDM 的 Lockfile 是跨版本的，所以需要解析、记录的包文件一瞬间就多了很多，比如 &lt;code&gt;charset-normalizer&lt;/code&gt; &lt;a href=&quot;https://pypi.org/project/charset-normalizer/3.3.2/#files&quot;&gt;一个版本&lt;/a&gt;
就包含了 90 个文件！而且不是所有包仓库（Package Index）都支持了 PEP 658，遍历这么多文件是不现实的。所以 PDM 作了一些妥协，引入了一个假设：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;同一个版本的不同文件的元数据是一样的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这不是很正确，没有任何一个规范规定如此。实际上你可以给一个包的不同文件指定完全不同的依赖，对于 sdist，元数据需要运行构建才能得到，其结果是完全不可预料的，甚至有可能这一秒得到的元数据和下一秒的不一样。但这个假设在大多数情况下是成立的，所以 PDM 选择了这个假设，以换取性能上的提升。其实并不是只有 PDM，Poetry 和 uv 也是这么做的。所以在 PDM 的 Lockfile 中，元数据是以包的版本为单位记录的，而且目前 PDM 对于每个包，只能锁定一个版本。换句话说，PDM 锁定的是版本，而不是某一个特定的文件，安装时再通过这个指定的版本，选择正确的文件下载并安装。这又需要引入另一条不那么正确的假设：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;每个版本都是完整的，包含当前版本支持的所有平台对应的包文件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这是一种折中，有时不得不为了性能牺牲一些正确性，对于那么没有覆盖到的 Corner case，PDM 大概率会解析失败。&lt;/p&gt;
&lt;p&gt;PDM 的 Lockfile 还有一个重要的特性是支持多种不同的 Lock 策略，留待下一篇文章再介绍了，感谢阅读。&lt;/p&gt;
</content:encoded></item><item><title>PDM Internals(1)</title><link>https://frostming.com/posts/en/2024/pdm-lockfile/</link><guid isPermaLink="false">https://frostming.com/en/2024/pdm-lockfile/</guid><description>Lock file</description><pubDate>Mon, 11 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;In order to answer some frequently asked questions and facilitate future contributors,
I plan to write a series of articles on the internals of PDM starting from this article.
This article will introduce the lock file of PDM, based on the latest version 2.12.
This English version was translated with assistance from LLM.
You can read the &lt;a href=&quot;/2024/pdm-lockfile&quot;&gt;original post&lt;/a&gt; in Chinese.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;What is a lock file?&lt;/h2&gt;
&lt;p&gt;Lock file is a file used to store pinned package versions, which records the package&apos;s dependencies and their versions. This file is common in package managers, such as &lt;code&gt;yarn.lock&lt;/code&gt;, &lt;code&gt;Cargo.lock&lt;/code&gt;, &lt;code&gt;go.sum&lt;/code&gt;, etc. In the Python ecosystem, Pipenv and Poetry also have their own lock files. PDM is also a package manager with lock files, which distinguishes it from other package managers without lock files, such as pip. There will be some behavioral changes with lock file. Many people ask in PDM&apos;s issue tracker, why pip can install but PDM cannot. Hopefully this article can answer this question.&lt;/p&gt;
&lt;p&gt;Having said that, it seems we need to define what the lock file does first. This doesn&apos;t seem to be an easy task, at least in PDM a lock file is used to &lt;strong&gt;constrain&lt;/strong&gt; the version of all packages that &lt;em&gt;might&lt;/em&gt; be installed during the installation process, as well as its source, checksum, etc., with the goal of providing reproducible Python environment. You can generate a lock file by running &lt;code&gt;pdm lock&lt;/code&gt;. PDM will also ensure that the lock file exists and is valid when you run &lt;code&gt;pdm install&lt;/code&gt;, and generate it if necessary.
Recently, a new round of &lt;a href=&quot;https://discuss.python.org/t/lock-files-again-but-this-time-w-sdists/46593&quot;&gt;lock file proposal discussion&lt;/a&gt; is underway. The discussion is quite lengthy. If you have the time, you can take a look at different understanding and expectations of lock files.&lt;/p&gt;
&lt;h2&gt;How to generate a lock file?&lt;/h2&gt;
&lt;h3&gt;Cross-version lock and lock for current environment&lt;/h3&gt;
&lt;p&gt;Initially, Python&apos;s package managers did not come with a lock file, but dependency resolution was still a necessary process. So what happens when pip installs a package &lt;code&gt;foo&lt;/code&gt;?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Access &lt;code&gt;https://pypi.org/simple/foo/&lt;/code&gt; to get all versions of &lt;code&gt;foo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Starting from the latest version that meets the requirements, check each package file one by one to see if it satisfies the &lt;strong&gt;current environment and Python version&lt;/strong&gt;. If it does, select this version for the next step.&lt;/li&gt;
&lt;li&gt;Get the dependency list from this file. Dependencies may also have environment requirements, so each dependency needs to be checked individually to see if it meets the &lt;strong&gt;current environment and Python version&lt;/strong&gt;. If it does, record this dependency.&lt;/li&gt;
&lt;li&gt;For dependencies that have been recorded but version pins not resolved yet, repeat step 1.&lt;/li&gt;
&lt;li&gt;If a matching version is not found, go back to step 2 and select the next file that meets the requirements.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can see that I&apos;ve bolded &lt;strong&gt;current environment and Python version&lt;/strong&gt;, and yes, the resolver takes the current environment and Python version into account when checking whether the condition is met. This is a lock that is only for the current environment, and at the time of writing, this is the way dependency resolution in most package managers works, with the exception of Poetry and PDM.&lt;/p&gt;
&lt;p&gt;For package managers that don&apos;t generate lock files, each installation of a dependency is resolved on-the-fly, taking into account the current environment and nothing else. But if a package manager generates a lock file, since its purpose is to reproduce the environment, it is possible that the installation will be performed on a different Python version or operating system. Then there is a need for a cross-version lock file, you could generate one for each target environment, but PDM chooses to record all package versions, and its environment information, in one lock file.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;requires-python&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;requires-python&lt;/code&gt; is a metadata field defined in &lt;a href=&quot;https://peps.python.org/pep-0621/#requires-python&quot;&gt;PEP 621&lt;/a&gt; and written in the &lt;code&gt;[project]&lt;/code&gt; table, but in fact a similar concept was introduced much earlier, &lt;code&gt; setuptools.setup()&lt;/code&gt; has the &lt;code&gt;python_requires&lt;/code&gt; parameter, which serves the same purpose of restricting which Python environments the package can be installed on. After the devastating update to Python 3, all packages or Python projects should theoretically have this field, indicating the range of Python versions it supports, at lease the mininum one.&lt;/p&gt;
&lt;p&gt;This field plays a critical role in PDM&apos;s dependency resolution. To illustrate its mechanism, let&apos;s look at an example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[project]
name = &quot;foo&quot;
requires-python = &quot;&amp;gt;=3.8&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now run &lt;code&gt;pdm add numpy&lt;/code&gt;. You will see the following output:&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Output&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Adding packages to default dependencies: numpy
${SITE_PACKAGES}/pdm/resolver/providers.py:196: PackageWarning: Skipping numpy@1.26.4 because it requires Python&amp;gt;=3.9 but the
project claims to work with Python&amp;gt;=3.8. Instead, another version of numpy that supports Python&amp;gt;=3.8 will be used.
If you want to install numpy@1.26.4, narrow down the `requires-python` range to include this version. For example, &quot;&amp;gt;=3.9&quot; should work.
  return self.repository.find_candidates(
${SITE_PACKAGES}/pdm/resolver/providers.py:196: PackageWarning: Skipping numpy@1.26.3 because it requires Python&amp;gt;=3.9 but the
project claims to work with Python&amp;gt;=3.8. Instead, another version of numpy that supports Python&amp;gt;=3.8 will be used.
If you want to install numpy@1.26.3, narrow down the `requires-python` range to include this version. For example, &quot;&amp;gt;=3.9&quot; should work.
  return self.repository.find_candidates(
${SITE_PACKAGES}/pdm/resolver/providers.py:196: PackageWarning: Skipping numpy@1.26.2 because it requires Python&amp;gt;=3.9 but the
project claims to work with Python&amp;gt;=3.8. Instead, another version of numpy that supports Python&amp;gt;=3.8 will be used.
If you want to install numpy@1.26.2, narrow down the `requires-python` range to include this version. For example, &quot;&amp;gt;=3.9&quot; should work.
  return self.repository.find_candidates(
${SITE_PACKAGES}/pdm/resolver/providers.py:196: PackageWarning: Skipping numpy@1.26.1 because it requires Python&amp;lt;3.13,&amp;gt;=3.9
but the project claims to work with Python&amp;gt;=3.8. Instead, another version of numpy that supports Python&amp;gt;=3.8 will be used.
If you want to install numpy@1.26.1, narrow down the `requires-python` range to include this version. For example, &quot;&amp;lt;3.13,&amp;gt;=3.9&quot; should work.
  return self.repository.find_candidates(
${SITE_PACKAGES}/pdm/resolver/providers.py:196: PackageWarning: Skipping numpy@1.26.0 because it requires Python&amp;lt;3.13,&amp;gt;=3.9
but the project claims to work with Python&amp;gt;=3.8. Instead, another version of numpy that supports Python&amp;gt;=3.8 will be used.
If you want to install numpy@1.26.0, narrow down the `requires-python` range to include this version. For example, &quot;&amp;lt;3.13,&amp;gt;=3.9&quot; should work.
  return self.repository.find_candidates(
${SITE_PACKAGES}/pdm/resolver/providers.py:196: PackageWarning: Skipping numpy@1.25.2 because it requires Python&amp;gt;=3.9 but the
project claims to work with Python&amp;gt;=3.8. Instead, another version of numpy that supports Python&amp;gt;=3.8 will be used.
If you want to install numpy@1.25.2, narrow down the `requires-python` range to include this version. For example, &quot;&amp;gt;=3.9&quot; should work.
  return self.repository.find_candidates(
${SITE_PACKAGES}/pdm/resolver/providers.py:196: PackageWarning: Skipping numpy@1.25.1 because it requires Python&amp;gt;=3.9 but the
project claims to work with Python&amp;gt;=3.8. Instead, another version of numpy that supports Python&amp;gt;=3.8 will be used.
If you want to install numpy@1.25.1, narrow down the `requires-python` range to include this version. For example, &quot;&amp;gt;=3.9&quot; should work.
  return self.repository.find_candidates(
${SITE_PACKAGES}/pdm/resolver/providers.py:196: PackageWarning: Skipping numpy@1.25.0 because it requires Python&amp;gt;=3.9 but the
project claims to work with Python&amp;gt;=3.8. Instead, another version of numpy that supports Python&amp;gt;=3.8 will be used.
If you want to install numpy@1.25.0, narrow down the `requires-python` range to include this version. For example, &quot;&amp;gt;=3.9&quot; should work.
  return self.repository.find_candidates(
INFO: Use `-q/--quiet` to suppress these warnings, or ignore them per-package with `ignore_package_warnings` config in [tool.pdm] table.
🔒 Lock successful
Changes are written to pyproject.toml.
Synchronizing working set with resolved packages: 1 to add, 0 to update, 0 to remove

  ✔️ Install numpy 1.24.4 success
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;As you can see it rejects a bunch of newer versions of numpy[^1] because they support Python versions as low as 3.9, and your project specifies a minimum Python version of 3.8. So if you try to install a package that has a dependency on &lt;code&gt;numpy&amp;gt;=1.26&lt;/code&gt;, PDM will fail to solve it and reports an error. A lot of users have asked this question, why is PDM still rejecting these new versions of packages when I&apos;m using Python version 3.10?&lt;/p&gt;
&lt;p&gt;The answer is that PDM always tries to make the lock file work on all the versions of Python you specify, &lt;strong&gt;it doesn&apos;t take into account which version of Python you are currently using&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The reason for this is that if you have &lt;code&gt;requires-python = &quot;&amp;gt;=3.8&quot;&lt;/code&gt; specified in your project and shared it, then you are giving full permission for a user to install the package using Python 3.8. At that point all dependencies must support Python 3.8, and &lt;code&gt;numpy@1.26&lt;/code&gt; clearly does not satisfy that. So in practice, &lt;strong&gt;the range specified by &lt;code&gt;requires-python&lt;/code&gt; in your project must be a subset of the &lt;code&gt;requires-python&lt;/code&gt; scopes of all dependent packages&lt;/strong&gt;. PDM will compute the appropriate value for you and show it in the warning message, as in the example above.&lt;/p&gt;
&lt;p&gt;[^1]: You might think that there are too many warnings, but I am worried that without this information, users will be more confused and not know why the result is like this. Fortunately, these warnings can be suppressed by adding &lt;code&gt;--quiet&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Markers&lt;/h3&gt;
&lt;p&gt;Another concept related to environmental restrictions is environment markers, which comes from the &lt;a href=&quot;https://peps.python.org/pep-0508/&quot;&gt;PEP 508&lt;/a&gt; specification. It is a conditional expression used to specify the installation conditions of packages. For example, &lt;code&gt;foo&amp;gt;=1.0; sys_platform == &quot;win32&quot;&lt;/code&gt; means that &lt;code&gt;foo&lt;/code&gt; will only be installed on the Windows platform. In the lock for current environment approach, if the resolver encounters such an expression and finds that the current environment does not meet this condition, then this package will be rejected; otherwise, &lt;code&gt;foo&amp;gt;=1.0&lt;/code&gt; will be accepted. While in cross-version lock approach, the expression cannot be evaluated at lock time, so this expression will be recorded as a whole &lt;code&gt;foo&amp;gt;=1.0; sys_platform == &quot;win32&quot;&lt;/code&gt;, and continue to resolve the dependencies of &lt;code&gt;foo&lt;/code&gt;. When installing the package, this expression will be evaluated to determine whether to install the package.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/pdm-lock-graph.png&quot; alt=&quot;pdm-lock-graph&quot; /&gt;&lt;/p&gt;
&lt;p&gt;However, it should be noted that dependencies of the constrained packages should also apply the same markers. That is, if &lt;code&gt;foo&lt;/code&gt; depends on &lt;code&gt;bar&lt;/code&gt;, then &lt;code&gt;bar&lt;/code&gt; should only be installed on Windows, too. This is known as the propagation of markers. In case &lt;code&gt;bar&lt;/code&gt; itself has a different environment marker, the two markers should be combined with logical AND. On the other hand, the same dependency may come from different parent packages, so the markers they &quot;inherit&quot; from their parents should be combined with logical OR.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/marker-propagation.png&quot; alt=&quot;marker-propagation&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;pdm.lock&lt;/code&gt;, the final calculation results of the markers will be recorded in the &lt;code&gt;markers&lt;/code&gt; field of each package. In this way, during installation, you only need to read this field to determine whether this package needs to be installed, without needing to traverse the dependency tree to find the information from the ancestors.&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;For example, this is the resolution result of &lt;code&gt;rich&lt;/code&gt;.&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# This file is @generated by PDM.
# It is not intended for manual editing.

[metadata]
groups = [ &quot;default&quot; ]
strategy = [
  &quot;cross_platform&quot;,
  &quot;inherit_metadata&quot;
]
lock_version = &quot;4.4.1&quot;
content_hash = &quot;sha256:37d2aae470ae5f416baf9366fdba3a83f22de379a8ab288ec6077f4ce3b0ec59&quot;

[[package]]
name = &quot;markdown-it-py&quot;
version = &quot;3.0.0&quot;
requires_python = &quot;&amp;gt;=3.8&quot;
summary = &quot;Python port of markdown-it. Markdown parsing, done right!&quot;
groups = [ &quot;default&quot; ]
dependencies = [ &quot;mdurl~=0.1&quot;, ]
files = [
  { file = &quot;markdown-it-py-3.0.0.tar.gz&quot;, hash = &quot;sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb&quot; },
  { file = &quot;markdown_it_py-3.0.0-py3-none-any.whl&quot;, hash = &quot;sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1&quot; },
]

[[package]]
name = &quot;mdurl&quot;
version = &quot;0.1.2&quot;
requires_python = &quot;&amp;gt;=3.7&quot;
summary = &quot;Markdown URL utilities&quot;
groups = [ &quot;default&quot; ]
files = [
  { file = &quot;mdurl-0.1.2-py3-none-any.whl&quot;, hash = &quot;sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8&quot; },
  { file = &quot;mdurl-0.1.2.tar.gz&quot;, hash = &quot;sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba&quot; },
]

[[package]]
name = &quot;pygments&quot;
version = &quot;2.17.2&quot;
requires_python = &quot;&amp;gt;=3.7&quot;
summary = &quot;Pygments is a syntax highlighting package written in Python.&quot;
groups = [ &quot;default&quot; ]
files = [
  { file = &quot;pygments-2.17.2-py3-none-any.whl&quot;, hash = &quot;sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c&quot; },
  { file = &quot;pygments-2.17.2.tar.gz&quot;, hash = &quot;sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367&quot; },
]

[[package]]
name = &quot;rich&quot;
version = &quot;13.7.1&quot;
requires_python = &quot;&amp;gt;=3.7.0&quot;
summary = &quot;Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal&quot;
groups = [ &quot;default&quot; ]
dependencies = [
  &quot;markdown-it-py&amp;gt;=2.2.0&quot;,
  &quot;pygments&amp;lt;3.0.0,&amp;gt;=2.13.0&quot;,
  &quot;typing-extensions&amp;lt;5.0,&amp;gt;=4.0.0; python_version &amp;lt; \&quot;3.9\&quot;&quot;,
]
files = [
  { file = &quot;rich-13.7.1-py3-none-any.whl&quot;, hash = &quot;sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222&quot; },
  { file = &quot;rich-13.7.1.tar.gz&quot;, hash = &quot;sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432&quot; },
]

[[package]]
name = &quot;typing-extensions&quot;
version = &quot;4.10.0&quot;
requires_python = &quot;&amp;gt;=3.8&quot;
summary = &quot;Backported and Experimental Type Hints for Python 3.8+&quot;
groups = [ &quot;default&quot; ]
marker = &quot;python_version &amp;lt; \&quot;3.9\&quot;&quot;
files = [
  { file = &quot;typing_extensions-4.10.0-py3-none-any.whl&quot;, hash = &quot;sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475&quot; },
  { file = &quot;typing_extensions-4.10.0.tar.gz&quot;, hash = &quot;sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb&quot; },
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;Note how the environment markers associated to &lt;code&gt;typing-extensions&lt;/code&gt; dependency are propagated to the &lt;code&gt;typing-extensions&lt;/code&gt; package. The implementation of PDM utilizes another library I wrote, &lt;a href=&quot;https://github.com/pdm-project/dep-logic&quot;&gt;dep-logic&lt;/a&gt;, which provides logical operation capabilities for markers.&lt;/p&gt;
&lt;h3&gt;Metadata&lt;/h3&gt;
&lt;p&gt;A dependency list of a package file, supporting Python version (&lt;code&gt;requires-python&lt;/code&gt;), all belong to the metadata of this package. PDM has two ways to obtain the metadata of a package. One is to download the file and then read its &lt;code&gt;METADATA&lt;/code&gt;/&lt;code&gt;PKG_INFO&lt;/code&gt; file content, and the other is to use the metadata link standardized in &lt;a href=&quot;https://peps.python.org/pep-0658/&quot;&gt;PEP 658&lt;/a&gt; to request the content of metadata separately.&lt;/p&gt;
&lt;p&gt;However, because PDM&apos;s lock file is cross-versioned, there are many more package files that need to be resolved and recorded. For example, &lt;a href=&quot;https://pypi.org/project/charset-normalizer/3.3.2/#files&quot;&gt;a single version of &lt;code&gt;charset-normalizer&lt;/code&gt;&lt;/a&gt; contains 90 files! And not all package indexes support PEP 658, it is unrealistic to traverse so many files. So PDM made a trade-off and introduced an assumption:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The metadata of different files for the same version are the same.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is not always the case, as there is no standard forcing this. In fact, you can specify completely different dependencies for different files in a package. For sdist, metadata even needs to be generated by running the build process, and the result can be completely unpredictable. It&apos;s even possible that the metadata obtained at one moment may differ from the next moment. However, this assumption holds true in most cases, so PDM has chosen to make this assumption in exchange for performance improvements. Not only PDM but also Poetry and uv operate in a similar way. Therefore, in PDM&apos;s lock files, metadata are recorded based on package versions; currently, PDM can only lock one version for each package. In other words, PDM locks versions rather than specific files. And during installation, it selects the correct file to download and install based on the specified version. This introduces another somewhat inaccurate assumption:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Each version is complete and contains package files corresponding to all platforms supported by the current version.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is a trade-off, sometimes you have to sacrifice some correctness for performance. For those corner cases that are not covered, PDM will likely fail to resolve.&lt;/p&gt;
&lt;p&gt;Another important feature of PDM&apos;s lock file is it supports various lock strategies. This is to be introduced in the next article. Thanks for reading.&lt;/p&gt;
</content:encoded></item><item><title>通过辨析去学习</title><link>https://frostming.com/posts/2024/learn-by-analysis/</link><guid isPermaLink="false">https://frostming.com/2024/learn-by-analysis/</guid><pubDate>Thu, 18 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;辨析是一种学习技巧，我经常使用这种方式。找出一个概念中的两个类似的事物，然后比较它们的不同之处。人们设置类似事物并不是多余的，通过辨析可以找到它们背后的逻辑。&lt;/p&gt;
&lt;p&gt;我上小学五年级的时候家里买了一本书，叫&lt;a href=&quot;https://books.google.com.hk/books/about/%E6%B1%89%E8%AF%AD%E8%81%94%E6%83%B3%E5%AD%97%E5%85%B8.html?id=WLnqAQAACAAJ&amp;amp;redir_esc=y&quot;&gt;《汉语联想词典》&lt;/a&gt;，
这本书甚至连豆瓣词条都没有。它大概是一本通俗版的《说文解字》，里面按部首索引汉字，通过汉字的字源字形讲述它们的本义引申义。这让只接触过《新华字典》的我产生了浓厚的兴趣。阅读过程中，汉字的六种造字法从一些模糊的印象变得清晰起来。
我对里面的同源近义字尤其感兴趣，它虽名为「词典」，我却把它当成了书来阅读，学会了一堆近义辨析：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;「木」分两半，左为「爿」，右为「片」&lt;/li&gt;
&lt;li&gt;窗在天为「窗」，在壁为「牖」&lt;/li&gt;
&lt;li&gt;双扇为「門」，单扇为「户」&lt;/li&gt;
&lt;li&gt;长尾曰「鸟」，短尾曰「隹」&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我觉得这太好玩了，以前从来不知道，也没想过。于是看完这本词典以后，语文里的汉字题对我来说就再也没难度了。也发现了知识不是独立的，两个事物对照着学，比单学一个，掌握得更深。&lt;/p&gt;
&lt;p&gt;举个例子，《说文》曰：「『兵』，械也。从廾持斤，并力之皃（貌）。」嗯，两只手（廾）拿着一把斧头（斤），这就是兵，很直接明了是吧。然而我又看到了「戒」字，《说文》里也是一句话：「警也。从廾持戈，以戒不虞。居拜切。」
此时一个问号就从我脑中冒出来了：双手持斤为兵，双手持戈为戒，为什么造字方法类似，造出字意义却不一样呢？或者说，为了表达这两种意思，为什么一个选了「斤」，一个选了「戈」作义符呢？
我想知道是不是有什么讲究。&lt;/p&gt;
&lt;p&gt;《说文解字》里不会有答案，于是我就上知乎&lt;a href=&quot;https://www.zhihu.com/question/23522162/answer/24835620&quot;&gt;提了这个问题，并得到了大佬的解答&lt;/a&gt;， 不仅长了古文字的知识，也长了兵器的知识。在此以我的感激和敬意，给当年的那个知乎。&lt;/p&gt;
&lt;p&gt;除了汉字，别的领域里，辨析的例子比比皆是。比如一个简单的横杠字符，在 Unicode 里就有 hyphen(-), en-dash(–), em-dash(—)好几种[^1]，它们各自使用的场景不同，在讲究的场合里不能乱用。&lt;/p&gt;
&lt;p&gt;在 Python 的对象模型里，关于获取属性的魔法方法有 &lt;code&gt;__getattr__&lt;/code&gt; 和 &lt;code&gt;__getattribute__&lt;/code&gt; 两个，它们的区别是什么，什么场景该用哪一个？&lt;/p&gt;
&lt;p&gt;《街霸》里的隆和肯为啥都会波动拳和升龙拳？他们俩使用起来又有什么细微的差别？[^2]&lt;/p&gt;
&lt;p&gt;「橘生淮南则为橘，生于淮北则为枳」，除了橘和枳，还有橙、柚、柠檬等一大堆，他们有什么关系？于是可以了解到芸香科和那乱七八糟的神奇谱系。&lt;/p&gt;
&lt;p&gt;这鸟明明长得差不多，一会叫雕，一会叫鹰，一会叫隼，一会叫鸢，为什么要区分名字？&lt;/p&gt;
&lt;p&gt;更不要说近义词辨析是提高英语的方法之一了，preserve, conserve, reserve 有什么区别[^3]？&lt;/p&gt;
&lt;p&gt;好了不多说了，感觉此时我就像孔乙己，炫耀着「你知道回字有四种写法吗？」&lt;/p&gt;
&lt;p&gt;[^1]: &lt;a href=&quot;https://www.grammarly.com/blog/hyphens-and-dashes/&quot;&gt;Hyphen vs. Dash – – — What’s the Difference?&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;[^2]:
&lt;a href=&quot;https://www.giantbomb.com/forums/super-street-fighter-iv-4489/can-someone-please-explain-the-difference-between--512917/&quot;&gt;Can someone please explain the difference between Ryu and Ken?
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;[^3]: &lt;a href=&quot;https://learningenglish.voanews.com/a/reserve-preserve-and-conserve-/6956353.html&quot;&gt;Preserve, Conserve, Reserve&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>我的 2023</title><link>https://frostming.com/posts/2024/2023-review/</link><guid isPermaLink="false">https://frostming.com/2024/2023-review/</guid><pubDate>Tue, 02 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;好几年没写年终总结，因为其实自己这一年，或者说每一年，达成的成就都不多，令人兴奋的更少，所以我可能没办法写出什么取得了长足的进步，憧憬来年更好的文字。
但另一方面，读我的这篇总结应该不会产生什么焦虑的情绪。&lt;/p&gt;
&lt;h2&gt;工作与技术&lt;/h2&gt;
&lt;p&gt;2023 年年初我也体验了一把「广进计划」&lt;a href=&quot;%E8%B0%90%E9%9F%B3%E6%A2%97%E3%80%8C%E8%B4%A2%E6%BA%90%E5%B9%BF%E8%BF%9B%E3%80%8D%E3%80%82&quot;&gt;^1&lt;/a&gt;，拿了补偿，也体会到了在一个和利益相关的团队中，各方如何产生分歧，最终导致决裂的整个过程。我也属于在漩涡中心，亲身经历了一次政治斗争。
这段故事我从没在公共平台说过，现在释然了，也不想再提，有些小丑终究是小丑。&lt;/p&gt;
&lt;p&gt;休息了三个月之后我加了现在的公司 &lt;a href=&quot;https://bentoml.com&quot;&gt;BentoML&lt;/a&gt;，有了我真正意义上的开源的工作，也非常有幸能和多位大佬共事，学到了很多东西。七月份的时候，也去了一次北京和大家线下见面，气氛非常好。
和别人介绍自己的工作时，别人先是羡慕我是远程工作，然后又揶揄我是深圳的负责人,权当说笑。沾公司的光，我去 DataFun &lt;a href=&quot;https://mp.weixin.qq.com/s/P6IxlcKFoyiEYUOlzKCFxA&quot;&gt;做了一次分享&lt;/a&gt;。
同样的主题，又在 &lt;a href=&quot;https://cn.pycon.org/2023/shenzhen/&quot;&gt;PyCon China 深圳站讲了一次&lt;/a&gt;，属于是相当鸡贼了，在此再次感谢 BentoML。&lt;/p&gt;
&lt;p&gt;是的，今年继续参与了 PyCon China，after party 的时候还发现另一个参与三年的讲师是我同乡同高中同届，这他妈也太巧了。&lt;/p&gt;
&lt;p&gt;2023 年只写了四篇博客，其中还有两篇是水的，所以我写这篇总结，当作补偿。&lt;/p&gt;
&lt;p&gt;这一年 PDM 也在稳定更新着，从 2.4.0 更新到了 2.11.1。上半年入门了一下 Rust，试图用 Rust 重写 PDM 的某些核心模块，后来也搁置了，我的 Rust 水平还停留在疯狂 &lt;code&gt;.clone()&lt;/code&gt; 的阶段。&lt;/p&gt;
&lt;h2&gt;生活&lt;/h2&gt;
&lt;p&gt;这一年我仿佛加重了精神内耗，对现状有诸多不满，主观的客观的都有，却都不知道如何改变。回顾整年，几乎想不起有哪些瞬间是让我感觉&lt;strong&gt;兴奋&lt;/strong&gt;的，如果有，在莲花山公园的风筝广场，把手里 800 米的线都放完，
路过的人都在问我风筝在哪的时候，可能算一个吧。&lt;/p&gt;
&lt;p&gt;困境的来源主要来自于家庭，女儿的自我意识越来越强了，逐渐刁蛮，一方面我真想让她离开我视线远一点让我清净一下，另一方面又舍不得她怕她觉得爸爸不爱她。可能没娃的人不太能理解这种矛盾的心情吧。
家庭生活中的大部分矛盾都类似这样，它既没有坏到可以强硬切割，也没有好到可以毫无怨言，于是就卡在当间，梗在喉咙，日复一日。这可能是中年危机的一种具体表现。&lt;/p&gt;
&lt;p&gt;今年转为远程工作，也意味着我失去了一个线下社交的圈子，这对于我这样一个 I 人来说，是一个很大的问题，我很难去主动构建一个新的圈子。所以我在工作、家庭之外，几乎没有其他生活了。&lt;/p&gt;
&lt;p&gt;今年开始学网球，和老婆一起，但我向来运动低能，在找击球点这件事上要比常人更难。而且我也一直做不到享受运动，倒不是体力不济，主要是要被教练监督，他虽然春风满面从不骂人，但菜是一种原罪，我自己会不好意思。
我总结，I 人可能更适合无监督学习。&lt;/p&gt;
&lt;p&gt;今年的出行不多，除了前面提到的去了北京，年初在广东省自驾游了一圈, 八月同家人去杭州上海转了一圈，国庆去了一次新加坡。2024 年想多去一些地方。&lt;/p&gt;
&lt;p&gt;对了，今年还去看了一次脱口秀演出，一次五月天演唱会。&lt;/p&gt;
&lt;h2&gt;阅读与观影&lt;/h2&gt;
&lt;p&gt;我是从 2022 年年中，才开始规律的阅读的，读书时代长辈劝我多读书读名著，我根本没听进去过.除了《三国演义》和金庸武侠这种自己感兴趣的,其他都是白板。
培养了两年习惯，我发现我的主要兴趣是文学类，间杂一些历史和政治类的书籍。至于心理和哲学类的，包括《XX心理学》《好好XX》《做一个XX》这种，我发现我完全读不进去。
得益于图书管系统的遍地开花，今年我也开始更多地阅读纸质书籍。&lt;/p&gt;
&lt;p&gt;今年读过的所有书里面，没有遇到像去年（2022年）那样令我大受震撼的作品。&lt;a href=&quot;https://book.douban.com/subject/36084340/&quot;&gt;《命运》&lt;/a&gt;
可以说不错，&lt;a href=&quot;https://book.douban.com/subject/26864984/&quot;&gt;《金色梦乡》&lt;/a&gt;在故事上更胜一筹。年底读的&lt;a href=&quot;https://book.douban.com/subject/35216559/&quot;&gt;《仙症》&lt;/a&gt;也给了我很多惊喜。&lt;/p&gt;
&lt;p&gt;名家名著也读了陀翁的&lt;a href=&quot;https://book.douban.com/subject/35406615/&quot;&gt;《罪与罚》&lt;/a&gt;，塞林格的&lt;a href=&quot;https://book.douban.com/subject/35574980/&quot;&gt;《九故事》&lt;/a&gt;，都没太读进去。我也不强求自己，这得看缘分，比如我所说的「令我大受震撼的作品」，就有《百年孤独》。
说到去年读过的好书，我还想顺便提一下另一部我非常推崇的作品&lt;a href=&quot;https://book.douban.com/subject/3633461//&quot;&gt;《一句顶一万句》&lt;/a&gt;，它里面有种鬼畜的感觉和《百年孤独》很像，而且刘震云把生活中的各种曲折无奈都写得太透了。&lt;/p&gt;
&lt;p&gt;电影方面，没有印象特别深刻的，比较喜欢的有&lt;a href=&quot;https://www.imdb.com/title/tt0439572/&quot;&gt;《闪电侠》&lt;/a&gt;&lt;a href=&quot;https://movie.douban.com/subject/35593344/&quot;&gt;《奥本海默》&lt;/a&gt;和&lt;a href=&quot;https://www.themoviedb.org/movie/768362&quot;&gt;《网络迷踪2》&lt;/a&gt;。刚刚看的&lt;a href=&quot;https://movie.douban.com/subject/35725869/&quot;&gt;《年会不能停！》&lt;/a&gt;也高出预期了，算不错的喜剧电影。
关于电影有个趣事，回想 2014 年到 2015 年那会，各大影院疯狂卷，那时几乎每周一部电影起步，昨天翻以前的记录，发现看了一部&lt;a href=&quot;https://movie.douban.com/subject/21340202/&quot;&gt;《抢劫坚果店》&lt;/a&gt;，但我对这电影毫无印象，甚至看了海报、短片，也死活想不起来。
我怀疑记忆断层了。这也说明那时候看电影的频率太高了，跟现在完全不一样。&lt;/p&gt;
&lt;p&gt;剧集的话&lt;a href=&quot;https://movie.douban.com/subject/35314632/&quot;&gt;《黑暗荣耀》&lt;/a&gt;&lt;a href=&quot;https://movie.douban.com/subject/35588177/&quot;&gt;《漫长的季节》&lt;/a&gt;很喜欢，也就跟着大热看一会，其他的抽不出大段时间来看，追不动。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://frostming.notion.site/2b783ddb021f4ad09fc0b4b7b7212a68?v=d5ad8f5e47c0405a937a60f70582fad8&quot;&gt;👉 完整的书影清单&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;2024&lt;/h2&gt;
&lt;p&gt;感谢所有帮助过我的人，感谢所有平台的关注者与读者，感谢伊洪！&lt;/p&gt;
&lt;p&gt;就这样吧，来年我更没有啥远大的目标，我只希望内心能更平静一些。&lt;/p&gt;
</content:encoded></item><item><title>两种风格的错误处理</title><link>https://frostming.com/posts/2023/error-handling/</link><guid isPermaLink="false">https://frostming.com/2023/error-handling/</guid><pubDate>Mon, 13 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;错误处理是编程语言中很重要的组成部分。一般来说，发生错误时，要立即中止程序正常逻辑的执行，转而执行错误处理逻辑，这个过程称为错误处理。
我用过的编程语言中，比较熟悉的两种错误处理方式，一种是异常抛出，一种是错误返回。它们各有优缺点，也有各自胜任的场景。&lt;/p&gt;
&lt;p&gt;先来看看它们各自是怎么处理错误的。&lt;/p&gt;
&lt;p&gt;以 Python 为例，抛出异常的方式是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def foo():
    # do something
    raise Exception(&quot;something wrong&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;处理异常的方式是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try:
    foo()
except Exception as e:
    # handle exception
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以 Go 为例，返回错误的方式是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func foo() (int, error) {
    // do something
    return 0, errors.New(&quot;something wrong&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;处理错误的方式是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;value, err := foo()
if err != nil {
    // handle error
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看上去它们完成的事情差不多，但如果我们去掉错误处理的代码，不管它，会变成这样：&lt;/p&gt;
&lt;p&gt;Python:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;foo()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Go:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;value, _ := foo()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两者造成的结果截然不同，Python 会&lt;strong&gt;上报异常&lt;/strong&gt;，Go 会&lt;strong&gt;忽略错误&lt;/strong&gt;。这代表了两种不同的哲学，前者若不处理错误即上报异常，让上层处理，而后者若不处理错误，则继续执行。&lt;/p&gt;
&lt;p&gt;似乎异常抛出的方式比较好，然而这种方式，应用在动态语言上，就出问题了，&lt;strong&gt;调用者不知道调用的这段代码会不会报错，报什么错&lt;/strong&gt;，这就导致程序永远会在无法预料的情况下崩溃。恰巧，现在两种主要的动态语言，Python 和 Javascript，都采用的这种方式。而一些开发者，为了保住 SLO 和 KPI，就会用 &lt;code&gt;try &amp;lt;一大坨&amp;gt; except:pass&lt;/code&gt; 的代码兜底。
底看似兜住了，其实早已千疮百孔。&lt;/p&gt;
&lt;p&gt;这不是抛出异常的错，这是动态语言的问题，Java 也是用第一种异常抛出的方式，但由于它有完善的异常标注和静态检查，异常也不会随意泄漏导致程序崩溃。&lt;/p&gt;
&lt;p&gt;相反，用 Go 语言的时候，你一看它返回了一个 &lt;code&gt;err&lt;/code&gt;，脑子就永远有根弦，要么必须写 &lt;code&gt;if err != nil&lt;/code&gt;，要么主动用 &lt;code&gt;_&lt;/code&gt; 忽略掉错误，采用任何一种方式，就算是再粗心的程序员，都清晰地知道自己在做什么，反而更有利于及时的处理错误。
写 Go 的时候感觉自己一直在 &lt;code&gt;if err != nil&lt;/code&gt; 正是因为每一个错误都被兜住了，不会漏掉。但尴尬的是，不是所有错误在本函数中都能处理，对于无法处理的错误，只能把错误返回给上层，而上层也不一定能处理，于是就一直 return。一个例子是用户交互程序，
你需要把一些关键错误信息显示在界面上，而这个错误的来源，可能是任意层级深度的，这时异常抛出的「直达天听」的优势就显现出来了。&lt;/p&gt;
&lt;p&gt;至于 Rust 的 &lt;code&gt;Result&amp;lt;T, E&amp;gt;&lt;/code&gt; 类型，本质上也是返回错误，它除了有一堆 &lt;code&gt;map, map_err, unwrap, unwrap_or_else&lt;/code&gt; 等方法方便人使用，还有 &lt;code&gt;?&lt;/code&gt; 运算符可以立即返回错误。比起 &lt;code&gt;if err != nil { return err }&lt;/code&gt; 来说，方便了太多。
但谁让 Golang 是大道至简，去掉这些糖，Rust 和 Go 的错误处理方式其实是一样的。&lt;/p&gt;
&lt;p&gt;总结，我认为异常抛出的方式，总体上是更省事的，你不知道怎么处理这个错误的时候就不处理，让上层去处理。而返回错误的方式，特别是在语言层面没有提供语法糖的时候，你就必须要处理错误。
但异常抛出的方式应用在动态语言上很容易造成错误的泄漏，这些语言可能反而会比较适合返回错误的方式。&lt;/p&gt;
</content:encoded></item><item><title>思想漫步</title><link>https://frostming.com/posts/2023/mind-wandering1/</link><guid isPermaLink="false">https://frostming.com/2023/mind-wandering1/</guid><pubDate>Tue, 15 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一&lt;/h2&gt;
&lt;p&gt;最近《乐夏》开播，我没看，但我偶尔能刷到一些片段。不知有没别的人和我有同样的发现，大部分摇滚主唱和民谣歌手的咬字
都有一些共同的特点，就是以北京腔为底的夸张化的卷舌，特别是在 zhi,chi,shi 这三个音上有很特殊的听感，就像是在故意大舌头。
可以听彭磊、老狼甚至大张伟的演唱感受下我说的是什么。这种咬字方法和港台流行乐有非常大的区别。这是不是所谓的「京圈」。&lt;/p&gt;
&lt;p&gt;有种感觉，就是「京圈」把持了这几类音乐的话语权，并隐然有排外的倾向。所以我希望有更多「九连真人」这类乐队的出现。&lt;/p&gt;
&lt;h2&gt;二&lt;/h2&gt;
&lt;p&gt;我回老家参加高中同学的婚礼，久违地进行了线下五连坐。震惊于 DOTA 的环境相比一年前的急转直下。一年前还能欢声笑语，现如今
只能跪求一胜。现在不是有没有开图，而是一局里有几人开图。现在这年头还在打 DOTA 的老人，要么是有吊打开图的实力，要么是和他们一起开图。
我们显然不属于前者，于是失望至极并封杀了这项活动。&lt;/p&gt;
&lt;p&gt;时光荏苒，大家大都结婚生子，难得死党齐聚，在和 15 年前相同的地点拍了一张合照。这 15 年里，私下单独见面是有的，但全部到齐还是头一次。
大家要么是忙于工作，要么是限于家庭。得到 15 这个数字的时候我都吓了一跳，大家分别的时候都约定下次再聚，但我明白这样的机会已绝无仅有，
人生能有几个 15 年？&lt;/p&gt;
&lt;h2&gt;三&lt;/h2&gt;
&lt;p&gt;我是从来不热衷于人际交往的，认为父母一辈所谓的「人脉」都是虚无飘渺并无实际用处（对我来说）。特别是当自己逐渐习惯于同温层的交流之后，
就认为这些陈年旧友都无话可谈，即便有同学朋友在同一座城市平时也疏于走动，见面间隔以年计算。但最近参加的一些聚会让我重新拾起了一些旧有的人际关系，分别有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;深圳黄田分局的公安（宝安机场）&lt;/li&gt;
&lt;li&gt;天津大学博导的女婿&lt;/li&gt;
&lt;li&gt;老家市里人民医院肿瘤科的医生&lt;/li&gt;
&lt;li&gt;教培机构的 Co-founder&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;他们非但没怪我平时不联系，反而很热情友好。可能是我变世俗了，我突然发现这些人际关系，可能真的会有用。其实参加一些线下聚会，也没什么不好。&lt;/p&gt;
&lt;h2&gt;四&lt;/h2&gt;
&lt;p&gt;上海之行泡汤，本来我还挺想去的，因为可以见见我七年未见的大学同学。太久没联系了，都不知道他结婚生子没有。我必须蹭着别的什么借口去，不能专程去见他，这太奇怪了。
现在应该是会去杭州，希望能成行，但去杭州只是去杭州，没法见同学了。&lt;/p&gt;
</content:encoded></item><item><title>关于写作与分享</title><link>https://frostming.com/posts/2023/sharing/</link><guid isPermaLink="false">https://frostming.com/2023/sharing/</guid><pubDate>Fri, 28 Jul 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;确实有挺久没有更新这个网站了，而且今年到现在为止只&lt;s&gt;写&lt;/s&gt;水了一篇博客。于是开始思考，是什么会趋使自己想要写一篇文章来分享呢？找到这个动机，或许能了解为啥自己不咋更新。
同时也更新一下以证明这网站没死。&lt;/p&gt;
&lt;p&gt;考虑文章的内容和取材，其一是技术文。这需要良好的输入，通透的理解，以及对知识的整理归纳，才能形成一篇有头有尾深入浅出的文章。我也不能说没有学新东西，只是实在懒得组织思考文章架构了。
平时有些心得体会，在 Twitter(X) 上锐评一下，140 字也足够了。有人说刚入门时分享欲是最旺盛的，现在要发个什么东西都会在无形中自我审查，会不会过于浅、过于钻。
比如我会非常喜欢一些细节的东西，可能别人读来难免有一种「回字有几种写法」的既视感，但我得承认，我还真好奇回字有几种写法。
我非常佩服长青的技术博客，比如&lt;a href=&quot;https://www.kawabangga.com/&quot;&gt;卡瓦邦噶&lt;/a&gt;，能学非常多的东西。&lt;/p&gt;
&lt;p&gt;其二是生活文，我个人是非常喜欢看这个题材的，无论是衣食住行，写成流水账也无所谓。而且特别容易在这种文章中流露清新自然的文笔。这里推荐一下文笔越发老到的&lt;a href=&quot;https://greyli.com/posts/&quot;&gt;李辉老师&lt;/a&gt;，期待他早日踏入文坛。
但我自己的话，因为平时没有在网上分享生活的习惯，写起来会有些突兀。不得不说是被某种「网络的人设」禁锢了。无论你承认与否，每个人在网络上都有自己的人设，要么是优秀的技术文章作者，
要么是喜欢分享生活的话痨，要么是戏谑的段子手，&lt;s&gt;要么是黄推指挥官&lt;/s&gt;。你一旦进入了这个人设，就会不自觉地继续维持这个形象。&lt;/p&gt;
&lt;p&gt;其三是心得和感悟，就像现在这篇文章一样，不描述具体的事情，只是从自己的一个念头出发，让它自行生长，发散思维，看它自己能变成什么样。这无疑是一种很好的思维训练，但这种最后要形成文章也是非常难的，
得有一定的控制，不能过度发散，需要在一个点聚拢收尾。一般人在入睡时刻念头是最旺盛的，无数思维枝枝蔓蔓野蛮生长，有的思维甚至开始在脑中自行成文。
如果有一种工具，能把这种文思立刻落成文字，我会非常需要，因为这些念头的最大问题是它只存在于那个漆黑寂静的深夜。等到第二天天亮，就算有些念头还记得的，自己会觉得非常幼稚、矫情——
我到底在发什么神经乱想这些，是万万不可能写出来的。李娟的&lt;a href=&quot;https://book.douban.com/subject/27090815/&quot;&gt;《记一忘三二》&lt;/a&gt;恰恰也写到作者同样的感受，她的办法是无论多晚，想到了就立刻披衣服起来写完，我感慨，难怪她是高产的作家。&lt;/p&gt;
&lt;p&gt;说到写文章是为了什么，或许是为了总结知识不致遗忘，或许是为了分享出去获得高阅读数的成就感，我不知道别人是怎样，我有一种特殊的满足，就是拥有自己的发布作品的满足感，无论发布媒介是印刷还是网站，我都会来回欣赏好几遍。
不是因为它有人读，而是因为它本身的存在。&lt;/p&gt;
&lt;p&gt;所以说了这么多，不发博客的原因找到了，还是自己想太多，思想负担太重了。写作就是写作，为啥要附带那么多旁的东西。话是这么说了，但我不一定改得了，哈哈。&lt;/p&gt;
</content:encoded></item><item><title>Python 打包的新动态</title><link>https://frostming.com/posts/2023/python-packaging-status/</link><guid isPermaLink="false">https://frostming.com/2023/python-packaging-status/</guid><pubDate>Thu, 09 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;今年以来，Python 打包社区依然很活跃，有对 &lt;a href=&quot;https://peps.python.org/pep-0582/&quot;&gt;PEP 582&lt;/a&gt; 的最新修改，也出现了一些新的提案，
本文将略作总结。&lt;/p&gt;
&lt;h2&gt;群众的呼声——统一的包管理器&lt;/h2&gt;
&lt;p&gt;在去年 9-10 月份的时候，PyPA 曾做了一次问卷调查，目的是了解用户对于 Python 打包现状的看法，以及对于未来的期望&lt;a href=&quot;%E7%BB%93%E6%9E%9C%E5%8F%AF%E4%BB%A5%E5%9C%A8%5B%E8%BF%99%E9%87%8C%5D(https://drive.google.com/file/d/1U5d5SiXLVkzDpS0i1dJIA4Hu5Qg704T9/view)%E6%9F%A5%E7%9C%8B%E3%80%82&quot;&gt;^1&lt;/a&gt;。在收集上来的问卷中，有以下用户的回复：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;回应 1:
Unify the multiple tools. It’s good to have new ideas and new implementations, but it has to converge after a while.
If my package has a compiled part, I’m stuck with setuptools, but all other tools are pushed forward while not covering
this feature.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;回应 2:
There should be one– and preferably only one –obvious way to do it. Get rid of the fragmentation&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;回应 3:
I definitely want to Python to introduce the One True packaging tool, ideally both easy as Rust’s cargo
(where building, adding dependencies, running tests, code checking and deploying are all subcommands) and extensible
(support for different build regimes, extensions in foreign languages etc). Package installing is easy, package building is a wild west at the moment, and no one tool is good enough for all use cases.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其中 &lt;code&gt;Cargo&lt;/code&gt; 被高频地提及，很多来自其他语言的用户对 Python 多个打包工具并存的现状感到困惑，希望 Python 社区能够统一打包工具，以便于用户更好地使用[^2]。
但我要为 Python 说句公道话，造成这种现状的原因并不是官方不给力，而是基于多种原因[^3][^4]：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Python 社区本身就是分裂的，有很多不同的开发者群体，每个群体都有自己的需求和工作流，这些需求往往是相互矛盾的，因此很难找到一个统一的方案来满足所有人的需求。这也是 Python 在近 10 年大流行之下，带来的结果。比如现在流行的
包管理工具 Pipenv, Poetry 和 PDM，基本上都是给 Web 开发者设计的，却很少考虑到 C 扩展包、MLer 的需求（只能说尽量支持，但是不是很好）。所以这些工具，给 Web 开发者使用是最合适的。&lt;/li&gt;
&lt;li&gt;PyPA(Python Packaging Authority)[^5] 只是一个松散的组织，没有一个强大的驱动，因此很难像 Rust, Dart, .NET 那样，由官方去推动一个统一的打包工具。所以有&lt;a href=&quot;https://discuss.python.org/t/how-do-people-feel-about-a-python-packaging-authority-pypa-python-packaging-team-renaming/14272&quot;&gt;提议&lt;/a&gt;将名称里的
Authority 改成 Association 以减少误解。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;引用一下 Thea Flowers 的推特：&lt;/p&gt;
&lt;p&gt;&amp;lt;blockquote className=&quot;twitter-tweet&quot;&amp;gt;
&amp;lt;p lang=&quot;en&quot; dir=&quot;ltr&quot;&amp;gt;
Go ahead and bookmark this so you can link to it every few months when another baby faced,
naive, precious little developer thinks that they can slay the hydra because they only see one
head.
&amp;lt;/p&amp;gt;
— Stargirl🌠 (@theavalkyrie) &amp;lt;a href=&quot;https://twitter.com/theavalkyrie/status/1614843364289179649?ref_src=twsrc%5Etfw&quot;&amp;gt;January 16, 2023&amp;lt;/a&amp;gt;
&amp;lt;/blockquote&amp;gt;
&amp;lt;script async src=&quot;https://platform.twitter.com/widgets.js&quot; charSet=&quot;utf-8&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/p&gt;
&lt;p&gt;不要把事情想得太简单，你可能只看到了九头蛇的一头。&lt;/p&gt;
&lt;p&gt;[^2]: &lt;a href=&quot;https://discuss.python.org/t/python-packaging-strategy-discussion-part-1/22420&quot;&gt;Python Discussion 上的讨论&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;[^3]: Pradyun Gedam: &lt;a href=&quot;https://pradyunsg.me/blog/2023/01/21/thoughts-on-python-packaging/&quot;&gt;Thoughts on the Python packaging ecosystem&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;[^4]: Thea Flowers: &lt;a href=&quot;https://twitter.com/theavalkyrie/status/1614842051580813318&quot;&gt;So You Want to Solve Python Packaging: A Practical Guide&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;[^5]: Pradyun Gedam: &lt;a href=&quot;https://pradyunsg.me/blog/2023/01/14/python-packaging-organisation/&quot;&gt;How the Python Packaging community is organised&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://peps.python.org/pep-0582/&quot;&gt;PEP 582&lt;/a&gt; 的最新修改&lt;/h2&gt;
&lt;p&gt;是的你没看错，PEP 582 并没有被遗忘，它的原作者 Kushal Das 又支楞起来了，在今年做了一些大的修改，基本上是重写了。相比 PDM 实现的 PEP 582 最初版本，一个最大的改变就是
目录结构变了，比如说一个包 &lt;code&gt;bottle&lt;/code&gt;，它的导入路径从原来的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;__pypackages__/3.9/lib/bottle
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改成了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;__pypackages__/lib/python3.9/site-packages/bottle  # Unix-like
__pypackages__/Lib/site-packages/bottle  # Windows
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个路径模式(Install scheme)，和现在已有的 Python 包安装路径是匹配的。好处在于，现在立即就可以用 &lt;code&gt;pip install --prefix __pypackages__&lt;/code&gt; 来实现把包安装到 &lt;code&gt;__pypackages__&lt;/code&gt; 中。
但坏处也有，注意到 Windows 的安装路径，是没有按 Python 版本号来区分的，这就意味着，如果你使用多个不同版本的 Python 安装到同一个位置，这些包是会互相覆盖的。实际使用中，
你根本无从得知这个包是来自哪个版本，造成了不小出问题的可能性。关于这一点，我也在 &lt;a href=&quot;https://discuss.python.org/t/pep-582-python-local-packages-directory/963/378?u=frostming&quot;&gt;Discussion&lt;/a&gt;
上提出来了，有 Core Developer 表示支持，作者也可能会采纳。但凡事有两面，如果在 Windows 路径中加入版本号，变成像 &lt;code&gt;__pypackages__/Lib/3.9/site-packages/bottle&lt;/code&gt; 这样，
就和已有的路径模式不一致，需要增加新的，那么等到周边工具完全支持，周期一下就会漫长很多。但这是用短期的代价换取长期的好处，我觉得是值得的，等到后面出问题再想改就改不动了。&lt;/p&gt;
&lt;p&gt;如果有读者用过 PDM 的 PEP 582 模式就会发现，无论如何改，都和 PDM 的实现不一样了[^6]。&lt;strong&gt;我也计划在下个大更新中把路径改成最新标准&lt;/strong&gt;，提前预告一下。&lt;/p&gt;
&lt;p&gt;除此之外，对于 PEP 582，还有以下几个争议点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果脚本所在目录没找到 &lt;code&gt;__pypackages__&lt;/code&gt;，是否去父级目录以及祖先目录去找？作者在提案中明确拒绝了，理由是安全的考量，因为用户可能执行 &lt;code&gt;/home/me/path/to/my/project/script.py&lt;/code&gt;，却无意中加载了 &lt;code&gt;/home/me/__pypackages__&lt;/code&gt;。
但说实话我没明白这个逻辑，如果恶意用户能在你 home 目录下拉屎，在安全这一块你就已经输了。而且严格控制只能加载当前目录的 &lt;code&gt;__pypackages__&lt;/code&gt; 无疑降低了这个提案的价值，现在这个提案仅仅是能降低一下初学者的
上手难度，不用管虚拟环境那些，愿景未免有点太小了，况且 Node.js 也是会去父目录寻找 &lt;code&gt;node_modules&lt;/code&gt; 的，大家快去说服作者。&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;__pypackages__&lt;/code&gt; 被加载时，是否要禁用系统的 site-packages？在我看来这是一个两难的选择，如果不禁用，确实会造成一些包版本冲突，但如果禁用了，那么就会造成一个结果，如果你在一个有 &lt;code&gt;__pypackages__&lt;/code&gt; 的目录下时，安装在全局的命令行工具就用不了了。
关于这些我也在 &lt;a href=&quot;https://discuss.python.org/t/pep-582-python-local-packages-directory/963/283?u=frostming&quot;&gt;Discussion 上做了一个总结&lt;/a&gt;，包括 PDM 是如何处理这些问题的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;[^6]: Pradyun Gedam: &lt;a href=&quot;https://pradyunsg.me/blog/2023/01/21/pdm-does-not-implement-pep-582/&quot;&gt;PDM does not implement PEP 582, at the time of writing&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;其他提案与动态&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://peps.python.org/pep-0704/&quot;&gt;PEP 704&lt;/a&gt;: 安装 Python 包时明确要求虚拟环境&lt;/h3&gt;
&lt;p&gt;由于 PEP 582 存在的种种问题，PEP 704 应运而生，这是一个与 PEP 582 竞争的提案，它同样面向初学者，解决他们对包安装位置的困惑。它把安装包时，需要虚拟环境改成了默认行为，并且在没有激活虚拟环境时抛出错误。&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://peps.python.org/pep-0691/&quot;&gt;PEP 691&lt;/a&gt;: 基于 JSON 的 Python 包索引的简单 API&lt;/h3&gt;
&lt;p&gt;之前的 Simple index(&lt;a href=&quot;https://peps.python.org/pep-0503/&quot;&gt;PEP 503&lt;/a&gt;) 完全是一个 HTML 页面，下载包时工具要自行解析 HTML。这个提案建议支持 JSON 格式的响应，这样带来的好处有：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;解析 JSON 比解析 HTML 更容易&lt;/li&gt;
&lt;li&gt;JSON 更方便表示结构化的数据，以后可以添加更多不同类型的字段&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个提案已经在 PyPI 上实现，安装器方面，pip 和 PDM(unearth) 也已经支持获取和解析 JSON 的响应。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;packaging 22.0&lt;/code&gt; 去掉了解析非标准版本字符串的支持&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/pypa/packaging&quot;&gt;packaging&lt;/a&gt; 自从 22.0 以后，不再支持解析非标准的版本字符串，并使用了自己实现的 Parser 替代了 &lt;code&gt;pyparsing&lt;/code&gt; 依赖。所有不符合 &lt;a href=&quot;https://peps.python.org/pep-0440/&quot;&gt;PEP 440&lt;/a&gt; 规范的版本字符串都会导致解析失败并报错。
但 PyPI 上仍有大量的包，使用了过时的不合规范的版本号定义，这就导致使用 PDM 安装包时，可能会遇到 &lt;code&gt;InvalidRequirementError&lt;/code&gt; 错误。举例来说：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;版本号或版本范围&lt;/th&gt;
&lt;th&gt;packaging&amp;lt;22&lt;/th&gt;
&lt;th&gt;packaging&amp;gt;=22&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1.0a1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1.0-rc1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1.0-beta.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1.a.b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;=1.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;!=1.*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;=1.0.*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;=1.0.0+g1213&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;由于 pip 采用 vendor 策略，暂时没有升级新版本的 packaging，所以 pip 不会遇到问题。&lt;/p&gt;
</content:encoded></item><item><title>友好的 Python：面向对象接口</title><link>https://frostming.com/posts/2022/friendly-python-oop/</link><guid isPermaLink="false">https://frostming.com/2022/friendly-python-oop/</guid><pubDate>Thu, 21 Jul 2022 15:04:17 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;很久没更新了，写这篇文章是因为受了&lt;a href=&quot;https://www.bilibili.com/video/BV1pS4y1v7Ra?vd_source=cbdc2ec0024a8824a1cf7172ce10282f&quot;&gt;高天直播 Code Review&lt;/a&gt;的启发，深刻感觉到 Python 的灵活和强大，导致了实现同样的功能不同的人会写出完全不一样的代码。Python 语法糖有很多，如何把握「甜度」？过犹不及，我就本人的口味来细说一下。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;免责声明&lt;/strong&gt;，本文有关代码好坏的论断纯属个人喜好，总结的规律均为信口开河，若要争论个高下大可不必。&lt;/p&gt;
&lt;h2&gt;写个配置类&lt;/h2&gt;
&lt;p&gt;小 F 是个后端程序员，他接到一个需求，写一个配置类作为项目配置的模型。很常见的需求嘛，先不管 Pydantic 之类的现成方案，就假设要造轮子好了。要怎么写呢？小 F 略加思索，写出来了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@dataclasses.dataclass
class Settings:
    db_user: str
    db_password: str
    db_host: str = &apos;localhost&apos;
    db_port: int = 3306
    ...  # 省略一百个配置项
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;漂亮！用上了 &lt;code&gt;dataclass&lt;/code&gt;，并提供了适当的默认值，小 F 还是很熟练嘛。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;小 F：算作者识相，没有故意安排我做反面教材。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;这个类要怎么使用呢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;settinigs = Settings(db_user=&apos;root&apos;, db_password=os.getenv(&quot;DB_PASSWORD&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;多种初始化方式&lt;/h2&gt;
&lt;p&gt;但一个配置类，哪能总是从参数去指定呢？一般这些东西，要么从环境变量读进去，要么从一个 JSON 或 YAML 文件读入，要么是某种配置管理系统。现在要怎么设计这个 API 呢？&lt;/p&gt;
&lt;p&gt;小 F 马上明白，这是构造函数重载啊，不过等下，Python 不支持重载[^1]，所幸 Python 非常动态和灵活，&lt;code&gt;__init__&lt;/code&gt; 可以接受多种参数嘛[^2]：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Settings:
    ...
    def __init__(self, **kwargs, from_env=False, from_file=None):
        if from_env:
            self._load_from_env()
        elif from_file:
            self._load_from_file(from_file)
        else:
            for k, v in kwargs.items():
                setattr(self, k, v)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;[^1]: 当然硬要用 &lt;code&gt;@singledispatch&lt;/code&gt; 去做重载也不是不行，但你真的要这样？&lt;/p&gt;
&lt;p&gt;[^2]: 为了说明问题，暂时去掉了 &lt;code&gt;@dataclass&lt;/code&gt;，用最朴素的 &lt;code&gt;__init__&lt;/code&gt; 实现。&lt;/p&gt;
&lt;p&gt;这种方法在我看来，有个最大的问题，就是传入它的参数**并不总是生效：**你传了 &lt;code&gt;from_env&lt;/code&gt;，那 &lt;code&gt;from_file&lt;/code&gt; 会被忽略，你传了 &lt;code&gt;from_file&lt;/code&gt;，那其他的 &lt;code&gt;kwargs&lt;/code&gt; 会被忽略，这对使用者是相当不友好的，他们必须看文档才知道这几个参数优先级是怎样的。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;小 F：你看你还是忍不住编排我了，我会这样写吗？然后他甩出来了另一个方案：&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Settings:
    ...

def load_from_env() -&amp;gt; Settings:
    ...

def load_from_file(filename: str) -&amp;gt; Settings:
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个方案就修正了前述的问题：既然不同构造方法接受的参数不一致，那我专门暴露一个初始化函数就可以了。没错，这种方法也被广泛使用，比如 &lt;code&gt;json.load()&lt;/code&gt; 和 &lt;code&gt;json.loads()&lt;/code&gt;，不同的方法，接受不同的参数。但这种方法有一点&lt;em&gt;小小&lt;/em&gt;的问题，要 import 的东西有点多，这里顶层就暴露了三个类和函数。小 F 的同事小 C 说，那这样可不可以：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Settings:
    ...

    def load_from_env(self) -&amp;gt; None:
        ...

    def load_from_file(self, file: str) -&amp;gt; None:
        ...

# 使用
settings = Settings()
settings.load_from_env()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我虽然在很多地方，包括之前公司的代码中看过这种写法，但我依然极其不推荐，原因是，如果 &lt;code&gt;Settings&lt;/code&gt; 有一些&lt;strong&gt;必填&lt;/strong&gt;参数，会在第一步实例化后得到一个&lt;strong&gt;不完全初始化&lt;/strong&gt;的对象。正确的做法是不实例化，而直接改用 &lt;code&gt;@classmethod&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Settings

    def __init__(self, ...):
        ...

    @classmethod
    def from_env(cls) -&amp;gt; Settings:
        ...

    @classmethod
    def from_file(cls, file: str) -&amp;gt; Settings:
        ...

# 使用
settings = Settings.from_env()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这其实就是 Python 的构造方法重载，只是给构造方法起了不同的名字。这也是 &lt;code&gt;classmethod&lt;/code&gt; 最主要的使用场景——使用某特殊方法构造一个实例，它们和 &lt;code&gt;__init__/__new__&lt;/code&gt; 方法的地位是等同的。&lt;/p&gt;
&lt;p&gt;一般实践上，我们会用 &lt;code&gt;__init__&lt;/code&gt; 实现最 verbose（自定义空间最大）的构造方法，而用 &lt;code&gt;classmethod&lt;/code&gt; 实现其他快捷的，可以从少数入参推断出全部参数的构造方法。而对于 &lt;code&gt;classmethod&lt;/code&gt; 与普通函数的取舍，如果要构造的对象是整个包的主要导出对象（类似于 &lt;code&gt;yaml&lt;/code&gt;, &lt;code&gt;json&lt;/code&gt;），则可以用函数，否则如果这个对象是某个辅助对象，比如 &lt;code&gt;Connection&lt;/code&gt;，&lt;code&gt;Config&lt;/code&gt;，则适合用 &lt;code&gt;classmethod&lt;/code&gt;，可以减少需要导入的成员。&lt;/p&gt;
&lt;h2&gt;多配置选择&lt;/h2&gt;
&lt;p&gt;现在如果要为生产、测试创建不同的配置，覆盖某种值，要怎么做呢？小 F 又说，继承呗：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ProductionSettings(Settings):
    ...

class TestSettings(Settings):
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假如我需要按一个 key(Production/Testing) 来选择配置，该如何做呢？最朴素的方法，建立一个 mapping：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;settings = {&quot;production&quot;: ProductionSettings, &quot;test&quot;: TestSettings}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;IT JUST WORKS.&lt;/p&gt;
&lt;p&gt;可是小 F 看不顺眼，觉得维护这个 mapping 很费劲。他看了一些 Python 的进阶书（不是说书不好），学会了一些&lt;strong&gt;高端&lt;/strong&gt;用法，他三下五除二，就改成了下面这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class SettingsMeta(type):
    mapping = {}

    def __init__(cls, name, bases, attrs):
        super().__init__(name, bases, attrs)
        cls.mapping[re.match(r&quot;(.+?)Settings$&quot;).group(1).lower()] = cls

class Settings(metaclass=SettingsMeta):
    ...
class ProductionSettings(Settings):
    ...

# 使用
production_settings = SettingsMeta.mapping[&quot;production&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;元类！斯~斯~斯国以！除了这需要一点时间才能看懂在做什么外，这么写有什么问题呢？有，这里有抽象泄漏的问题：&lt;code&gt;Settings&lt;/code&gt; 的子类，保存到了元类 &lt;code&gt;SettingsMeta&lt;/code&gt;上，而这个元类是创建 &lt;code&gt;Settings&lt;/code&gt; 的「类工厂」，这里就形成了循环：&lt;code&gt;Settings -&amp;gt; SettingsMeta -&amp;gt; Settings&lt;/code&gt;。使用者不应该感知到元类的存在，也就不应该调用他上面的属性。小 F 又说，这个 &lt;code&gt;mapping&lt;/code&gt; 属性可以在 &lt;code&gt;Settings&lt;/code&gt; 上使用啊，这没错，但这问题更大了，所有子类都有这个 &lt;code&gt;mapping&lt;/code&gt;，也就是说，下面这种用法是可以的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test_settings = ProductSettings.mapping[&quot;test&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就完全不 make sense 了。同之前引入 &lt;code&gt;classmethod&lt;/code&gt; 解决不完全初始化的对象一样，我们应该从根本上杜绝存在这种诡异代码的可能性。&lt;/p&gt;
&lt;p&gt;我们千万要警惕这种「炫技」的倾向，如果有多种实现方案，一定要选择最直截了当简单明白的方法。另一个原则是，你提供的东西，最好只提供&lt;strong&gt;刚好所需要&lt;/strong&gt;的接口，而不暴露多余的接口。&lt;code&gt;SettingsMeta&lt;/code&gt; 元类就是一个反例，其实你只需要用一个 &lt;code&gt;mapping&lt;/code&gt;，它自己倒是自动更新了，却导致了所有配置类多了一个 &lt;code&gt;mapping&lt;/code&gt; 属性（即使你换成用一个方法获取，或是用 &lt;code&gt;__init_subclass__&lt;/code&gt;，也会污染到它的子类，这是这种方法的最大问题）。&lt;/p&gt;
&lt;p&gt;所以说，高端的食材，不是，高端的语言特性往往需要在适当时候使用。其实，用一个额外的字典来存映射没什么不好，如果不想手写 &lt;code&gt;mapping&lt;/code&gt;，也可以用&lt;a href=&quot;https://frostming.com/2021/07-07/friendly-python-1/#%E6%B3%A8%E5%86%8C%E4%B8%AD%E5%BF%83&quot;&gt;注册机制&lt;/a&gt;，能更干净的达到效果。&lt;/p&gt;
&lt;h2&gt;减少重复(DRY)&lt;/h2&gt;
&lt;p&gt;本来打算结束，篇幅不够，那再加一个需求：让配置支持默认从环境变量中取值，并且更新环境变量能立刻生效。
这就需要每次读取值的时候都访问一次环境变量，这不就是 &lt;code&gt;@property&lt;/code&gt; 的用武之地吗？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Settings:
    @property
    def db_url(self):
        return self._db_url or os.getenv(&quot;CONFIG_DB_URL&quot;)

    @property
    def db_password(self):
        return self._db_password or os.getenv(&quot;CONFIG_DB_PASSWORD&quot;)

    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样一来，重复的代码就多起来了，要怎么 DRY 一下呢？这里又有不止一种方法了， 用 &lt;code&gt;__getattr__&lt;/code&gt; 吗？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Settings:
    def __getattr__(self, name):
        try:
            return os.environ[&quot;CONFIG_&quot; + name.upper()]
        except KeyError:
            raise AttributeError(name)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种方法，违背了一条 Python 之禅，也就是我引用最多最有意义的一条：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Explicit is better than implicit.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;你根本无法知道这个 &lt;code&gt;Settings&lt;/code&gt; 到底支持多少个配置项，你只要设置 &lt;code&gt;CONFIG_FOO&lt;/code&gt;，就能用 &lt;code&gt;settings.foo&lt;/code&gt; 得到它的值，就算已经用了 &lt;code&gt;AttributeError&lt;/code&gt; 防御不当使用，这威力也不必要地过大了。我推荐的方式，是用&lt;a href=&quot;https://docs.python.org/3/howto/descriptor.html&quot;&gt;描述符&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ConfigItem:
    def __set_name__(self, owner, name):
        self.name = name
        self.env_name = &quot;CONFIG_&quot; + name.upper()

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance._data[self.name] or os.getenv(self.env_name)

# 使用
class Settings:
    db_url = ConfigItem()
    db_password = ConfigItem()
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用描述符的最大好处，是他对补全很友好，而且可以加 type hint。后续如果要支持修改配置、值的校验、类型转换等，也很方便，比如接受一个校验函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ConfigItem:
    def __init__(self, validate_func=None):
        if validate_func is None:
            validate_func = lambda self, x: x
        self.validate_func = validate_func

    def __set_name__(self, owner, name):
        self.name = name
        self.env_name = &quot;CONFIG_&quot; + name.upper()

    def __get__(self, instance, owner):
        if instance is None:
            return self
        value = instance._data[self.name] or os.getenv(self.env_name)
        return self.validate_func(instance, value)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用起来更是相当酷炫：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
class Settings:

    db_user = ConfigItem()

    @ConfigItem
    def db_password(self, value):
        if len(value) &amp;lt; 8:
            raise ValueError(&quot;Password must be at least 8 characters&quot;)
        return value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再细看一眼，是不是特别像一个 ORM 了，接着扩展下去，这其实就是一个 ORM 或者类似 pydantic 的轮子的雏形。&lt;/p&gt;
</content:encoded></item><item><title>PDM 2.0 有什么新特性？</title><link>https://frostming.com/posts/2022/pdm-2/</link><guid isPermaLink="false">https://frostming.com/2022/pdm-2/</guid><pubDate>Sun, 03 Jul 2022 09:49:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;/2022/pdm-2-en/&quot;&gt;The English Version&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://pdm-project.org&quot;&gt;PDM&lt;/a&gt; 在最近发布了 &lt;a href=&quot;https://github.com/pdm-project/pdm/releases/tag/2.0.0&quot;&gt;2.0.0&lt;/a&gt; 版本，新特性已基本完成。本文将介绍这次更新的内容。详细改动日志在&lt;a href=&quot;https://pdm-project.org/2.0/dev/changelog/&quot;&gt;这里&lt;/a&gt;可以看到。&lt;/p&gt;
&lt;h2&gt;虚拟环境成为项目的默认配置&lt;/h2&gt;
&lt;p&gt;PDM 在建立之初，是标榜自己是一个支持 &lt;a href=&quot;https://www.python.org/dev/peps/pep-0582/&quot;&gt;PEP 582&lt;/a&gt; 包结构的包管理器。但无奈，经过两年的观望，PEP 582 仍旧停留在 Draft 状态，并且迟迟没有进展。
它虽然在一开始令人眼前一亮并吸引了大批初始用户，但这也成为&lt;a href=&quot;https://gist.github.com/astrojuanlu/9a7419d3281d4689ae05df613e0bf9c0?permalink_comment_id=4208005#gistcomment-4208005&quot;&gt;PDM 被主流接纳的一个阻碍&lt;/a&gt;，限制了它的推广。同时，虚拟环境在各种 IDE 和工具中有更好的支持。
现在我希望 PDM 并不仅是一个个人兴趣的项目，并且是一个支持 Python 打包最新规范的正经的包管理器，所以在 2.0 中，我们将虚拟环境成为项目的默认配置。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在 &lt;code&gt;pdm init&lt;/code&gt; 中，如果你没有选择一个已有虚拟环境中的解释器，PDM 会询问是否需要创建一个新的虚拟环境。如果创建，则会把这个虚拟环境作为项目环境，
否则，还是会启用 PEP 582 包结构。&lt;/li&gt;
&lt;li&gt;当你克隆一个已有的项目，在项目中第一次执行 &lt;code&gt;pdm install&lt;/code&gt; 时，PDM 会检查项目中是否存在一个 &lt;code&gt;__pypackages__&lt;/code&gt; 文件夹[^1]，如果存在，会使用 PEP 582 包结构，
否则会&lt;strong&gt;自动为你创建一个虚拟环境&lt;/strong&gt;并在其中安装依赖。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们尽可能保证旧的项目不会变化，而只是新项目的默认方式变了。在文档中，PEP 582 也从首页最显眼的位置移动到了子页面中。所以 PDM 依然支持 PEP 582，只是不是默认的方式。&lt;/p&gt;
&lt;h2&gt;PDM 搭配其他后端&lt;/h2&gt;
&lt;p&gt;PDM 虽然有一个自己的后端[^2] &lt;a href=&quot;https://github.com/pdm-project/pdm-pep517&quot;&gt;&lt;code&gt;pdm-pep517&lt;/code&gt;&lt;/a&gt; 但它其实没有和任何后端绑定，你依然可以使用比如 &lt;a href=&quot;https://flit.pypa.io/&quot;&gt;&lt;code&gt;flit-core&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://hatch.pypa.io/&quot;&gt;&lt;code&gt;hatchling&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://setuptools.pypa.io/&quot;&gt;&lt;code&gt;setuptools&lt;/code&gt;&lt;/a&gt;
作为后端，只要它支持读取 &lt;a href=&quot;https://www.python.org/dev/peps/pep-0621/&quot;&gt;PEP 621&lt;/a&gt; 的元数据。甚至，你可以混用其他的包管理工具，例如，你完全可以用 &lt;a href=&quot;https://flit.pypa.io/&quot;&gt;&lt;code&gt;flit&lt;/code&gt;&lt;/a&gt; 来打包你的项目，而只把 PDM 作为依赖管理的工具来使用，因为前者不具备依赖管理的功能。&lt;/p&gt;
&lt;p&gt;这就是拥抱标准所带来的巨大好处——工具只要遵循同一个规范，它们之间就可以共同存在，各自完成自己的工作。&lt;/p&gt;
&lt;h2&gt;不再允许在项目依赖中包含 Editable 的包&lt;/h2&gt;
&lt;p&gt;原先，在 &lt;code&gt;pyproject.toml&lt;/code&gt; 中的 &lt;code&gt;[project]:dependencies&lt;/code&gt; 中你可以包含类似 &lt;code&gt;-e ./mypackage&lt;/code&gt;, &lt;code&gt;-e git+https://github.com/psf/requests.git@main&lt;/code&gt; 这样的
以 Editable 方式安装的包。但这是不符合 &lt;a href=&quot;https://www.python.org/dev/peps/pep-0631/&quot;&gt;PEP 631&lt;/a&gt; 规范的，所以在 2.0 中，我们不再支持这种依赖，已有的 editable 包会弹出警告。&lt;/p&gt;
&lt;p&gt;但别担心，你还是可以在 &lt;code&gt;[tool.pdm.dev-dependencies]&lt;/code&gt; 中包含 Editable 的包，因为实际上它们只在开发中有用。&lt;/p&gt;
&lt;h2&gt;PDM 全局配置路径遵循 XDG 目录规范&lt;/h2&gt;
&lt;p&gt;原先 PDM 的全局配置是存在 &lt;code&gt;~/.pdm&lt;/code&gt; 下面的，但在 2.0 中，它们将被放置在 &lt;code&gt;$CONFIG_HOME&lt;/code&gt; 下面。具体的值为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linux: &lt;code&gt;$XDG_CONFIG_HOME/pdm&lt;/code&gt; (一般为 &lt;code&gt;~/.config/pdm&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;MacOS: &lt;code&gt;~/Library/Preferences/pdm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Windows: &lt;code&gt;%USERPROFILE%\AppData\Local\pdm&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你需要做一次性迁移(Linux 为例)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ mv ~/.pdm ~/.config/pdm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;感谢 &lt;a href=&quot;https://github.com/noirbizarre&quot;&gt;@noirbizarre&lt;/a&gt; 的贡献。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;增加 &lt;a href=&quot;https://pdm-project.org/2.0/usage/cli_reference/#exec-0--publish&quot;&gt;&lt;code&gt;pdm publish&lt;/code&gt;&lt;/a&gt; 命令&lt;/h2&gt;
&lt;p&gt;是的，这个功能是很多用户都希望拥有的，我们终于在 PDM 2.0 中加上了！直接执行 &lt;code&gt;pdm publish&lt;/code&gt;，PDM 会自动打包项目，然后上传到 PyPI。当然，在这之前，
你需要配置好上传所需要的用户密钥。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ pdm config repository.pypi.username &amp;lt;username&amp;gt;
$ pdm config repository.pypi.password &amp;lt;password&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;UI 的 rich 化&lt;/h2&gt;
&lt;p&gt;PDM 2.0 把 UI 的渲染从原来的 &lt;code&gt;click&lt;/code&gt; + &lt;code&gt;halo&lt;/code&gt; 改成了 &lt;a href=&quot;https://rich.readthedocs.io/en/latest/&quot;&gt;&lt;code&gt;rich&lt;/code&gt;&lt;/a&gt;，后者提供了一站式的体验和灵活的配置，构建出了强大而美观的 UI。大部分
UI 都保持了原样，但 rich 的实现更加简单，也更少的 bug。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/20220703105157.png&quot; alt=&quot;pdm add&quot; /&gt;
&lt;img src=&quot;https://webp.frostming.com/images/20220703105333.png&quot; alt=&quot;pdm list&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;感谢 &lt;a href=&quot;https://github.com/daylinmorgan&quot;&gt;@daylinmorgan&lt;/a&gt; 的贡献。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;不再依赖 pip 内部的 API&lt;/h2&gt;
&lt;p&gt;PDM 1.x 中寻找包和下载包的部分用到了部分 &lt;code&gt;pip&lt;/code&gt; 的 API，但 &lt;code&gt;pip&lt;/code&gt; 从来不是作为一个库使用的，而且它遵循的是 &lt;a href=&quot;https://calver.org/&quot;&gt;CalVer&lt;/a&gt; 版本发布，所以即使在小版本的升级中
也会破坏 API 的兼容性，导致 PDM 坏掉。从前 PDM 只能在依赖中限定 pip 的版本范围，但问题是 pip 作为一个基础工具，在不同的 Linux 发行版中可能有各种 patch 导致不能兼容。
所以我们彻底摒弃了使用 &lt;code&gt;pip&lt;/code&gt; 的内部 API，转而自己造了一个轮子 &lt;a href=&quot;https://github.com/frostming/unearth&quot;&gt;&lt;code&gt;unearth&lt;/code&gt;&lt;/a&gt; 来使用。这将增加稳定性，也方便了下游的打包者。&lt;/p&gt;
&lt;h2&gt;全面强化的用户脚本系统&lt;/h2&gt;
&lt;p&gt;在 PDM 之前的版本中我们已经加入了用户脚本系统(&lt;code&gt;[tool.pdm.scripts]&lt;/code&gt;，类似 &lt;code&gt;package.json&lt;/code&gt; 中的 &lt;code&gt;scripts&lt;/code&gt;)，在 2.0 版本中，我们继续增加了许多功能，
让这个系统变得更加强大和灵活。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;感谢 &lt;a href=&quot;https://github.com/noirbizarre&quot;&gt;@noirbizarre&lt;/a&gt; 的贡献。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;composite &lt;a href=&quot;https://pdm-project.org/2.0/usage/scripts/#composite&quot;&gt;复合脚本&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;你可以使用复合脚本来组合多个脚本，它们之间将顺序执行，其中有任一脚本失败，整个复合脚本就失败。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[tool.pdm.scripts]
lint = &quot;flake8&quot;
test = &quot;pytest&quot;
all = { composite = [
  &quot;lint&quot;,
  &quot;test&quot;
] }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行 &lt;code&gt;pdm run all&lt;/code&gt; 就会执行 &lt;code&gt;lint&lt;/code&gt; 和 &lt;code&gt;test&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;用户脚本作为根命令执行&lt;/h3&gt;
&lt;p&gt;如果你有一个脚本 &lt;code&gt;start&lt;/code&gt;，那么 &lt;code&gt;pdm start&lt;/code&gt; 和 &lt;code&gt;pdm run start&lt;/code&gt; 同样会执行这个脚本，只要脚本名称没有和其他的命令冲突。&lt;/p&gt;
&lt;h2&gt;增加更多的钩子&lt;/h2&gt;
&lt;p&gt;钩子(hook)是在 PDM 执行特定事件前后会触发的回调动作，这可以让插件的开发更加容易。在 PDM 2.0 中我们新增以下钩子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pre_publish/post_publish&lt;/code&gt;：在上传发布包之前和之后触发&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pre_run/post_run&lt;/code&gt;：在运行 &lt;code&gt;pdm run&lt;/code&gt; 之前和之后触发&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pre_script/post_script&lt;/code&gt;：在运行&lt;strong&gt;单个&lt;/strong&gt;脚本之前和之后触发&lt;/li&gt;
&lt;li&gt;&lt;code&gt;post_use&lt;/code&gt;：在使用 &lt;code&gt;pdm use&lt;/code&gt; 切换 Python 解释器之后触发&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体使用方法请查阅&lt;a href=&quot;https://pdm-project.org/2.0/usage/hooks/&quot;&gt;文档&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;感谢 &lt;a href=&quot;https://github.com/noirbizarre&quot;&gt;@noirbizarre&lt;/a&gt; 的贡献。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;增加 &lt;code&gt;--skip&lt;/code&gt; 选项跳过某些钩子或脚本&lt;/h2&gt;
&lt;p&gt;有了如此多的钩子，很多时候你并不希望它们全都触发，可以使用 &lt;code&gt;--skip&lt;/code&gt; 选项来跳过某些钩子或脚本。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 跳过 pre_install
pdm install --skip pre_install
# 跳过 pre_publish 和 post_publish
pdm publish --skip pre_publish,post_publish
# 跳过所有 post_* 以及 pre_start 钩子和脚本
pdm run --skip:post --skip pre_start start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;感谢 &lt;a href=&quot;https://github.com/noirbizarre&quot;&gt;@noirbizarre&lt;/a&gt; 的贡献。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;致谢&lt;/h2&gt;
&lt;p&gt;在 PDM 2.0 开发中收到了很多帮助、反馈和贡献，特别是 &lt;a href=&quot;https://github.com/noirbizarre&quot;&gt;@noirbizarre&lt;/a&gt;（他是 &lt;a href=&quot;https://github.com/noirbizarre/flask-restplus&quot;&gt;&lt;code&gt;flask-restplus&lt;/code&gt;&lt;/a&gt; 的作者）贡献了大部分用户脚本的功能。
在这里我想贴上他在一个&lt;a href=&quot;https://github.com/pdm-project/pdm/discussions/1162#discussioncomment-3041456&quot;&gt;讨论&lt;/a&gt;中对 PDM 的评价，引用就不翻译了：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I think the PEP-582 support even if opt-in is already an argument.&lt;/p&gt;
&lt;p&gt;Given I started using PDM recently after a painful year (maybe 2) looking for a decent solution (Python project management and packaging have been hell recently):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;it just work, no tricks need, no special version (seems logical but just try any other and you will understand)&lt;/li&gt;
&lt;li&gt;scripts support (game changer to me, Poetry only have it through &lt;code&gt;poet&lt;/code&gt; and &lt;code&gt;Pipenv&lt;/code&gt; ones are limited (no env, no shell chaining, no composition...)&lt;/li&gt;
&lt;li&gt;supports PEP-621: Poetry and Pipenv don&apos;t, &lt;code&gt;setuptools&lt;/code&gt; has a just recently experimental support&lt;/li&gt;
&lt;li&gt;lock file support + pnpm-like cache&lt;/li&gt;
&lt;li&gt;editables support (only project so far having both PEP-621 and editables support)&lt;/li&gt;
&lt;li&gt;single tool batteries included (&lt;code&gt;setuptools&lt;/code&gt; and &lt;code&gt;flit&lt;/code&gt; don&apos;t cover the full workflow, &lt;code&gt;poetry&lt;/code&gt; is going under heavy refactoring leading to 2 non-stable codebases, and Pipenv does not support packaging and extensions are very limited, mostly IDE integration)&lt;/li&gt;
&lt;li&gt;powerful extensions support + dynamic ecosystem: (I can attest it, I contributed a lot lately, everything has been reviewed and accepted, very quickly and the tone was good/kind). I can add transparent and realistic roadmap&lt;/li&gt;
&lt;li&gt;great doc !&lt;/li&gt;
&lt;li&gt;extensive test suite&lt;/li&gt;
&lt;li&gt;have I said it just work 🎉&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And I&apos;m sure some points are missing from the list.&lt;/p&gt;
&lt;p&gt;To make it clear: PDM is as of today the only Python project management and packaging tool covering the full lifecycle, supporting all recently published PEP on packaging, supporting lock and being able to handle both libs and apps. (And this this why I&apos;m currently in the process of migrating all my projects on PDM)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;安装试用新版 PDM&lt;/h2&gt;
&lt;p&gt;欢迎使用和测试新版 PDM。&lt;/p&gt;
&lt;p&gt;如果是用 &lt;code&gt;install-pdm.py&lt;/code&gt; 安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -sSL https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py | python3 - --prerelease
# Or Windows powershell:
(Invoke-WebRequest -Uri https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py -UseBasicParsing).Content | python - --prerelease
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是用 &lt;code&gt;pipx&lt;/code&gt; 安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pipx install --pip-args=&quot;--pre&quot; pdm
# 或者更新已有
pipx upgrade --pip-args=&quot;--pre&quot; pdm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;[^1]: 你只需要上传一个空的 &lt;code&gt;__pypackages__&lt;/code&gt; 到 git 上，而把安装的包 ignore 掉&lt;/p&gt;
&lt;p&gt;[^2]: 在 Python 打包中后端是指读取元数据进行构建、打包的工具(如 &lt;code&gt;setuptools&lt;/code&gt;)，而前端是指提供用户界面以修改元数据的工具(如 &lt;code&gt;pip&lt;/code&gt;)&lt;/p&gt;
</content:encoded></item><item><title>What&apos;s New In PDM 2.0?</title><link>https://frostming.com/posts/en/2022/pdm-2/</link><guid isPermaLink="false">https://frostming.com/en/2022/pdm-2/</guid><pubDate>Sun, 03 Jul 2022 09:49:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;/2022/pdm-2/&quot;&gt;中文版本&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://pdm-project.org&quot;&gt;PDM&lt;/a&gt; has released &lt;a href=&quot;https://github.com/pdm-project/pdm/releases/tag/2.0.0&quot;&gt;2.0.0&lt;/a&gt; recently, it is mostly complete feature-wise. This post lists and illustrates the
important new features, and more details can be seen in the &lt;a href=&quot;https://pdm-project.org/2.0/dev/changelog/&quot;&gt;changelog&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Virtualenv becomes the default&lt;/h2&gt;
&lt;p&gt;When PDM was first created, it was advertised as a package manager supporting the &lt;a href=&quot;https://www.python.org/dev/peps/pep-0582/&quot;&gt;PEP 582&lt;/a&gt; package structure.
However, after two years of waiting, the PEP 582 is still in Draft status and has been slow to progress.
Although it was a hit at the beginning and attracted a large number of initial users, this has been
&lt;a href=&quot;https://gist.github.com/astrojuanlu/9a7419d3281d4689ae05df613e0bf9c0?permalink_comment_id=4208005#gistcomment-4208005&quot;&gt;an impediment to being accepted by the mainstream&lt;/a&gt;, limiting its promotion. Also, virtual environments have better support in IDEs and tools.
Now I hope that PDM is not only a project of personal interest, but also a package manager that supports the latest Python packaging standards
and aims at general developers. So in 2.0, we made virtualenv the default setup for the project.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In &lt;code&gt;pdm init&lt;/code&gt;, if you choose a non-venv interpreter, PDM will ask if a new virtual environment needs to be created.
If answered yes, it will create a new one and use it as the project environment. Otherwise, PEP 582 will still be used.&lt;/li&gt;
&lt;li&gt;When you clone an existing project and execute &lt;code&gt;pdm install&lt;/code&gt;(or other commands) for the first time in the project,
PDM will check if &lt;code&gt;__pypackages__&lt;/code&gt; directory exists in the project root[^1] and use PEP 582 layout if so.
Otherwise, a new virtualenv will be created and install packages into it later.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;As much as possible, we make sure that the old project does not break, but only the default way of the new project changes.
In the documentation, PEP 582 has also been moved from its most prominent position on the front page to a subpage.
In another word, PDM still supports PEP 582, just not in the default way.&lt;/p&gt;
&lt;h2&gt;Work with other PEP 517 backends&lt;/h2&gt;
&lt;p&gt;Although PDM is shipped with its own backend[^2] &lt;a href=&quot;https://github.com/pdm-project/pdm-pep517&quot;&gt;&lt;code&gt;pdm-pep517&lt;/code&gt;&lt;/a&gt;, it indeed isn&apos;t locked-in with any backend.
You can still use backends like &lt;a href=&quot;https://flit.pypa.io/&quot;&gt;&lt;code&gt;flit-core&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://hatch.pypa.io/&quot;&gt;&lt;code&gt;hatchling&lt;/code&gt;&lt;/a&gt; and &lt;a href=&quot;https://setuptools.pypa.io/&quot;&gt;&lt;code&gt;setuptools&lt;/code&gt;&lt;/a&gt;,
as long as it supports reading metadata from &lt;a href=&quot;https://www.python.org/dev/peps/pep-0621/&quot;&gt;PEP 621&lt;/a&gt;. You can even mix PDM with other package managers. For instance,
you can use &lt;a href=&quot;https://flit.pypa.io/&quot;&gt;&lt;code&gt;flit&lt;/code&gt;&lt;/a&gt; to build and package your project and use PDM only as a dependency management tool,
since the former does not have dependency management capabilities.&lt;/p&gt;
&lt;p&gt;This is the great benefit of embracing standards--tools can co-exist with each other, each doing its own job,
as long as they follow the standards.&lt;/p&gt;
&lt;h2&gt;Editable packages are no longer allowed in project dependencies&lt;/h2&gt;
&lt;p&gt;Originally, in &lt;code&gt;[project]:dependencies&lt;/code&gt; in &lt;code&gt;pyproject.toml&lt;/code&gt; you could include something like &lt;code&gt;-e ./mypackage&lt;/code&gt;, &lt;code&gt;-e git+https://github.com/psf/requests.git@main&lt;/code&gt;
that will be installed in editable mode. However, this is not compliant with the &lt;a href=&quot;https://www.python.org/dev/peps/pep-0631/&quot;&gt;PEP 631&lt;/a&gt; specification,
in 2.0 we therefore no longer support this kind of dependencies, and existing editable packages will trigger a warning on installation.&lt;/p&gt;
&lt;p&gt;But don&apos;t worry, you can still include editable packages in &lt;code&gt;[tool.pdm.dev-dependencies]&lt;/code&gt;, because they are actually only useful in development.&lt;/p&gt;
&lt;h2&gt;The PDM global configurations are relocated&lt;/h2&gt;
&lt;p&gt;Originally, the global configuration of PDM existed under &lt;code&gt;~/.pdm&lt;/code&gt;, but in 2.0 they will be placed under &lt;code&gt;$CONFIG_HOME&lt;/code&gt;. The specific values are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linux: &lt;code&gt;$XDG_CONFIG_HOME/pdm&lt;/code&gt; (&lt;code&gt;~/.config/pdm&lt;/code&gt; for the most of time)&lt;/li&gt;
&lt;li&gt;MacOS: &lt;code&gt;~/Library/Preferences/pdm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Windows: &lt;code&gt;%USERPROFILE%\AppData\Local\pdm&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You need to do a one-time migration (Linux for example).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ cp -r ~/.pdm/* ~/.config/pdm/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Thanks to &lt;a href=&quot;https://github.com/noirbizarre&quot;&gt;@noirbizarre&lt;/a&gt; for the contribution.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Add &lt;a href=&quot;https://pdm-project.org/2.0/usage/cli_reference/#exec-0--publish&quot;&gt;&lt;code&gt;pdm publish&lt;/code&gt;&lt;/a&gt; command&lt;/h2&gt;
&lt;p&gt;Yes, this is a feature that many users would like to have, and we&apos;ve finally added it in PDM 2.0!
Execute &lt;code&gt;pdm publish&lt;/code&gt; and PDM will automatically package the project and upload it to PyPI.
Of course, before you do that, you need to configure the repository credentials.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ pdm config repository.pypi.username &amp;lt;username&amp;gt;
$ pdm config repository.pypi.password &amp;lt;password&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The UI is richified&lt;/h2&gt;
&lt;p&gt;PDM 2.0 changed the UI rendering from &lt;code&gt;click&lt;/code&gt; + &lt;code&gt;halo&lt;/code&gt; to &lt;a href=&quot;https://rich.readthedocs.io/en/latest/&quot;&gt;&lt;code&gt;rich&lt;/code&gt;&lt;/a&gt;, which provides a one-stop experience and flexible configuration to build a powerful and beautiful UI.
The UI remains mostly the same, but the rich implementation is simpler and less buggy.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/20220703105157.png&quot; alt=&quot;pdm add&quot; /&gt;
&lt;img src=&quot;https://webp.frostming.com/images/20220703105333.png&quot; alt=&quot;pdm list&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Thanks to &lt;a href=&quot;https://github.com/daylinmorgan&quot;&gt;@daylinmorgan&lt;/a&gt; for the contribution.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Abandon the usage of pip&apos;s internals&lt;/h2&gt;
&lt;p&gt;PDM 1.x used to use some &lt;code&gt;pip&lt;/code&gt; internal APIs to find and download packages, but &lt;code&gt;pip&lt;/code&gt; is not made as a library and it follows the &lt;a href=&quot;https://calver.org/&quot;&gt;CalVer&lt;/a&gt; release, so even in patch upgrades
the API may change and cause PDM to break. Previously, PDM could only limit the version range of pip in the dependencies, but the problem is that pip, as a basic tool,
may have been patched in different Linux distributions that may not be compatible.
So we completely abandoned the usage of pip&apos;s internals and instead built our own wheel &lt;a href=&quot;https://github.com/frostming/unearth&quot;&gt;&lt;code&gt;unearth&lt;/code&gt;&lt;/a&gt; for the same work.
This will increase stability and also make things easier for downstream packagers.&lt;/p&gt;
&lt;h2&gt;The enhanced user scripts&lt;/h2&gt;
&lt;p&gt;In previous versions PDM we have added support for user scripts(&lt;code&gt;[tool.pdm.scripts]&lt;/code&gt;, like &lt;code&gt;scripts&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt;). In version 2.0, we have continued to add many features that
to make this system more powerful and flexible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Thanks to &lt;a href=&quot;https://github.com/noirbizarre&quot;&gt;@noirbizarre&lt;/a&gt; for the contribution.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://pdm-project.org/2.0/usage/scripts/#composite&quot;&gt;Composite script&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;You can use composite scripts to combine multiple scripts that will execute sequentially, and if any one of them fails, the entire composite script fails.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[tool.pdm.scripts]
lint = &quot;flake8&quot;
test = &quot;pytest&quot;
all = { composite = [
  &quot;lint&quot;,
  &quot;test&quot;
] }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running &lt;code&gt;pdm run all&lt;/code&gt; will execute &lt;code&gt;lint&lt;/code&gt; and &lt;code&gt;test&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;User scripts as root commands&lt;/h3&gt;
&lt;p&gt;If you have a script named &lt;code&gt;start&lt;/code&gt;, then &lt;code&gt;pdm start&lt;/code&gt; and &lt;code&gt;pdm run start&lt;/code&gt; will also execute this script, as long as the script name does not conflict with other commands.&lt;/p&gt;
&lt;h2&gt;Add more hooks&lt;/h2&gt;
&lt;p&gt;Hooks are callback actions that are triggered before and after a specific event happenning in PDM, which can benefit the plugin development.
In PDM 2.0 we have added the following hooks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pre_publish/post_publish&lt;/code&gt;: triggered before and after publish&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pre_run/post_run&lt;/code&gt;: triggered before and after a &lt;code&gt;pdm run&lt;/code&gt; execution&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pre_script/post_script&lt;/code&gt;: triggered before and after an &lt;strong&gt;individual&lt;/strong&gt; script is run&lt;/li&gt;
&lt;li&gt;&lt;code&gt;post_use&lt;/code&gt;: triggered after the Python interpreter is changed by &lt;code&gt;pdm use&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Please refer to &lt;a href=&quot;https://pdm-project.org/2.0/usage/hooks/&quot;&gt;documentation&lt;/a&gt; for details on how to use them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Thanks to &lt;a href=&quot;https://github.com/noirbizarre&quot;&gt;@noirbizarre&lt;/a&gt; for the contribution.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Add &lt;code&gt;--skip&lt;/code&gt; to skip hooks or scripts&lt;/h2&gt;
&lt;p&gt;With so many hooks, sometimes you don&apos;t want them all to fire. You can use the &lt;code&gt;--skip&lt;/code&gt; option to skip some hooks or scripts.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 跳过 pre_install
pdm install --skip pre_install
# 跳过 pre_publish 和 post_publish
pdm publish --skip pre_publish,post_publish
# 跳过所有 post_* 以及 pre_start 钩子和脚本
pdm run --skip:post --skip pre_start start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Thanks to &lt;a href=&quot;https://github.com/noirbizarre&quot;&gt;@noirbizarre&lt;/a&gt; for the contribution.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Acknowledgements&lt;/h2&gt;
&lt;p&gt;I received a lot of feedbacks and contributions during the development of PDM 2.0, especially from &lt;a href=&quot;https://github.com/noirbizarre&quot;&gt;@noirbizarre&lt;/a&gt; (who is the author of &lt;a href=&quot;https://github.com/noirbizarre/flask-restplus&quot;&gt;&lt;code&gt;flask-restplus&lt;/code&gt;&lt;/a&gt;). He contributed most of the user script functionality.
Here I would like to post his comments about PDM in a &lt;a href=&quot;https://github.com/pdm-project/pdm/discussions/1162#discussioncomment-3041456&quot;&gt;discussion&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I think the PEP-582 support even if opt-in is already an argument.&lt;/p&gt;
&lt;p&gt;Given I started using PDM recently after a painful year (maybe 2) looking for a decent solution (Python project management and packaging have been hell recently):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;it just work, no tricks need, no special version (seems logical but just try any other and you will understand)&lt;/li&gt;
&lt;li&gt;scripts support (game changer to me, Poetry only have it through &lt;code&gt;poet&lt;/code&gt; and &lt;code&gt;Pipenv&lt;/code&gt; ones are limited (no env, no shell chaining, no composition...)&lt;/li&gt;
&lt;li&gt;supports PEP-621: Poetry and Pipenv don&apos;t, &lt;code&gt;setuptools&lt;/code&gt; has a just recently experimental support&lt;/li&gt;
&lt;li&gt;lock file support + pnpm-like cache&lt;/li&gt;
&lt;li&gt;editables support (only project so far having both PEP-621 and editables support)&lt;/li&gt;
&lt;li&gt;single tool batteries included (&lt;code&gt;setuptools&lt;/code&gt; and &lt;code&gt;flit&lt;/code&gt; don&apos;t cover the full workflow, &lt;code&gt;poetry&lt;/code&gt; is going under heavy refactoring leading to 2 non-stable codebases, and Pipenv does not support packaging and extensions are very limited, mostly IDE integration)&lt;/li&gt;
&lt;li&gt;powerful extensions support + dynamic ecosystem: (I can attest it, I contributed a lot lately, everything has been reviewed and accepted, very quickly and the tone was good/kind). I can add transparent and realistic roadmap&lt;/li&gt;
&lt;li&gt;great doc !&lt;/li&gt;
&lt;li&gt;extensive test suite&lt;/li&gt;
&lt;li&gt;have I said it just work 🎉&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And I&apos;m sure some points are missing from the list.&lt;/p&gt;
&lt;p&gt;To make it clear: PDM is as of today the only Python project management and packaging tool covering the full lifecycle, supporting all recently published PEP on packaging, supporting lock and being able to handle both libs and apps. (And this this why I&apos;m currently in the process of migrating all my projects on PDM)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Install and try the new PDM version&lt;/h2&gt;
&lt;p&gt;Welcome to use and test the new PDM version.&lt;/p&gt;
&lt;p&gt;If you install with &lt;code&gt;install-pdm.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -sSL https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py | python3 - --prerelease
# Or Windows powershell:
(Invoke-WebRequest -Uri https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py -UseBasicParsing).Content | python - --prerelease
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you install with &lt;code&gt;pipx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pipx install --pip-args=&quot;--pre&quot; pdm
# or update the installed version
pipx upgrade --pip-args=&quot;--pre&quot; pdm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;[^1]: You only need to upload an empty &lt;code&gt;__pypackages__&lt;/code&gt; to the source code management and ignore the packages in it.&lt;/p&gt;
&lt;p&gt;[^2]:
In Python packaging, a backend is the tool that reads the metadata to build a package(like &lt;code&gt;setuptools&lt;/code&gt;),
and a front-end is the tool that provides the user interface to modify the metadata(like &lt;code&gt;pip&lt;/code&gt;)&lt;/p&gt;
</content:encoded></item><item><title>Debian 系统上捉摸不定的 Python</title><link>https://frostming.com/posts/2022/03-27/python-on-debian/</link><guid isPermaLink="false">https://frostming.com/2022/03-27/python-on-debian/</guid><pubDate>Sun, 27 Mar 2022 21:46:03 GMT</pubDate><content:encoded>&lt;p&gt;在上周的周记中我记了一句：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;pdm 提了几个 issue，都和 debian 系统的 python 有关，i hate it&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;本文是对这句话的一个扩展。作为一个 Python 打包工具的开发者，非常痛恨 Debian 系统，所以我在回复 laixintao 时说道：
Python 打包系统的混乱，Debian 系统是要居大功的。其实不止 Debian 是如此，下面开始吐槽，非常不严谨，吐槽完我就跑。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;Python 中的 Install Scheme&lt;/h2&gt;
&lt;p&gt;Install Scheme 的概念，中文大概是翻译成「安装蓝图」？意思就是提供了一个路径的集合，告诉 Python 包的安装器（如 &lt;code&gt;pip&lt;/code&gt;），什么文件应该放到哪个路径下。
本文只打算讨论 &lt;code&gt;purelib&lt;/code&gt; 和 &lt;code&gt;platlib&lt;/code&gt;，也就是 Python 库应该放到哪里。Install Scheme 可以通过以下函数获得：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import sysconfig
install_paths = sysconfig.get_paths()
print(install_paths[&apos;purelib&apos;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正常情况下，比如&lt;a href=&quot;https://frostming.com/2019/03-13/where-do-your-packages-go/&quot;&gt;我之前的文章提到的一样&lt;/a&gt;，Python 库是放到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Posix: &lt;code&gt;$path_prefix/lib/pythonX.Y/site-packages&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Windows: &lt;code&gt;$path_prefix/Lib/site-packages&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;$path_prefix&lt;/code&gt; 是 Python 解释器所在的路径前缀。在这里我们先请优秀学生 Windows 回到座位上，来说说 Posix 的问题，比如 Python 路径是 &lt;code&gt;/usr/bin/python3.9&lt;/code&gt;，那么 &lt;code&gt;/usr&lt;/code&gt; 是路径前缀，上述路径则变为 &lt;code&gt;/usr/lib/python3.9/site-packages&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Debian：事实并非如此&lt;/h2&gt;
&lt;p&gt;基本所有的 Linux 发行版都会自己打包 Python 库，他们不信任 PyPI，所有用到的 Python 库都拿源码下来，自己打包。那问题来了，那么多 Python 包，那么多 Python 版本，怎么打，怎么安装？他们发现了一点：纯 Python 库是比较兼容的，不需要每个 Python 版本都打一个包，只需要整个 Python 3 打一个包就可以了，所以有 &lt;code&gt;python3-pip&lt;/code&gt;, &lt;code&gt;python3-requests&lt;/code&gt; 这种命名方式的包。安装的时候，也把它们放在一个地方，所有 Python 3 版本都可以用。其他不是纯 Python 的包，再分版本存放。（这段是我臆想，不严谨）&lt;/p&gt;
&lt;p&gt;所以，在 Debian 上，就有了下面三个路径存放 Python 库：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;/usr/lib/python3/dist-packages&lt;/code&gt; 放 &lt;code&gt;apt&lt;/code&gt; 安装的纯 Python 库&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/lib/python3.9/dist-packages/&lt;/code&gt; 放 &lt;code&gt;apt&lt;/code&gt; 安装的带扩展的 Python 库&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/local/lib/python3.9/dist-packages/&lt;/code&gt; 放 &lt;code&gt;pip3&lt;/code&gt; 安装的 Python 库&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这属于自己的设计了，那么就需要改安装库和导入库的逻辑。Debian 维护了&lt;a href=&quot;https://salsa.debian.org/cpython-team/python3/-/tree/master/debian/patches&quot;&gt;一系列的补丁&lt;/a&gt; 来干这件事，改完之后，&lt;code&gt;sys.path&lt;/code&gt; 会包含上面三个路径，&lt;code&gt;site-packages&lt;/code&gt; 的路径从中去除了，而 &lt;code&gt;pip3&lt;/code&gt; 也会安装包到第三个路径。所以要记住，发行版上自带的 Python 和 pip 都是&lt;strong&gt;特制的&lt;/strong&gt;，你用从官网和 PyPI 上下载的去替换是会出问题的。&lt;/p&gt;
&lt;p&gt;OK，这没问题，也能接受，反正你自己魔改自己发布到 &lt;code&gt;apt&lt;/code&gt;，只要闭环了用户是感知不到变化的。但问题是，&lt;strong&gt;这些补丁是不完备的！&lt;/strong&gt;。为了改默认的 &lt;code&gt;sys.path&lt;/code&gt;，Debian 只修改了 &lt;code&gt;site&lt;/code&gt; 模块，以及 &lt;code&gt;distutils&lt;/code&gt;[^1] 模块，而没有修改上面的 &lt;code&gt;sysconfig&lt;/code&gt; 模块，而 &lt;code&gt;distutils&lt;/code&gt; 模块已经在 &lt;a href=&quot;https://peps.python.org/pep-0632/&quot;&gt;PEP 632&lt;/a&gt; 里被 Deprecated 了，官方推荐的替代就是 &lt;code&gt;sysconfig&lt;/code&gt;。这个问题随着 Python 3.10 的发布而暴露了出来，Debian 的维护者也意识到这个问题，最新的补丁里已经修改了 &lt;code&gt;sysconfig&lt;/code&gt;，但不是很小心，造成了一些回归问题。这样的后果就是，如果用户切换发行版的不同版本，以及 Python 3.10 或 3.10 以下，这个 Install Scheme 安装到哪里完全捉摸不定，不可预测，那么包管理器就很难做了（还能怎样，当然是原谅（兼容）啊，难道威胁用户，不许用 &lt;code&gt;apt&lt;/code&gt; 的版本吗？）&lt;/p&gt;
&lt;p&gt;[^1]: &lt;code&gt;distutils.sysconfig&lt;/code&gt; 中也包含一些和 &lt;code&gt;sysconfig&lt;/code&gt; 模块中作用类似的函数。&lt;/p&gt;
&lt;h2&gt;到底有多捉摸不定？&lt;/h2&gt;
&lt;p&gt;我&lt;a href=&quot;https://github.com/frostming/debian-test/actions/runs/2048081987&quot;&gt;做了一个测试&lt;/a&gt;，分别测试 Python 3.9 和 3.10，以及 &lt;code&gt;pip&lt;/code&gt; 使用 &lt;code&gt;apt&lt;/code&gt; 的版本和 &lt;code&gt;get-pip.py&lt;/code&gt; 安装的版本，在旧版本 Debian[^2] 和 &lt;code&gt;debian:testing&lt;/code&gt; 中获取 Install Scheme 的值。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Image&lt;/th&gt;
&lt;th&gt;Python 3.9&lt;/th&gt;
&lt;th&gt;Python 3.10&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ubuntu:focal&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sysconfig&lt;/code&gt;: 未修改&amp;lt;br/&amp;gt;&lt;code&gt;site&lt;/code&gt;: 已修改&amp;lt;br/&amp;gt;&lt;code&gt;distutils&lt;/code&gt;: 已修改&amp;lt;br/&amp;gt;&lt;code&gt;get-pip.py&lt;/code&gt; 安装的 &lt;code&gt;pip&lt;/code&gt; 把库安装到 &lt;code&gt;site-packages&lt;/code&gt;下&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sysconfig&lt;/code&gt;: 已修改[^3]&amp;lt;br/&amp;gt;&lt;code&gt;site&lt;/code&gt;: 已修改&amp;lt;br/&amp;gt;&lt;code&gt;distutils&lt;/code&gt;: api 已移除&amp;lt;br/&amp;gt;&lt;code&gt;get-pip.py&lt;/code&gt; 安装 &lt;code&gt;pip&lt;/code&gt; &lt;strong&gt;失败&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;debian:testing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sysconfig&lt;/code&gt;: 未修改&amp;lt;br/&amp;gt;&lt;code&gt;site&lt;/code&gt;: 已修改&amp;lt;br/&amp;gt;&lt;code&gt;distutils&lt;/code&gt;: 已修改&amp;lt;br/&amp;gt;&lt;code&gt;get-pip.py&lt;/code&gt; 安装的 &lt;code&gt;pip&lt;/code&gt; 把库安装到 &lt;code&gt;site-packages&lt;/code&gt;下&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sysconfig&lt;/code&gt;: 已修改&amp;lt;br/&amp;gt;&lt;code&gt;site&lt;/code&gt;: 已修改&amp;lt;br/&amp;gt;&lt;code&gt;distutils&lt;/code&gt;: 已修改&amp;lt;br/&amp;gt;&lt;code&gt;get-pip.py&lt;/code&gt; 安装的 &lt;code&gt;pip&lt;/code&gt; 把库安装到 &lt;code&gt;dist-packages&lt;/code&gt;下&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这里已修改是指返回 &lt;code&gt;dist-packages&lt;/code&gt; 的路径，而未修改是指返回 &lt;code&gt;site-packages&lt;/code&gt; 的路径。测试脚本见仓库。&lt;/p&gt;
&lt;p&gt;[^2]: 因为 deadsnakes ppa 只支持 ubuntu，我在这里用的其实是 &lt;code&gt;ubuntu:focal&lt;/code&gt; 镜像。&lt;/p&gt;
&lt;p&gt;[^3]: 在 focal 上没有 Python 3.10 的系统包，所以这里是从 deadsnakes ppa 安装的。&lt;/p&gt;
&lt;p&gt;可以看到在 Python &amp;lt; 3.10 上虽然补丁不完备，行为倒还是统一的，但在 Python 3.10 上就出幺蛾子了，简直就是一团乱麻，Python 环境太难了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bonus&lt;/strong&gt;: MacOS 上的 Python，取决于它是不是 framework，安装路径也有区别，但这已经在 CPython 的标准库中得到支持，不是用补丁方式解决的。
所以大家对下面这个主张没有异议了吧：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Windows 上的 Python 环境是最清爽干净的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;如何避坑？&lt;/h2&gt;
&lt;p&gt;两条建议：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;如果要在容器中使用 Python，用 &lt;code&gt;python&lt;/code&gt; 系列的镜像&lt;/strong&gt;。那里面的 Python 是标准化的，不是特制的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如果用户系统是 Debian 系，不要在系统路径中安装包&lt;/strong&gt;，对，&lt;code&gt;--user&lt;/code&gt; 都不行，甚至 &lt;code&gt;python3-pip&lt;/code&gt; 都不要装以绝后患。而应该使用虚拟环境（&lt;code&gt;python3-venv&lt;/code&gt; 包）和 &lt;code&gt;pipx&lt;/code&gt;。或者用 PDM 或 Poetry 这种包管理工具。因为只有在虚拟环境中，Python 库的安装路径永远是 &lt;code&gt;site-packages&lt;/code&gt;，无论在哪个系统上。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;一些 GitHub issues&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/deadsnakes/issues/issues/182&quot;&gt;deadsnakes/issues#182&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pypa/pip/issues/10978&quot;&gt;pypa/pip#10978&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>利用 GitHub Action 和快捷指令解决 Logseq 的最后一米</title><link>https://frostming.com/posts/2022/03-20/logseq-journal-automation/</link><guid isPermaLink="false">https://frostming.com/2022/03-20/logseq-journal-automation/</guid><pubDate>Sun, 20 Mar 2022 21:28:52 GMT</pubDate><content:encoded>&lt;p&gt;我是一个 Logseq 的新手玩家，在使用 Logseq 的时候，有一个最大的痛点，
就是 Logseq 不能随时随地记日记(Journal)，本地化存储是 Logseq 的优点，但也造成一些不便。比如我躺上床上，在淋浴中，在运动中想到一个点子，
难道我要记到脑子里，等第二天再记到电脑上，还是我立刻打开电脑记下来呢，这两种方式对中年人都不友好。我把这个问题称作 Logseq 的最后一米问题。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;好吧不装了，我不是伊洪，但如果你读过他的&lt;a href=&quot;https://github.com/yihong0618/gitblog/issues/198&quot;&gt;巧妙利用 iOS 的快捷指令配合 GitHub Actions 实现自动化&lt;/a&gt; 这篇文章的话，应该对这个套路有所了解了。整体思路就是利用快捷指令，把要记的日志发送给 GitHub 触发一个 GitHub Action workflow，在这个 workflow 里去更新 journal 文件。当然，这样的话你本地的 Logseq 就得配置定时从 GitHub 拉取最新的 commit，麻烦是有些麻烦，但无论如何解决了我的问题。&lt;/p&gt;
&lt;h2&gt;创建 GitHub Action&lt;/h2&gt;
&lt;p&gt;在你的 Logseq 同步仓库中创建 &lt;a href=&quot;https://gist.github.com/frostming/81dbde38a97b0330e6cdcbde3068df5e&quot;&gt;&lt;code&gt;.github/workflows/add_journal.yml&lt;/code&gt;&lt;/a&gt;，提交即可。&lt;/p&gt;
&lt;h2&gt;申请 Personal Access Token&lt;/h2&gt;
&lt;p&gt;因为要往仓库里提交文件，所以要先&lt;a href=&quot;https://github.com/settings/tokens&quot;&gt;在 GitHub 上申请一个 Personal Access Token&lt;/a&gt;，token 的权限要把 &lt;code&gt;workflow&lt;/code&gt; 勾上：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/workflow_guide.png&quot; alt=&quot;workflow_guide&quot; /&gt;&lt;/p&gt;
&lt;p&gt;复制好 token 以备使用。&lt;/p&gt;
&lt;h2&gt;获得 workflow ID&lt;/h2&gt;
&lt;p&gt;直接抄伊洪的作业，利用 GitHub RESTful API 获取：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl https://api.github.com/repos/${owner}/${repo}/actions/workflows -H &quot;Authorization: token d8xxxxxxxxxx&quot; # 换成上一步复制好的token
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回的 JSON 响应中的 &lt;code&gt;workflows[0].id&lt;/code&gt; 就是 workflow ID，如果有多个 workflow 注意选择。把 ID 复制好以备使用。&lt;/p&gt;
&lt;h2&gt;设置 iOS 快捷指令&lt;/h2&gt;
&lt;p&gt;打开&lt;a href=&quot;https://www.icloud.com/shortcuts/8e06b8772e84432bb660a4fd42019a75&quot;&gt;这个链接&lt;/a&gt;把快捷指令添加到 iOS 设备上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/shortcut2.jpeg&quot; alt=&quot;shortcut2&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把 URL 中的 &lt;code&gt;{workflow_id}&lt;/code&gt; 换成上一步复制好的 workflow ID&lt;/li&gt;
&lt;li&gt;再点获取 URL 右边的 ▶️ 展开&lt;/li&gt;
&lt;li&gt;把头部中的 Authorization 设置为 &lt;code&gt;token {your_token}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;点右上角设置，添加到主屏幕&lt;/li&gt;
&lt;li&gt;关闭保存，快捷指令就设置好了&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;大功告成！&lt;/h2&gt;
&lt;p&gt;现在试试点主屏幕的「写 Journal」，填入内容，等待片刻，一条新日志就会添加到当天的 Journal 中，&lt;strong&gt;你甚至可以对 Siri 说：Hey Siri, 写 Journal&lt;/strong&gt;。&lt;/p&gt;
</content:encoded></item><item><title>在 Python 中使用 vendor 的方法</title><link>https://frostming.com/posts/2022/03-12/how-to-vendor-in-python/</link><guid isPermaLink="false">https://frostming.com/2022/03-12/how-to-vendor-in-python/</guid><pubDate>Sat, 12 Mar 2022 10:18:49 GMT</pubDate><content:encoded>&lt;p&gt;本文介绍了在 Python 库中 vendor 第三方库的正确方法。我知道这篇文章的受众非常狭窄，大部分 Python 开发者都不会也不需要用到这个技术，
但是本着分享的精神还是把它总结一二，作为软件的作者更是应该尊重所有其他库的作者的劳动。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;WHAT - vendor 是什么？&lt;/h2&gt;
&lt;p&gt;Vendor，直译供应商，在软件中（比如 C, Go 等语言中），是一种把第三方库的代码直接内嵌到软件中的方式。
它不同于通过依赖文件指定的方式，第三方库的代码是直接包含在软件中的，有可能原样保留也有可能经过修改，所以需要注意各种 License 的限制，
特别是如果上游库采用了 GPL 系列的协议，使用 vendor 的软件也是要受到传染的。&lt;/p&gt;
&lt;h2&gt;WHY - Python 中什么时候要用到 vendor？&lt;/h2&gt;
&lt;p&gt;正如我开头说的，适用范围非常狭窄，有三种场景：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;软件特性限制其必须是自包含，零依赖的。&lt;/p&gt;
&lt;p&gt;在 Python 的世界中，最重度使用 vendor 的库就是我们天天都要用的 &lt;code&gt;pip&lt;/code&gt;。&lt;code&gt;pip._vendor&lt;/code&gt; 中包含了 25 个依赖。&lt;code&gt;pip&lt;/code&gt; 是现行标准的 Python 安装器，所以它不能
有任何依赖，否则为了装 &lt;code&gt;pip&lt;/code&gt;，要先装这些依赖，而这些依赖又只能通过 &lt;code&gt;pip&lt;/code&gt; 安装，这就递归了。除此之外，还包括像 &lt;code&gt;setuptools&lt;/code&gt; 这样的基础构建工具。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;软件依赖某上游库的&lt;strong&gt;特定版本&lt;/strong&gt;。这还包含上游库频繁 breaking change，导致 API 不稳定的情况。如果简单地在依赖中指定 &lt;code&gt;third-party-lib==1.0.0&lt;/code&gt;，
会导致与之共存的同样依赖此库的软件无法解析版本，造成依赖冲突。而如果转成 vendor，就相当于把这个非常严格的依赖限制去掉了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;软件需要对上游库作一些变更，而由于上游库的维护问题，这些变更无法通过 PR 等方式合入上游并发布。在符合开源协议约束的情况下，可以通过 vendor 把源代码嵌入到软件中
并自行修改。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其实，针对上述的第 2、3 种场景，也不是非 vendor 不可。除了 vendor，还可以 fork 到自己的 git 仓库，再使用 &lt;a href=&quot;https://pip.pypa.io/en/stable/topics/vcs-support&quot;&gt;git 依赖&lt;/a&gt; 引入，或者发布为一个&lt;a href=&quot;https://pypi.org/project/click8/&quot;&gt;新的 PyPI 包&lt;/a&gt;。只是 vendor 是一个最轻松的方式。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;还有一个限制条件：对 Python 来说，只有纯 Python 的库才能 vendor。&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;HOW - 应该如何 vendor？&lt;/h2&gt;
&lt;p&gt;vendor 并不是简单地复制粘贴这种传统艺能就解决了的，在我看来，它还要注意以下两点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;vendor 必须要遵守开源协议，并把协议文件也放到 vendor 目录中。&lt;/li&gt;
&lt;li&gt;对源代码有修改时，需要记录 patch 文件，以便时机成熟时，反馈回上游。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以，vendor 并不是复制粘贴，只是在开源框架下对现状的一种妥协，我们最终的目标，是消灭 vendor。&lt;/p&gt;
&lt;p&gt;在 Python 中，除了把 vendor 库都放到代码库下一个目录中（比如 &lt;code&gt;mypackage/vendor&lt;/code&gt;）以外，还需要修改所有的 import 语句，指向到这个目录中。
比如，把 &lt;code&gt;import requests&lt;/code&gt; 改成 &lt;code&gt;from mypackage.vendor import requests&lt;/code&gt;。PDM 中也包含了这样一个目录，我是使用和 &lt;code&gt;pip&lt;/code&gt; 相同的工具来管理 vendor 的。
这个工具是 &lt;a href=&quot;https://pypi.org/project/vendoring/&quot;&gt;vendoring&lt;/a&gt;，文档很少（因为就没人要用）。它包含以下几个功能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;读取一个 &lt;code&gt;requirements.txt&lt;/code&gt; 下载依赖到指定目录&lt;/li&gt;
&lt;li&gt;下载所有库的 LICENSE 文件到这个目录中&lt;/li&gt;
&lt;li&gt;从一个指定路径读取 patch 文件并应用到源代码中&lt;/li&gt;
&lt;li&gt;重写所有 import 语句，指向到 vendor 目录中&lt;/li&gt;
&lt;li&gt;更新 vendor 的版本&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;使用过程，也大致按上面的步骤。首先建立一个 &lt;code&gt;mypackage/vendor&lt;/code&gt; 目录，在其中创建一个 &lt;code&gt;vendors.txt&lt;/code&gt;，填写依赖（&lt;code&gt;requirements.txt&lt;/code&gt; 格式）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;requests==2.24.1
click==8.0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在项目根路径下的 &lt;code&gt;pyproject.toml&lt;/code&gt; 中，添加以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[tool.vendoring]
destination = &quot;mypackage/vendor/&quot; # vendor目录路径
requirements = &quot;mypackage/vendor/vendors.txt&quot; # requirements路径
namespace = &quot;mypackage.vendor&quot; # import 重命名前缀
protected-files = [
  &quot;__init__.py&quot;,
  &quot;README.md&quot;,
  &quot;vendors.txt&quot;
] # 每次重新 vendor 时需要保留的文件
patches-dir = &quot;tasks/patches&quot; # patch 文件目录

[tool.vendoring.transformations]
substitute = [ # 重命名没有覆盖到的 import，文件替换规则
  { match = &apos;__import__(&quot;requests&quot;)&apos;, replace = &apos;__import__(&quot;mypackage.vendor.requests&quot;)&apos; }
]
drop = [ # 需要从 vendor 库中去除的文件
  &quot;bin/&quot;,
  &quot;*.so&quot;,
  &quot;typing.*&quot;,
  &quot;*/tests/&quot;
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后运行 &lt;code&gt;vendoring sync&lt;/code&gt;，就会自动把 vendor 全部准备好了。&lt;/p&gt;
&lt;p&gt;对于 patch 文件，其实就是 &lt;code&gt;git diff&lt;/code&gt; 的输出，有了这个文件，git 就可以从源代码重新建立 vendor 目录。生成方法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;配置好以后跑一次 &lt;code&gt;vendoring sync&lt;/code&gt;，把文件提交到本地仓库（只 commit 不 push）&lt;/li&gt;
&lt;li&gt;修改源代码&lt;/li&gt;
&lt;li&gt;运行 &lt;code&gt;git diff --patch &amp;lt;file_path&amp;gt; &amp;gt; &amp;lt;patches_dir&amp;gt;/&amp;lt;file_name&amp;gt;.patch&lt;/code&gt;，把 patch 文件保存到 &lt;code&gt;patches_dir&lt;/code&gt; 中&lt;/li&gt;
&lt;li&gt;审核 patch 文件，把其中已经被修改的 import 语句恢复成原始的 import 语句，比如 &lt;code&gt;from mypackage.vendor import requests&lt;/code&gt; 改成 &lt;code&gt;import requests&lt;/code&gt;[^1]&lt;/li&gt;
&lt;li&gt;运行 &lt;code&gt;git add . &amp;amp;&amp;amp; git commit --amend&lt;/code&gt;，提交修改&lt;/li&gt;
&lt;li&gt;再次运行 &lt;code&gt;vendoring sync&lt;/code&gt; 验证一下，如果一切正常，应该不会产生任何变更，说明这个 vendor 过程是 reproducible 的&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;[^1]: 至于为何要这么做，因为 apply patch 是先于 import 重写的，所以 patch 文件中，应该都是未重写的 import 语句。修改时要注意不要改动任何空白字符，patch 文件对空白是敏感的。&lt;/p&gt;
</content:encoded></item><item><title>PyCon SZ Meetup: PDM - Python 打包的新体验</title><link>https://frostming.com/posts/2021/12-21/pdm-talk-pyconsz/</link><guid isPermaLink="false">https://frostming.com/2021/12-21/pdm-talk-pyconsz/</guid><pubDate>Tue, 21 Dec 2021 10:17:45 GMT</pubDate><content:encoded>&lt;p&gt;这是我在今年 11 月 PyCon China Shenzhen Meetup 上做的线下演讲。由于之前我在线上的会议上介绍了 Python 打包的历史进程，所以这次我打算
厚颜无耻地&lt;strong&gt;单纯&lt;/strong&gt;给 PDM 打广告，原本就是打算现场 demo，没怎么准备。结果到了会场之后发现播放设备和讲者是分离的，我临时在会场录制了一个 demo。
线上和线下演讲体验相差很大，线下虽然不会和录制的演讲一样语气机械化，但也会造成一直嘴瓢，很多下意识的连接词。&lt;/p&gt;
&lt;p&gt;仓促准备，错误在所难免，姑且一看，对于了解 PDM 还是很有帮助的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;视频:&lt;/strong&gt; https://www.bilibili.com/video/BV1A3411x7Yv?t=10604.1 &lt;br /&gt;
&lt;strong&gt;Slides:&lt;/strong&gt; https://slides.fming.dev/pdm/&lt;/p&gt;
</content:encoded></item><item><title>argparse 的高级用法</title><link>https://frostming.com/posts/2021/11-23/advanced-argparse/</link><guid isPermaLink="false">https://frostming.com/2021/11-23/advanced-argparse/</guid><pubDate>Tue, 23 Nov 2021 15:44:57 GMT</pubDate><content:encoded>&lt;p&gt;Python 里的 &lt;code&gt;argparse&lt;/code&gt; 大家都不陌生，是用来解析命令行参数的标准库，它的用法大致是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import argparse

parser = argparse.ArgumentParser(description=&apos;Greet to some body&apos;)
parser.add_argument(
    &apos;-n&apos;, &apos;--name&apos;, default=&apos;John Doe&apos;, help=&apos;name of the person to greet&apos;)

args = parser.parse_args()
print(f&apos;Hello, {args.name}!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这非常地平凡，如果用 &lt;code&gt;click&lt;/code&gt; 的话，可以更快地写出来相同的功能：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import click

@click.command()
@click.option(&quot;-n&quot;, &quot;--name&quot;, default=&quot;John Doe&quot;, help=&quot;name of the person to greet&quot;)
def cli(name):
    print(f&apos;Hello, {name}!&apos;)

cli()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两者的区别在于 &lt;code&gt;argparse&lt;/code&gt; 是统一解析得到参数值再自己处理，而 &lt;code&gt;click&lt;/code&gt; 可以直接把参数值传给装饰的函数。后者的方式更有利于代码解耦，更容易维护。&lt;/p&gt;
&lt;p&gt;我在做 PDM 的时候最初也是选择的&lt;code&gt;click&lt;/code&gt;，PDM 的命令行有一系列的子命令，而 &lt;code&gt;click&lt;/code&gt; 的嵌套命令组（&lt;code&gt;click.Group&lt;/code&gt;）也提供了强大的支持，帮助我很好地完成了这个工作。
然而当我更深入地写下去，试图加一些更复杂的功能时，我发现了 &lt;code&gt;click&lt;/code&gt; 的不足之处，并促使我最终选择了 &lt;code&gt;argparse&lt;/code&gt;，到目前看来 &lt;code&gt;argparse&lt;/code&gt; 提供的能力能很好地胜任工作。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;继承和扩展&lt;/h2&gt;
&lt;p&gt;假如我们已经用 &lt;code&gt;click&lt;/code&gt; 写了下面这样的命令行界面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# bot.py
import click

@click.group()
def cli():
    pass

@cli.command()
@click.option(&quot;-n&quot;, &quot;--name&quot;, default=&quot;John Doe&quot;, help=&quot;name of the person to greet&quot;)
def greet(name):
    print(f&apos;Hello, {args.name}!&apos;)

@cli.command()
@click.option(&quot;-n&quot;, &quot;--name&quot;, default=&quot;John Doe&quot;, help=&quot;name of the person to say goodbye&quot;)
def goodbye(name):
    print(f&apos;Goodbye, {args.name}!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个命令行包含两个子命令 &lt;code&gt;greet&lt;/code&gt; 和 &lt;code&gt;goodbye&lt;/code&gt;，
现在我把这个 bot 库发布了，并希望用户能在这个基础上添加新的命令，用 &lt;code&gt;click&lt;/code&gt;，这很容易：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# test.py
from bot import cli

@cli.command()
def test():
    print(&apos;test&apos;)

cli()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在这个命令行就新增了一个 &lt;code&gt;test&lt;/code&gt; 子命令了。这就是 &lt;a href=&quot;https://flask.palletsprojects.com/en/1.1.x/cli/&quot;&gt;Flask CLI&lt;/a&gt; 的扩展方法。
但是我想在这个基础上，还想提供新增&lt;strong&gt;命令选项&lt;/strong&gt;的功能，比如在原来的 &lt;code&gt;greet&lt;/code&gt; 命令上加一个 &lt;code&gt;--verbose&lt;/code&gt; 选项，如果为真就啰嗦地问好，否则简洁地问好。这如何做到呢？
这就涉及到给原来 &lt;code&gt;greet&lt;/code&gt; 函数加一个参数，并改变函数的行为读取这个参数。查看 API 文档得知这个函数保存在生成的 &lt;code&gt;Command&lt;/code&gt; 对象的 &lt;code&gt;callback&lt;/code&gt; 属性上，那我只能写
一个新函数，替换掉它，那么如果我不想把原函数抄一遍，只想继承和扩展，那就只能把原函数保留好，并在新函数里调用它。&lt;/p&gt;
&lt;p&gt;这整个流程，在我看来，无异于 Monkey patch，在一个支持 OOP 的语言里，本不应该如此，于是我就开始寻找其他的替代方案。
当然，最后我找到了 &lt;code&gt;argparse&lt;/code&gt;，下面说说我是怎么用 &lt;code&gt;argparse&lt;/code&gt; 实现 PDM 的命令行界面的。&lt;/p&gt;
&lt;h2&gt;argparse 的进击&lt;/h2&gt;
&lt;h3&gt;argparse 的子命令&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;argparse&lt;/code&gt; 也是支持子命令的，而且子命令也可有自己的子命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;parser = argparse.ArgumentParser()

subparsers = parser.add_subparsers()
greet_parser = subparsers.add_parser(&apos;greet&apos;)
greet_parser.add_argument(&apos;-n&apos;, &apos;--name&apos;, default=&apos;John Doe&apos;, help=&apos;name of the person to greet&apos;)
goodbye_parser = subparsers.add_parser(&apos;goodbye&apos;)
goodbye_parser.add_argument(&apos;-n&apos;, &apos;--name&apos;, default=&apos;John Doe&apos;, help=&apos;name of the person to say goodbye&apos;)
args = parser.parse_args()
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这看上去比 &lt;code&gt;click&lt;/code&gt; 费劲多了，而且还只是拿到解析结果，没有处理，但这个缺点也让 &lt;code&gt;argparse&lt;/code&gt; 更加灵活，我们可以控制它如何找到对应的处理方法。
继承和扩展，这不就是 OOP 的思想吗？那么我是不是可以把这个面条型的代码改成 OOP 的呢？&lt;/p&gt;
&lt;h3&gt;argparse 的 OOP 化&lt;/h3&gt;
&lt;p&gt;原则是把每个一个子命令放到它自己的类里面，我把上面的这个代码分离一下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 根命令相关
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
# 子命令greet相关
greet_parser = subparsers.add_parser(&apos;greet&apos;)
greet_parser.add_argument(&apos;-n&apos;, &apos;--name&apos;, default=&apos;John Doe&apos;, help=&apos;name of the person to greet&apos;)
# 子命令goodbye相关
goodbye_parser = subparsers.add_parser(&apos;goodbye&apos;)
goodbye_parser.add_argument(&apos;-n&apos;, &apos;--name&apos;, default=&apos;John Doe&apos;, help=&apos;name of the person to say goodbye&apos;)
# 根命令相关
args = parser.parse_args()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到中间两个子命令的写法高度一致，只有一个操作，就是 &lt;code&gt;add_argument&lt;/code&gt;，那么我把这个方法放到要实现的子命令类里面，并利用一些
&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC&quot;&gt;IoC&lt;/a&gt; 的技巧，得到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Command:
    &quot;&quot;&quot;基类&quot;&quot;&quot;
    def add_arguments(self, parser):
        pass  # 可以不实现，即不包含任何参数

class GreetCommand(Command):
    &quot;&quot;&quot;greet 命令实现&quot;&quot;&quot;
    def add_arguments(self, parser):
        parser.add_argument(&apos;-n&apos;, &apos;--name&apos;, default=&apos;John Doe&apos;, help=&apos;name of the person to greet&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是根解析器中的挂载方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
for name, command in subcommands.items():  # type: Dict[str, Type[Command]]
    cmd_instance = command()
    subparser = subparsers.add_parser(name)
    # subparser 是一个和 parser 一样的解析器对象
    cmd_instance.add_arguments(subparser)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我实例化了 &lt;code&gt;command&lt;/code&gt;，而没有直接用 &lt;code&gt;classmethod&lt;/code&gt;，是方便在实例化的时候传入一些与根解析器相关的信息。
这样我就实现了命令解析的解耦，与子命令有关的参数在自己的类中的 &lt;code&gt;add_argument&lt;/code&gt; 添加就可以了。&lt;/p&gt;
&lt;h3&gt;处理方法的路由&lt;/h3&gt;
&lt;p&gt;现在我们只是实现了子命令的参数添加，但还需要针对不同的子命令选择不同的处理方法。暂时还不知道怎样做，不管，先把这个方法放到 &lt;code&gt;Command&lt;/code&gt; 类里面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Command:
    &quot;&quot;&quot;基类&quot;&quot;&quot;
    ...
    def handle(self, args):
        pass  # 可以不实现，即不做任何处理

class GreetCommand(Command):
    ...
    def handle(self, args):
        print(f&apos;Hello {args.name}!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;怎样在解析到这个子命令的时候路由到这个子命令的处理方法呢？这得了解 &lt;code&gt;argparse&lt;/code&gt; 的解析过程。
&lt;code&gt;argparse&lt;/code&gt; 是拿到 &lt;code&gt;sys.argv&lt;/code&gt; 之后按顺序看，如果找到一个参数就把结果中对应这个参数的值赋好，如果找到一个子命令的名称则取得这个子命令的解析器
递归调用这个解析器去解析剩下的命令行参数。也就是说如果没有匹配到这个子命令是不会执行任何该子命令的相关动作，也不会把这个子命令的参数加入到解析器中。
而相同层级的子命令必然是互斥的，不可能存在同时匹配到多个子命令的情况。比如 &lt;code&gt;python cli.py greet goodbye&lt;/code&gt; 匹配到的是 &lt;code&gt;greet&lt;/code&gt; 命令，而 &lt;code&gt;goodbye&lt;/code&gt;
会被当作 &lt;code&gt;greet&lt;/code&gt; 的参数在 &lt;code&gt;greet&lt;/code&gt; 自己的解析器中解析。&lt;/p&gt;
&lt;p&gt;那么我们可以在匹配到这个子命令时，把它的处理方法保存到解析结果里，就可以了。只需要稍微修改一下子命令的挂载过程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for name, command in subcommands.items():
    cmd_instance = command()
    subparser = subparsers.add_parser(cmd_instance.name)
    subparser.set_defaults(handle=cmd_instance.handle)
    cmd_instance.add_arguments(subparser)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里通过 &lt;code&gt;set_defaults&lt;/code&gt; 把 handle 的值设置为 &lt;code&gt;cmd_instance.handle&lt;/code&gt;，它的作用是如果在解析完以后结果中没有 &lt;code&gt;handle&lt;/code&gt;，则它的值为 &lt;code&gt;cmd_instance.handle&lt;/code&gt;。
并且这个行为只有在解析到这个子命令的时候才会生效，因为它是作用在 &lt;code&gt;subparser&lt;/code&gt; 上的。&lt;/p&gt;
&lt;p&gt;那么最后的处理逻辑就非常自然了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;args = parser.parse_args()
if hasatter(args, &apos;handle&apos;):
    args.handle(args)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;参数复用&lt;/h3&gt;
&lt;p&gt;有了 OOP 的利器，我就可以来减少一些重复代码了。注意到 &lt;code&gt;greet&lt;/code&gt; 和 &lt;code&gt;goodbye&lt;/code&gt; 都有一个 &lt;code&gt;-n/--name&lt;/code&gt; 参数，类型是一样的。添加参数是在 &lt;code&gt;add_argument&lt;/code&gt; 里做的，
再次 IoC 一下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Argument:
    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs

    def add_to_parser(self, parser):
        parser.add_argument(*self.args, **self.kwargs)

name_option = Argument(&quot;-n&quot;, &quot;--name&quot;, help=&quot;name of the person&quot;, default=&quot;John Doe&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再进一步，在 &lt;code&gt;Command&lt;/code&gt; 类中添加类属性 &lt;code&gt;arguments&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Command:
    arguments = [name_option]
    def add_arguments(self, parser):
        for arg in self.arguments:
            arg.add_to_parser(parser)
        self.subcommand_add_arguments(parser)

    def subcommand_add_arguments(self, parser):
        # 原来的add_arguments改名为此函数
        pass
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;升级后的 argparse 用法&lt;/h2&gt;
&lt;p&gt;现在回到我开始的需求，继承与扩展，如果我要新增一个子命令，只需要继承基类 &lt;code&gt;Command&lt;/code&gt;，实现 &lt;code&gt;subcommands_add_arguments&lt;/code&gt; 和 &lt;code&gt;handle&lt;/code&gt; 方法，
再添加到 &lt;code&gt;subcommands&lt;/code&gt; 中就可以了（添加的方法会暴露出来）。&lt;/p&gt;
&lt;p&gt;而若要在已有的命令上做修改，只需要继承原有的命令类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyGreetCommand(GreetCommand):
    def subcommand_add_arguments(self, parser):
        super().subcommand_add_arguments(parser)
        parser.add_argument(&quot;-t&quot;, &quot;--test&quot;, action=&quot;store_true&quot;, help=&quot;run under test environment&quot;)

    def handle(self, args):
        if args.test:
            print(&quot;I&apos;m under testing, no time to greet&quot;)
        else:
            super().handle(args)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;挂载的时候还用原来的命令名 &lt;code&gt;greet&lt;/code&gt; 就可以覆盖原有的命令了。&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;我们利用了 Python 的动态特性，加上合理的技巧（IoC）实现了 &lt;code&gt;argparse&lt;/code&gt; 的 OOP 化。
PDM 就是使用了这个方法实现了可扩展的命令行解析，完整的命令类在
&lt;a href=&quot;https://github.com/pdm-project/pdm/tree/main/src/pdm/cli/commands&quot;&gt;pdm/cli/commands&lt;/a&gt;，命令解析的组装过程在
&lt;a href=&quot;https://github.com/pdm-project/pdm/blob/main/src/pdm/core.py&quot;&gt;pdm/core.py&lt;/a&gt; 可以看到。其实 &lt;code&gt;pip&lt;/code&gt; 和 &lt;code&gt;Django&lt;/code&gt; 的命令行写法
也类似，只是实现不尽相同。另外，在理解了 &lt;code&gt;argparse&lt;/code&gt; 的工作方式之后，
我也得以向 CPython 提交了我的&lt;a href=&quot;https://github.com/python/cpython/pull/29574&quot;&gt;第一个贡献&lt;/a&gt;。&lt;/p&gt;
</content:encoded></item><item><title>PyCon China 2021 演讲——Python 打包 101</title><link>https://frostming.com/posts/2021/10-20/pycon-china-2021/</link><guid isPermaLink="false">https://frostming.com/2021/10-20/pycon-china-2021/</guid><pubDate>Wed, 20 Oct 2021 14:43:07 GMT</pubDate><content:encoded>&lt;p&gt;我在今年的&lt;a href=&quot;https://cn.pycon.org/2021&quot;&gt;PyCon China 2021&lt;/a&gt;上在线做了一个演讲，题目是「Python 打包 101」。Python 打包是个每个 Python 开发都可能用到并且需要了解的知识。演讲试图从 Python 打包的历史进程开始，阐述 pip install 背后发生的过程，同时介绍当前 Python 打包的最佳实践。&lt;/p&gt;
&lt;p&gt;同是我是分会场的主持，为了能减轻负担我选择了录视频，这也是我第一次录视频，有几个体会：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;自己回放时感觉有点拿腔拿调，和平常说话不一样；&lt;/li&gt;
&lt;li&gt;我是事先准备了讲稿，讲下来发现会比现场讲快不少。这段演讲如果我现场讲应该要 30 分钟；&lt;/li&gt;
&lt;li&gt;中间如果出现错误，做一个停顿再重新开始，后期剪辑很方便，所以不会出现要求高导致录了好几遍的情况，那样的话对讲者本人是个折磨。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;视频:&lt;/strong&gt; https://www.bilibili.com/video/BV1444y1v7Jh?share_source=copy_web &lt;br /&gt;
&lt;strong&gt;Slides:&lt;/strong&gt; https://slides.fming.dev/python-packaging&lt;/p&gt;
</content:encoded></item><item><title>再谈 Python 中的继承（译）</title><link>https://frostming.com/posts/2021/07-30/python-subclassing-redux-cn/</link><guid isPermaLink="false">https://frostming.com/2021/07-30/python-subclassing-redux-cn/</guid><pubDate>Fri, 30 Jul 2021 17:32:28 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;本文是 &lt;a href=&quot;https://hynek.me/articles/python-subclassing-redux/&quot;&gt;Subclassing in Python Redux&lt;/a&gt; 的中文版。在阅读的过程中，我发现与我的「友好的 Python」不谋而合，故向作者请求翻译此文。版权归原作者 Hynek Schlawack 所有。除非特别说明，本文所有的「我」均指原作者 Hynek。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;继承与组合之间的冲突就和面向对象编程一样古老。一些最新的语言，如 &lt;em&gt;Go&lt;/em&gt; 和 &lt;em&gt;Rust&lt;/em&gt;，证明了你不需要继承也能编写代码。但是具体在 Python 语言中，有什么实用的继承的方法呢？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;任何长期关注我的人都知道，我是坚定地站在组合而非继承的阵营。然而 &lt;strong&gt;Python 设计如此，有时如果不用继承，你就无法写出惯常的代码&lt;/strong&gt;。我写这篇文章的目的就是思考这个问题，这个「有时」是何时，并解开我对这个问题的直觉[^1]。&lt;/p&gt;
&lt;p&gt;[^1]: 首先明确我不打算谈论标准库的 API。诚然，&lt;code&gt;SimpleHTTPServer&lt;/code&gt; 要求你必须继承，但这是一个 API 的选择，并不是 Python 的固有设计。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我知道这篇文章很长。事实上，这是我自 2006 年毕业论文以来写的最长的一篇文章。客观地说，我应该把它分成至少三个部分。这样会更有利于（SEO！社交媒体！点击率！），也更有可能让人们真正读到最后。&lt;/p&gt;
&lt;p&gt;但我还是希望它单独成篇。我希望它是我多年来所学的精髓的提炼。当然，你想停下来就停下来，下次再读——反正这篇文章就在这里。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们先从细微之处开始。为什么许多关于继承的讨论如此令人沮丧，没有结果？其中一个原因是继承的类型不止一种。正如那篇精彩的文章&lt;a href=&quot;https://www.sicpers.info/2018/03/why-inheritance-never-made-any-sense/&quot;&gt;《为什么继承没有任何意义》&lt;/a&gt;所解释的&lt;a href=&quot;%E8%99%BD%E7%84%B6%E5%BE%88%E6%9C%89%E8%A7%81%E5%9C%B0%EF%BC%8C%E4%BD%86%E9%93%BE%E6%8E%A5%E7%9A%84%E6%96%87%E7%AB%A0%E5%8F%AF%E8%83%BD%E9%9A%BE%E4%BB%A5%E7%90%86%E8%A7%A3%EF%BC%8C%E5%9B%A0%E4%B8%BA%E5%AE%83%E4%BD%BF%E7%94%A8%E7%9A%84%E9%BB%91%E8%AF%9D%E5%8F%AF%E8%83%BD%E5%AF%B9%E4%BD%A0%E5%BE%88%E9%99%8C%E7%94%9F%EF%BC%8C%E8%BF%99%E5%8F%96%E5%86%B3%E4%BA%8E%E4%BD%A0%E5%AF%B9%E5%85%B6%E4%BB%96%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E7%9A%84%E7%BB%8F%E9%AA%8C%E3%80%82%E4%BD%A0%E4%B8%8D%E9%9C%80%E8%A6%81%E9%98%85%E8%AF%BB%E5%AE%83%E6%9D%A5%E7%90%86%E8%A7%A3%E8%BF%99%E7%AF%87%E5%8D%9A%E6%96%87%E3%80%82%E5%8F%A6%E4%B8%80%E6%96%B9%E9%9D%A2%EF%BC%8C%E8%AF%BB%E5%AE%8C%E8%BF%99%E7%AF%87%E5%8D%9A%E6%96%87%E5%90%8E%E5%8F%AF%E8%83%BD%E4%BC%9A%E6%9B%B4%E5%AE%B9%E6%98%93%E7%90%86%E8%A7%A3%E3%80%82&quot;&gt;^2&lt;/a&gt;，&lt;strong&gt;继承有有三种类型，不应混为一谈&lt;/strong&gt;——无论你对继承有什么看法。&lt;/p&gt;
&lt;p&gt;这有可能造成三类人互相争论，每个人都认为自己的方式正确，永远不去找共同点。我们都看到过这些讨论是如何展开的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;根据我的经验，如果严格分开使用——一种是好的，一种是可选但有用的，还有一种是差的。继承的大多数问题都源于我们试图同时使用一种以上的继承类型——或者在对象设计中重点使用差的那种类型。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在所有情况下，&lt;strong&gt;你都要牺牲阅读的便利来换取写代码的便利&lt;/strong&gt;。这不一定是坏事，软件设计在于权衡，你可以得出结论，在某些情况下这是完全值得的。这就是为什么&lt;strong&gt;我不指望你会赞同下面的所有内容&lt;/strong&gt;，但我希望能引发一些思考，以帮助你在未来做出决定。&lt;/p&gt;
&lt;p&gt;但现在絮叨到此为止，让我们看看这三种类型。从差的那一种开始。&lt;/p&gt;
&lt;h2&gt;类型一：代码共享&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Namespaces are one honking great idea – let’s do more of those!&lt;/em&gt; &amp;lt;cite&amp;gt;— Tim Peters，Python 之禅&amp;lt;/cite&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;大多数对继承的批评来自于代码共享，这是理所当然的。我不觉得我有什么可以补充的，所以我打算直接放几个比我更睿智和更杰出的作品链接：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://python-patterns.guide/gang-of-four/composition-over-inheritance/&quot;&gt;组合优于继承原则&lt;/a&gt;，Brandon Rhodes,&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=3MNVP9-hglc&quot;&gt;对象继承的终结，新模块化的开始&lt;/a&gt;，Augie Fackler 与 Nathaniel Manista，PyCon US 2013,&lt;/li&gt;
&lt;li&gt;以及 &lt;a href=&quot;https://www.youtube.com/watch?v=OMPfEXIlTVE&quot;&gt;无招胜有招&lt;/a&gt;，Sandi Metz，RailsConf 2015.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;简而言之，总体上有三个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不止一个轴上的变化&lt;/strong&gt;。这是，Brandon 的文章和 Sandi 演讲的后半部分的主要思想。这不容易解释，所以我将直接引用他们的作品，但其本质是：如果你想定制一个类的多于一个行为方面，通过继承共享代码是行不通的。它会导致&lt;a href=&quot;https://python-patterns.guide/gang-of-four/composition-over-inheritance/#problem-the-subclass-explosion&quot;&gt;子类爆炸&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;这不是一种观点或一种权衡。这是一个事实。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;类和实例命名空间混淆&lt;/strong&gt;。如果你在一个继承自一个或多个基类的类中有一个属性 &lt;code&gt;self.x&lt;/code&gt;，那么你就需要研究并耗费脑力来找出 &lt;code&gt;x&lt;/code&gt; 的来源。阅读代码时如此，调试时也如此。&lt;/p&gt;
&lt;p&gt;这也意味着总是存在这样的危险：在同一层次结构中的两个类，它们彼此不认识，却拥有一个同名的属性。虽然 Python 有双下划线前缀（&lt;code&gt;__x&lt;/code&gt;）的概念来处理这种情况，但这被认为是不可取的，因为它更像是君子协定。&lt;/p&gt;
&lt;p&gt;问题在于，&lt;strong&gt;如果不是各方面都知情，就不可能达成知情共识&lt;/strong&gt;。这个问题在多重继承及其极端形式&lt;a href=&quot;https://en.wikipedia.org/wiki/Mixin&quot;&gt;混入（mixin)&lt;/a&gt;中加倍地恶化。你依赖那些你可能无法控制的、彼此不了解的类，在同一个命名空间中共存。&lt;/p&gt;
&lt;p&gt;另一个问题是，&lt;strong&gt;你无法控制从基类暴露给用户的方法和属性&lt;/strong&gt;。它们就在那里，污染了你的 API。随着时间的推移，你的基类不断发展，添加或重命名方法和属性，变化可能在发生。这就是 &lt;code&gt;attrs&lt;/code&gt;（以及最终的 &lt;code&gt;dataclasses&lt;/code&gt;）选择使用类装饰器而不是子类的其中一个原因：你必须慎重对待你附加到类中的东西，以防不小心把一些东西泄露给所有的子类。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;从属关系不明&lt;/strong&gt;。这是前一个问题的一个特例，也是 Augie 和 Nathaniel 演讲的重点。如果每个方法都在 &lt;code&gt;self&lt;/code&gt; 上，那么在看调用的时候就搞不清它来自哪里了。除非你非常仔细，否则每一次尝试理解控制流都会以捕风捉影而告终。一旦涉及到多重继承，你最好阅读一下 &lt;a href=&quot;https://www.python.org/download/releases/2.3/mro/&quot;&gt;MRO&lt;/a&gt; 和 &lt;a href=&quot;https://docs.python.org/3/library/functions.html#super&quot;&gt;&lt;code&gt;super()&lt;/code&gt;&lt;/a&gt; 的内容。我认为，如果一个可概括为「&lt;code&gt;super()&lt;/code&gt; 是干什么用的？」的&lt;a href=&quot;https://stackoverflow.com/questions/576169/understanding-python-super-with-init-methods&quot;&gt;问题&lt;/a&gt;能在 StackOverflow 上得到近 3000 次的赞和超过 1000 个收藏，那就有些不对劲了。&lt;/p&gt;
&lt;p&gt;如果你构建的 API 需要继承来实现或覆盖已有方法并能在其他地方调用，那么所有这些都会变得更加麻烦。&lt;em&gt;Twisted&lt;/em&gt; 和 &lt;em&gt;asyncio&lt;/em&gt; 都分别在他们的 &lt;code&gt;Protocol&lt;/code&gt;[^3] 类中犯了这些错，给我留下了永久的阴影。最常见的问题是，要找出哪些方法是存在的（尤其是在像 &lt;a href=&quot;https://twistedmatrix.com/documents/current/api/twisted.internet.protocol.Protocol.html&quot;&gt;&lt;em&gt;Twisted&lt;/em&gt;&lt;/a&gt; 这样的深层次结构中）非常麻烦，以及如果你方法的名字错了一点点，基类找不到，往往会静默地失败[^4]。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;「基于继承的设计也是一个巨大的错误」可能是编程中最常说的一句话。&amp;lt;cite&amp;gt;— Cory Benfield 的推特&amp;lt;/cite&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;[^3]: 与下面要谈的 &lt;code&gt;typing.Protocol&lt;/code&gt; 完全无关。&lt;/p&gt;
&lt;p&gt;[^4]: 我在这里点出 Twisted，因为当我们意识到我们的错误时，我就是核心团队的一员。这是一个公认的错误，而不是隐藏的。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;只有当我需要改变一个不受我控制的类的行为时，我才使用继承来共享代码。我认为这是一种不那么恶劣的&lt;a href=&quot;https://en.wikipedia.org/wiki/Monkey_patch&quot;&gt;猴子补丁（monkeypatch）&lt;/a&gt; 的方式。通常情况下，用适配器、外观、代理或装饰器模式会更好，但在有些情况下，如果你只想改变一个小的细节，你需要委托的方法数量会让你抓狂。&lt;/p&gt;
&lt;p&gt;在任何情况下，都&lt;strong&gt;不要&lt;/strong&gt;把它当成你设计中的核心部分。&lt;/p&gt;
&lt;h2&gt;类型二：抽象数据类型（接口）&lt;/h2&gt;
&lt;p&gt;抽象数据类型（ADT）主要是为了收紧接口协议。你可以说你想要一个具有某些属性、方法的对象，而不关心其他的。在许多语言中，它们被称为接口，听起来没有那么高大上，所以我从现在开始将使用这个术语。&lt;/p&gt;
&lt;p&gt;由于 Python 是动态类型的语言，而且类型注解是可选的，所以&lt;strong&gt;你不需要正式的接口&lt;/strong&gt;。然而，有一种明确定义接口的方法还是非常有帮助的，你需要它来使一段代码发挥作用。而且，自从 &lt;a href=&quot;http://www.mypy-lang.org/&quot;&gt;Mypy&lt;/a&gt; 这样的类型检查器出现后，它们已经成为某种&lt;strong&gt;经过验证的 API 文档&lt;/strong&gt;，我觉得这很好。&lt;/p&gt;
&lt;p&gt;例如，你想写一个函数，接受具有 &lt;code&gt;read()&lt;/code&gt; 方法的对象，你将以某种方式定义一个具有该方法的接口 &lt;code&gt;Reader&lt;/code&gt;（下面即将解释如何定义），并像这样使用它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def printer(r: Reader) -&amp;gt; None:
    print(r.read())

printer(FooReader())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你的 &lt;code&gt;print()&lt;/code&gt; 函数并不关心 &lt;code&gt;read()&lt;/code&gt; 在做什么，只要它返回一个可以打印的字符串。它可以返回一个预定义的字符串，读取一个文件，或者进行一个网络 API 调用。 &lt;code&gt;printer()&lt;/code&gt; 不在乎，如果你试图在它身上调用 &lt;code&gt;read()&lt;/code&gt; 以外的任何其他方法，你的类型检查器就会报警。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Python 标准带有两种定义接口的方法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.python.org/3.9/library/abc.html&quot;&gt;抽象基类&lt;/a&gt;（ABC）是 &lt;a href=&quot;https://zopeinterface.readthedocs.io/&quot;&gt;zope.interface&lt;/a&gt; 的低配版，使用&lt;em&gt;名义子类型&lt;/em&gt;工作。它从 Python 2.6 起就存在了，标准库中用得到处都是。&lt;/p&gt;
&lt;p&gt;请注意，不是每个抽象基类都是抽象数据类型。有时它只是一个不完整的类，你应该通过继承它并实现其抽象方法来完成它——而不是一个接口。不过，这种区别并不总是百分百清晰的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.python.org/dev/peps/pep-0544/&quot;&gt;协议&lt;/a&gt;（Protocol）通过使用&lt;em&gt;结构子类型&lt;/em&gt;来避免继承。它是在 Python 3.8 中添加的，但是 &lt;a href=&quot;https://pypi.org/project/typing-extensions/&quot;&gt;typing-extensions&lt;/a&gt; 可以让它最低在 Python 3.5 中可用。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;em&gt;名义子类型&lt;/em&gt;和&lt;em&gt;结构子类型&lt;/em&gt;这两个词太大了，但好在解释起来很直接。&lt;/p&gt;
&lt;h3&gt;名义子类型&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;名义子类型&lt;/em&gt;意思是你必须告诉类型系统，你的类是一个接口定义的子类型。ABC 通常通过继承来实现这一点，但你也可以使用 &lt;a href=&quot;https://docs.python.org/3/library/abc.html#abc.ABCMeta.register&quot;&gt;&lt;code&gt;register()&lt;/code&gt; 方法&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;下面展示了你如何从上面的介绍中定义 &lt;code&gt;Reader&lt;/code&gt; 接口并将 &lt;code&gt;FooReader&lt;/code&gt; 和 &lt;code&gt;BarReader&lt;/code&gt; 标记为它的实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import abc

class Reader(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def read(self) -&amp;gt; str: ...

class FooReader(Reader):
    def read(self) -&amp;gt; str:
        return &quot;foo&quot;

class BarReader:
    def read(self) -&amp;gt; str:
        return &quot;bar&quot;

Reader.register(BarReader)

assert isinstance(FooReader(), Reader)
assert isinstance(BarReader(), Reader)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 &lt;code&gt;FooReader&lt;/code&gt; 没有一个叫做 &lt;code&gt;read&lt;/code&gt; 的方法，实例化就会在运行时失败。如果你像 &lt;code&gt;BarReader&lt;/code&gt; 那样使用 &lt;code&gt;register()&lt;/code&gt; 方式，接口在运行时不会被验证，它将成为一个（正如文档中所说）「虚拟子类」。这让你可以自由地使用更加动态的，或者说神奇的手段来提供所需的接口。由于 &lt;code&gt;register()&lt;/code&gt; 唯一参数是实现对象，你可以把它作为一个类的装饰器来使用，省去两行空行。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;名义子类型&lt;/em&gt;不仅接受，而且鼓励多重继承，因为在理想情况下，没有方法，没有行为被继承，也就没有可能混合，只有类的身份被复合。一个类可以实现许多不同的接口，接口越小越好。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;使用 ABC 定义接口的一个「好处」是，通过继承它们，你可以在抽象基类中添加普通的方法来偷渡到代码共享。但正如一开始提到的：&lt;a href=&quot;https://www.sicpers.info/2018/03/why-inheritance-never-made-any-sense/&quot;&gt;混合子类类型是个坏主意&lt;/a&gt;。通过继承实现代码共享是个坏主意，多重继承使它成为一个更坏的主意。&lt;/p&gt;
&lt;p&gt;公平地说，我已经看到了这种模式的良好应用，但你必须非常谨慎地使用这个方法。在 Python 中的一个成例是，当你需要根据其他定义良好的行为来实现一大堆&lt;a href=&quot;https://lists.archive.carbon60.com/python/python/124634#124634&quot;&gt;魔法方法&lt;/a&gt;[^5]。一个好的例子是 &lt;code&gt;collections.UserDict&lt;/code&gt;。基于上述原因，它不是很好，但在 Python 的约束和文化中，它是一个很好的权衡。然而，在 &lt;code&gt;UserDict&lt;/code&gt; 的例子中，当你试图在你的子类上增加比预期的 &lt;code&gt;dict&lt;/code&gt; 更多的行为时，它就会出问题。然后，关于&lt;a href=&quot;#%E7%B1%BB%E5%9E%8B%E4%B8%80%EF%BC%9A%E4%BB%A3%E7%A0%81%E5%85%B1%E4%BA%AB&quot;&gt;继承共享代码的那一节&lt;/a&gt;中的问题就会重新出现。为了避免这种情况，让类保持&lt;a href=&quot;https://en.wikipedia.org/wiki/Single-responsibility_principle&quot;&gt;单一责任&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;[^5]: 有点令人困惑的是，这也被称为「实现一个协议」。在 Python 文档中搜索 protocol 这个词，可以得到一堆结果。&lt;/p&gt;
&lt;h3&gt;结构子类型&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;结构子类型&lt;/em&gt;也就是&lt;em&gt;鸭子类型&lt;/em&gt;：如果你的类满足了一个&lt;a href=&quot;https://docs.python.org/3/library/typing.html#typing.Protocol&quot;&gt;协议&lt;/a&gt;的约束，它就会自动被认为是它的一个子类型。因此，一个类可以从各种包中实现许多协议而无需它们的存在！&lt;/p&gt;
&lt;p&gt;默认情况下，这只对类型检查器起作用，但如果你应用 &lt;code&gt;typing.runtime_checkable()&lt;/code&gt;，你也可以对它们执行 &lt;code&gt;isinstance()&lt;/code&gt; 检查。&lt;/p&gt;
&lt;p&gt;上节中的例子如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from typing import Protocol, runtime_checkable

@runtime_checkable
class Reader(Protocol):
    def read(self) -&amp;gt; str: ...

class FooReader:
    def read(self) -&amp;gt; str:
        return &quot;foo&quot;

assert isinstance(FooReader(), Reader)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如你所见，&lt;code&gt;FooReader&lt;/code&gt; 根本不知道 &lt;code&gt;Reader&lt;/code&gt; 协议的存在！&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;我非常喜欢 &lt;code&gt;Protocol&lt;/code&gt;，因为它允许我完全不受干扰地定义我需要的接口，而且这个定义可以和接口的消费者共存。当你在同一个代码库中对同一个接口有不同的实现时，这点就非常有用。例如，你可以有一个接口 &lt;code&gt;MailSender&lt;/code&gt;，在生产环境中发送电子邮件，但在开发中只是打印到控制台[^6]。&lt;/p&gt;
&lt;p&gt;[^6]: HBO 你好!&lt;/p&gt;
&lt;p&gt;或者，如果你只使用第三方类的一个小子集，并希望明确是哪个子集。这就是很好的（而且是经过验证的！）文档，在为你的测试伪造实现的时候也有帮助。&lt;/p&gt;
&lt;p&gt;关于 &lt;code&gt;Protocol&lt;/code&gt; 和&lt;em&gt;结构子类型&lt;/em&gt;的更多细节，请查看 &lt;a href=&quot;https://twitter.com/glyph&quot;&gt;glyph&lt;/a&gt; 的&lt;a href=&quot;https://glyph.twistedmatrix.com/2020/07/new-duck.html&quot;&gt;《我想要一只新鸭子》&lt;/a&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;虽然这种类型的继承大多是无害的，但由于 &lt;code&gt;typing.Protocol&lt;/code&gt; 和抽象基类的 &lt;code&gt;register()&lt;/code&gt; 方法，你&lt;strong&gt;不需要对 Python 中的抽象数据类型进行继承&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;类型三：特化&lt;/h2&gt;
&lt;p&gt;所以我们已经介绍了一个有害的继承类型和一个不必要的继承类型，终于要说到好的类型。事实上，即便你想，在 Python 中你也无法绕过这种继承方式。除非你不想使用 &lt;code&gt;Exception&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;有趣的是，特化常常被误解。直观地说，这很容易：如果我们说一个类 B 特化了基类 A，其实就是说类 B 是具有额外属性的 A。一只狗是一种动物，A350 是一架客机。它们拥有基类的所有属性，并增加了属性、方法，或者只是在一个层次结构中增加了一个位置[^7]。&lt;/p&gt;
&lt;p&gt;[^7]: 例如，对于 &lt;code&gt;Exception&lt;/code&gt; 来说，一个很常见的情况是只标注某个异常是 &lt;code&gt;ValueError&lt;/code&gt; 的一个子类，而不增加新的方法、属性或行为。&lt;/p&gt;
&lt;p&gt;尽管这种简单很诱人，但它经常被错误地使用。最臭名昭著的错误是认为正方形是长方形的特化，因为从几何学上讲，它是一个特例。然而，&lt;strong&gt;正方形并不是拥有额外行为属性的长方形&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;你不能在所有可以使用长方形的地方使用正方形，除非代码知道它也接受一个正方形&lt;a href=&quot;%E4%B8%80%E4%B8%AA%E5%B8%B8%E8%A7%81%E7%9A%84%E4%BE%8B%E5%AD%90%E6%98%AF%EF%BC%8C%E6%9C%89%E4%B8%80%E6%AE%B5%E4%BB%A3%E7%A0%81%E6%98%AF%E7%94%A8%E6%9D%A5%E7%BB%99%E9%95%BF%E6%96%B9%E5%BD%A2%E7%8B%AC%E7%AB%8B%E6%93%8D%E4%BD%9C%E5%AE%BD%E5%BA%A6%E5%92%8C%E9%AB%98%E5%BA%A6%E7%9A%84%E3%80%82%E5%AF%B9%E6%AD%A3%E6%96%B9%E5%BD%A2%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%BF%85%E9%A1%BB%E5%9C%A8%E6%94%B9%E5%8F%98%E5%AE%BD%E5%BA%A6%E6%97%B6%E9%9A%90%E5%BC%8F%E5%9C%B0%E6%94%B9%E5%8F%98%E9%AB%98%E5%BA%A6%EF%BC%88%E5%8F%8D%E4%B9%8B%E4%BA%A6%E7%84%B6%EF%BC%89%EF%BC%8C%E6%88%96%E8%80%85%E5%BC%95%E5%8F%91%E4%B8%80%E4%B8%AA%E9%94%99%E8%AF%AF%E3%80%82&quot;&gt;^8&lt;/a&gt;。如果你不能把一个对象当作其基类的实例来交互，你就违反了&lt;a href=&quot;https://en.wikipedia.org/wiki/Liskov_substitution_principle&quot;&gt;里氏替换原则&lt;/a&gt;[^9]，你就不能写多态的代码。&lt;/p&gt;
&lt;p&gt;[^9]: &lt;a href=&quot;https://en.wikipedia.org/wiki/SOLID&quot;&gt;SOLID&lt;/a&gt; 中的 L。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你仔细观察，你会发现上一节的接口是特化的一个特例。你总是把一个通用的 API 协议特化为一些具体的东西。关键的区别在于，抽象的数据类型是......嗯......抽象的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;我发现特化在表示一个具有严格层次结构的数据时相当有用。&lt;/p&gt;
&lt;p&gt;例如，设想你想把电子邮箱账户表示为类。它们都共享一些数据，比如它们在数据库中的 ID 和邮箱地址，此外，根据账户的类型，它们（可以）有额外的属性。重要的是，这些增加的属性和方法几乎没有改变现有的属性和方法。例如，一个在服务器上存储电子邮件的邮箱需要哈希过的密码作为登录信息，而一个接收电子邮件并只将其转发到另一个邮箱地址的账户则不需要[^10]。&lt;/p&gt;
&lt;p&gt;你将有以下的四种方法。&lt;/p&gt;
&lt;p&gt;[^10]: 为了简洁起见，我把这个例子写得很短。显然，对于两个各有两个字段的类型，这么大费周章是没有意义的。我还使用了 Python 3.10 风格的类型注解，其中可以使用 &lt;code&gt;|&lt;/code&gt; 来代替 &lt;code&gt;typing.Union&lt;/code&gt;。因此 &lt;code&gt;str | None&lt;/code&gt; 等同于 &lt;code&gt;Union[str, None]&lt;/code&gt;，而后者又等同于 &lt;code&gt;Optional[str]&lt;/code&gt;。你也可以使用容器类型，如 &lt;code&gt;list[str]&lt;/code&gt;，而不用从 &lt;code&gt;typing&lt;/code&gt; 中导入 &lt;code&gt;List&lt;/code&gt;。如果你 &lt;code&gt;from __future__ import annotations&lt;/code&gt;，而且不需要像 &lt;code&gt;typing.get_type_hints()&lt;/code&gt; 那样的运行时类型自省，并且你的 Mypy 足够新的话，你甚至可以在更旧的 Python 版本中使用这种语法。&lt;/p&gt;
&lt;h3&gt;方法 1：为每种情况专门创建一个类&lt;/h3&gt;
&lt;p&gt;这些将是你最终想要的类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Mailbox:
    id: UUID
    addr: str
    pwd: str

class Forwarder:
    id: UUID
    addr: str
    targets: list[str]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;地址的类型标注在类中，每个类只有它要用的字段。如果你的模型如此简单，这绝对已经足够了。只有在你有更多的字段和类型时，去除重复代码的尝试才是有意义的。&lt;/p&gt;
&lt;p&gt;你添加到任何一个类中的任何方法都将完全独立于另一个类，不留下任何混淆的空间。你也可以配合&lt;a href=&quot;https://docs.python.org/3/library/typing.html#typing.Union&quot;&gt;联合类型&lt;/a&gt;的类型检查：&lt;code&gt;Mailbox | Forwarder&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;通常，在任何情况下都可以从这种方法开始，因为&lt;a href=&quot;https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction&quot;&gt;代码重复要比错误的抽象的代价要小得多&lt;/a&gt;。你能直接看到所有可能的字段，使进一步的设计决策容易得多。&lt;/p&gt;
&lt;h3&gt;方法 2：只创建一个类，把字段变成可选的&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;条件总是会恶化，条件会重复产生。&amp;lt;cite&amp;gt;— Sandi Metz，无招胜有招&amp;lt;/cite&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当时不计一切代码避免继承，同时避免重复自己时，你最终很可能得到以下的结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class AddrType(enum.Enum):
    MAILBOX = &quot;mailbox&quot;
    FORWARDER = &quot;forwarder&quot;

class EmailAddr:
    type: AddrType
    id: UUID
    addr: str

    # Only useful if type == AddrType.MAILBOX
    pwd: str | None
    # Only useful if type == AddrType.FORWARDER
    target: list[str] | None
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从技术上讲，这更 &lt;a href=&quot;https://en.wikipedia.org/wiki/Don%27t_repeat_yourself&quot;&gt;DRY&lt;/a&gt; 了，但这样会让类的实例用起来更加别扭。大多数字段的类型与存在完全取决于 &lt;code&gt;type&lt;/code&gt; 字段的值，而 &lt;code&gt;type&lt;/code&gt; 的存在仅仅因为所有的地址类型都共用同一个类。&lt;/p&gt;
&lt;p&gt;这与我最喜欢的设计原则相矛盾，即让&lt;a href=&quot;https://blog.ploeh.dk/2016/02/10/types-properties-software-designing-with-types/&quot;&gt;非法状态无法表示&lt;/a&gt;，而且使用类型检查器进行合理的检查也变得不可能，因为类型检查器会一直告诉你你在访问可能是 &lt;code&gt;None&lt;/code&gt; 的字段。&lt;/p&gt;
&lt;p&gt;事实上，所有在这个类上的行为都会被混在一起，这导致了大量的条件（&lt;code&gt;if-elif-else&lt;/code&gt; 语句），大大增加了你代码的复杂性。多态的全部意义就是为了避免这种情况。&lt;/p&gt;
&lt;p&gt;拥有可选的属性[^11] &lt;a href=&quot;https://glyph.twistedmatrix.com/2015/09/python-option-types.html&quot;&gt;有可能是一面红旗&lt;/a&gt;。拥有需要注释来解释何时使用它们的字段则是&lt;a href=&quot;https://www.google.de/search?q=may+day+rally&amp;amp;tbm=isch&quot;&gt;五月节集会&lt;/a&gt;。正如有争议的类型注解一样，在这种情况下，它清楚地向你指出了你的模型有问题。如果没有类型检查，你必须注意到你的代码超过了它本应有的复杂，而这就不是那么直接了。&lt;/p&gt;
&lt;p&gt;[^11]: 可以为 &lt;code&gt;None&lt;/code&gt; 的属性。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;你可以让这种情况稍微不那么痛苦，把特定邮箱的数据移到一个类中，然后只让那个字段可选。这样做好一些，但仍然别扭，并且毫无必要。&lt;/p&gt;
&lt;h3&gt;方法 3：组合&lt;/h3&gt;
&lt;p&gt;这种方法把上一个方法反了过来，虽然这在我们过于简单的数据模型中用起来很傻，但是我们假设 &lt;code&gt;EmailAddr&lt;/code&gt; 有更多的字段，以至于它值得被包装成一个独立的类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class EmailAddr:
    id: UUID
    addr: str

class Mailbox:
    email: EmailAddr
    pwd: str

class Forwarder:
    email: EmailAddr
    targets: list[str]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种方法并没那么差！我们没有可选字段，所有的数据关系都很清楚。就可读性和清晰性而言，没有什么可抱怨的。&lt;/p&gt;
&lt;p&gt;只是这种方法也很别扭，你不需要咨询 Guido 就能意识到它一点也不 Pythonic。那么，尽管组合应该比继承更好，为什么它看起来如此矫揉造作呢？因为 &lt;code&gt;EmailAddr&lt;/code&gt; 和 &lt;code&gt;Mailbox/Forwarder&lt;/code&gt; 的关系太密切了，甚至给地址字段命名都很奇怪。组合并没有让我们失望，但在这种情况下，强迫建立一个包含的关系，感觉就像是在违背规律。&lt;/p&gt;
&lt;h3&gt;方法 4：创建一个基类，然后特化它&lt;/h3&gt;
&lt;p&gt;最后，在我看来是最符合人体工程学，DRY，明显，适合类型检查的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class EmailAddr:
    id: UUID
    addr: str

class Mailbox(EmailAddr):
    pwd: str

class Forwarder(EmailAddr):
    targets: list[str]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只要你有一个邮箱，你就知道它有一个 &lt;code&gt;pwd&lt;/code&gt; 字段，类型检查器也知道。类型是在类中标注的，所以你不必在一个字段中重复它。严格意义上 &lt;code&gt;Mailbox&lt;/code&gt; 是一个 &lt;code&gt;EmailAddr&lt;/code&gt; 加上更多。&lt;/p&gt;
&lt;p&gt;至于代码，你现在必须了解&lt;strong&gt;子类的职责规则&lt;/strong&gt;，比如前面提到的里氏替换原则。这带来了额外的复杂度和脑力的开销，但是边界和责任更加清晰了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;继承需要你的了解和自律。组合则机械地迫使你遵守纪律，尽管它会让代码显得笨拙。&lt;/p&gt;
&lt;p&gt;这可能是让你用组合最简单的理由：它给你留下的错误空间更小。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;和所有类型的继承一样，代码可读性会受到影响，因为你必须在头脑中组装出最终的类，才能知道存在哪些字段。但实际上你得到的是与第一种方法相同的类。只要你不做得太过分，并且最好让定义在物理上相互接近，在这种情况下，这是最好的权衡。&lt;/p&gt;
&lt;p&gt;这种方法非常有用，我在&lt;a href=&quot;https://pem.readthedocs.io/en/stable/&quot;&gt;我的 PEM 文件解析库&lt;/a&gt;中&lt;a href=&quot;https://github.com/hynek/pem/blob/main/src/pem/_core.py&quot;&gt;使用&lt;/a&gt;到了，至今仍不后悔。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;从本节中可以得出一个一般建议：首先要关注你的数据的结构，然后才是如何处理它。&lt;/p&gt;
&lt;p&gt;一旦你确定了结构，行为就会更自然。一个很好的例子是 &lt;a href=&quot;https://sans-io.readthedocs.io/&quot;&gt;Sans I/O&lt;/a&gt; 运动，它显然是数据优先，因为在设计上，行为应该是可以随意替换。&lt;/p&gt;
&lt;p&gt;只要你在特化的同时避免方法之间的跨层级互动，就应该没有大问题。但是要经常问问自己，一个函数是否足够了？尤其是当你在两个或更多的类之间协调工作，并且没有多态可利用的时候。如果你不能决定一个方法属于哪个类，那么答案往往是都不属于。&lt;/p&gt;
&lt;p&gt;最后一定要了解一下 &lt;a href=&quot;https://hynek.me/articles/serialization/&quot;&gt;&lt;code&gt;@singledispatch&lt;/code&gt;&lt;/a&gt;；如果你没了解过，会觉得它是魔法。&lt;/p&gt;
&lt;p&gt;作为额外收获，如果你遵循这些方法，你会得到高度可测的对象。&lt;/p&gt;
&lt;h3&gt;蟒蛇之外&lt;/h3&gt;
&lt;p&gt;上述最后一种方法非常好用，以至于「我们没有继承」的 Go 也具备这个能力，只是叫做&lt;a href=&quot;https://golang.org/doc/effective_go#embedding&quot;&gt;嵌套&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type EmailAddr struct {
    addr string
}

type Mailbox struct {
    EmailAddr
    pwd string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样 &lt;code&gt;Mailbox&lt;/code&gt; 的实例也拥有了 &lt;code&gt;addr&lt;/code&gt; 属性，就好像它自己定义了一样：&lt;a href=&quot;https://play.golang.org/p/WSjJA6MYUDb&quot;&gt;https://play.golang.org/p/WSjJA6MYUDb&lt;/a&gt;。但你在初始化时仍然必须显式传入，而且没有实际的层次结构。没有 &lt;code&gt;super()&lt;/code&gt;。你只能从侧面调用。这是一个对现状的妥协。&lt;/p&gt;
&lt;p&gt;回顾之前的章节，这是方法 3 的语法，但其实在很多层面，你得到了方法 1 中的类。&lt;/p&gt;
&lt;p&gt;在 Go 中看到这一点对我来说是一个启示，因为我自己基于直觉的子类启发式方法符合这种模式，但我不知道如何阐述。现在我可以说，当我可以，并且愿意在 Go 中使用嵌套时，我就是在用继承。&lt;/p&gt;
&lt;h2&gt;接下来呢？&lt;/h2&gt;
&lt;p&gt;至于可读性，适当的组合比继承要好。由于&lt;strong&gt;读代码比写代码的时候要多得多&lt;/strong&gt;，所以一般情况下要避免使用子类，特别是不要混合多种类型的继承，也不要使用继承来共享代码。不要忘了，更多的时候，&lt;strong&gt;你需要的只是一个函数而已&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;重要的是要记住，在一个基于继承的设计中，不是想停用继承就可以停止。&lt;strong&gt;基于组合的设计从头到尾都是不同的&lt;/strong&gt;，所以你可能要改变一些观念和手段。&lt;/p&gt;
&lt;p&gt;尽管不是基于 Python 的，但我所知道的最好的对 OOP 设计的介绍是&lt;a href=&quot;https://sandimetz.com/99bottles&quot;&gt;《99 瓶 OOP》&lt;/a&gt;，如果你还没读过建议一读。它不仅相当有指导意义，而且读起来也很有趣。&lt;/p&gt;
&lt;p&gt;为了不显得我敷衍，我准备用一个具体的例子作为总结。&lt;/p&gt;
&lt;h3&gt;案例分析&lt;/h3&gt;
&lt;p&gt;我将使用那本特别精彩的&lt;a href=&quot;https://www.cosmicpython.com/&quot;&gt;《Python 架构模式》&lt;/a&gt;中润色好的代码，该书是我帮助审阅的，绝对值得你的时间和金钱。我在这里使用它是因为 Harry——他是该书的作者之一——在我抱怨过后让我写一篇博文。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;我们的目标是实现仓库模式：一个允许你向数据仓库中添加和检索对象的类。此外，它还必须记住所有添加或检索的对象，并将其放在一个叫做 &lt;code&gt;seen&lt;/code&gt; 的字段上，但这不是这篇博文所关注的。&lt;/p&gt;
&lt;p&gt;一个重要的设计目标是让存储仓库可插拔，所以它可以，譬如说在生产中使用 Postgres 这样的数据库，在单元测试中则使用字典。但是记录对象的代码对于所有的实现都是一样的，因此你希望这部分共享代码。&lt;/p&gt;
&lt;p&gt;特化在这里不起作用，因为它方向是错的：跟踪仓库是「普通」仓库的特化。因此，我们想要共享的代码会出现在子类中。这里用不上。&lt;/p&gt;
&lt;p&gt;因此，这本书使用了我最不喜欢的基于继承的代码共享类型：模板方法模式。这意味着基类提供了一个整体的控制流程，而子类则填补了一些细节：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户实例化一个子类，&lt;/li&gt;
&lt;li&gt;然后调用基类上的方法，&lt;/li&gt;
&lt;li&gt;其中又调用了子类中的方法。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在这个例子中，子类需要实现的方法是 &lt;code&gt;_add_product&lt;/code&gt; 和 &lt;code&gt;_get_by_sku&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class AbstractRepository(abc.ABC):
    seen: set[Product]

    def __init__(self) -&amp;gt; None:
        self.seen = set()

    def add_product(self, product: Product) -&amp;gt; None:
        self._add_product(product)
        self.seen.add(product)

    def get_by_sku(self, sku: str) -&amp;gt; Product | None:
        product = self._get_by_sku(sku)
        if product:
            self.seen.add(product)

        return product

    @abc.abstractmethod
    def _add_product(self, product: Product):
        raise NotImplementedError

    @abc.abstractmethod
    def _get_by_sku(self, sku: str) -&amp;gt; Product | None:
        raise NotImplementedError
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，每个子类都必须定义 &lt;code&gt;_add_product()&lt;/code&gt; 和 &lt;code&gt;_get_by_sku()&lt;/code&gt; 方法。然后用户调用 &lt;code&gt;AbstractRepository&lt;/code&gt; 的&lt;code&gt;add_product()&lt;/code&gt; 和 &lt;code&gt;get_by_sku()&lt;/code&gt; 方法，这些方法又委托给子类的 &lt;code&gt;_add_product()&lt;/code&gt; 和 &lt;code&gt;_get_by_sku()&lt;/code&gt; ，同时记住它看到过哪些 Product 类型的对象[^12]。&lt;/p&gt;
&lt;p&gt;[^12]: 为方便讨论，&lt;code&gt;Product&lt;/code&gt; 的数据结构无需关注，只需要知道它有一个 &lt;code&gt;str&lt;/code&gt; 类型的 &lt;code&gt;sku&lt;/code&gt; 字段。&lt;/p&gt;
&lt;p&gt;热心的读者会马上发现继承的原罪：它将接口的定义与子类的共享代码混在一起。如果你想复习一下为什么这很糟糕，回头去看一下《继承没有任何意义》（我在介绍中已经贴过链接了）。&lt;/p&gt;
&lt;p&gt;更实际的问题是，如果你想理解代码的工作流，由于去向和来源在类层级中来回横跳，这会很困难。&lt;/p&gt;
&lt;p&gt;即使对用户来说也是如此，因为公共 API 是由抽象基类定义的，而不是你实际实例化的那个类！这一点在文档系统中往往处理得不好，你不得不在阅读时跳来跳去。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;当面对这样的代码，想摆脱子类的桎梏，有两个选择：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;对类进行包装&lt;/strong&gt;。不要让它成为 &lt;code&gt;self&lt;/code&gt; 的一部分，而是把它存储在一个实例属性中。根据需要委托给该实例的方法。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;对行为参数化&lt;/strong&gt;。一旦你需要在多维度定制一个类的行为，而通过继承共享代码的方式又不可行时，这就是要走的路。听起来很复杂，但 Sandi Metz 在前面提到的《无招胜有招》的演讲中完美地展示了这一点，只用几行代码就可以实现排序和格式的自定义。&lt;/p&gt;
&lt;p&gt;对大多数人来说，直到点击的时候，你才会掌握——至少我是这样。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们的例子很简单：我们只想做具体存储库要做的事情，再加上其他的事情[^13]。因此，我们选择一号方案。如果你稍稍眯起眼睛，你会发现这里的模板子类的方式也不过是在包装一个类。除了命名空间混在一起，控制流混乱之外。&lt;/p&gt;
&lt;p&gt;[^13]: 技术上来说，这其实就是「装饰器模式」。我们维持原始 API，并加上其他行为。&lt;/p&gt;
&lt;h3&gt;仓库类&lt;/h3&gt;
&lt;p&gt;我们通过一个叫 &lt;code&gt;Repository&lt;/code&gt; 的协议来定义接口，取代之前的拥有一堆代码的抽象基类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Repository(typing.Protocol):
    def add_product(self, product: Product) -&amp;gt; None: ...
    def get_by_sku(self, sku: str) -&amp;gt; Product | None: ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，如果你不用类型注解，你可以忽略这一步。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;一个简单的使用字典来存储数据的实现大概像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class DictRepository:
    _storage: dict[str, Product]

    def __init__(self):
        self._storage = {}

    def add_product(self, product: Product) -&amp;gt; None:
        self._storage[product.sku] = product

    def get_by_sku(self, sku: str) -&amp;gt; Product | None:
        return self._storage.get(sku)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仓库必须实现这两个承诺的公共方法，但整个类是自包含的。没有任何命名冲突的风险。它只有一个职责：保存和检索产品。它也不需要知道有一个叫做 &lt;code&gt;Repository&lt;/code&gt; 的协议存在；类型检查器会帮你弄清楚它是一个实现。&lt;/p&gt;
&lt;h3&gt;跟踪仓库类&lt;/h3&gt;
&lt;p&gt;下一步，让我们来基于 &lt;code&gt;Repository&lt;/code&gt; 实现跟踪功能，只需要在外面包装一层：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class TrackingRepository:
    _repo: Repository
    seen: set[Product]

    def __init__(self, repo: Repository) -&amp;gt; None:
        self._repo = repo
        self.seen = set()

    def add_product(self, product: Product) -&amp;gt; None:
        self._repo.add_product(product)
        self.seen.add(product)

    def get_by_sku(self, sku: str) -&amp;gt; Product | None:
        product = self._repo.get_by_sku(sku)
        if product:
            self.seen.add(product)

        return product
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个类由两个东西组合而成：一个只知道是实现了 &lt;code&gt;Repository&lt;/code&gt; 的对象，和一些 &lt;code&gt;Products&lt;/code&gt;。如果你在 &lt;code&gt;_repo&lt;/code&gt; 属性上使用其他未被 &lt;code&gt;Repository&lt;/code&gt; 接口承诺的东西，无需执行代码，类型检查器就会对你报警。&lt;/p&gt;
&lt;h3&gt;小结&lt;/h3&gt;
&lt;p&gt;这个版本我喜欢多了，因为它有一个清晰的程序流。你知道方法和属性来自哪里，而不需要检查任何基类。&lt;/p&gt;
&lt;p&gt;这种清晰的代价是仓库必须保存在我们的类上（&lt;code&gt;_repo&lt;/code&gt;），并调用 &lt;code&gt;self._repo.add_product()&lt;/code&gt; 而不是 &lt;code&gt;self._add_product()&lt;/code&gt; 。这就需要多打点字。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;另一方面，我们最终得到了两个独立的小类，它们之间唯一的协定是一个严格的、明确的接口。这不仅易于&lt;strong&gt;阅读&lt;/strong&gt;和&lt;strong&gt;理解&lt;/strong&gt;，而且也易于&lt;strong&gt;测试&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;作为结束前的寄司：如果你想知道如何为代码写测试，而不是像所有的测试教程中那样只是字符串操作或两个数字相加，我希望你现在看到，学习更好的 OOP 设计也会对你有所帮助。&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;哇，你熬过来了! 谢谢你坚持不懈地支持我! 我的最终目标是在讨论中加入更多的细微差别。我想让你明白，使用 &lt;code&gt;Exception&lt;/code&gt; 并不意味着也要使用模板方法模式，因为「两者都是继承」。我希望我稍微成功了一点。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;由于其长度，这篇文章不太可能得到大量的「看、读、转发/投票」分享。更有可能的是，它在你打开的标签页、你的阅读队列中多停留了一些时间！如果你能以某种方式分享它，帮助它传播，那再好不过。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;欢迎&lt;a href=&quot;mailto:hs@ox.cx&quot;&gt;告诉我&lt;/a&gt;你对本文的看法，或者你花了多少杯咖啡才读完它。我不打算在公共讨论上花太多时间，因为它们往往会变得过于激烈、迂腐和教条。我写这篇文章的原因之一是能直接对他们甩 URL，希望对你也能起到同样的作用!&lt;/p&gt;
&lt;p&gt;我正在根据本文准备一个演讲，所以如果你希望在你的会议或者公司中加入这个演讲，请联系我！只要我有机会亲临现场。&lt;/p&gt;
&lt;p&gt;最后，如果你想看到更多像这样的内容，可以考虑&lt;a href=&quot;https://hynek.me/say-thanks/&quot;&gt;支持我&lt;/a&gt;。&lt;/p&gt;
</content:encoded></item><item><title>友好的 Python：接口友好</title><link>https://frostming.com/posts/2021/07-23/friendly-python-2/</link><guid isPermaLink="false">https://frostming.com/2021/07-23/friendly-python-2/</guid><pubDate>Fri, 23 Jul 2021 16:04:17 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://frostming.com/2021/07-07/friendly-python-1/&quot;&gt;上一篇&lt;/a&gt;说到写代码要对开发者、接手者友好，需要让程序扩展起来比较容易，实现「高内聚」。同样地，对用户来说，程序使用起来是否友好也是决定了他用不用你的软件的一大要素。本文我们就先说一说其中的一种使用情形：作为上游库对下游提供接口（API）。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;场景&lt;/h2&gt;
&lt;p&gt;小 F 新加入了某大厂的一个基础组件部门，要负责维护一个组件的 Python SDK。这个组件本身是 Java 写的，小 F 看了一眼文档的 Quick Start，眉头逐渐紧锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from awesome_sdk.client import AwesomeClient
from awesome_sdk.core.auth.basic import AwesomeBasicAuth
from awesome_sdk.core.connection.tcp import AwesomeTCPConnection

# 新建一个认证对象
auth = AwesomeBasicAuth(username=os.getenv(&quot;USERNAME&quot;), password=os.getenv(&quot;PASSWORD&quot;))
# 新建连接
connection = AwesomeTCPConnection(host=&quot;127.0.0.1&quot;, port=5762, timeout=10, retry_times=3, auth=auth)
# 新建客户端对象
client = AwesomeClient(connection=connection, type=&quot;test&quot;, scope=&quot;read&quot;)
# 获取资源
print(client.get_resources())
# 关闭连接
connection.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看看这优秀的组合使用，用户用起来就像在拼乐高玩具：组成腿和脚……组成躯干和手臂……我来组成头部&lt;a href=&quot;%E5%A6%82%E6%9E%9C%E4%BD%A0%E5%AF%B9%E8%BF%99%E8%AF%9D%E6%9C%89%E4%BA%9B%E8%80%B3%E7%86%9F%EF%BC%8C%E4%BD%A0%E5%8F%AF%E8%83%BD%E5%B7%B2%E7%BB%8F%E6%9A%B4%E9%9C%B2%E5%B9%B4%E9%BE%84%EF%BC%8C%E8%BF%99%E6%98%AF%E5%8A%A8%E7%94%BB%E3%80%8A%E6%88%98%E7%A5%9E%E9%87%91%E5%88%9A%E3%80%8B%E6%9C%BA%E5%99%A8%E4%BA%BA%E5%90%88%E4%BD%93%E7%9A%84%E5%8F%B0%E8%AF%8D%E3%80%82&quot;&gt;^1&lt;/a&gt;！可是凑近闻一闻，小 F 仿佛闻到了爪哇咖啡的味道。没错，这个 Python 版的 SDK 最初是由组件的 Java 开发顺便写的[^2]。具体问题在哪呢？可以总结为以下三点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先 import 就写了三行，而且 import 路径巨长，&lt;/li&gt;
&lt;li&gt;其次，为了得到一个客户端对象，不得不先初始化一系列的基础对象来组装，&lt;/li&gt;
&lt;li&gt;再次，要设置的参数居然多达 9 个（举例说明，实际可能更糟）。这些参数多亏命名规范，作用能猜得一二，否则不看文档很难知道意义。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;[^2]: 我对 Java 不熟，可能有生成工具自动转换而来。案例纯属虚构，如有雷同，八成是某云（逃。&lt;/p&gt;
&lt;h2&gt;默认参数&lt;/h2&gt;
&lt;p&gt;首先来解决参数的问题，我们真的需要让用户指定这么多参数吗？不然，&lt;strong&gt;我们应该尽可能提供合理的默认值&lt;/strong&gt;。这里「合理」的意思是在大多数情况下，无需更改就能正常工作，达到真正的 Quick Start 的目的。具体到案例中，&lt;code&gt;Connection&lt;/code&gt; 对象的大部分参数都不是必需的：&lt;code&gt;host&lt;/code&gt; 未指定时默认 localhost，&lt;code&gt;port&lt;/code&gt; 是该组件的本征端口号，SDK 知道，没必要要推给用户指定，&lt;code&gt;timeout&lt;/code&gt; 和 &lt;code&gt;retry&lt;/code&gt; 给个大部分时候都足够的值就好了，如果用户要自定，可以再从 API 文档或 IDE 浮出的函数签名中知道如何实现。现在只剩一个 &lt;code&gt;auth&lt;/code&gt; 是必填的了，这非常合理：认证信息无法预知。&lt;/p&gt;
&lt;p&gt;你可能会说，这个指责对 Java 不公平——它不支持默认参数。确实，但既然做了一个 Python SDK，就得入乡随俗，按 Pythonic 的来。&lt;/p&gt;
&lt;h2&gt;化繁为简&lt;/h2&gt;
&lt;p&gt;我们再次来审视这段基础的代码，把用户必填的信息摘出来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;认证信息，用户名密码&lt;/li&gt;
&lt;li&gt;客户端的 scope 信息，包括 &lt;code&gt;name&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;仅此而已，其他的参数用默认值，对于一个 Quick Start 来说足够了。那么屈屈这么几个参数，有必要涉及三个对象创建吗？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如无必要，勿增实体 &amp;lt;cite&amp;gt;——奥卡姆剃刀&amp;lt;/cite&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因为，引入一个新的类就意味着必须多一个导入。导入的东西多了，如果还是从同一个地方导入，那下游就很可能偷懒换成 &lt;code&gt;from awesome import *&lt;/code&gt;。我眼睁睁看过这种情况的发生。那么，如何精简对象呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Client 是创建的核心，是所有 API 的归属对象，保留。&lt;/li&gt;
&lt;li&gt;Connection 仅仅是用来指定连接的参数，可以换成 Client 对象上的 &lt;code&gt;connect&lt;/code&gt; 方法。&lt;/li&gt;
&lt;li&gt;Auth 是存储认证信息的容器，在 Python 中要用一个容器大可不必引入一个新的类，元组足矣。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;改造以后，开头的代码变成了下面这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
from awesome_sdk import AwesomeClient

client = AwesomeClient(type=&quot;test&quot;, scope=&quot;read&quot;, auth=(os.getenv(&quot;USERNAME&quot;), os.getenv(&quot;PASSWORD&quot;)))

with client.connect():
    print(client.get_resources())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里 &lt;code&gt;connect()&lt;/code&gt; 返回了一个上下文对象，让用户无需关心关闭连接的操作。改造以后我不敢说这绝对 Pythonic 了，但市面上大部分的 Client 库大致都长这样（回想一下 mysqlclient, pika, redis 等等）。&lt;/p&gt;
&lt;h2&gt;由简入繁&lt;/h2&gt;
&lt;p&gt;自然，作为一个功能强大的 Client 库，背后远不止现在看起来的这么简单。注意一下原始的版本中为何要创建这么多类，你就会发现 Connection 的类名是叫做 &lt;code&gt;AwesomeTCPConnection&lt;/code&gt;，也就是说可能要支持 UDP 连接；Auth 的类名叫做 &lt;code&gt;AwesomeBasicAuth&lt;/code&gt;，意思是可能要支持除了用户名密码之外的其他认证方法。&lt;/p&gt;
&lt;p&gt;我们其实已经在上面的接口中留出了可能：&lt;code&gt;connect()&lt;/code&gt; 方法接受一个 &lt;code&gt;cls&lt;/code&gt; 参数，默认值就是 &lt;code&gt;AwesomeTCPConnection&lt;/code&gt;，看，默认参数的作用又体现了。你可以替换成 &lt;code&gt;AwesomeUDPConnection&lt;/code&gt;。至于 &lt;code&gt;auth&lt;/code&gt;，传入一个元组，SDK 内部会自动基于此创建&lt;code&gt;AwesomeBasicAuth&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if isinstance(auth, tuple):
    auth = AwesomeBasicAuth(*auth)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你需要其他的认证类型，自己实例化显式传入就好了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;client = AwesomeClient(..., auth=SSHAuth(...))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里充分利用了 Python 的动态特性，一个参数可以是任何类型，换作静态语言的 SDK，不太可能这么设计。但是，这种方法也不能滥用，如果一个参数接受 7，8 种类型，那代码坏味道就出来了。我们把 BasicAuth 支持元组传入，只是因为它确实已经常用到一定的程度了。其实，该创建的实体一个也没少，只是对用户隐藏了。&lt;/p&gt;
&lt;p&gt;大道至简，然内涵极深，既能化繁为简，也能由简入繁，若达到这种来去自如的境界，就可称得上是优秀的 API 了，用户都想用，用了又用。&lt;/p&gt;
&lt;h2&gt;实例&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;我不创造 API， 我&lt;strong&gt;设计&lt;/strong&gt; API &amp;lt;cite&amp;gt;——Kenneth Reitz&amp;lt;/cite&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;说到 API 设计就绕不开 &lt;a href=&quot;https://github.com/psf/requests.git&quot;&gt;requests&lt;/a&gt;，我们来看看它的设计，略举几处：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;/api&lt;/code&gt; 下面的 &lt;code&gt;get&lt;/code&gt;，&lt;code&gt;post&lt;/code&gt; 等接口，直接暴露在 &lt;code&gt;requests&lt;/code&gt; 的命名空间下，均衍生自 &lt;code&gt;request&lt;/code&gt;，而后者是创建了一个 &lt;code&gt;Session&lt;/code&gt; 对象，调用其中的 &lt;code&gt;Session.request()&lt;/code&gt; 方法。这里就利用了隐藏实体的技巧，当用户想保持会话时，依然可以自己实例化 &lt;code&gt;Session&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;requests.get&lt;/code&gt; 方法中，&lt;code&gt;verify&lt;/code&gt; 参数既可以是布尔，也可以是指向证书的路径；&lt;code&gt;auth&lt;/code&gt; 参数既可以是（用户名，密码）元组，也可以是一个 &lt;code&gt;Auth&lt;/code&gt; 实例，利用了参数的多类型。而大多数时候，只需要 &lt;code&gt;requests.get(url)&lt;/code&gt; 就够了。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;requests.post&lt;/code&gt; 方法中，只给 &lt;code&gt;data&lt;/code&gt; 参数时是 &lt;code&gt;x-www-form-urlencoded&lt;/code&gt; 类型，只给 &lt;code&gt;json&lt;/code&gt; 参数是是 &lt;code&gt;application/json&lt;/code&gt; 类型，同时给 &lt;code&gt;data&lt;/code&gt; 和 &lt;code&gt;files&lt;/code&gt; 参数时是 &lt;code&gt;multipart/form-data&lt;/code&gt; 类型。灵活多变，用户无需指定任何一个 Header，填写那根本记不住的 Content-Type。&lt;/li&gt;
&lt;li&gt;用户想自定义请求数据返回，则继承 &lt;code&gt;requests.adapters.BaseAdapter&lt;/code&gt; 自己实现一个，然后通过 &lt;code&gt;Session.mount()&lt;/code&gt; 挂载就好了，可以不经互联网，读取本地数据，也可以&lt;a href=&quot;https://github.com/seanbrant/requests-wsgi-adapter&quot;&gt;与 WSGI app 直接交互&lt;/a&gt;，随心所欲。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;撇开作者个人不谈，requests 的源码还是非常值得一读的，能提升你的 API 设计能力。&lt;/p&gt;
&lt;p&gt;就说到这。&lt;/p&gt;
</content:encoded></item><item><title>友好的 Python：扩展友好</title><link>https://frostming.com/posts/2021/07-07/friendly-python-1/</link><guid isPermaLink="false">https://frostming.com/2021/07-07/friendly-python-1/</guid><pubDate>Wed, 07 Jul 2021 13:47:28 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;时隔两个月没有更新博客，这次准备来个专题「友好的 Python」。虽然我脑海中想好了几个主题，但具体写什么还不知道，这个系列能写几篇也不知道。构思一篇博客真的是太难了，至少对我这种懒人来说。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;Python 是一门相当灵活动态的语言，这就导致实现一件事情可用的方法往往不止一个，于是就有很多人质疑 Python 之禅中的这一句话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;There should be one-- and preferably only one --obvious way to do it. &amp;lt;cite&amp;gt;—Tim Peters&amp;lt;/cite&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;大家质疑的理由没错，这句 Python 之禅也没错——如果你能找到这样一种 preferable，obvious 的方法，那它就是 Pythonic 的。Pythonic 这个形容词虽然虚无缥渺，但我觉得这个定义是比较符合的。&lt;/p&gt;
&lt;p&gt;忘了在哪里看到的：一个资深程序员写的代码，要能让新人看懂，一个大师级程序员写的代码，能让 CS 专业的大一学生看懂。写的代码不仅要追求性能优功能强，还有一个重要的特质——友好。友好的界面能吸引更多用户，友好的代码结构能吸引更多的贡献者。所以本文是「友好的 Python」的其中一个主题：对开发者友好之扩展友好。&lt;/p&gt;
&lt;h2&gt;场景&lt;/h2&gt;
&lt;p&gt;（&lt;em&gt;此处致敬 @piglei&lt;/em&gt;）小 F 收到一个需求，做一个新闻聚合机器人，从一些资讯站上获取新闻，发到 IM 的频道中，并且允许用户指定某个来源。小 F 经过一番思考，觉得这是一个简单的爬虫程序，于是很快写出了主体部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# main.py
class NewsGrabber:
    def get_news(self, source: Optional[str] = None) -&amp;gt; Iterable[News]:
        # TODO

    def format_news(self, news: Iterable[News]) -&amp;gt; str:
        result: List[str] = []
        for item in self.get_news():
            result.append(self._format_one_news(item))
        return &apos;\n&apos;.join(result)

    def send_message(self, message: str) -&amp;gt; None:
        channel = self._read_config()
        self._send_to_channel(message, channel)

    def run(self, source: Optional[str] = None) -&amp;gt; None:
        news = self.get_news()
        message = self.format_news(news)
        self.send_message(message)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;初次尝试&lt;/h2&gt;
&lt;p&gt;小 F 也是个老 Pythonista 了，他觉得自己写的这段非常优雅，把 &lt;code&gt;get_news()&lt;/code&gt; 留了出来，因为新闻来源有多个，他打算应用设计模式中的「策略模式」，把每种来源作为一个单独的策略类，暴露相同的接口，他还用上了抽象类，做了一个基类出来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# sources/base.py
from abc import ABC, abstractmethod

class BaseSource(ABC):
    url: str

    def get_page(self) -&amp;gt; HTML:
        return lxml.etree.HTML(requests.get(self.url).text)

    def iter_news(self) -&amp;gt; Iterable[News]:
        return self.extract_news(self.get_page())

    @abstractmethod
    def extract_news(self, html: HTML) -&amp;gt; Iterable[News]:
        pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着，他通过子类做出了 HackerNews，V2EX，Reddit 的策略类 &lt;code&gt;HNSource&lt;/code&gt;，&lt;code&gt;V2Source&lt;/code&gt;，&lt;code&gt;RedditSource&lt;/code&gt;。最后实现 &lt;code&gt;get_news&lt;/code&gt; 方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# main.py
import itertools
from sources import HNSource, V2Source, RedditSource

class NewsGrabber:
    def get_news(self, source: Optional[str] = None) -&amp;gt; Iterable[News]:
        if source is None:
            return itertools.chain(HNSource().iter_news(), V2Source().iter_news(), RedditSource().iter_news())
        if source == &apos;HN&apos;:
            return HNSource().iter_news()
        elif source == &apos;V2&apos;:
            return V2Source().iternews()
        elif source == &apos;Reddit&apos;:
            return RedditSource().iternews()
        else:
            raise ValueError(f&quot;Not supported source: {source}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;*: &lt;code&gt;itertools.chain()&lt;/code&gt; 可以拼接多个可迭代对象，依次迭代。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;功能上线，领导很满意，小伙伴们现在能在 IM 里直接看新闻了。&lt;/p&gt;
&lt;h2&gt;消灭 if-else&lt;/h2&gt;
&lt;p&gt;过了一礼拜，领导要加一个新闻源 Python China，小 F 觉得自己架子搭得很好了，于是就交给了新来的小 M 去做，小 M 看完代码，很快啊，就加好了功能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在 &lt;code&gt;sources/&lt;/code&gt; 下面新建一个 &lt;code&gt;pychina.py&lt;/code&gt;，实现了&lt;code&gt;PyChinaSource&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;sources/__init__.py&lt;/code&gt; 中新增 &lt;code&gt;from sources.other import PyChinaSource&lt;/code&gt; &lt;em&gt;**&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;main.py&lt;/code&gt; 中加上 &lt;code&gt;from sources import PyChinaSource&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;get_news()&lt;/code&gt; 中新增 &lt;code&gt;elif source == &apos;other&apos;&lt;/code&gt; 的情形&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;em&gt;**: 这可以将 import path 缩短&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;功能上线了，运行无 bug，但一天之后大家发现没有指定新闻源的时候永远看不到 Python China 的新闻。读者应该很快发现了，有处改动漏掉了：&lt;code&gt;if source is None&lt;/code&gt; 的情况下应该加上 &lt;code&gt;PyChinaSource&lt;/code&gt;。复盘之后小 F 接锅：新增一个策略，涉及的改动点太多了，一个不熟悉代码的人很容易漏掉。&lt;/p&gt;
&lt;p&gt;于是小 F 略加改动，创建了一个字典来保存所有策略，消灭掉了 if-else:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source_map = {&apos;HN&apos;: HNSource(), &apos;V2&apos;: V2Source(), &apos;Reddit&apos;: RedditSource(), &apos;PyChina&apos;: PyChinaSource()}

class NewsGrabber:
    def get_news(self, source: Optional[str] = None) -&amp;gt; Iterable[News]:
        if source is None:
            return itertools.chain.from_iterable(source.iter_news() for source in source_map.values())
        try:
            return source_map(source).iter_news()
        except KeyError:
            raise ValueError(f&quot;Not supported source: {source}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这下改动点减少了一个（&lt;code&gt;get_news()&lt;/code&gt; 内部不用改动，但&lt;code&gt;source_map&lt;/code&gt;新增一个改动点）。&lt;/p&gt;
&lt;h2&gt;注册中心&lt;/h2&gt;
&lt;p&gt;小 F 发现这样改动点还是太多了，主要原因是这个字典得自己写，很浪费精力。有没有办法自动生成这个映射呢？用注册大法！首先写一个注册方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# sources/base.py
source_map: Dict[str, BaseSource] = {}

def register(source_cls):
    source_map[source_cls.name] = source_cls()
    return source_cls
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后修改下各新闻源子类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# sources/hn.py
from sources.base import BaseSource, register

@register
class HNSource(BaseSource):
    name = &quot;HN&quot;

    # 省略其他方法
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样做的好处是，所有和一个新闻源相关的参数都集中到一处了，开发者在扩展新的新闻源的时候，关注点无需在不同文件中跳来跳去。免去了「东市买骏马，西市买鞍鞯」的苦恼，一站式的体验，让程序更「友好」了。当然，在 &lt;code&gt;sources/__init__.py&lt;/code&gt; 中还是得导入这些文件（&lt;code&gt;from sources import hn&lt;/code&gt; 就够了，无需导入具体子类）。&lt;/p&gt;
&lt;p&gt;在实际开发中，只要遇到类似「通过某短名反查具体对象」的场景，就可以上注册中心。各大 Web 框架的路由无不是这个模式的应用。用注册中心永远好过 &lt;code&gt;eval&lt;/code&gt; 或者从 &lt;code&gt;globals()&lt;/code&gt; 里面反查对象，前者才是 Pythonic 的。&lt;/p&gt;
&lt;h2&gt;启用魔法&lt;/h2&gt;
&lt;p&gt;改完之后小 F 数了一数，现在如果要扩展一个新闻源，改动点还剩两个：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;新增的子类文件&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;sources/__init__.py&lt;/code&gt; 中导入一次&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Python 这么自由，一定有办法再削减的，于是小 F 根据使用 Django 的经验想到，可以扫描 &lt;code&gt;sources/&lt;/code&gt; 目录下面的所有文件，获取所有新闻源，至于源的名字，放到类变量里去就好了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# sources/hn.py
class HNSource(BaseSource):
    name = &quot;HN&quot;

    # 省略其他方法
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# main.py
import importlib
from sources import source_map

for name in os.listdir(&quot;sources&quot;):
    if not name.endswith(&quot;.py&quot;) or name == &quot;base.py&quot;:
        # 跳过抽象基类文件
        continue
    importlib.import_module(f&quot;sources.{name}&quot;)  # 动态导入
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;导入模块的时候会隐式地更新 &lt;code&gt;source_map&lt;/code&gt;，由于 &lt;code&gt;source_map&lt;/code&gt; 是可变对象，所以可以先导入，再更新它。现在如果要新增一个新闻源，只要复制粘贴出一个新文件，依葫芦画瓢改改就行了，小 F 可以放心地把这个活交给新人，因为整个程序扩展起来非常友好。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本文介绍了如何使用 Python 的特性把一个功能扩展的开发逐步收拢到只有一个改动点。改动收拢，出 bug 的可能性就小。上面的案例并非脱离实际，而是我在项目实践中经常遇到的一个场景——策略与注册，pdm 的 CLI 命令就是通过这个手段组装起来的。值得注意的是，上面虽然通过启用魔法把扩展操作改进得非常友好，却损失了一些阅读代码的友好度——它把一些显式的操作变得有些隐晦（在 for 循环中 &lt;code&gt;import_module&lt;/code&gt; 的副作用无法一眼看出）。所以应该酌情使用，代码并不是越酷炫越好的，强大的武器永远要用在合适的地方。&lt;/p&gt;
</content:encoded></item><item><title>重新思考自定义容器类的实现</title><link>https://frostming.com/posts/2021/05-19/custom-dict/</link><guid isPermaLink="false">https://frostming.com/2021/05-19/custom-dict/</guid><pubDate>Wed, 19 May 2021 17:03:47 GMT</pubDate><content:encoded>&lt;p&gt;随便水一篇「雕虫小技」，想到哪算哪。读本文前假设已读过&lt;a href=&quot;https://treyhunner.com/2019/04/why-you-shouldnt-inherit-from-list-and-dict-in-python/&quot;&gt;这篇文章&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在 Python 中如何编写一个自定义的字典类？大家可能被告诉要使用&lt;code&gt;collections.abc&lt;/code&gt;中的类作为基类而不是&lt;code&gt;dict&lt;/code&gt;。&lt;code&gt;dict&lt;/code&gt;也不是任何时候都不能做基类——当你没有重载任何内建方法时可以直接继承&lt;code&gt;dict&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;但实际场景千变万化，我们不能被几条规则限制了我们的思考，我们是基于什么来选择基类的呢？&lt;/p&gt;
&lt;h2&gt;我们需要什么样的鸭子&lt;/h2&gt;
&lt;p&gt;Python 的类型系统和多态基于鸭子类型，只要这个对象有&lt;strong&gt;我需要的&lt;/strong&gt;所有特性我就能使用它，不管它类型为何。那么针对自定义字典，都是鸭子，我们需要什么样的鸭子呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;collections.UserDict&lt;/code&gt;: 机器鸭，拥有所有鸭子的技能。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;collections.abc.Mapping&lt;/code&gt;&lt;a href=&quot;%E5%8F%96%E5%86%B3%E4%BA%8E%E6%98%AF%E5%90%A6%E5%8F%AF%E5%8F%98%E5%8F%AF%E9%80%89%E6%8B%A9%60collections.abc.MutableMapping%60%EF%BC%8C%E4%B8%8B%E5%90%8C%E3%80%82&quot;&gt;^1&lt;/a&gt;: 一个神奇的鸭子外壳，得按要求穿到身上，任你是什么东西都立即拥有了鸭子的技能，和长相。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dict&lt;/code&gt;: 鸭子本鸭，所有基于此的动物都是鸭子的基因变异。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;给我翻译翻译&lt;/h2&gt;
&lt;p&gt;为什么这么说？&lt;code&gt;collections.UserDict&lt;/code&gt;是开箱即用，还方便小量修改，要改哪个行为，直接覆写就好了。但核心数据结构是写死的，可自定义空间不大。与之相对，&lt;code&gt;collections.abc.Mapping&lt;/code&gt;给了你很大自由度，它没有自带的&lt;code&gt;__init__&lt;/code&gt;方法，数据是存在自身还是存在远端都全凭你决定。而用&lt;code&gt;dict&lt;/code&gt;，要写自定义逻辑就得小心，容易造出四不像。&lt;/p&gt;
&lt;p&gt;除此之外，大部分使用起来都和普通字典并无两样，除了两个地方，其中一个是&lt;code&gt;isinstance&lt;/code&gt;，虽然有条最佳实践是「检查它的行为而不是类型」推荐尽量不用&lt;code&gt;isinstance&lt;/code&gt;，实在要用也要用&lt;code&gt;isinstance(obj, collections.abc.Mapping)&lt;/code&gt;，这对于上述三种派生的类都能返回正确的结果。&lt;/p&gt;
&lt;p&gt;还有一个地方，使用场景不如&lt;code&gt;isinstance&lt;/code&gt;那样广泛，就是&lt;code&gt;json.dumps&lt;/code&gt;，我认为这里绝对需要改进，因为&lt;code&gt;json.dumps&lt;/code&gt;的策略选择是基于&lt;code&gt;isinstance(obj, dict)&lt;/code&gt;的[^2]！Python 居然没有一个让&lt;code&gt;json.dumps&lt;/code&gt;读取的魔法方法，方便自定义类支持 JSON 序列化。导致&lt;code&gt;json.dumps&lt;/code&gt;的这一特性，只对&lt;code&gt;dict&lt;/code&gt;的派生类生效。&lt;/p&gt;
&lt;p&gt;[^2]: 见 https://docs.python.org/zh-cn/3/library/json.html#json.JSONDecoder&lt;/p&gt;
&lt;h2&gt;dict 重回视野&lt;/h2&gt;
&lt;p&gt;有的时候用户期待这个对象在所有地方都兼容普通 dict 的行为，比如一个附带格式属性的 JSON 解析器，用户期待解析结果能正常用 Python 标准库的&lt;code&gt;json&lt;/code&gt;序列化。这时告诉用户用&lt;code&gt;json.dumps(dict(obj))&lt;/code&gt;并不是一个选项。为这支持这万恶的&lt;code&gt;json.dumps&lt;/code&gt;必须重新考虑基类的选择了。&lt;/p&gt;
&lt;p&gt;用&lt;code&gt;dict&lt;/code&gt;做基类，容易发生覆写不完全的问题，而&lt;code&gt;collections.abc.&lt;/code&gt;恰好可以补上这些缺口。只需要实现协议要求的抽象方法即可。但数据存储方面，&lt;strong&gt;必须保存一份干净数据&lt;/strong&gt;在&lt;code&gt;dict&lt;/code&gt;本身，这样才能正确使用依赖&lt;code&gt;dict&lt;/code&gt;的方法。&lt;/p&gt;
&lt;p&gt;举例说明&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
class MyDict(collections.abc.MutableMapping, dict):
    def __init__(self, data):
        dict.__init__(self, data)
        # 执行一些解析逻辑，把结果保存到属性中
        self._data = self._parse(data)

    def __getitem__(self, key):
        # 注意这里我们没有从dict本身取数据，这是完全可以的
        return self._data[key]

    def __setitem__(self, key, value):
        # 但写数据时必须同时更新dict中的数据
        dict.__setitem__(self, key, value)
        # 更新其他属性
        self._update_data(key, value)

    # 省略了一些必要方法
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原则是在所有&lt;strong&gt;写数据&lt;/strong&gt;的地方调用一次&lt;code&gt;dict&lt;/code&gt;自身的方法&lt;a href=&quot;%E6%B3%A8%E6%84%8F%E8%BF%99%E9%87%8C%E6%97%A0%E6%B3%95%E4%BD%BF%E7%94%A8%60super()%60%EF%BC%8C%E5%BF%85%E9%A1%BB%E6%98%BE%E5%BC%8F%E6%8C%87%E5%AE%9A%E5%9F%BA%E7%B1%BB%E9%80%9A%E8%BF%87%60self%60%E4%BC%A0%E9%80%92%E8%87%AA%E8%BA%AB&quot;&gt;^3&lt;/a&gt;，例子中用的是&lt;code&gt;value&lt;/code&gt;，但也可以是经过清洗后的一份数据，这样&lt;code&gt;json.dumps(obj)&lt;/code&gt;就会产生这份干净数据序列化后的结果。&lt;/p&gt;
&lt;p&gt;所以 Best practice 说得再好，也有可能有例外，思考为什么这么做更重要。&lt;/p&gt;
</content:encoded></item><item><title>自建、免费、开源的评论系统解决方案</title><link>https://frostming.com/posts/2021/04-28/self-host-comment-system/</link><guid isPermaLink="false">https://frostming.com/2021/04-28/self-host-comment-system/</guid><pubDate>Wed, 28 Apr 2021 18:07:51 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;2024.1.2 更新&lt;/strong&gt;: 本文中的评论系统已经迁移到了 &lt;a href=&quot;https://artalk.js.org/&quot;&gt;Artalk&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我最近把评论系统切换到了&lt;a href=&quot;https://cusdis.com&quot;&gt;Cusdis&lt;/a&gt;，这是一个非常年轻的项目，我是看着 GitHub Repo 从建立到现在近 900 个 star 的。产品体验不错，在开源协作的过程中也有很多收获，觉得有必要推荐一下，并且介绍下自己用的 workflow 所以有了这篇水文。&lt;/p&gt;
&lt;h2&gt;我为什么选择 Cusdis&lt;/h2&gt;
&lt;p&gt;评论系统有以下几种选择：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;公司产品，最有名的比如 Disqus，好处是使用人数多方便互动，不用自己管理 Infra，缺点是不由你说了算，比如&lt;a href=&quot;https://kinsta.com/blog/disqus-ads/&quot;&gt;强行给你加广告付费才能去除&lt;/a&gt;（驱动我换评论系统的最大原因），以及被墙及跑路的风险。&lt;/li&gt;
&lt;li&gt;白嫖后端产品，常见被白嫖的有 GitHub(&lt;a href=&quot;https://utteranc.es/&quot;&gt;utterances&lt;/a&gt;)，LeanCloud(&lt;a href=&quot;https://valine.js.org/&quot;&gt;Valine&lt;/a&gt;)，优点是省心，缺点是不好导出迁移。&lt;/li&gt;
&lt;li&gt;自造轮子产品，比如&lt;a href=&quot;https://frostming.com/2019/11-22/comment-system/&quot;&gt;我曾经就做过一个&lt;/a&gt;，优点是完全自主，缺点是要做好没有 bug 还是有很多细节要考虑，而且维护 infra 也是一个开销。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Cusdis 其实三类全占，它既有一个 Hosted 服务你可以直接把数据托管在上面，也给了你自己部署自建数据库的自由。而且它支持从 Disqus 导入评论数据。于是我就尝试了一下，最后发现整个方案我挺满意，重点是全白嫖不花钱，下面分享一下。&lt;/p&gt;
&lt;h2&gt;我使用的工作流&lt;/h2&gt;
&lt;h3&gt;数据库&lt;/h3&gt;
&lt;p&gt;Cusdis 支持连接你指定的 PostgreSQL 数据库实例，为了省心我首先想到了 DBaaS，但之前对这块不太熟，找了下各大知名云，都不是永久免费。于是我想到了&lt;a href=&quot;https://www.heroku.com/&quot;&gt;Heroku&lt;/a&gt;，对于免费的实例只有 PostgreSQL 是可以免费用的，而 Cusdis 又（暂时）只支持连接 PostgreSQL，一切都是刚刚好。&lt;/p&gt;
&lt;p&gt;注册帐号登录之后，进入到 Dashboard，右上角 Create new app 新增一个 app，区域选美国&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/20210428184818.png&quot; alt=&quot;20210428184818&quot; /&gt;&lt;/p&gt;
&lt;p&gt;转到 Resources 页面 Add-ons 里面搜索 PostgreSQL 并添加，这样一个数据库就好了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/20210428184956.png&quot; alt=&quot;20210428184956&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后转到 Settings 页面，查看 Config Vars，把&lt;code&gt;DATABASE_URL&lt;/code&gt;复制出来，就可以直接给 Cusdis 用了。&lt;/p&gt;
&lt;h3&gt;服务部署&lt;/h3&gt;
&lt;p&gt;我选择自己部署服务，Cusdis 提供的部署方式有两个：Docker 和 Vercel，用 Docker 的话还是需要一个自己的服务器，而服务器是！要！花！钱！的！所以排除，Cusdis 本身服务端是用 Next.js 开发的所以用 Vercel 部署就非常自然了。点击&amp;lt;a href=&quot;https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fdjyde%2Fcusdis&amp;amp;env=USERNAME,PASSWORD,DB_URL,JWT_SECRET&amp;amp;envDescription=Environment%20variables%20reference&amp;amp;envLink=https%3A%2F%2Fcusdis.com%2Fdoc&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&amp;gt;&amp;lt;img src=&quot;https://vercel.com/button&quot; data-origin=&quot;https://vercel.com/button&quot; alt=&quot;Deploy with Vercel&quot;/&amp;gt;&amp;lt;/a&amp;gt;这个按钮就能一键部署，然后配置好一些必要的环境变量就可以了，完成后直接访问分配的域名就能看到管理后台了。搞掂！&lt;/p&gt;
&lt;h3&gt;自动更新&lt;/h3&gt;
&lt;p&gt;Cusdis 是一个正在快速演进的项目，我希望有任何改进和 Bug 修复都立即更新到我的后台上，所以我用了 GitHub Action 这个大杀器，定时 pull 上游代码提交到 fork，非常丝滑，具体 workflow 可以看&lt;a href=&quot;https://github.com/frostming/cusdis/blob/sync/.github/workflows/sync.yml&quot;&gt;这里&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;新评论通知&lt;/h3&gt;
&lt;p&gt;有新评论到达时通知当然是必需的，可以参考&lt;a href=&quot;https://cusdis.com/doc#/features/notification?id=self-host&quot;&gt;文档的配置&lt;/a&gt;在 Vercel 中配置必要的环境变量就可以了。值得注意的是如果你用的是 Gmail，它会需要你设置一个独立密码才能给外部 app 调用 SMTP 服务，可以在 Google 帐户设置里启用。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Update in 1.1.2&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;现在 Cusdis 还支持 Webhook 式推送通知，你可以建一个服务来接收消息，再通过 Telegram Bot 给自己发通知。这个功能用 Serverless 云函数就能实现了，也有很多好用且免费的服务。我用的是一个刚出现的 Python 云函数服务&lt;a href=&quot;https://www.napkin.io/?ref=producthunt&quot;&gt;Napkin.io&lt;/a&gt;，这是&lt;a href=&quot;https://gist.github.com/frostming/08a3db1bce709ffa2dd3cf32bab2422e&quot;&gt;我写的 Napkin&lt;/a&gt;，你可以 Copy 过去改用自己的 token 和 chat ID，点 Deploy 就跑起来了，非常方便。最后记得在 Cusdis 后台登记 Webhook 的地址，使用效果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/20210430110151.png&quot; alt=&quot;20210430110151&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;开源贡献&lt;/h2&gt;
&lt;p&gt;Cusdis 是一个年轻的开源评论系统，有很多特性尚未支持，我这个搞 Python 的也提交过几次贡献，它的服务端是 Next.js，组件是 Svelte 写的，理解起来并不复杂。如果你也和我一样喜欢折腾喜欢 unstable 追新产品，不妨试试 Cusdis，帮助改善。最后也推荐一下&lt;a href=&quot;https://lutaonan.com&quot;&gt;作者的博客&lt;/a&gt;，内容质量高，非常启发思考。&lt;/p&gt;
</content:encoded></item><item><title>我最近做开源的体会</title><link>https://frostming.com/posts/2021/03-30/my-open-source/</link><guid isPermaLink="false">https://frostming.com/2021/03-30/my-open-source/</guid><pubDate>Tue, 30 Mar 2021 19:25:18 GMT</pubDate><content:encoded>&lt;p&gt;最近每天早上醒来的第一件事就是看邮件，做开源这么久，好像突然变忙起来了，之前从来没有过的分身乏术的感觉也涌现了出来。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;有段时间不写博客，就会浑身难受，实在没写的就更新下近况。&amp;lt;cite&amp;gt;― laixintao&amp;lt;/cite&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;那就水篇文章来谈谈我最近做开源的体会吧。&lt;/p&gt;
&lt;h2&gt;一个性能非常拉垮的 Markdown 解析器&lt;/h2&gt;
&lt;p&gt;我在 2018 年的时候造了一个 Markdown 的轮子用来解析、渲染 markdown 文档，名字叫&lt;a href=&quot;https://github.com/frostming/marko&quot;&gt;Marko&lt;/a&gt;。其实我不爱造轮子，如果有能用的修修补补也就用了，但有个需求，我实在是没找到合适的可用的。我希望能方便地给 markdown 解析器增加自定义的元素和解析逻辑，但现有的流行的库都有不同程度的不足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Python-Markdown/markdown&quot;&gt;Python-Markdown&lt;/a&gt;, 文档极其简陋，我不知道有谁成功地给它写了扩展。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/lepture/mistune&quot;&gt;mistune&lt;/a&gt;, 大佬 lepture 的作品，速度相当快。但它的基于树状正则匹配的解析逻辑&lt;a href=&quot;%E8%BF%99%E4%B8%AA%E8%AF%8D%E6%98%AF%E6%88%91%E5%8F%91%E6%98%8E%E7%9A%84%EF%BC%8C%E6%84%8F%E6%80%9D%E6%98%AF%E5%85%88%E6%8A%8A%E6%96%87%E6%A1%A3%E5%88%92%E5%88%86%E6%88%90%E5%9D%97%E7%BA%A7%E5%85%83%E7%B4%A0%EF%BC%8C%E5%86%8D%E5%9C%A8%E5%9D%97%E7%BA%A7%E5%85%83%E7%B4%A0%E4%B8%AD%E8%A7%A3%E6%9E%90%E5%AD%90%E5%85%83%E7%B4%A0%E5%8F%8A%E8%A1%8C%E9%97%B4%E5%85%83%E7%B4%A0%EF%BC%8C%E6%98%AF%E4%B8%80%E4%B8%AA%E5%88%86%E6%B2%BB%E7%9A%84%E6%80%9D%E6%83%B3%E3%80%82&quot;&gt;^1&lt;/a&gt;限制了扩展的可能性，你只能加一些小打小闹的扩展，文档里给的&lt;a href=&quot;https://mistune.readthedocs.io/en/v0.8.4/&quot;&gt;扩展例子&lt;/a&gt;，就是恰好能用的之一，想改动解析逻辑，几乎是不可能的事。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更重要的原因是，这两个都是基于最原始的 John Gruber 提出的 Markdown spec——没有 spec，这就造成了在很多 corner case，有可能渲染不符合预期，甚至 crash。当然，正是因为选择了一个相对自由的框架，才能在性能方面一骑绝尘。&lt;/p&gt;
&lt;p&gt;再加上当时我正则表达式出了师，膨胀的一比&lt;a href=&quot;%E4%B8%80%E5%88%99%E8%BD%B6%E4%BA%8B%EF%BC%9A%E8%85%BE%E8%AE%AF%E7%9A%84%E7%AC%AC%E4%B8%89%E9%9D%A2%EF%BC%8C%E5%B0%B1%E9%97%AE%E4%BA%86%E4%B8%80%E9%A2%98%E6%AD%A3%E5%88%99%EF%BC%8C%E8%BF%99%E4%B8%8D%E6%92%9E%E4%BA%86%E6%9E%AA%E5%8F%A3%E3%80%82&quot;&gt;^2&lt;/a&gt;，我先是写了个玩具的 TOML 解析器试手，然后就想自己写一个 Markdown 解析器。现在想来，我一个编译原理都没学过的野路子，连 FSM 和 BNF 都不知道，简直是不自量力。因为我的目标是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;遵守&lt;a href=&quot;https://commonmark.org/&quot;&gt;CommonMark spec&lt;/a&gt;[^3]&lt;/li&gt;
&lt;li&gt;解析和渲染过程独立，方便自定义两者任一阶段，以及观察 AST 结果&lt;/li&gt;
&lt;li&gt;统一所有元素的接口，方便 subclass 扩展&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;直到动手做了之后我才知道我错了，CommonMark 的 spec 之变态导致我（菜得）只能（想出）线性解析&lt;a href=&quot;%E5%85%88%E8%A7%A3%E6%9E%90%E5%AE%8C%E5%89%8D%E9%9D%A2%E7%9A%84%EF%BC%8C%E6%89%8D%E8%83%BD%E8%A7%A3%E6%9E%90%E5%90%8E%E9%9D%A2%E7%9A%84%E3%80%82&quot;&gt;^4&lt;/a&gt;，而且我大胆猜测 BNF 也不好使。每做一步解析之前，要先试探着往前，不行再回退回来，正则用得又到处都是，结果就是性能不能指望了，我一跑 benchmark 崩溃了，比最慢的还慢。但这样得到的好处就是 90% 的元素解析[^5]和 100% 的元素渲染接口我都统一了，这已经足够应对大部分扩展的需求。&lt;/p&gt;
&lt;p&gt;本以为就做为练手项目算了，不打算宣传，却发现无意间也帮助到了很多人，他们可以用 Marko 来很快扩展出自己的解析器，比如&lt;a href=&quot;https://github.com/lebigot/markdown_to_BGG&quot;&gt;这个&lt;/a&gt;以及&lt;a href=&quot;https://github.com/frostming/marko/issues/81&quot;&gt;这个&lt;/a&gt;，这是做开源最欣慰的时刻吧。&lt;/p&gt;
&lt;p&gt;[^3]: CommonMark 并非唯一的 Markdown spec，其它的还有&lt;a href=&quot;https://www.vfmd.org/&quot;&gt;vfmd&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;[^5]: 还有 10% 天杀的元素，它们的解析相互依赖，无法分隔出来，堪称&lt;a href=&quot;https://spec.commonmark.org/0.29/#appendix-a-parsing-strategy&quot;&gt;CommonMark 最恶心的部分&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;渐渐庞大的 PDM&lt;/h2&gt;
&lt;p&gt;与之相反，PDM 却是一个在同类产品中有&lt;a href=&quot;https://frostming.com/2021/03-26/pm-review-2021/&quot;&gt;性能优势&lt;/a&gt;的项目。最近几乎所有业余时间以及部分摸鱼时间都在搞这个。曾经我经常在发版之后进入贤者时间，像观赏一个艺术品一样审视把玩，以为没有什么大的改进可做了。这样的日子一去不复返了，用户量上升以后需求和 bug 还是渐渐出现，关键是我还觉得它们提得好有道理啊，得做啊。欣慰的是，我最近可能收获了若干铁杆用户，疯狂在 Repo 里互动，而我正在做一个&lt;a href=&quot;https://github.com/pdm-project/pdm/pull/351&quot;&gt;大 feature&lt;/a&gt;，目前来看反馈非常正面，我突然有种「我为用户提供了价值」的感觉。&lt;/p&gt;
&lt;p&gt;还有两位老哥，热衷于给 PDM 加 type hint，提了个&lt;a href=&quot;https://github.com/pdm-project/pdm/pull/354&quot;&gt;这么大的 PR&lt;/a&gt;，改了 61 个文件，我哭笑不得，这可咋 review 啊，自动生成的 type hint 得人工过一遍才行吧。&lt;/p&gt;
&lt;p&gt;这样功能一直加下去，PDM 的代码终究会变得越来越坏，我自认为现在的代码库己不算容易维护的代码了。而且，PDM 的 bug 都出得非常匪夷所思，就是一眼看不出来线索的那种，主要是路径也挺深，还&lt;a href=&quot;https://twitter.com/frostming90/status/1374685609592115203?s=21&quot;&gt;各种不科学&lt;/a&gt;：只有 Linux 挂, 只有 Mac 挂, 只有 Python 3.7 挂，批跑挂单个跑不挂，本地跑不挂上 CI 挂，时挂时不挂……天天被教育，我真的够够的了，时常要深呼吸一下，眺望远方，才能重振旗鼓。探其原因，都是我要对 Python 环境动手脚而自己作的死。&lt;/p&gt;
&lt;p&gt;做开源，就是这样欣慰与闹心共存着的吧。&lt;/p&gt;
</content:encoded></item><item><title>A Review: Pipenv vs. Poetry vs. PDM</title><link>https://frostming.com/posts/en/2021/pm-review-2021/</link><guid isPermaLink="false">https://frostming.com/en/2021/pm-review-2021/</guid><description>From the perspective of performance and correctness</description><pubDate>Fri, 26 Mar 2021 11:51:00 GMT</pubDate><content:encoded>&lt;h1&gt;Abstract&lt;/h1&gt;
&lt;p&gt;It is 2021 and we are all using or heard of package managers in Python, among which are Pipenv and Poetry. I also built a new package manager &lt;a href=&quot;https://frostming.com/2021/01-22/introducing-pdm/&quot;&gt;PDM&lt;/a&gt; to solve similar problems. There exist some comparisons between them around the community, but this article is not going to talk about the user interface or their versatility, it is going to focus on two important aspects: performance and correctness.&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h1&gt;Setup&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Pipenv&lt;/strong&gt;: &lt;code&gt;HEAD@275f7e151eb0aa17702215165a371df7da9ad476&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Poetry&lt;/strong&gt;: &lt;code&gt;1.1.5&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PDM&lt;/strong&gt;: &lt;code&gt;HEAD@d72ba5d2ee0b0305f917d8739667ba78465c5cc8&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Python version&lt;/strong&gt;: &lt;code&gt;3.9.1&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;Performance&lt;/h1&gt;
&lt;p&gt;Dependency set(in poetry format):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[tool.poetry.dependencies]
python = &quot;^3.9&quot;
requests = { git = &quot;https://github.com/psf/requests.git&quot;, tag = &quot;v2.25.0&quot; }
numpy = &quot;^1.19&quot;

[tool.poetry.dev-dependencies]
pytest = &quot;^5.2&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This includes a Git dependency and a heavy binary dependency. In the following result, the cost times are measured in seconds.&lt;/p&gt;
&lt;p&gt;Unless explicitly given, the measurements are done against the &lt;code&gt;install&lt;/code&gt; command.&lt;/p&gt;
&lt;h2&gt;Result&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Pipenv&lt;/th&gt;
&lt;th&gt;Poetry&lt;/th&gt;
&lt;th&gt;PDM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Clean cache, no lockfile&lt;/td&gt;
&lt;td&gt;98&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;With cache, no lockfile&lt;/td&gt;
&lt;td&gt;117&lt;/td&gt;
&lt;td&gt;66&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clean cache, reuse lockfile*&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;145&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;With cache, reuse lockfile**&lt;/td&gt;
&lt;td&gt;145&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;*&lt;/strong&gt;: Test with command &lt;code&gt;poetry add click&lt;/code&gt; &lt;code&gt;pipenv install --keep-outdated click&lt;/code&gt; &lt;code&gt;pdm add click&lt;/code&gt; respectively.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;**&lt;/strong&gt;: Commands are the same as above.&lt;/p&gt;
&lt;h2&gt;Performance Review&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Pipenv has a problematic cache system, which slows down the performance with the existence of caches&lt;/li&gt;
&lt;li&gt;Poetry and PDM both benefit a lot from the caches, PDM takes even less time.&lt;/li&gt;
&lt;li&gt;Pipenv uses a very different mechanism to reuse the lock file — it runs full locking first then modifies the content of the old lock file, while PDM can reuse the pinned versions in the lock file. Poetry improves a little with the lock file existing.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Correctness&lt;/h1&gt;
&lt;p&gt;The goal of these 3 package managers is to produce a reproducible environment and they all try their best to make it work on cross-platforms and python versions. Let&apos;s see how it turns out.&lt;/p&gt;
&lt;h2&gt;Python Compatibility&lt;/h2&gt;
&lt;p&gt;The project file is designed as following(Poetry):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[tool.poetry.dependencies]
python = &quot;^3.6||^2.7&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And PDM:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[project]
requires-python = &quot;&amp;gt;=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;They both reference to the same range of Python versions to support. Although Pipenv doesn&apos;t support a Python version range constraint, I will also post its results here as a reference.&lt;/p&gt;
&lt;p&gt;Now let&apos;s add one dependency &lt;code&gt;pytest&lt;/code&gt; from a clean project. This dependency is chosen purposely because it has a different version set to support Python 2 and Python 3.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Result of Poetry:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ poetry add pytest
...
Using version ^6.2.2 for pytest

Updating dependencies
Resolving dependencies...

  SolverProblemError

  The current project&apos;s Python requirement (&amp;gt;=2.7,&amp;lt;3.0 || &amp;gt;=3.6,&amp;lt;4.0) is not compatible with some of the required packages Python requirement:
    - pytest requires Python &amp;gt;=3.6, so it will not be satisfied for Python &amp;gt;=2.7,&amp;lt;3.0

  Because no versions of pytest match &amp;gt;6.2.2,&amp;lt;7.0.0
   and pytest (6.2.2) requires Python &amp;gt;=3.6, pytest is forbidden.
  So, because foo depends on pytest (^6.2.2), version solving failed.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lock failed as &lt;code&gt;6.2.2&lt;/code&gt; was picked but it was not compatible with python 2.7. Anyhow Poetry shows a well human-readable error message telling people what happened and how to solve it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Result of Pipenv&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Depending on Python 3 or Python 2 is used to create the virtualenv(with &lt;code&gt;--three/--two&lt;/code&gt; option), &lt;code&gt;pytest&lt;/code&gt; is locked to &lt;code&gt;6.2.2&lt;/code&gt; and &lt;code&gt;4.6.11&lt;/code&gt; respectively. The lock file surely can&apos;t work on both Python 2 and Python 3 environment at the same time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Result of PDM&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ pdm add pytest
Adding packages to default dependencies: pytest
✔ 🔒 Lock successful
...

  ✔ Install pytest 4.6.11 successful

...
🎉 All complete!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pytest&lt;/code&gt; is correctly resolved to &lt;code&gt;4.6.11&lt;/code&gt; that supports all versions within the &lt;code&gt;requires-python&lt;/code&gt; contraint.&lt;/p&gt;
&lt;h2&gt;Version Tree Searching&lt;/h2&gt;
&lt;p&gt;A dependency resolver should be able to search for other candidates when the current one causes version conflicts.&lt;/p&gt;
&lt;p&gt;Let&apos;s take the example from &lt;a href=&quot;https://github.com/python-poetry/poetry#dependency-resolution&quot;&gt;Poetry&apos;s README&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Result of Pipenv&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ pipenv install oslo.utils==1.4.0
...
There are incompatible versions in the resolved dependencies:
  pbr!=0.7,&amp;lt;1.0,&amp;gt;=0.6 (from oslo.utils==1.4.0-&amp;gt;-r C:\Users\FROSTM~1\AppData\Local\Temp\pipenvkbeeio2trequirements\pipenv-0zsj0laj-constraints.txt (line 3))
  pbr!=2.1.0,&amp;gt;=2.0.0 (from oslo.i18n==5.0.1-&amp;gt;oslo.utils==1.4.0-&amp;gt;-r C:\Users\FROSTM~1\AppData\Local\Temp\pipenvkbeeio2trequirements\pipenv-0zsj0laj-constraints.txt (line 3))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unable to resolve* since Pipenv failed to search for lower versions of &lt;code&gt;oslo.i18n&lt;/code&gt; to find one that is compatible with &lt;code&gt;pbr&amp;lt;1.0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;*&lt;/strong&gt;: Be aware that Pipenv&apos;s strategy is &quot;lock after install&quot;, so the incompatible package will be installed into the environment before the lock failure is reported.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Result of Poetry&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;As illustrated in the README, poetry successfully resolves with &lt;code&gt;oslo.i18n==2.1.0&lt;/code&gt; . It searches along the candidate list of &lt;code&gt;oslo.i18n&lt;/code&gt; and discard those that bring conflicts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Result of PDM&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ pdm add oslo.utils==1.4.0
...
  ✔ Install oslo.i18n 2.1.0 successful
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly, PDM also locks successfully with the same version of &lt;code&gt;oslo.i18n&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Environment Marker Propagation&lt;/h2&gt;
&lt;p&gt;To make a cross-platform project template, we often need to define some platform-specific dependencies and those also may have their subdependencies. These platform-specific dependencies may not be able to build successfully on the source system. We don&apos;t want these dependencies and subdependencies to be installed on the systems that do not match the requirements.&lt;/p&gt;
&lt;p&gt;Let&apos;s start by adding a dependency &lt;code&gt;gevent; os_name == &quot;posix&quot;&lt;/code&gt;, which has several subdependencies. Commands are run from a Windows computer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Result of Pipenv(Pipfile.lock)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;_meta&quot;: {
    &quot;hash&quot;: {
      &quot;sha256&quot;: &quot;9ce84144d1fc47c173581ae74c51ffbe28ac242b1fe0e1642915e803f15d3063&quot;
    },
    &quot;pipfile-spec&quot;: 6,
    &quot;requires&quot;: {},
    &quot;sources&quot;: [
      {
        &quot;name&quot;: &quot;pypi&quot;,
        &quot;url&quot;: &quot;https://pypi.org/simple&quot;,
        &quot;verify_ssl&quot;: true
      }
    ]
  },
  &quot;default&quot;: {
    &quot;gevent&quot;: {
      &quot;hashes&quot;: [&quot;...&quot;],
      &quot;markers&quot;: &quot;os_name == &apos;posix&apos;&quot;,
      &quot;version&quot;: &quot;==21.1.2&quot;
    }
  },
  &quot;develop&quot;: {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only &lt;code&gt;gevent&lt;/code&gt; is resolved with the marker in the lock file and Pipenv stops trying to find its children dependencies when the marker doesn&apos;t match the current system. This means if you use this Pipfile.lock to deploy on the target Linux server, some significant dependencies WILL NOT be installed!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Result of Poetry&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;gevent&lt;/code&gt; together with &lt;code&gt;greenlet&lt;/code&gt;, &lt;code&gt;cffi&lt;/code&gt;, &lt;code&gt;pycparser&lt;/code&gt;, &lt;code&gt;zope.event&lt;/code&gt;, &lt;code&gt;zope.interface&lt;/code&gt; are pinned in the lock file and don&apos;t get installed to the environment, which is the expected behavior. But I didn&apos;t figure out how to add a requirement with markers from the CLI and I have to manually write it in &lt;code&gt;pyproject.toml&lt;/code&gt;. Further, if I run &lt;code&gt;poetry add pycparser&lt;/code&gt; after that, &lt;code&gt;pycparser&lt;/code&gt; can be installed correctly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Result of PDM&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The same result as Poetry, except that in &lt;code&gt;pdm.lock&lt;/code&gt;, children dependencies also have the marker &lt;code&gt;os_name == &quot;posix&quot;&lt;/code&gt; so that installers won&apos;t have to search the dependency tree to see whether a single package should be installed.&lt;/p&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;On the performance perspective, Pipenv doesn&apos;t play well due to its design choice that it integrates with other third-party tools and libraries instead of building its own. Pipenv can only wrap, combine, and do a little improvement on those upstream libraries. Moreover, Pipenv doesn&apos;t meet the goal of reproducible environment as well. It can produce a determinsitic installation setup on the source system but it is not a good idea to deploy to a different system without a careful check.&lt;/p&gt;
&lt;p&gt;On contrast, Poetry and PDM are both doing great on performance and correctness, PDM is even better especially on the time cost and compatible dependency resolving. If you do not know this tool yet, &lt;a href=&quot;https://pdm-project.org&quot;&gt;start now&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>留住正在消失的</title><link>https://frostming.com/posts/2021/02-03/the-vanishing/</link><guid isPermaLink="false">https://frostming.com/2021/02-03/the-vanishing/</guid><pubDate>Wed, 03 Feb 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;（放一首歌纪念潇洒哥）&lt;/p&gt;
&lt;p&gt;「我的技能是 LRU」这并不是一句玩笑话，我们的大脑记忆本就是基于这种模型运作的。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;LRU（&lt;em&gt;Least Recently Used&lt;/em&gt;）顾名思义，只有最近经常使用的才会留下，其他更久远的只能消失了。我离开学校七八年，至今仍非常喜欢看一些数学题解，甚至拿起手边的纸笔演算一番，实在没什么可做，比如写码的间隙，我也会在纸上写写字。我这么努力的做一些完全没必要的事，是因为我很怕，怕这些技能生疏了，就捡不起来了。&lt;/p&gt;
&lt;p&gt;其实人际关系也是一样的。一个有趣的事实：我 2017 年注册的推有 342 个 fo，我 2015 年注册的 GitHub 有 348 个 fo，而我 2010 年注册的微博，只有 303 个 fo。我现在处在一个圈子，他们基本都活跃于社交网络，有自己的网站，在某些地方进行着输出，只要用心地找，搜索引擎会找到他们的痕迹。但以前维护的关系，比如我微博上的那些人，便随着微博的没落而渐渐无迹可寻。还有社会上的其他人，乃至各地大厂小厂打工的同行，他们兢兢业业地做着自己的工作，从不在网络上显露自己，于是一旦我们不再共事，就不再有消息。&lt;/p&gt;
&lt;p&gt;我有个读研时很好的朋友，刚毕业那两年还经常联系，但后来由于我生活变化，就联系得少了，他几乎不发动态，我不知他和女朋友结婚了没有，生娃了没有，工作有没有变化，打开微信聊天窗想寒暄几句，又怕过于突兀于是作罢。其实如果他能多发点动态，我能点个赞评论几句，就有了交流的动机。我读完 &lt;a href=&quot;https://lutaonan.com/blog/be-a-creator/&quot;&gt;《做这个世界的生产者》&lt;/a&gt;这篇文章以后深有感触，多做输出，多生产，可能可以&lt;a href=&quot;https://laike9m.com/blog/dui-kang-xiao-shi-de-yong-qi,130/&quot;&gt;对抗正在消失的&lt;/a&gt;吧。&lt;/p&gt;
&lt;p&gt;我从两年前&lt;a href=&quot;https://frostming.com/2019/01-21/pinyin-wubi/&quot;&gt;从五笔切换到了双拼&lt;/a&gt;，已经到了和用五笔时相同的打字速度。但我最近决定折腾 Rime 输入法，顺便用回五笔，才发现所谓肌肉记忆不过如此，生疏滞涩，令我非常沮丧。时不时会码出拼音打出错字，多字词组更是很少去用。这除了肌肉记忆，其实更多的是大脑模型——打拼音时看到想到一个字，大脑自动转换成读音，而五笔需要大脑自动解析为字根，而后者就快要被大脑 LRU 废弃掉了。于是我决定用回五笔，比如这篇文章 800 字，我用五笔耗时 50 分钟，真是想砸键盘了。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;好吧，最后这个「了」字，我又打成了「国」（五笔码是&lt;code&gt;l&lt;/code&gt;）。&lt;/em&gt;&lt;/p&gt;
</content:encoded></item><item><title>You don&apos;t really need a virtualenv</title><link>https://frostming.com/posts/en/2021/introducing-pdm/</link><guid isPermaLink="false">https://frostming.com/en/2021/introducing-pdm/</guid><pubDate>Fri, 22 Jan 2021 11:04:00 GMT</pubDate><content:encoded>&lt;p&gt;When you develop a Python project, you need to install the project&apos;s dependencies. For a long time, tutorials and articles have told you to use a virtual environment to isolate the project&apos;s dependencies. This way you don&apos;t contaminate the working set of other projects, or the global interpreter, to avoid possible version conflicts. We usually have to do these things:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ python3 -m venv venv  # make a virtualenv named `venv`
$ . venv/bin/activate   # activate the virtualenv
(venv) $ pip install -r requirements.txt  # install the dependencies
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are also workflow tools that simplify this process, such as &lt;a href=&quot;https://pipenv.pypa.io&quot;&gt;Pipenv&lt;/a&gt; and &lt;a href=&quot;https://python-poetry.org/&quot;&gt;Poetry&lt;/a&gt;. They create virtual environments for you without perception and then install dependencies into them. They are used by a wide range of users. A virtual environment contains a Python interpreter and has the same directory structure as a normal installation so that it can be used as if it were a standalone Python installation.&lt;/p&gt;
&lt;h2&gt;The problems with virtual environments&lt;/h2&gt;
&lt;p&gt;Virtualenvs help us isolate project dependencies, but things get tricky when it comes to nested venvs: One installs the virtualenv manager(like Pipenv or Poetry) using a venv encapsulated Python, and creates more venvs using the tool which is based on an encapsulated Python. One day a minor release of Python is out and one has to check all those venvs and upgrade them if required before they can safely delete the out-dated Python version.&lt;/p&gt;
&lt;p&gt;Another scenario is global tools. There are many tools that are not tied to any specific virtualenv and are supposed to work with each of them. Examples are profiling tools and third-party REPLs. We also wish them to be installed in their own isolated environments. It&apos;s impossible to make them work with virtualenv, even if you have activated the virtualenv of the target project you want to work on because the tool is lying in its own virtualenv and it can only see the libraries installed in it. So we have to install the tool for each project.&lt;/p&gt;
&lt;p&gt;I&apos;ve been maintaining the Pipenv project as a collaborator for the past two years and became a member of PyPA in early 2020. I am always thinking if the virtual environment is really a must-to-have for Python projects, just like &lt;code&gt;npm&lt;/code&gt;, it doesn&apos;t need a cloned &lt;code&gt;node&lt;/code&gt; binary, but just a &lt;code&gt;node_modules&lt;/code&gt; directory that is unique for each project.&lt;/p&gt;
&lt;h2&gt;PEP 582 -- Python local packages directory&lt;/h2&gt;
&lt;p&gt;The solution has been existing for a long time. &lt;a href=&quot;https://www.python.org/dev/peps/pep-0582/&quot;&gt;PEP 582&lt;/a&gt; was originated in 2018 and is still a draft proposal till the time I wrote this article, but I found out it is exactly what &lt;code&gt;node_modules&lt;/code&gt; are in Python.&lt;/p&gt;
&lt;p&gt;Say you have a project with the following structure:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├── __pypackages__
│   └── 3.8
│       └── lib
└── my_script.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As specified in the PEP 582, if you run &lt;code&gt;python3.8 /path/to/my_script.py&lt;/code&gt;, &lt;code&gt;__pypackages__/3.8/lib&lt;/code&gt; will be added to &lt;code&gt;sys.path&lt;/code&gt;, and the libraries inside will become import-able in &lt;code&gt;my_script.py&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now let&apos;s review the two problems I mentioned in the last section and see how they change with the power of PEP 582. For the first problem, the main cause is that the virtual environment is bound to a cloned Python interpreter on which the subsequent library searching based. It takes advantage of Python&apos;s existing mechanisms without any other complex changes but makes the entire virtual environment to become unavailable when the Python interpreter is stale. With the local packages directory, you don&apos;t have a Python interpreter any more, the library path is directly appended to &lt;code&gt;sys.path&lt;/code&gt;, so you can freely move and copy it.&lt;/p&gt;
&lt;p&gt;For the second, once again, you just call the tool against the project you want to analyze, and the &lt;code&gt;__pypackages__&lt;/code&gt; sitting inside the project will be loaded automatically. This way you only need to keep one copy of the global tool and make it work with multiple projects.&lt;/p&gt;
&lt;h2&gt;PDM -- A new Python package manager and workflow tool&lt;/h2&gt;
&lt;p&gt;Starting from the PEP, I made &lt;a href=&quot;https://pdm-project.org&quot;&gt;PDM&lt;/a&gt;, a new Python package manager and workflow tool that leverages PEP 582 to get rid of virtualenv entirely. It installs dependencies into the local package directory &lt;code&gt;__package__&lt;/code&gt; and makes Python interpreters aware of it with &lt;a href=&quot;https://pdm-project.org/#enable-pep-582-globally&quot;&gt;a very simple setup&lt;/a&gt;. It is not only an implementation of PEP 582 but also the only package manager that supports &lt;a href=&quot;https://www.python.org/dev/peps/pep-0621/&quot;&gt;PEP 621&lt;/a&gt;, a new metadata format based on &lt;code&gt;pyproject.toml&lt;/code&gt; which becomes the standard recently. It is foreseen that pip will also gradually support this format. Besides, PDM uses the same dependency resolver as pip and has a full-featured plugin system, allowing for community-contributed plugins to enhance the functionalities.&lt;/p&gt;
&lt;p&gt;In PDM, PEP 582 is not mandatory, you can also stick with virtualenv. PDM can detect &lt;strong&gt;existing&lt;/strong&gt; venvs but not create new ones.&lt;/p&gt;
&lt;p&gt;Another thing that is noteworthy is its dependency resolution mechanism -- it tries to lock versions that are compatible with the &lt;code&gt;requires-python&lt;/code&gt; value of the project. Say your project requires Python 2.7 or 3.6 upper and you want to add &lt;code&gt;pytest&lt;/code&gt; as a development dependency, in Pipenv(ver. &lt;code&gt;2020.11.15&lt;/code&gt;) you have to pin &lt;code&gt;pytest = &quot;&amp;lt;5&quot;&lt;/code&gt; manually in &lt;code&gt;Pipfile&lt;/code&gt;. And in Poetry(ver. &lt;code&gt;1.1.4&lt;/code&gt;) if you run &lt;code&gt;poetry add -D pytest&lt;/code&gt; you will get:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;The current project&apos;s Python requirement (&amp;gt;=2.7,&amp;lt;3.0 || &amp;gt;=3.6,&amp;lt;4.0) is not compatible with some of the required packages Python requirement:
    - pytest requires Python &amp;gt;=3.6, so it will not be satisfied for Python &amp;gt;=2.7,&amp;lt;3.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yes, it tells you to upgrade your Python requires version. However, in PDM, you can lock successfully:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ pdm add -d pytest
Adding packages to dev dependencies: pytest
✔ 🔒 Lock successful
Changes are written to pdm.lock.
Changes are written to pyproject.toml.
Synchronizing working set with lock file: 11 to add, 0 to update, 0 to remove

  ✔ Install atomicwrites 1.4.0 successful
  ✔ Install colorama 0.4.4 successful
  ✔ Install packaging 20.8 successful
  ✔ Install more-itertools 5.0.0 successful
  ✔ Install pyparsing 2.4.7 successful
  ✔ Install attrs 20.3.0 successful
  ✔ Install pluggy 0.13.1 successful
  ✔ Install py 1.10.0 successful
  ✔ Install pytest 4.6.11 successful
  ✔ Install six 1.15.0 successful
  ✔ Install wcwidth 0.2.5 successful

🎉 All complete!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, &lt;code&gt;pytest&lt;/code&gt; is pinned to &lt;code&gt;4.*&lt;/code&gt; that is compatible with Python 2.7 and Python 3.6+.&lt;/p&gt;
&lt;p&gt;For more features and demos, please refer to the &lt;a href=&quot;https://pdm-project.org&quot;&gt;PDM&apos;s website&lt;/a&gt; and &lt;a href=&quot;https://github.com/frostming/pdm&quot;&gt;GitHub repo&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Some random words&lt;/h2&gt;
&lt;p&gt;You may have seen this &lt;a href=&quot;https://xkcd.com/927/&quot;&gt;comic&lt;/a&gt; before, there are already so many package managers in Python&apos;s world, do we need a new one? No, I think. Gladly we have seen many improvements in the Python packaging ecosystem and the official installer pip, such as &lt;a href=&quot;https://www.python.org/dev/peps/pep-0517/&quot;&gt;PEP 517&lt;/a&gt;/&lt;a href=&quot;https://www.python.org/dev/peps/pep-0518/&quot;&gt;PEP 518&lt;/a&gt;, and the &lt;a href=&quot;https://discuss.python.org/t/announcement-pip-20-2-release/4863&quot;&gt;new dependency resolver&lt;/a&gt;, more to come in the future. But before the day comes, why not try something different from the traditional, why not make something new that makes at least myself happy. If you think so, PDM should be an option, hopefully.&lt;/p&gt;
</content:encoded></item><item><title>Python打包指南2021</title><link>https://frostming.com/posts/2020/12-25/python-packaging/</link><guid isPermaLink="false">https://frostming.com/2020/12-25/python-packaging/</guid><pubDate>Fri, 25 Dec 2020 16:32:00 GMT</pubDate><content:encoded>&lt;p&gt;大家圣诞快乐，&lt;a href=&quot;/tags?name=%E9%9B%95%E8%99%AB%E5%B0%8F%E6%8A%80&quot;&gt;雕虫小技&lt;/a&gt;栏目又和大家见面了，谁让咱不会那些个屠龙之技，只好捉几个虫子玩玩了。
写这篇文章是因为过去的两年关于&lt;code&gt;pip&lt;/code&gt;和 Python 包管理有几个重要的 PEP 发布，然而网上（中文世界）的打包发布教程很少有针对此的更新。
再加上我成为 PyPA 的成员已经尸位素餐快一年了，还是应该来做点贡献。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;setup.py 真难写&lt;/h2&gt;
&lt;p&gt;似乎从有 Python 打包以来就有了&lt;code&gt;setuptools&lt;/code&gt;这个库，你能搜到的教程，涉及打包发布的，都会让你编写那个可怕的&lt;code&gt;setup.py&lt;/code&gt;。
不知道谁能完全掌握那个东西的写法，我到现在都还不太会。说几个常用的配置：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;指定依赖和可选依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;setup(
    install_requires=[&quot;flask&quot;, &quot;flask-migrate&quot;, &quot;sqlalchemy&quot;],
    extras_require={&quot;mysql&quot;: [&quot;mysqlclient&quot;], &quot;pgsql&quot;: [&quot;psycopg2&quot;]}
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意那两个 key 分别是&lt;code&gt;install_requires&lt;/code&gt;和&lt;code&gt;extras_require&lt;/code&gt;，别写错了。此外，如果你需要根据条件增减依赖的话，不要用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSTALL_REQUIRES = [&quot;flask&quot;]
if sys.platform == &quot;win32&quot;:
    INSTALL_REQUIRES.append(&quot;pywin32&quot;)
setup(install_requires=INSTALL_REQUIRES)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而应该使用&lt;a href=&quot;https://www.python.org/dev/peps/pep-0508/#environment-markers&quot;&gt;Environment Markers&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSTALL_REQUIRES = [
    &quot;flask&quot;,
    &quot;pywin32; sys_platform == &apos;win32&apos;&quot;
]
setup(install_requires=INSTALL_REQUIRES)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;发布可执行程序到&lt;code&gt;/bin&lt;/code&gt;下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;setup(
    entry_points={
        &quot;console_scripts&quot;: [&quot;mybin=mypackage.main:cli&quot;]
    }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者 ini 写法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;setup(
    entry_points=&quot;&quot;&quot;[console_scripts]
    mybin = mypackage.main:cli
    &quot;&quot;&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;任选其一。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;包含 data 文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;setup(
    include_package_data=True    # 从MANIFEST.in中读取配置
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;setup(
    package_data={&quot;&quot;: [&quot;*.json&quot;]}  # 包含所有json文件
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;指定源代码结构，如果你使用的是&lt;code&gt;src/&lt;/code&gt;存放包的源码这种项目结构，可以：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;setup(
    package_dir={&quot;&quot;: &quot;src&quot;}
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;打包上传和安装&lt;/h2&gt;
&lt;h3&gt;打包&lt;/h3&gt;
&lt;p&gt;好了，这个万恶的&lt;code&gt;setup.py&lt;/code&gt;我已经写好了，咱要发布 PyPI 了。第一步，打包成可分发的文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ python setup.py sdist bdist_wheel --universal
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令会同时生成源代码包（Source Distribution），和二进制包（Binary Distribution）。当然，大部分的 Python 发布包中并不真的包含二进制，
只是沿用了软件工程中的一般叫法。其中&lt;code&gt;bdist_wheel&lt;/code&gt;生成的二进制包是 wheel 格式（需要安装&lt;code&gt;wheel&lt;/code&gt;才能打包），&lt;code&gt;--universal&lt;/code&gt;的意思是这个二进制包对所有
支持的 Python 版本和 ABI 都适用，「 一处打包，到处使用」，生成的文件名类似：&lt;code&gt;my_package-0.1.0-py3-none-any.whl&lt;/code&gt;。如果你包中有 C 扩展，
也就是打包出来的 wheel 会真的有二进制文件时就不能加这个 flag 了，这时生成的文件名类似：&lt;code&gt;my_package-0.1.0-cp38-cp38-win_amd64.whl&lt;/code&gt;。
这个文件名不是乱来的，是要遵循一定规则，下载器能直接从这个文件名获得这个包的基本信息：
&lt;img src=&quot;https://webp.frostming.com/images/20201225160452.png&quot; alt=&quot;20201225160452&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;上传&lt;/h3&gt;
&lt;p&gt;可能有老的教程，让你直接用&lt;code&gt;python setup.py sdist bdist_wheel register upload&lt;/code&gt;打包上传一步到位，这个方式已经过时了不推荐使用。正确的方法应该用&lt;a href=&quot;https://pypi.org/project/twine&quot;&gt;twine&lt;/a&gt;工具:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ twine upload dist/*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你要把上传放到 CI 里自动执行，最好生成一个 token 来使用，访问 https://pypi.org/manage/account/token/ 按提示生成一个 token，使用的时候只要用命令指定下用户名和密码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;twine upload --username __token__ --password ${{ secrets.PYPI_TOKEN }} dist/*
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;安装&lt;/h3&gt;
&lt;p&gt;把包上传到 PyPI 以后，&lt;code&gt;pip install my-package&lt;/code&gt;的时候是怎么安装的呢？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;访问&lt;code&gt;https://pypi.org/simple/my-package&lt;/code&gt;，解析所有链接&lt;/li&gt;
&lt;li&gt;若是 whl 文件，判断是否与当前 Python 版本、ABI、平台适配，加入到候选列表&lt;/li&gt;
&lt;li&gt;从&lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;标签中读取&lt;code&gt;data-requires-python&lt;/code&gt;属性，判断是否与当前 Python 版本兼容，加入候选列表&lt;/li&gt;
&lt;li&gt;若是源代码包，直接加入候选列表&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最终在候选列表中优先选择 whl 文件为待安装的包，将包下载到本地，候选包的选择可以由&lt;code&gt;pip install&lt;/code&gt;的&lt;code&gt;--only-binary&lt;/code&gt;和&lt;code&gt;--no-binary&lt;/code&gt;选项控制。&lt;/p&gt;
&lt;p&gt;现在准备安装了，如果待安装的是 whl，那就非常简单，直接解压（whl 文件是一种 zip 格式），放到目标目录即可，解压后产生的文件除了代码或二进制以外，还会包含一个&lt;code&gt;my_package-0.1.0.dist-info/&lt;/code&gt;目录，包含这个包的元数据信息，比如有哪些文件、文件 hash 值、entry_points 等等。&lt;/p&gt;
&lt;p&gt;如果待安装的文件是源代码包，那么需要把这个压缩包解压到一个临时目录，根据包指定的方式编译构建，生成 whl 文件，再用 whl 安装同样的方法放到目标目录中。而这个指定的编译方式，在 PEP 517 提案之前，是调用&lt;code&gt;python setup.py install&lt;/code&gt;命令。在 PEP 517 发布之后，则由 PEP 517 的 build backend 控制。&lt;/p&gt;
&lt;p&gt;注意，在 PEP 517 提案之后的今天，&lt;strong&gt;永远不要&lt;/strong&gt;再用&lt;code&gt;python setup.py install&lt;/code&gt;，&lt;code&gt;python setup.py build&lt;/code&gt;这两种方式安装和构建包了，所有的 PyPI 上的包，都必须通过 wheel 格式安装，如果没有 wheel 包的，则必须提供符合 PyPA 规范的源码包，经过 PEP 517 构建为 wheel 格式之后再安装，&lt;code&gt;pip install &amp;lt;package&amp;gt;&lt;/code&gt;背后就是这样做的。为了更好地掌握，你也可以分开执行这两个步骤：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ pip wheel foo-0.1.0.tar.gz -d dist/
$ pip install dist/foo-0.1.0-py3-none-any.whl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PEP 517 的目的就是通过统一协议抽象这两个过程，使它能后端无关化，PyPA 针对两者也分开有独立的工具：&lt;a href=&quot;https://github.com/pypa/build&quot;&gt;pypa/build&lt;/a&gt;和&lt;a href=&quot;https://github.com/pradyunsg/installer&quot;&gt;pradyunsg/installer&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;setuptools 不再是唯一的选择&lt;/h2&gt;
&lt;p&gt;PEP 517 的内容简单来说，就是在项目根目录下的&lt;code&gt;pyproject.toml&lt;/code&gt;定义了两个特殊属性[^1]：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[build-system]
requires = [
  &quot;setuptools &amp;gt;= 40.8.0&quot;,
  &quot;wheel&quot;
]
build-backend = &quot;setuptools.build_meta:__legacy__&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这个就是&lt;code&gt;setuptools&lt;/code&gt;的 PEP 517 的配置，这样可以让老的项目，能直接用 PEP 517 的方式构建。如果你的项目中并没有&lt;code&gt;pyproject.toml&lt;/code&gt;文件，&lt;code&gt;pip&lt;/code&gt;能自动填充为此缺省配置。其中&lt;code&gt;requires&lt;/code&gt;意为这个 backend 依赖的包列表，&lt;code&gt;build-backend&lt;/code&gt;则为 backend 的具体位置。这个 backend 需要实现几个约定的接口：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;get_requires_for_build_wheel&lt;/code&gt;，构建 wheel 需要的依赖列表，这个一般没有特殊要求都是空&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_requires_for_build_sdist&lt;/code&gt;，构建 sdist 需要的依赖列表，同上&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prepare_metadata_for_build_wheel&lt;/code&gt;，生成一个 wheel 要用的&lt;code&gt;dist-info/&lt;/code&gt;文件夹&lt;/li&gt;
&lt;li&gt;&lt;code&gt;build_wheel&lt;/code&gt;，生成 wheel 文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;build_sdist&lt;/code&gt;，生成 sdist 文件&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;有了这些接口，&lt;code&gt;pip&lt;/code&gt;以及其他可能的 frontend 就能从源代码构建一个 wheel 出来。因此，&lt;code&gt;pyproject.toml&lt;/code&gt;必须被包含在源代码包中。&lt;/p&gt;
&lt;p&gt;有了 PEP 517 的协议规范以后，backend 和 frontend 就能自由组合，不再是非&lt;code&gt;setuptools&lt;/code&gt;不可了，实现了 PEP 517 的 backend 有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/python-poetry/poetry-core&quot;&gt;Poetry-core&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pypi.org/project/flit-core/&quot;&gt;Flit-core&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/frostming/pdm-pep517&quot;&gt;pdm-pep517&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;[^1]: 其实还有第三个属性&lt;code&gt;backend-path&lt;/code&gt;，当你的 backend 是在本地时使用。&lt;/p&gt;
&lt;h2&gt;所以我可以不用写 setup.py 了&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;setup.py&lt;/code&gt;作为一个元数据的定义格式是有问题的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;必须由 Python 运行，无法静态解析&lt;/li&gt;
&lt;li&gt;由于第 1 点，有注入恶意代码的操作可行性&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以需要指定一个元数据的配置格式，这个格式规范最近也定下来了，它就是 PEP 621，也是使用&lt;code&gt;pyproject.toml&lt;/code&gt;来定义的。而且，&lt;a href=&quot;https://pdm-project.org&quot;&gt;PDM&lt;/a&gt;已经支持这个配置格式了，仅此一家。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;阅读链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://packaging.python.org/&quot;&gt;Python Packaging User Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://setuptools.readthedocs.io/en/latest/&quot;&gt;setuptools 文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Python 包构建接口 - &lt;a href=&quot;https://www.python.org/dev/peps/pep-0517/&quot;&gt;PEP 517&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Wheel 包格式 - &lt;a href=&quot;https://www.python.org/dev/peps/pep-0427/&quot;&gt;PEP 427&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Python 包元数据格式 - &lt;a href=&quot;https://www.python.org/dev/peps/pep-0621/&quot;&gt;PEP 621&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.infoworld.com/article/3487701/snake-bites-beware-malicious-python-libraries.html&quot;&gt;Snake bites: Beware malicious Python libraries&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>遥远的她</title><link>https://frostming.com/posts/2020/12-12/distant-her/</link><guid isPermaLink="false">https://frostming.com/2020/12-12/distant-her/</guid><pubDate>Sat, 12 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;吃完晚饭略感燥热，把外套脱了。打开朋友圈，我知道北京下了第一场雪。饭店熙熙攘攘，我却思绪澎湃。&lt;/p&gt;
&lt;p&gt;我和北京断了关系，已经六年了。&lt;/p&gt;
&lt;p&gt;时间回到我八岁那年，那时记忆已模糊，只是后来听大人饭后时常提起，那时我第一次从电视上认识了一所大学，并把她当成了我的目标。
她就是北京大学，那是 1998 年，北大的百年校庆，一位伟人做了讲话。&lt;/p&gt;
&lt;p&gt;后来的事就是，我并没有什么主角光环，没有成为励志的主角。我去了中科大，一所离开了北京就再也没有回去的学校。&lt;/p&gt;
&lt;p&gt;你知道那种感觉，就是你暗恋的初恋，没有开始就已结束。我混过了在合肥的四年，所幸绩点还行，科大保研名额也充裕，当时众多科研院所，我选择了微电子所。
虽然微电子并不是我感兴趣的专业，但这也是我能面过的，唯一一个北京的院所了。现在回想起来，科大对我有恩，我却像一个不孝之子，匆匆地离开了，头也没回。
没能和兄弟们好好告别，毕业礼也没能参加，这个遗憾，可能会伴随终身。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/yellow-leaves.jpg&quot; alt=&quot;yellow-leaves&quot; /&gt;&lt;/p&gt;
&lt;p&gt;时隔多年又有机会和初恋产生联系，我就勉强了自己和她在一起。三年的时间不长也不短，我也体会了冬天的初雪、北京的秋、故宫的红墙、后海的冰。
白嫖了几场北电的话剧，去了两场五月天的演唱会，有几个聊得来的朋友，和令我珍惜的友情，这无疑让我畅快。记得有一次我老婆（当时是女友）来京，
我们从地坛公园走到国子监。在孔庙的老槐树下坐着看夕阳，真的美极了。我能感受到，这夕阳从几百年前起，就是这样了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/mayday-nest.jpg&quot; alt=&quot;mayday-nest&quot; /&gt;&lt;/p&gt;
&lt;p&gt;北京具有的这种气质让我着迷，但北京不止这种气质。&lt;/p&gt;
&lt;p&gt;我曾在初冬的深夜独自从实验室回宿舍，暖黄的路灯和闪烁的红绿灯在我视野中虚化成了光斑，冷风从我没穿秋裤的裤脚灌进来，我感到莫名的孤独。
&lt;img src=&quot;https://webp.frostming.com/images/snow.jpg&quot; alt=&quot;snow&quot; /&gt;
我曾在面试失败后走在知春路的大街，往来都是忙碌的人们。他们面无表情，奔各自的前程。寒冷让人们都缩在袖筒里，连同人情味也一同隔绝。
我甚至在想如果当时就倒地不起，会不会有人来管我。&lt;/p&gt;
&lt;p&gt;北京大路宽敞，路两旁却萧索异常，有时甚至晚上九点刚过，已经乌黑寂静。有次国庆节，当我还是穷学生的时候，曾和几个亦是外地来的死党，
为了找住的地方，从二环走到三环，除了高墙大院，机关单位，鲜少烟火之处。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/pku.jpg&quot; alt=&quot;pku&quot; /&gt;&lt;/p&gt;
&lt;p&gt;渐渐认识到了北京的冰冷，使我明白勉强不一定有好结果，这里终究不是定居之地。东四十条胡同不属于我，三里屯不属于我，回龙观不属于我，簋街不属于我。
那么缘分就到这吧。当你期待，却总失望，当你不抱希望，却忽然给你惊喜。记得我离开那天，雷雨欲来，当我去乘地铁，最后一次路过健德门桥的时候，我回头望了望天空。
金黄的夕阳穿透澄澈的天空，嗯，雾霾总算是消散了。
&lt;img src=&quot;https://webp.frostming.com/images/goodbye-beijing.jpg&quot; alt=&quot;goodbye-beijing&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我知道 那些夏天&lt;br /&gt;
就像青春一样回不来&lt;br /&gt;
代替梦想的也只能是勉为其难 &amp;lt;cite&amp;gt;― 宋冬野《安和桥》&amp;lt;/cite&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;双十二夜于松山湖&lt;/p&gt;
</content:encoded></item><item><title>代码高亮分词对比</title><link>https://frostming.com/posts/2020/12-08/highlight-lexer-comparison/</link><guid isPermaLink="false">https://frostming.com/2020/12-08/highlight-lexer-comparison/</guid><pubDate>Tue, 08 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在做独立博客的时候，特别是对于程序员来说，代码高亮是很重要的一个组件。我也接触过几款不同的代码高亮引擎。衡量一个高亮引擎的好坏有很多不同的方面：分词、性能、稳定性、主题丰富性。本文将专注分词的表现，对几款流行的高亮引擎以及 IDE 做一个横向对比。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;什么是分词&lt;/h2&gt;
&lt;p&gt;要把一段代码高亮输出，主要工作流程大概如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/image-20201208104533768.png&quot; alt=&quot;image-20201208104533768&quot; /&gt;&lt;/p&gt;
&lt;p&gt;分词的过程就类似于画画的线稿，线稿越精细，上色的自由度就越高，最终得到的输出就有可能越丰富好看。作为一个面向颜值的工程师，对颜值可以说非常看重了。不管着色主题好看与否，分词的精细程度才是关键之处。分词分好了，怎么上色无非是主题作者的事情。&lt;/p&gt;
&lt;h2&gt;对比的对象&lt;/h2&gt;
&lt;p&gt;测试例子代码是 Python，因为我也主要关注 Python 代码的分词表现，主题统一用 Monokai 并做了微调以求尽量统一。根据分词进行在前端或者后端，本次参加对比的选手有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前端分词：&lt;a href=&quot;https://highlightjs.org/&quot;&gt;Highlight.js&lt;/a&gt;, &lt;a href=&quot;https://prismjs.com/&quot;&gt;Prism.js&lt;/a&gt;，送到 HTML 中的是未标注的代码段&lt;/li&gt;
&lt;li&gt;Python 后端分词：&lt;a href=&quot;https://pygments.org/&quot;&gt;Pygments&lt;/a&gt;， 送到 HTML 中的是已标注的代码段&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外请到了几位大佬下场指导：他们分别是编辑器界扛把子 Vim、GUI 编辑器扛把子 VSCode，以及专用 IDE 扛把子 PyCharm（没有人比我更懂 Python 分词）。
他们仅作为天花板的参考。编辑器都尽量用最简单的配置，禁用多余的扩展。&lt;/p&gt;
&lt;h2&gt;对比结果&lt;/h2&gt;
&lt;p&gt;废话少说，我拉了一个清单，把例子代码中涉及到的语法元素做了大概的总结，渲染结果可以在&lt;a href=&quot;https://frostming.github.io/highlight-lexer-comparison/&quot;&gt;这里&lt;/a&gt;查看。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Highlight.js&lt;/th&gt;
&lt;th&gt;Prism.js&lt;/th&gt;
&lt;th&gt;Pygments&lt;/th&gt;
&lt;th&gt;Vim&lt;/th&gt;
&lt;th&gt;VSCode&lt;/th&gt;
&lt;th&gt;PyCharm&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;区分 built-in&lt;/td&gt;
&lt;td&gt;✔️[^1]&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;识别 operator&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;区分 import keywords&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;区分 magic method&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;区分 doc-string 与 string&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;解析 f-string&lt;/td&gt;
&lt;td&gt;✔️&lt;a href=&quot;%E6%95%B4%E4%BD%93%E8%AF%86%E5%88%AB%EF%BC%8C%E8%80%8C%E6%B2%A1%E6%9C%89%E7%BB%86%E8%87%B4%E5%88%86%E8%AF%8D&quot;&gt;^2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;解析 decorator&lt;/td&gt;
&lt;td&gt;✔️&lt;a href=&quot;%E6%95%B4%E4%BD%93%E8%AF%86%E5%88%AB%EF%BC%8C%E8%80%8C%E6%B2%A1%E6%9C%89%E7%BB%86%E8%87%B4%E5%88%86%E8%AF%8D&quot;&gt;^2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;✔️&lt;a href=&quot;%E6%95%B4%E4%BD%93%E8%AF%86%E5%88%AB%EF%BC%8C%E8%80%8C%E6%B2%A1%E6%9C%89%E7%BB%86%E8%87%B4%E5%88%86%E8%AF%8D&quot;&gt;^2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;区分参数与 identifier&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;区分 self 与参数&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;区分 class 与 identifier&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;区分 annotation[^3]&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;[^1]: Highlight.js 很奇怪，&lt;code&gt;int&lt;/code&gt;, &lt;code&gt;object&lt;/code&gt;能识别，但&lt;code&gt;float&lt;/code&gt;却没有&lt;/p&gt;
&lt;p&gt;[^3]: 只是一个例子，PyCharm 还有很多其他独有的分词元素&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;我们可以看到三个对比者中 Prism.js 和 Pygments 不相上下，Prism.js 只差一点，但 Pygments 毕竟是 Python 实现所以可以理解。最差的是 Highlight.js，综合网上评价的 bug 比较多的情况，不建议选择。&lt;/p&gt;
&lt;p&gt;用前端分词的好处是配置简单，只需要额外几个 script 就完成。用 Pygments 则需要对后端代码做适当改动。不过&lt;a href=&quot;https://github.com/Python-Markdown/markdown&quot;&gt;python-markdown&lt;/a&gt;和&lt;a href=&quot;https://github.com/frostming/marko&quot;&gt;Marko&lt;/a&gt;都提供了对应的扩展，可以在 Markdown 转换 HTML 的时候就通过 Pygments 标注好代码段，这也不是很大的问题。考虑到 Prism.js 已经能有比较好的表现了，&lt;strong&gt;我首推 Prism.js 做博客的代码高亮。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;而三个产品距离专业的代码编辑器都还有很大的距离。至于 Vim，因为我不会用或者不会配，可能会有更好的表现，希望懂的同学补充。&lt;/p&gt;
&lt;h2&gt;更新于 2021-11-24&lt;/h2&gt;
&lt;p&gt;最近发现了一款新的高亮引擎 &lt;a href=&quot;https://github.com/shikijs/shiki&quot;&gt;shiki&lt;/a&gt;，它底层用的 lexer 是 TextMate 的 language 文件，这也是 VSCode 所采用的。所以 shiki 可以支持和 VSCode 几乎一样的的语法高亮。我的博客也最近切换到了 shiki，它是我现在最推荐的高亮引擎。&lt;/p&gt;
</content:encoded></item><item><title>PEP 582 的开发日志(续)</title><link>https://frostming.com/posts/2020/11-26/pdm-pep582-contd/</link><guid isPermaLink="false">https://frostming.com/2020/11-26/pdm-pep582-contd/</guid><description>无限接近node.js的本地包体验</description><pubDate>Thu, 26 Nov 2020 03:04:20 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;更新于 2020.12.7&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这篇文章是&lt;a href=&quot;https://frostming.com/2020/04-12/pdm-pep582&quot;&gt;PEP 582 的开发日志&lt;/a&gt;的后续，因为按照之前的实现方法，有几个缺陷：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;为了可执行文件能直接全局运行，需要在文件里塞私货&lt;/li&gt;
&lt;li&gt;需要魔改&lt;code&gt;lib&lt;/code&gt;目录下的&lt;code&gt;site.py&lt;/code&gt;，可能造成冲突&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但是，非常 Exciting 地，现在 PDM 的 PEP 582 可以说是完全态了！先看下 Demo：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/pdm-gif.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;依赖被安装在了隔离的目录&lt;code&gt;__pypackages__&lt;/code&gt;，但运行却可以通过全局解释器&lt;code&gt;python&lt;/code&gt;运行。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;没有&lt;code&gt;activate&lt;/code&gt;，改任何 shell 变量&lt;/li&gt;
&lt;li&gt;没有用一个包装过的&lt;code&gt;python&lt;/code&gt;可执行文件&lt;/li&gt;
&lt;li&gt;没有&lt;code&gt;pdm run&lt;/code&gt;前缀&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了证明依赖确实没有被安装在全局解释器下，我演示了在别的目录&lt;code&gt;import flask&lt;/code&gt;返回失败。简而言之，就是&lt;strong&gt;用全局的解释器，加载隔离的依赖目录&lt;/strong&gt;，无限接近 Node.js 的体验。你所要做的，只是&lt;code&gt;export PYTHONPEP582=1&lt;/code&gt;，但其实这也是一个 feature 开关，未来可能不再需要。&lt;/p&gt;
&lt;h2&gt;这是怎么做到的&lt;/h2&gt;
&lt;p&gt;难道我魔改了 Python 解释器？不不不，秘诀还是在 Python 的&lt;code&gt;site&lt;/code&gt;模块中。前文提到过，&lt;code&gt;site&lt;/code&gt;模块有个最大的特点，就是&lt;strong&gt;除非你显式加上&lt;code&gt;-S&lt;/code&gt;参数，它会在 Python 启动时作为模块执行&lt;/strong&gt;，这就为一些稀奇古怪的 startup 钩子提供了可能&lt;a href=&quot;%E4%BD%A0%E4%BB%AC%E5%8F%AF%E8%83%BD%E7%9F%A5%E9%81%93%60PYTHONSTARTUP%60%E4%B9%9F%E5%8F%AF%E4%BB%A5%E6%8E%A7%E5%88%B6%E5%90%AF%E5%8A%A8%E7%9A%84%E8%A1%8C%E4%B8%BA%EF%BC%8C%E4%BD%86%E8%BF%99%E4%B9%9F%E9%9C%80%E8%A6%81%E9%A2%9D%E5%A4%96%E9%85%8D%E7%BD%AE%EF%BC%8C%E6%95%85%E4%B8%8D%E8%80%83%E8%99%91&quot;&gt;^1&lt;/a&gt;。&lt;code&gt;site&lt;/code&gt;模块的执行流程大概是这样的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/image-20201126104508094.png&quot; alt=&quot;image-20201126104508094&quot; /&gt;&lt;/p&gt;
&lt;p&gt;要在这里加私货，有 4 个途径：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;魔改一个&lt;code&gt;site.py&lt;/code&gt;使得&lt;code&gt;python -m site&lt;/code&gt;的时候执行到这里&lt;/li&gt;
&lt;li&gt;写一个&lt;code&gt;.pth&lt;/code&gt;文件，使用&lt;code&gt;import&lt;/code&gt;开头的行，达成执行其中代码的效果（）&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;userbase&lt;/code&gt;中添加&lt;code&gt;.pth&lt;/code&gt;文件&lt;/li&gt;
&lt;li&gt;写一个&lt;code&gt;usercustomize.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;写一个&lt;code&gt;sitecustomize.py&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;咱们挨个分析，1 和 2 都要在全局的 Python 目录中塞文件，这里有个重大的问题——文件权限，所以直接不予考虑了。
而 3 和 4 存在另外的两个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;执行到&lt;code&gt;userbase&lt;/code&gt;加载时，&lt;code&gt;site-packages&lt;/code&gt;还没加载，所以不能实现屏蔽掉&lt;code&gt;site-packages&lt;/code&gt;的作用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;userbase&lt;/code&gt;&lt;strong&gt;并不是无条件加载&lt;/strong&gt;，当检测到是&lt;code&gt;venv&lt;/code&gt;模块创建的虚拟环境时则会跳过&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以剩下的第 5 个方案成了最终的选择，虽然它会屏蔽掉可能已经存在的&lt;code&gt;sitecustomize.py&lt;/code&gt;，但这也是很小的问题了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;更新按&lt;/strong&gt;
在最初其实选择的是方案 2，忽略了文件系统权限的问题，这里要感谢&lt;a href=&quot;https://github.com/Aloxaf&quot;&gt;@Aloxaf&lt;/a&gt;指出了&lt;a href=&quot;https://github.com/frostming/pdm/issues/185&quot;&gt;问题&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;于是&lt;code&gt;sitecustomize.py&lt;/code&gt;中会执行一个顶层函数，将&lt;code&gt;site-packages&lt;/code&gt;去掉并加载上&lt;code&gt;__pypackages__&lt;/code&gt;目录。而用户需要将这个文件所在的目录加到&lt;code&gt;PYTHONPATH&lt;/code&gt;中（PDM 提供了快捷命令）。&lt;/p&gt;
&lt;h2&gt;一些感想&lt;/h2&gt;
&lt;p&gt;其实这次改进要感谢 Poetry discord 里面的一个提问&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/image-20201126105751011.png&quot; alt=&quot;image-20201126105751011&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我一直都知道&lt;code&gt;.pth&lt;/code&gt;的用法，但没有去想到可以用它来魔改 Python 解释器，只是微小的一步。有些时候要对已经实现的代码时常审视，说不定就找到了新的途径。&lt;/p&gt;
&lt;p&gt;以上。&lt;/p&gt;
</content:encoded></item><item><title>我是如何摸鱼的</title><link>https://frostming.com/posts/2020/11-19/github-action-game/</link><guid isPermaLink="false">https://frostming.com/2020/11-19/github-action-game/</guid><description>利用GitHub Action在你的个人README加魔法</description><pubDate>Thu, 19 Nov 2020 03:06:00 GMT</pubDate><content:encoded>&lt;p&gt;可能是我之前 star 过几个 GitHub Profile 项目，现在我 GitHub 首页右侧推荐总是时不时有类似的 Repo 出现。&lt;/p&gt;
&lt;p&gt;有一类 Profile 比较特殊，它们有社区互动模块，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/timburgan/timburgan&quot;&gt;国际象棋&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/JonathanGin52/JonathanGin52&quot;&gt;四子棋&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/JessicaLim8/JessicaLim8&quot;&gt;词云统计&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/benjaminsampica/benjaminsampica&quot;&gt;摇骰子&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;受之启发，我也花 mo 了 yu 半天时间撸了一个五子棋游戏放在我的 Profile 中。现在棋盘只有 9×9，高手对决可能无法分胜负，但已经留了空间能灵活改变。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/image-20201119105305918.png&quot; alt=&quot;image-20201119105305918&quot; /&gt;&lt;/p&gt;
&lt;p&gt;简单说下这些带互动部分的 Profile 的实现原理吧。其实都是依靠强大的 GitHub Actions。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;用户点击棋盘上的空白，提交一个 issue，包含预设的标题、内容和 label，这些信息全都能放在一个 URL 里，非常方便。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Issue 提交后，会触发一个 GitHub Action，运行脚本，读取 Issue 的标题，取出要进行的动作，改变棋盘。这个 action 的触发器是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;on:
  issues:
    types: [opened]

jobs:
  move:
    runs-on: ubuntu-latest
    if: startsWith(github.event.issue.title, &apos;gomoku|&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将此动作的元数据和游戏统计信息写到 Repo 中，重新生成 README，提交、push。Push 信息中带上&lt;code&gt;Close #&amp;lt;issue_num&amp;gt;&lt;/code&gt;把 Issue 关掉。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总得来说 GitHub Actions 还是挺好玩的，自从这个功能开放以来，各种骚操作层出不穷。特别是 Travis CI 开始掉链子，大家纷纷转向 GitHub Actions。怎样，来我主页下个棋吧？&lt;/p&gt;
</content:encoded></item><item><title>关于近况的说明</title><link>https://frostming.com/posts/2020/10-23/recent-news/</link><guid isPermaLink="false">https://frostming.com/2020/10-23/recent-news/</guid><pubDate>Fri, 23 Oct 2020 06:29:43 GMT</pubDate><content:encoded>&lt;p&gt;不得不来更新博客了，因为 GitHub 发邮件给我，要是 6 个月内仓库没有活动的话，就给我禁用掉定时同步的 GitHub Action。万万没想到更新博客居然是被 GitHub 催了？&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;最近换了工作，离开了鹅厂，感谢那些曾经一起战斗过的伙伴，Keep Fighting。我只是个咸鱼，换了工作以后感觉非常舒服，工作可以按自己的节奏来（也许是暂时）。最大的好处是不用再 Keep an eye on the IM 来应对各种咨询和需求，使得自己正在做的工作不能连续。现在可以在一个碰到的坎上专心死磕，没有人打扰。对了，工作地点也非常满意，楼下就是地铁公交，只要我愿意，可以回家吃个午饭。啊，久违的走读的体验。&lt;/p&gt;
&lt;p&gt;搞开源对于我来说就像一种娱乐活动，就像打游戏一样那种。我最近搞了一个新的库叫&lt;a href=&quot;https://github.com/frostming/pycomplete&quot;&gt;pycomplete&lt;/a&gt;，可以生成各种 shell 的 TAB 补全脚本。因为我发现 Python 的自动补全基本都歧视 Windows/PowerShell，经过我的一番研究&amp;lt;del&amp;gt;和借鉴&amp;lt;/del&amp;gt;，最终搞定了 PowerShell 的补全，然后一口气给我常用的工具都生成了补全脚本&lt;a href=&quot;%E6%88%91%E7%8E%B0%E5%9C%A8%E4%BD%BF%E7%94%A8%E7%9A%84%E8%A1%A5%E5%85%A8%E8%84%9A%E6%9C%AC%E9%83%BD%E6%94%BE%E5%9C%A8%5Bmyrc%5D(https://github.com/frostming/myrc)%E8%BF%99%E4%B8%AA%E4%BB%93%E5%BA%93%E9%87%8C&quot;&gt;^1&lt;/a&gt;，用得非常爽。话说回来，换工作有个初衷是希望能领个 Mac 来用，但还是没有如愿，工作电脑是一个性能超级强悍的 Windows 台式。其实用 Windows 这么久以来，发现 Windows 还是非常适合做生产力工具的，PowerShell 也很强大，很多人都低估了。&lt;/p&gt;
&lt;p&gt;[^1]: 我给 pip 提个了&lt;a href=&quot;https://github.com/pypa/pip/pull/9025&quot;&gt;PR&lt;/a&gt;支持 PowerShell 的补全&lt;/p&gt;
&lt;p&gt;过了这个月，房子的装修应该也差不多了，下个月希望找时间和家人一起出去玩一玩。&lt;/p&gt;
</content:encoded></item><item><title>Meetup首秀，Live coding翻车</title><link>https://frostming.com/posts/2020/06-11/meetup/</link><guid isPermaLink="false">https://frostming.com/2020/06-11/meetup/</guid><description>Frost为何变自闭</description><pubDate>Thu, 11 Jun 2020 07:06:26 GMT</pubDate><content:encoded>&lt;p&gt;由于疫情的关系，今年的 Python Meetup 得以在线上举行，我也头一回报名了演讲。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;视频录像：https://www.bilibili.com/video/BV1KT4y1J7PU&lt;/li&gt;
&lt;li&gt;Slides 地址：https://slides.fming.dev/pep582&lt;/li&gt;
&lt;li&gt;Live coding 仓库地址：https://github.com/frostming/package-manager-demo&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在开始简单的复盘发生了什么。&lt;/p&gt;
&lt;p&gt;我一开始就敲定了本次演讲的主题是 PEP 582，然后附带展示下我今年做的项目&lt;a href=&quot;https://github.com/frostming/pdm&quot;&gt;pdm&lt;/a&gt;。但转念一想，感觉这个 PEP 的内容不够撑起一次演讲，我也想摆脱一下 Explain and show 的演讲模式，于是就&lt;strong&gt;不自量力&lt;/strong&gt;地选择准备一场 Live Coding。现在看来这个选择不太明智：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;其实完整地演示 PDM 需要花费很多时间，特别是我还有很多重要的特性没有在演讲中演示（全局包管理、插件系统）。我在这里耗时估计太乐观了。&lt;/li&gt;
&lt;li&gt;Live Coding 选择的主题略显复杂，涉及到三个数据模型的相互作用和一些 Python 打包的机制，需要费时间解释这些东西。&lt;/li&gt;
&lt;li&gt;Live Coding 本身的不可控因素太多了，事先只演练了三次，实际演示时任何微小的错误都有可能犯，而依赖解析造成了 Debug 的成本比较大。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/uranusjr&quot;&gt;Tsu-ping&lt;/a&gt;竟然偷偷阻击我，我要是知道他要来我肯定不干 Live Coding 的事&amp;gt;_&amp;lt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但不管怎么样我还是干了，干了还是翻车了，翻车以后我还是自闭了，自闭归自闭我还是来复盘了。就这样吧。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;好了如果大家围观完我的窘迫以后，想知道问题出在哪了导致我没有成功？答案就在 import 语句之后的第一行代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PYTHON_VERSION = platform.version()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一句希望返回 Python 的版本号，但实际返回了当前系统的版本！正确的应该是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PYTHON_VERSION = platform.python_version()
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Chrome开发者工具指北</title><link>https://frostming.com/posts/2020/06-10/f12-network/</link><guid isPermaLink="false">https://frostming.com/2020/06-10/f12-network/</guid><description>F12 Network篇</description><pubDate>Wed, 10 Jun 2020 05:10:32 GMT</pubDate><content:encoded>&lt;p&gt;Chrome Dev Tools，Chrome 开发者工具，俗称 F12。其实不仅在 Chrome 上有，基本上所有的现代浏览器都带这个工具。它是调整样式、调试 JS、查看前后端收发数据的不二神器。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;在 Chrome 浏览器中呼出 F12 有三种方法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;右上角三个点按钮调出菜单——更多工具——开发者工具(Ctrl + Shift + I)&lt;/li&gt;
&lt;li&gt;顾名思义，键盘快捷键&amp;lt;kbd&amp;gt;F12&amp;lt;/kbd&amp;gt;一键呼出&lt;/li&gt;
&lt;li&gt;在页面元素上右键点击——审查元素，或者叫检查&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/image-20200610100929539.png&quot; alt=&quot;image-20200610100929539&quot; /&gt;&lt;/p&gt;
&lt;p&gt;呼出以后会显示在页面的下方，如果觉得这样太扁不方便看信息，可以点右上角三个点的按钮调整布局，分别是新窗口打开、靠在左侧、靠在下方，靠在右侧：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/image-20200610101110639.png&quot; alt=&quot;image-20200610101110639&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到工具的顶栏有很多标签：本文先介绍最常用也是最重要的「Network」页，其他标签将在后续文章中介绍。&lt;/p&gt;
&lt;h2&gt;预备知识：HTTP 请求过程&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/image-20200610105753662.png&quot; alt=&quot;image-20200610105753662&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这是浏览器和后端服务器之间的数据流动示意图&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;浏览器和服务器之间可能隔了千山万水，相互之间的数据交换必须由 HTTP 请求——响应完成（图中箭头）&lt;/li&gt;
&lt;li&gt;一个页面中包含的 HTML, CSS, JavaScript 均由浏览器这边处理，后端（Django）统统不认识这些文件，当成普通文本看待。&lt;/li&gt;
&lt;li&gt;请求体是浏览器生成给服务器读的，响应体是由服务器生成给浏览器读的，只是这个响应体可能是 HTML 页面、可能是文件、可能是 JSON 而已。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;而浏览器和服务器之间传送了什么数据，对于排查问题是非常有用的，Network 在这里就相当于路口的监控，进来了谁，出去了谁，一目了然。如果请求的数据是对的而行为不正确，那肯定是服务器的问题；反之如果发的数据就是错的，那就是页面的问题。这样一下就可以把排查的范围缩小一半。所以不要再出了问题一个劲盯着无关的地方大眼瞪小眼。F12 打开先看监控，OK?&lt;/p&gt;
&lt;p&gt;一个 HTTP 请求主要包含以下部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Method: 请求的方法，是 GET 还是 POST 或者其他&lt;/li&gt;
&lt;li&gt;URL: 请求的地址，可能还包含 URL 参数（形如&lt;code&gt;?key1=value1&amp;amp;key2=value2&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Headers: 请求的头部，包含一些请求的元数据。&lt;/li&gt;
&lt;li&gt;Body: 请求体，发送的请求内容&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;而一个 HTTP 响应主要包含以下部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Headers: 响应的头部，包含一些响应的元数据。&lt;/li&gt;
&lt;li&gt;Body: 响应体，返回的响应内容&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Network 面板能看啥&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/image-20200610114145824.png&quot; alt=&quot;image-20200610114145824&quot; /&gt;&lt;/p&gt;
&lt;p&gt;打开 Network 面板，然后刷新页面，可以看到当前页面发送的所有请求，其中大部分是加载静态文件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Method: 请求方法&lt;/li&gt;
&lt;li&gt;Status: 返回的状态码&lt;/li&gt;
&lt;li&gt;Size: 响应大小，如果是带&quot;cache&quot;字眼说明没有请求到后端，而是从缓存中获得的[^1]&lt;/li&gt;
&lt;li&gt;Time: 载入耗时&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从这个列表，加载了哪些文件，是否有加载失败，加载耗时如何都一目了然。有了这些信息能做的事情就多了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分析页面响应速度的瓶颈，优化渲染速度&lt;/li&gt;
&lt;li&gt;查看与后端通信成功情况，方便 Debug&lt;/li&gt;
&lt;li&gt;查看页面的数据来源，以便仿造请求，爬虫利器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而上图中高亮的类别可以精细过滤请求类型，XHR 是专门查看 Ajax 请求的，JS, CSS, Img 则可以分别查看这些静态文件的加载情况。&lt;/p&gt;
&lt;h2&gt;查看请求详情&lt;/h2&gt;
&lt;p&gt;举例来说，比如现在我开发了博客的评论功能，想知道发送评论是否正常工作。那么打开 Network 面板，在页面中添加一条评论并提交，在 Network 中就应该能看到一条请求的记录，为防止页面刷新记录丢失，可以勾选上 Preserve 框：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/image-20200610115840043.png&quot; alt=&quot;image-20200610115840043&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果列表已经太多内容可以点击清空按钮&lt;img src=&quot;https://webp.frostming.com/images/image-20200610121959116.png&quot; alt=&quot;clear button&quot; /&gt;清空当前列表。可以看到&lt;code&gt;comment&lt;/code&gt;请求方法是&lt;code&gt;POST&lt;/code&gt;，已经返回 200 成功了。点选这条记录，可以在右侧看到请求响应的详情：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/image-20200610121445910.png&quot; alt=&quot;image-20200610121445910&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从上到下依次是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;General: 请求总体信息，URL，Method，返回状态码等&lt;/li&gt;
&lt;li&gt;Response Headers: 响应头&lt;/li&gt;
&lt;li&gt;Request Headers: 请求头&lt;/li&gt;
&lt;li&gt;Request Body: 请求体，如果是 form 表单则显示发送表单的内容&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样你就可以知道浏览器发了什么给服务器，又从服务器收到了什么内容。&lt;/p&gt;
&lt;h2&gt;Network 的其他强大功能&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/image-20200610121833101.png&quot; alt=&quot;image-20200610121833101&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在请求记录上右键点击可调出菜单，可以做很多事情。比如在新标签打开、清除浏览器缓存/Cookies、将连接复制为 PowerShell 脚本、fetch 调用、curl 命令等等。&lt;/p&gt;
&lt;p&gt;此外在 Network 面板中按&amp;lt;kbd&amp;gt;Ctrl&amp;lt;/kbd&amp;gt;&amp;lt;kbd&amp;gt;F&amp;lt;/kbd&amp;gt;，可以搜索某个具体的数据内容，是在哪一个请求中返回的，这无疑对写爬虫有巨大帮助。&lt;/p&gt;
&lt;p&gt;[^1]: 这就是为什么更新了后端静态文件没有生效的原因。解决方法很简单：&amp;lt;kbd&amp;gt;Ctrl&amp;lt;/kbd&amp;gt;&amp;lt;kbd&amp;gt;F5&amp;lt;/kbd&amp;gt;可以强制刷新，或者在 Network 面板右键点击该文件的记录然后选择&quot;Clear browser cache&quot;&lt;/p&gt;
</content:encoded></item><item><title>写在30岁生日之前</title><link>https://frostming.com/posts/2020/05-27/before-birthday/</link><guid isPermaLink="false">https://frostming.com/2020/05-27/before-birthday/</guid><pubDate>Wed, 27 May 2020 10:32:22 GMT</pubDate><content:encoded>&lt;p&gt;转眼就快到三十岁了，临近这个坎人就莫名地变焦虑。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;这半年在两个答辩、面试中受到挫败，使我陷入空前的自我怀疑中——我为什么这么失败？这种自我怀疑让我无心工作，消极懈怠。然而与之形成鲜明对比的是，我在公司和社区中都被人以「大佬」「大神」称呼，一开始我多番辞谢，后来也拦不住了。这种不对等让我陷入了很大的矛盾之中，心态有了些许变化。&lt;/p&gt;
&lt;p&gt;五月份参加了一次 Python Meetup 的演讲，结果喜闻乐见地翻车了。幸好组织者至今也没有要把录像放出来的迹象，否则放出来了我也是不敢看的。&lt;/p&gt;
&lt;p&gt;紧接着又接受了一次播客录制的邀约，是我一直关注的「捕蛇者说」。还是挺有收获，主要是和 laike9m, TP, Manjusaka 他们熟络了起来，如果今年 PyCon 会在线下举办，不是不可以去上海面基一波，嘿嘿。&lt;/p&gt;
&lt;p&gt;在 Django 的水友群中人生头一回地做了直播，但内容难度的平衡和节目趣味性还在艰难地探索中。新手菜鸟的入门帮助一直是个头疼的问题，也不是那么容易。人往高处走，我也不能满足于在群里当大佬，计算机的基础知识仍然是欠缺的，UNP, AUP 也不知什么时候能读完。&lt;/p&gt;
&lt;p&gt;近些年很多事情都悄悄地变了，原先是跟着别人学习的小弟，现在渐渐成为有经验的老人。看着年轻的后浪们，有了韶华不可追的感觉。如果我年轻时能好好利用时间，不至于到现在，不如很多比我年龄小的人。「90 后」原来是后浪的代名词，现在我却有了「晚了」「来不及了」的心情，Oh, shit! 都怪这个社会贩卖的焦虑。&lt;/p&gt;
&lt;p&gt;还好房子也有了，女儿也很可爱。闺女的生日和我也就隔几天，现在是越来越有自我意识了，不是很好搞定:(，但我每天仍然最期待看到她。&lt;/p&gt;
&lt;p&gt;生日快乐！&lt;/p&gt;
</content:encoded></item><item><title>Web 服务的进程托管</title><link>https://frostming.com/posts/2020/05-24/process-management/</link><guid isPermaLink="false">https://frostming.com/2020/05-24/process-management/</guid><description>新手问题Sticker系列</description><pubDate>Sun, 24 May 2020 13:17:48 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;「入门」标签的文章是我写给新手入门者的&amp;lt;del&amp;gt;解疑文章&amp;lt;/del&amp;gt;水文，也是给自己的知识有个地方做做梳理。如果本文对你没有帮助，可以不看。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;在开发 Web 服务（或者叫 App，后文中 App 和服务概念等同）的时候，最后一步就是启动服务器运行你的 App。在大部分的教程中，这里的选择通常是 uwsgi 或者 gunicorn。你会发现，它运行起来以后，会占用你当前的一个终端会话，进入「长运行模式」，就像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[2020-05-23 22:54:57 +0800] [13077] [INFO] Starting gunicorn 20.0.4
[2020-05-23 22:54:57 +0800] [13077] [INFO] Listening at: unix:/tmp/***.socket (13077)
[2020-05-23 22:54:57 +0800] [13077] [INFO] Using worker: sync
[2020-05-23 22:54:57 +0800] [13084] [INFO] Booting worker with pid: 13084

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里最后一行是没有光标的，你没法执行其他命令，除非用&amp;lt;kbd&amp;gt;Ctrl&amp;lt;/kbd&amp;gt;+&amp;lt;kbd&amp;gt;C&amp;lt;/kbd&amp;gt;退出进程。这时假如你关闭终端、关闭 SSH 连接客户端(PuTTy, Xshell 之类)，Web 服务进程就立刻退出了，那不是白忙活了吗？这是因为你在终端中运行的所有进程，父进程都是当前终端会话，并且绑定了标准输入输出。很多人知道可以在命令末尾加上&lt;code&gt;&amp;amp;&lt;/code&gt;把进程转为后台运行，但这样的后台进程并没有改变它的父进程，所以终端会话结束以后这个进程依然会不在。那么如何解决这个问题呢？我下面提供了三种解决方法，推荐程度也逐次提高。如果懒，就直接看第三种方案。&lt;/p&gt;
&lt;p&gt;在后续介绍三种方案时，假定你运行服务器的命令是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ gunicorn -b :8888 -w 4 my_blog.wsgi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请根据个人情况做相应改动，&lt;strong&gt;教程并不是用来百分百复制粘贴的&lt;/strong&gt;。如果是在虚拟环境中运行，只需要将虚拟环境的路径加到前面即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ /path/to/my/venv/bin/gunicorn -b :8888 -w 4 my_blog.wsgi
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;nohup&lt;/h2&gt;
&lt;p&gt;nohup 命令可以将进程变成不挂起的，（默认情况下）它会把标准输出和标准错误输入重定向到当前目录的&lt;code&gt;nohup.txt&lt;/code&gt;文件中，并且将进程的父进程改成 1，也就是 1 号进程，这样终端退出以后，此进程将继续持续运行，我们将这种进程叫做&lt;strong&gt;守护进程&lt;/strong&gt;[^1]。使用方法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ nohup gunicorn -b :8888 -w 4 my_blog.wsgi &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意前面加上了&lt;code&gt;nohup&lt;/code&gt;以及末尾的&lt;code&gt;&amp;amp;&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;更正&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;nohup 并不会将进程的父进程改成 1, 只会把 &lt;code&gt;SIGHUP&lt;/code&gt; 的设置为 ignore, 这样退出会话的时候 bash 发送的 &lt;code&gt;SIGHUP&lt;/code&gt; 被忽略, 从而成为孤儿进程被 1 号进程接管. Thanks @Ooth-Gray&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;[^1]: 关于&lt;code&gt;nohup&lt;/code&gt;命令的作用和守护进程的定义本文只做粗浅介绍，只为提供解决的方法。如果对原理作用不清楚，推荐阅读&lt;a href=&quot;https://www.kawabangga.com/posts/3849&quot;&gt;laixintao 的这篇博文&lt;/a&gt;和&lt;a href=&quot;http://blog.lujun9972.win/blog/2018/04/20/nohup,setsid%E4%B8%8Edisown%E7%9A%84%E4%B8%8D%E5%90%8C%E4%B9%8B%E5%A4%84/index.html&quot;&gt;nohup,setsid 与 disown 的不同之处&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;supervisor&lt;/h2&gt;
&lt;p&gt;用&lt;code&gt;nohup&lt;/code&gt;虽然能将进程转为后台运行，但它缺少一个很重要的功能：异常重启和开机自启动的功能。你重启服务器必须得记得去启动下你的服务器。所以更强大的、专门的进程管理工具就应运而生。&lt;a href=&quot;http://supervisord.org/&quot;&gt;supervisor&lt;/a&gt;是用 Python 写的一款进程管理器，它支持进程异常重启、日志存储，并且提供了一个命令行程序来查看、管理当前的进程。使用方法如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;安装&lt;pre&gt;&lt;code&gt;$ pip install supervisor
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;生成配置文件&lt;pre&gt;&lt;code&gt;$ echo_supervisord_conf &amp;gt; /etc/supervisord.conf
$ mkdir /etc/supervisor.d
&lt;/code&gt;&lt;/pre&gt;
编辑&lt;code&gt;/etc/supervisord.conf&lt;/code&gt;文件，将文件最后两行取消注释&lt;pre&gt;&lt;code&gt;[include]
files = supervisor.d/*.ini
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;编写应用进程的配置&lt;code&gt;/etc/supervisor.d/my_blog.ini&lt;/code&gt;，文件中&lt;code&gt;;&lt;/code&gt;开头的行是注释行，如果需要该配置生效则取消注释&lt;pre&gt;&lt;code&gt;[program:myblog]
command=/path/to/my/venv/bin/gunicorn -b :8888 -w 4 my_blog.wsgi              ; 启动进程的命令
;process_name=%(program_name)s ; process_name expr (default %(program_name)s)
;numprocs=1                    ; number of processes copies to start (def 1)
directory=/path/to/my_blog                ; 运行命令时先切换到此目录下
;umask=022                     ; umask for process (default None)
;priority=999                  ; the relative start priority (default 999)
;autostart=true                ; start at supervisord start (default: true)
;startsecs=1                   ; # of secs prog must stay up to be running (def. 1)
;startretries=3                ; max # of serial start failures when starting (default 3)
;autorestart=unexpected        ; when to restart if exited after running (def: unexpected)
;exitcodes=0,2                 ; &apos;expected&apos; exit codes used with autorestart (default 0,2)
;stopsignal=QUIT               ; signal used to kill process (default TERM)
;stopwaitsecs=10               ; max num secs to wait b4 SIGKILL (default 10)
;stopasgroup=false             ; send stop signal to the UNIX process group (default false)
;killasgroup=false             ; SIGKILL the UNIX process group (def false)
;user=myuser                   ; 启动进程的用户，推荐不要用root用户，否则注释此行
redirect_stderr=true          ; 重定向错误到输出 (默认false)
stdout_logfile=/a/path        ; 标准输出的日志地址，会将所有print到终端的输出输出到指定的文件中
;stdout_logfile_maxbytes=1MB   ; max # logfile bytes b4 rotation (default 50MB)
;stdout_logfile_backups=10     ; # of stdout logfile backups (0 means none, default 10)
;stdout_capture_maxbytes=1MB   ; number of bytes in &apos;capturemode&apos; (default 0)
;stdout_events_enabled=false   ; emit events on stdout writes (default false)
;stderr_logfile=/a/path        ; stderr log path, NONE for none; default AUTO
;stderr_logfile_maxbytes=1MB   ; max # logfile bytes b4 rotation (default 50MB)
;stderr_logfile_backups=10     ; # of stderr logfile backups (0 means none, default 10)
;stderr_capture_maxbytes=1MB   ; number of bytes in &apos;capturemode&apos; (default 0)
;stderr_events_enabled=false   ; emit events on stderr writes (default false)
;environment=A=&quot;1&quot;,B=&quot;2&quot;       ; process environment additions (def no adds)
;serverurl=AUTO                ; override serverurl computation (childutils)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;启动&lt;code&gt;supervisord&lt;/code&gt;:&lt;pre&gt;&lt;code&gt;$ supervisord
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;进程的查看、终止与启动&lt;pre&gt;&lt;code&gt;$ supervisorctl status    # 查看进程状态
$ supervisorctl stop my_blog    # 终止my_blog进程
$ supervisorctl start my_blog    # 启动my_blog进程
$ supervisorctl restart my_blog    # 重新启动my_blog进程
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;systemd&lt;/h2&gt;
&lt;p&gt;systemd 是现在比较新的 Linux 发行版都自带的一个进程管理器&lt;a href=&quot;%E4%BD%BF%E7%94%A8%60systemctl%60%E6%A3%80%E6%9F%A5%E4%B8%8B%E4%BD%A0%E7%9A%84%E7%B3%BB%E7%BB%9F%E6%9C%89%E6%B2%A1%E6%9C%89%E5%AE%89%E8%A3%85%EF%BC%8C%E5%A6%82%E6%9E%9C%E6%B2%A1%E6%9C%89%EF%BC%8C%E5%88%99%E5%85%88%E5%B0%9D%E8%AF%95%E7%94%A8%E7%B3%BB%E7%BB%9F%E7%9A%84%E5%8C%85%E7%AE%A1%E7%90%86%E5%B7%A5%E5%85%B7%E5%AE%89%E8%A3%85%60systemd%60%EF%BC%8C%E5%90%A6%E5%88%99%E5%B0%B1%E5%8F%AA%E8%83%BD%E7%94%A8%60supervisor%60%E4%BA%86%E3%80%82&quot;&gt;^2&lt;/a&gt;，使用自带的，就不用再费劲安装别的库了，干净又快捷，强力推荐用这个方法。使用方法也很简单，创建以下文件内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=My blog service

[Service]
Type=forking
ExecStart=gunicorn -b :8888 -w 4 my_blog.wsgi
KillMode=process
Restart=on-failure
RestartSec=3s

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保存到&lt;code&gt;/etc/systemd/system/my_blog.service&lt;/code&gt;。然后执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ systemctl enable my_blog
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样你的进程就自动加入开机自启动了，同样，systemd 也可以查看、启动、停止进程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ systemctl status my_blog    # 查看进程状态
$ systemctl stop my_blog    # 终止my_blog进程
$ systemctl start my_blog    # 启动my_blog进程
$ systemctl restart my_blog    # 重新启动my_blog进程
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是这么 Easy!&lt;/p&gt;
</content:encoded></item><item><title>从 Python 的魔法方法说开去</title><link>https://frostming.com/posts/2020/05-12/python-magic-method/</link><guid isPermaLink="false">https://frostming.com/2020/05-12/python-magic-method/</guid><pubDate>Tue, 12 May 2020 13:25:40 GMT</pubDate><content:encoded>&lt;p&gt;一天我在群里看到这样一个有意思的 Python 现象：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; import os
&amp;gt;&amp;gt;&amp;gt; r=os.popen(&apos;ls&apos;)
&amp;gt;&amp;gt;&amp;gt; r.__next__()
&apos;0B4581EB10DBC182A83D85B0024F1E70.jpg\n&apos;
&amp;gt;&amp;gt;&amp;gt; next(r)
Traceback (most recent call last):
  File &quot;&amp;lt;stdin&amp;gt;&quot;, line 1, in &amp;lt;module&amp;gt;
TypeError: &apos;_wrap_close&apos; object is not an iterator
&amp;gt;&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你对 Python 的魔法方法有所了解，就能发现这里的奇怪之处：&lt;code&gt;popen&lt;/code&gt;的对象有&lt;code&gt;__next__()&lt;/code&gt;方法，但却不能被&lt;code&gt;next()&lt;/code&gt;调用，也就不是个迭代器。还有这种事吗？于是我们来看源码，看看&lt;code&gt;popen()&lt;/code&gt;到底返回了个什么对象（省略了无关代码）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def popen(cmd, mode=&quot;r&quot;, buffering=-1):
    ...
    return _wrap_close(io.TextIOWrapper(proc.stdin), proc)

# Helper for popen() -- a proxy for a file whose close waits for the process
class _wrap_close:
    def __init__(self, stream, proc):
        self._stream = stream
        self._proc = proc
    def __getattr__(self, name):
        return getattr(self._stream, name)
    def __iter__(self):
        return iter(self._stream)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;popen()&lt;/code&gt;返回了一个&lt;code&gt;_wrap_close&lt;/code&gt;对象，而后者仅仅是一个 Iterable，而不是 Iterator（没有定义&lt;code&gt;__next__()&lt;/code&gt;）。然而，&lt;code&gt;_wrap_close&lt;/code&gt;却定义了&lt;code&gt;__getattr__()&lt;/code&gt;魔法方法，这样所有其他找不到的属性、方法就会传递给&lt;code&gt;self._stream&lt;/code&gt;对象，而这个对象有&lt;code&gt;__next__()&lt;/code&gt;方法。这就解释了为什么&lt;code&gt;r.__next__()&lt;/code&gt;能调用成功。&lt;/p&gt;
&lt;p&gt;所以，&lt;strong&gt;Python 对于魔法方法的调用是基于这个类有没有定义此方法吗？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;答案是肯定的，查看 Python 源码中&lt;code&gt;next()&lt;/code&gt;内建函数的实现，可以看到下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define PyIter_Check(obj) \
    (Py_TYPE(obj)-&amp;gt;tp_iternext != NULL &amp;amp;&amp;amp; \
     Py_TYPE(obj)-&amp;gt;tp_iternext != &amp;amp;_PyObject_NextNotImplemented)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;判断一个&lt;code&gt;obj&lt;/code&gt;是不是迭代器，是基于&lt;code&gt;Py_TYPE(obj)&lt;/code&gt;是否有&lt;code&gt;__next__()&lt;/code&gt;方法，而不是&lt;code&gt;obj&lt;/code&gt;本身。&lt;code&gt;__next__()&lt;/code&gt;如此，其他魔法方法也是一样。&lt;/p&gt;
&lt;p&gt;问题解决了，我们可以得到下面的推论：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;动态修改（或者叫 monkey patch）一个实例的魔法方法，是不生效的。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;看下面的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; class Foo: pass
...    foo = Foo()
&amp;gt;&amp;gt;&amp;gt; foo.__str__ = lambda: &apos;42&apos;    # &amp;lt;= 企图修改foo的__str__方法
&amp;gt;&amp;gt;&amp;gt; print(foo)
&amp;lt;__main__.Foo object at 0x1024f7fd0&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;foo&lt;/code&gt;的字符串依然是原来的默认值，没有改变。要想改变，必须修改&lt;code&gt;Foo.__str__&lt;/code&gt;方法。&lt;/p&gt;
&lt;p&gt;下面这段是额外的思考，可能比较绕：&lt;/p&gt;
&lt;p&gt;再回头去看最开始的例子，这个问题之所以奇怪，是因为它用了&lt;code&gt;__getattr__()&lt;/code&gt;让&lt;strong&gt;实例&lt;/strong&gt;获得了并不存在于&lt;strong&gt;类&lt;/strong&gt;中的属性。也就是说，原来的&lt;strong&gt;类&lt;/strong&gt;并没有获得这些额外的属性。而魔法行为的判断是基于&lt;strong&gt;类&lt;/strong&gt;中是否有这个魔法方法。这两件事合起来看，那我是不是可以通过&lt;strong&gt;元类&lt;/strong&gt;中的&lt;code&gt;__getattr__()&lt;/code&gt;方法让&lt;strong&gt;类&lt;/strong&gt;获得本不属于它的魔法方法，继而使得&lt;strong&gt;实例&lt;/strong&gt;具有某些行为呢？说干就干：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class IterMeta(type):
    def __getattr__(self, name):
        if name == &apos;__next__&apos;:
            return lambda x: 42
        return super().__getattr__(name)

class Foo(metaclass=IterMeta):
    pass

foo = Foo()
next(foo)
# TypeError: &apos;Foo&apos; object is not an iterator
foo.__next__()
# AttributeError: &apos;Foo&apos; object has no attribute &apos;__next__&apos;
Foo.__next__(foo)
# 42
Foo.__next__ = lambda x: 42
next(foo)
# 42
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不能！明明&lt;code&gt;Foo&lt;/code&gt;能获取到&lt;code&gt;__next__()&lt;/code&gt;属性，看来&lt;code&gt;(Py_TYPE(obj)-&amp;gt;tp_iternext&lt;/code&gt;并不会触发&lt;code&gt;__getattr__&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我用 Python 的时间不可谓短，也自认对 Python 的语言特性比较了解了，但 Python 却总能时不时让我意外一下，这是什么情况？&lt;/p&gt;
</content:encoded></item><item><title>使用 GitHub Actions 实现博客自动化部署</title><link>https://frostming.com/posts/2020/04-26/github-actions-deploy/</link><guid isPermaLink="false">https://frostming.com/2020/04-26/github-actions-deploy/</guid><pubDate>Sun, 26 Apr 2020 04:52:08 GMT</pubDate><content:encoded>&lt;p&gt;如果大家以前是用过静态博客，比如 Hugo、Hexo，可能配置过自动部署，也就是提交代码到源文件分支，自动生成静态文件提交到静态分支。静态博客的部署都是基于文件，目标只是一个 Git 仓库，一切都比较自然。那么如果是喜欢折腾，使用了动态博客呢？这里就涉及到服务器远程登录了。下面介绍一下我使用的方法。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;我看过很多同学部署网站，都是手动 FTP 推包，手动 ssh 连上服务器操作重启。这种方式一是操作烦琐，二是不推崇总是在生产环境人工操作，因为人工操作越多，越容易出错。推文件——重启这种重复性动作，应该交给机器人去做，把自己从运维中解放出来，只有在十分紧急的情况，才登录到服务器上。&lt;/p&gt;
&lt;h2&gt;使用 GitHub Actions 自动化&lt;/h2&gt;
&lt;p&gt;实现代码提交的自动化工作流，要依靠持续集成（或者加上持续交付）服务。现在主流的公用免费的持续集成服务有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Travis CI&lt;/li&gt;
&lt;li&gt;Jenkins&lt;/li&gt;
&lt;li&gt;Circle CI&lt;/li&gt;
&lt;li&gt;Azure Pipeline&lt;/li&gt;
&lt;li&gt;GitHub Actions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中 GitHub Actions 是 GitHub 自家的持续集成及自动化工作流服务，简单易用，也是本文推荐使用的服务。它使用起来非常简单，只要在你的仓库根目录建立&lt;code&gt;.github/workflows&lt;/code&gt;文件夹，将你的工作流配置(YAML 文件)放到这个目录下，就能启用 GitHub Actions 服务。&lt;/p&gt;
&lt;h2&gt;建立 SSH 密钥对&lt;/h2&gt;
&lt;p&gt;要把文件部署到远程服务器，首先要解决登录校验的问题。要么用密码登录、要么用 SSH 密钥登录。这里推荐用第二种方式，因为密码可能要定期更换，而用 SSH 密钥可以一劳永逸。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;假设当前用户是 root，是其他用户也行。生成 SSH 密钥对：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ mkdir -p ~/.ssh &amp;amp;&amp;amp; cd ~/.ssh
$ ssh-keygen -t rsa -f mysite
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里一路回车就行，执行完成后，会在&lt;code&gt;~/.ssh&lt;/code&gt;下生成两个文件：&lt;code&gt;mysite&lt;/code&gt;（私钥）和&lt;code&gt;mysite.pub&lt;/code&gt;（公钥）。其中私钥是你的个人登录凭证，&lt;strong&gt;不可以分享给他人&lt;/strong&gt;，如果别人得到了你的私钥，就能登录到你的服务器。公钥则需要放到登录的目标服务器上。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将公钥&lt;code&gt;mysite.pub&lt;/code&gt;的内容贴到目标服务器的&lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt;中，如果上一步你直接是在服务器中执行，则只要：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ cat mysite.pub &amp;gt;&amp;gt; authorized_keys
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;否则，手动复制公钥的内容，粘贴到&lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt;后面即可，若文件或目录不存在，可以自己创建。这一步的目的，是告诉目标服务器：「我以后用这个私钥登录，你需要允许哈」。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确保服务器&lt;code&gt;~/.ssh&lt;/code&gt;文件夹的权限低于 711，我这里直接用 600（仅本用户可读写）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ chmod 600 -R ~/.ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后，查看私钥文件&lt;code&gt;mysite&lt;/code&gt;，将内容复制下来以备后续使用，私钥的文件内容大致如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;将自动化配置写到 GitHub 仓库&lt;/h2&gt;
&lt;p&gt;打开你的网站代码仓库，点击 Settings 标签，找到 Secrets 设定：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//webp.frostming.com/images/image-20200426115120829.png&quot; alt=&quot;image-20200426115120829&quot; /&gt;&lt;/p&gt;
&lt;p&gt;选择 Add a new secret，添加一个配置项&lt;code&gt;DEPLOY_KEY&lt;/code&gt;，将刚才复制的私钥的内容粘贴在其中。然后，你可以像我上图中一样，把你的服务器 host 和用户名也添加到配置中。这里用户名应该与你上一步操作使用的登录用户一致。&lt;/p&gt;
&lt;p&gt;添加在这里的配置，将只对你可见，不用担心会泄露给他人。&lt;/p&gt;
&lt;h2&gt;编写工作流文件&lt;/h2&gt;
&lt;p&gt;好，准备工作都做好了，现在我们来写自动化工作流的配置。&lt;/p&gt;
&lt;p&gt;在仓库根目录中创建&lt;code&gt;.github/workflows&lt;/code&gt;文件夹，再创建一个 YAML 文件，文件名自定，我这里起名叫&lt;code&gt;deploy.yml&lt;/code&gt;，所以文件的完整路径应该为&lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt;，我将配置的意义写在注释中，文件内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Deploy site files

on:
  push:
    branches:
      - master # 只在master上push触发部署
    paths-ignore: # 下列文件的变更不触发部署，可以自行添加
      - README.md
      - LICENSE

jobs:
  deploy:
    runs-on: ubuntu-latest # 使用ubuntu系统镜像运行自动化脚本

    steps: # 自动化步骤
      - uses: actions/checkout@v2 # 第一步，下载代码仓库

      - name: Deploy to Server # 第二步，rsync推文件
        uses: AEnterprise/rsync-deploy@v1.0 # 使用别人包装好的步骤镜像
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} # 引用配置，SSH私钥
          ARGS: -avz --delete --exclude=&apos;*.pyc&apos; # rsync参数，排除.pyc文件
          SERVER_PORT: &quot;22&quot; # SSH端口
          FOLDER: ./ # 要推送的文件夹，路径相对于代码仓库的根目录
          SERVER_IP: ${{ secrets.SSH_HOST }} # 引用配置，服务器的host名（IP或者域名domain.com）
          USERNAME: ${{ secrets.SSH_USERNAME }} # 引用配置，服务器登录名
          SERVER_DESTINATION: /home/fming/mysite/ # 部署到目标文件夹
      - name: Restart server # 第三步，重启服务
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SSH_HOST }} # 下面三个配置与上一步类似
          username: ${{ secrets.SSH_USERNAME }}
          key: ${{ secrets.DEPLOY_KEY }}
          # 重启的脚本，根据自身情况做相应改动，一般要做的是migrate数据库以及重启服务器
          script: |
            cd /home/fming/mysite/
            python manage.py migrate
            supervisorctl restart web
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以发现 GitHub Actions 的最大特点就是有很多第三方提供的镜像，已经把一些常用的步骤封装好了，你只需要填下配置即可。而这些镜像也很容易提供，发布在自己的 GitHub 仓库即可，所以扩展性很强。&lt;/p&gt;
&lt;p&gt;把文件写好，提交到仓库，就可以发现 GitHub Actions 已经启动了！可以在提交历史后面的状态，或者 Actions 标签中看到运行的状态。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//webp.frostming.com/images/image-20200426124204056.png&quot; alt=&quot;image-20200426124204056&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;有 GitHub Actions 这个利器，除了自动部署，还可以做自动备份，自动 XXX……只要你想，你甚至能提交代码自动触发房间开灯。这些奇技淫巧，就留给读者自己去探索了。当然，这些都必须围绕一个 GitHub 代码仓库来做。推荐大家把自己用到的代码都放到 Git 上管理，一是可以备份方便重建，二是可以利用这些周边的生态，来让你的生活更简单。不要再用百度网盘存代码、用 FTP 客户端传文件了。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;参考链接&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://help.github.com/cn/actions&quot;&gt;GitHub Actions（半）中文文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/marketplace?type=actions&amp;amp;query=&quot;&gt;GitHub Actions 市场&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>PEP 582的开发日志</title><link>https://frostming.com/posts/2020/04-12/pdm-pep582/</link><guid isPermaLink="false">https://frostming.com/2020/04-12/pdm-pep582/</guid><description>PDM 实现 PEP 582 遇到的坑</description><pubDate>Sun, 12 Apr 2020 04:29:14 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://www.python.org/dev/peps/pep-0582/&quot;&gt;PEP 582&lt;/a&gt; 是 Python 的一个隔离项目环境的提案。PDM 作为现有的唯一一个具有完备 PEP 582 支持的包管理器，在实现的过程中也并非一帆风顺。本文将介绍一些关键 PEP 582 特性的实现方法和历程。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;加载项目包目录&lt;/h2&gt;
&lt;p&gt;这是 PEP 582 的核心，也是事实上提案唯一阐明的事情，就是项目的包都会安装在&lt;code&gt;__pypackages__/X.Y/lib&lt;/code&gt;下面。在 Python 中如何挂载一个额外路径到包搜索路径中呢？再简单不过，就是利用环境变量&lt;code&gt;PYTHONPATH&lt;/code&gt;。这也是现在所有 PEP 582 的实现使用的方法，包括:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/cs01/pythonloc/blob/a6ed86e3ca91f9ecd80c7bf85d75fd3db5355e88/pythonloc/pythonloc.py#L27-L33&quot;&gt;pythonloc&lt;/a&gt;，一个 PEP 582 的试验项目&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/David-OConnor/pyflow/blob/fe152cc714ed3a6eaf412b2612f6e7ed9951a87e/src/main.rs#L1435-L1441&quot;&gt;pyflow&lt;/a&gt;，一个用 Rust 做的 Python 包管理器&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/frostming/pdm/blob/5b91a1c635ef188bdfd1ab171e02041e5a26f112/pdm/models/environment.py#L156-L161&quot;&gt;pdm 的实现&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但仅仅做到这里是不够的，在 pythonloc 的 README 中提到：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This PEP first looks to &lt;code&gt;__pypackages__&lt;/code&gt; but will fall back to looking in site-packages. This is not entirely hermetic and could lead to some confusion around which packages are being used. I would prefer the default search path be only &lt;code&gt;__pypackages__&lt;/code&gt; and nothing else.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;那么如何让 Python 启动时不要加载 site-packages 呢？这个特性也是我最近才实现的。乍一看 site-packages 好像是 Python 的机制，不好做手脚，但经过一番搜索我发现了 Python 的内置模块&lt;code&gt;site&lt;/code&gt;就是&lt;a href=&quot;https://docs.python.org/3/library/site.html&quot;&gt;控制这个事情&lt;/a&gt;的：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This module is automatically imported during initialization&lt;/strong&gt;. The automatic import can be suppressed using the interpreter’s &lt;code&gt;-S&lt;/code&gt; option.&lt;/p&gt;
&lt;p&gt;Importing this module will append site-specific paths to the module search path and add a few builtins, unless &lt;code&gt;-S&lt;/code&gt; was used. In that case, this module can be safely imported with no automatic modifications to the module search path or additions to the builtins. To explicitly trigger the usual site-specific additions, call the &lt;code&gt;site.main()&lt;/code&gt; function.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我觉得找到了解决方法：在用户&lt;code&gt;pdm run python&lt;/code&gt;的时候，自动给他添加&lt;code&gt;-S&lt;/code&gt;参数不就行了？看看效果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ pdm run python -S -c &quot;import sys;print(sys.path)&quot;
[
    &apos;&apos;,
    &apos;/Users/fming/wkspace/github/pdm-test/__pypackages__/3.8/lib&apos;,
    &apos;/Users/fming/Library/PythonUp/versions/3.8/lib/python38.zip&apos;,
    &apos;/Users/fming/Library/PythonUp/versions/3.8/lib/python3.8&apos;,
    &apos;/Users/fming/Library/PythonUp/versions/3.8/lib/python3.8/lib-dynload&apos;
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Perfect like a shit! 不是吗？&lt;code&gt;sys.path&lt;/code&gt;里面有本地的包目录，但没有&lt;code&gt;site-packages&lt;/code&gt;了。问题解决了吗？没有！先喘口气，看看这个&lt;code&gt;sys.path&lt;/code&gt;，不知有没人发现问题在哪。&lt;/p&gt;
&lt;p&gt;好了不卖关子了，这里面缺少了&lt;code&gt;.pth&lt;/code&gt;文件[^1]包含的搜索路径。通常&lt;code&gt;setuptools&lt;/code&gt;在安装可编辑(editable)包的时候会在&lt;code&gt;__pypackages__/X.Y/lib&lt;/code&gt;下面塞一个&lt;code&gt;easy-install.pth&lt;/code&gt;文件，用来把可编辑包的真正路径给包含进&lt;code&gt;sys.path&lt;/code&gt;中来，而这个过程恰好是由&lt;code&gt;site.py&lt;/code&gt;完成的，现在把它禁掉了就都没了。&lt;/p&gt;
&lt;p&gt;[^1]: .pth 文件中包含的路径可以被加载到&lt;code&gt;sys.path&lt;/code&gt;中，参考&lt;a href=&quot;https://docs.python.org/3/library/site.html&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;那么只能走另外的路了，其实除了&lt;code&gt;easy-install.pth&lt;/code&gt;，&lt;code&gt;setuptools&lt;/code&gt;还会添加一个&lt;code&gt;site.py&lt;/code&gt;来完成这个.pth 文件的加载。这个文件会在 Python 启动时执行，那就可以在这里操作&lt;code&gt;sys.path&lt;/code&gt;去掉 site-packages 的路径了。具体改动可以看&lt;a href=&quot;https://github.com/frostming/pdm/pull/104&quot;&gt;这个 PR&lt;/a&gt;，改完之后再看看效果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ pdm run python -c &quot;import sys;print(sys.path)&quot;
[
    &apos;&apos;,
    &apos;/Users/fming/wkspace/github/pdm-test/__pypackages__/3.8/lib&apos;,
    &apos;/Users/fming/wkspace/github/pdm-test&apos;,
    &apos;/Users/fming/Library/PythonUp/versions/3.8/lib/python38.zip&apos;,
    &apos;/Users/fming/Library/PythonUp/versions/3.8/lib/python3.8&apos;,
    &apos;/Users/fming/Library/PythonUp/versions/3.8/lib/python3.8/lib-dynload&apos;
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这其中第二个路径，就是通过&lt;code&gt;easy-install.pth&lt;/code&gt;加载的路径。&lt;/p&gt;
&lt;h2&gt;执行可执行文件时自动加载项目包目录&lt;/h2&gt;
&lt;p&gt;除了通过&lt;code&gt;pdm run&lt;/code&gt;加载项目包目录，我还希望在（外部）直接执行可执行文件时自动加载项目包目录。因为包都是通过 PDM 安装的，我们自然可以在可执行文件里做修改来达成这个效果。&lt;/p&gt;
&lt;p&gt;先简单说下 PDM 安装包的过程，无论是什么形式的依赖定义，最终都会构造出一个 wheel 包的格式，再安装这个 wheel 包。如果你打开&lt;code&gt;__pypackages__/X.Y/bin&lt;/code&gt;下面的任意一个可执行文件看，会发现它们的内容都是差不多的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/Users/fming/Library/PythonUp/bin/python3
# -*- coding: utf-8 -*-
import re
import sys

from wheel.cli import main
if __name__ == &apos;__main__&apos;:
    sys.argv[0] = re.sub(r&apos;(-script\.pyw|\.exe)?$&apos;, &apos;&apos;, sys.argv[0])
    sys.exit(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第六行是实际的入口，只有这一行会变动，于是我猜测一定是有模板填充。所以我全局搜索特征字符串，果然在&lt;code&gt;distlib/scripts.py&lt;/code&gt;里面找到了这个模板，它是&lt;code&gt;ScriptMaker&lt;/code&gt;类的一属性，而&lt;code&gt;ScriptMaker&lt;/code&gt;刚好是作为&lt;code&gt;wheel.install()&lt;/code&gt;的参数传进去的。那么解决方法就比较明显了——自己构造&lt;code&gt;ScriptMaker&lt;/code&gt;实例，然后修改&lt;code&gt;script_template&lt;/code&gt;属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;maker.script_template = maker.script_template.replace(
    &quot;import sys&quot;,
    &quot;import sys\nsys.path.insert(0, {!r})&quot;.format(paths[&quot;platlib&quot;]),
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;import sys&lt;/code&gt;后面直接加了一句，插入包的路径。&lt;/p&gt;
&lt;p&gt;问题还没有完全解决，对于可编辑的包，并不是由 wheel 格式安装的，查看可编辑包的可执行文件，可以发现内容稍有不同：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/Users/fming/Library/PythonUp/bin/python3
# EASY-INSTALL-ENTRY-SCRIPT: &apos;pdm-test&apos;,&apos;console_scripts&apos;,&apos;pdm-test&apos;
__requires__ = &apos;pdm-test&apos;
import re
import sys
from pkg_resources import load_entry_point

if __name__ == &apos;__main__&apos;:
    sys.argv[0] = re.sub(r&apos;(-script\.pyw?|\.exe)?$&apos;, &apos;&apos;, sys.argv[0])
    sys.exit(
        load_entry_point(&apos;pdm-test&apos;, &apos;console_scripts&apos;, &apos;pdm-test&apos;)()
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类似的，这个文件内容也是有一个模板，只是在&lt;code&gt;setuptools&lt;/code&gt;中，解决方法也是一样，就不赘述了。&lt;/p&gt;
</content:encoded></item><item><title>浅谈 Python 库的插件系统设计</title><link>https://frostming.com/posts/2020/03-16/plugin-system-2/</link><guid isPermaLink="false">https://frostming.com/2020/03-16/plugin-system-2/</guid><description>安装即生效的插件</description><pubDate>Mon, 16 Mar 2020 06:56:48 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://frostming.com/2020/03-16/plugin-system-1&quot;&gt;上一篇文章&lt;/a&gt;介绍了可选配型插件的实现的例子，这篇文章继续说说安装即生效的插件原理。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;安装即生效的插件&lt;/h2&gt;
&lt;p&gt;如果使用方只用把插件加到依赖里，安装以后这个插件就自动生效了，那使用方岂不是非常方便？但 Python 是个运行时的动态语言，所有代码需要生效都要实际执行它，那么这个执行时谁来做，什么时机执行呢？&lt;/p&gt;
&lt;h3&gt;插件宿主加载并执行&lt;/h3&gt;
&lt;p&gt;第一种方法最为自然，宿主预留出加载插件的地方，执行到这个地方，就把&lt;strong&gt;当前所有安装的插件&lt;/strong&gt;载入，并调用执行。那么关键就是如何寻找当前所有安装的插件了，Python 包提供了这样的机制，叫做&lt;code&gt;entry point&lt;/code&gt;。简单来说，就是 Python 的库打包时，像包信息中注册写入一个配置，把某个 Python 对象注册为特定类型（类型需要与宿主约定好）的载入点，宿主则可以通过&lt;code&gt;pkg_resources.iter_entry_points(ep_type)&lt;/code&gt;扫描所有这些载入点，把注册好的对象导入进来。插件起作用的方法，既可以调用这个对象的某个函数，也可以在插件顶层代码中实现，因为导入插件会执行一次&lt;code&gt;import&lt;/code&gt;，所有的顶层代码都会执行一次。&lt;/p&gt;
&lt;p&gt;如果大家写过命令行程序，就知道定义命令行入口的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# setup.py

setup(
    ...
    entry_points={
        &quot;console_scripts&quot;: [&quot;mycli = mypackage.cli:main&quot;]
    }
    ...
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的&lt;code&gt;console_scripts&lt;/code&gt;其实就是最常用的一种载入点类型，包安装器会找到这个载入点将对应的命令行入口对象生成为一个命令行脚本。&lt;/p&gt;
&lt;h3&gt;利用 Python 的启动机制执行&lt;/h3&gt;
&lt;p&gt;但如果这个宿主没有为插件预留入口，或者它没有设计成可扩展的，那我们也有办法&amp;lt;del&amp;gt;硬插进去&amp;lt;/del&amp;gt;。原理就在 Python 的&lt;code&gt;site&lt;/code&gt;模块中，看看它的&lt;a href=&quot;https://docs.python.org/3/library/site.html&quot;&gt;官方文档&lt;/a&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A path configuration file(.pth) is a file whose name has the form name.pth and exists in one of the four directories mentioned above; its contents are additional items (one per line) to be added to sys.path. Non-existing items are never added to sys.path, and no check is made that the item refers to a directory rather than a file. No item is added to sys.path more than once. Blank lines and lines beginning with # are skipped. &lt;strong&gt;Lines starting with import (followed by space or tab) are executed.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;看到最后一句话了吗，你只要在一个&lt;code&gt;.pth&lt;/code&gt;结尾的文件中写上一句&lt;strong&gt;以 import 开头&lt;/strong&gt;的语句，并将这个文件随包发布&lt;a href=&quot;%E8%BF%99%E4%B8%AA%E6%96%87%E4%BB%B6%E5%BF%85%E9%A1%BB%E5%AE%89%E8%A3%85%E5%9C%A8%E9%A1%B6%E5%B1%82%E7%9B%AE%E5%BD%95%EF%BC%8C%E5%92%8C%E5%8C%85%E5%90%8C%E7%BA%A7%EF%BC%8C%E5%8D%B3%E6%94%BE%E5%9C%A8%60site-packages%60%E7%9B%AE%E5%BD%95%E4%B8%8B%E3%80%82&quot;&gt;^1&lt;/a&gt;，那么这行语句就会在 Python 启动时自动执行。基于 Python 的动态特性，你几乎能在运行时修改任何东西，所以这行语句能做什么就大有发挥的空间了。当然，这种没有被宿主允许的走后门行为，还是不如第一种方法好。&lt;/p&gt;
&lt;h2&gt;使用安装即生效插件的项目&lt;/h2&gt;
&lt;h3&gt;Flask CLI&lt;/h3&gt;
&lt;p&gt;相比于上一篇文章写的 Flask 扩展方法，可能更少的人知道 Flask 还可以安装即生效的方法，安装额外的命令。实现的方法就是前文提到的插件宿主加载并执行方法。扩展的&lt;code&gt;setup.py&lt;/code&gt;写法为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# setup.py

setup(
    ...
    entry_points={
        &quot;flask.command&quot;: [&quot;foo = mypackage.cli:main&quot;]
    }
    ...
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完这个包以后，你就可以用&lt;code&gt;flask foo&lt;/code&gt;这条命令了。&lt;/p&gt;
&lt;h3&gt;Pytest&lt;/h3&gt;
&lt;p&gt;Pytest 也有海量的插件可用，它是基于&lt;code&gt;pluggy&lt;/code&gt;框架构建的插件系统，除了那些顶层可用的函数、fixtures，pytest 还预定义了很多钩子，在插件中可以实现这些钩子函数达到修改 pytest 的效果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pytest_addoption(parser&lt;/code&gt; 添加命令行选项&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pytest_collection_modifyitems(config, items)&lt;/code&gt; 修改收集到的测试用例列表&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pytest_configure(config)&lt;/code&gt; 读取配置项&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pytest_cmdline_main(config)&lt;/code&gt; 修改主函数逻辑&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pytest 使用的 entry_points 类型叫做&lt;code&gt;pytest11&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;PDM&lt;/h3&gt;
&lt;p&gt;在做 PDM 的插件系统的时候，我也借鉴了这些项目的经验。首先必须留出插件载入点，通过 entry_points 的方式载入插件，其次我希望暴露的对象尽可能少，插件的入口尽可能少。
这样就要求 PDM 中的基本对象类型，都是可以继承然后替换的。所以我做了一个主入口对象，用来承载所有这些信息，插件作者只要读入这个对象，就可以做出想要的修改了，核心代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Core:
    &quot;&quot;&quot;A high level object that manages all classes and configurations
    &quot;&quot;&quot;

    def __init__(self):
        self.version = __version__

        self.project_class = Project
        self.repository_class = PyPIRepository
        self.resolver_class = Resolver
        self.synchronizer_class = Synchronizer

        self.parser = None
        self.subparsers = None

    def register_command(
        self, command: Type[BaseCommand], name: Optional[str] = None
    ) -&amp;gt; None:
        &quot;&quot;&quot;Register a subcommand to the subparsers,
        with an optional name of the subcommand.
        &quot;&quot;&quot;
        command.project_class = self.project_class
        command.register_to(self.subparsers, name)

    @staticmethod
    def add_config(name: str, config_item: ConfigItem) -&amp;gt; None:
        &quot;&quot;&quot;Add a config item to the configuration class&quot;&quot;&quot;
        Config.add_config(name, config_item)

    def load_plugins(self):
        &quot;&quot;&quot;Import and load plugins under `pdm.plugin` namespace
        A plugin is a callable that accepts the core object as the only argument.

        :Example:

        def my_plugin(core: pdm.core.Core) -&amp;gt; None:
            ...

        &quot;&quot;&quot;
        for plugin in pkg_resources.iter_entry_points(&quot;pdm.plugin&quot;):
            plugin.load()(self)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两个函数，&lt;code&gt;register_command()&lt;/code&gt; 可以添加、修改子命令，&lt;code&gt;add_config()&lt;/code&gt; 可以添加、修改配置项，&lt;code&gt;load_plugins()&lt;/code&gt; 用来载入所有 entry_ponts 并执行，执行时会把主入口对象当做参数传给插件对象。entry_point 的名称为&lt;code&gt;pdm.plugins&lt;/code&gt;。&lt;/p&gt;
</content:encoded></item><item><title>浅谈 Python 库的插件系统设计</title><link>https://frostming.com/posts/2020/03-16/plugin-system-1/</link><guid isPermaLink="false">https://frostming.com/2020/03-16/plugin-system-1/</guid><description>可选配的插件</description><pubDate>Mon, 16 Mar 2020 04:50:49 GMT</pubDate><content:encoded>&lt;p&gt;插件(Plug-in)，扩展(Extension)或增件(Addon)，都差不多指的是一个东西：为一个已有软件增添额外功能的组件。给软件设计一个易用和强大的插件系统，能让你的软件寿命更长，让整个社区来共同建设，符合开源的精神。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;上周末我给&lt;a href=&quot;https://github.com/frostming/pdm.git&quot;&gt;PDM&lt;/a&gt;实现了一个插件系统，于是就顺便利用这篇文章总结一下 Python 库里面用到的插件系统的设计方法。大体说来，插件分两种类型：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;安装了以后需要写配置、写代码让插件生效——我称之为可选配的插件&lt;/li&gt;
&lt;li&gt;安装了以后插件功能即生效，或者程序运行时自动生效——我称之为安装即生效的插件&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面我会分别对这两种类型，结合一些项目的例子来说明。&lt;/p&gt;
&lt;h2&gt;可选配的插件&lt;/h2&gt;
&lt;p&gt;可选配的插件一般用在 Python 库中[^1]，特点是可配置，可调整插件参数，但需要写额外的代码或配置来装载它。&lt;/p&gt;
&lt;p&gt;[^1]: Python 库(Library)是针对 Python 应用(Application)而言的，前者主要用来 import，发布到 PyPI 上，后者主要是用来 run，一般不发布到 PyPI 上。&lt;/p&gt;
&lt;h3&gt;Requests&lt;/h3&gt;
&lt;p&gt;作为 Python 中最著名的库没有之一，Requests 的层级划分和模块解耦做得非常好。这样开发者想在上面做二次开发非常容易，有种随心所欲的感觉。主要的扩展点有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果想自定义网关、请求处理的方式，自定义一个类继承&lt;code&gt;requests.adapters.BaseAdapter&lt;/code&gt;。这个类的实例可以通过&lt;code&gt;session.mount(prefix, adapter)&lt;/code&gt;加载到&lt;code&gt;session&lt;/code&gt;中。比如&lt;a href=&quot;https://pypi.org/project/requests-wsgi-adapter/&quot;&gt;requests-wsgi-adapter&lt;/a&gt;就把请求发给了 WSGI 应用，而不是 Internet 地址。&lt;/li&gt;
&lt;li&gt;如果想自定义请求认证的方式，自定义一个类继承&lt;code&gt;requests.auth.AuthBase&lt;/code&gt;。这个类的实例可以直接传给&lt;code&gt;session&lt;/code&gt;API 的&lt;code&gt;auth=&lt;/code&gt;参数。&lt;/li&gt;
&lt;li&gt;如果只是想修改返回的响应，可以增加&lt;code&gt;response&lt;/code&gt;钩子函数，赋给&lt;code&gt;session.hooks&lt;/code&gt;属性。&lt;/li&gt;
&lt;li&gt;如果想封装一系列的操作，包括 Cookie、认证、响应处理等，可以自定义一个&lt;code&gt;Session&lt;/code&gt;类继承&lt;code&gt;requests.Session&lt;/code&gt;，比如&lt;a href=&quot;https://requests-oauthlib.readthedocs.io/en/latest/&quot;&gt;Requests-OAuthlib&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Flask&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Flask 说：「本框架什么功能也没有，你上 GitHub 上找啊，那里的扩展又多，说话又好听，只有靠扩展才能勉强生活这样子。」&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以 Flask 的插件系统设计也是相当优秀的，所有的扩展点都收拢到了&lt;code&gt;flask.Flask&lt;/code&gt;app 对象上，扩展中只用接受到这个对象，然后对它进行一顿改造就完了。一些扩展点有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;绑定一个视图蓝图：&lt;code&gt;app.register_blueprint()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;请求前、请求后钩子：&lt;code&gt;@app.before_request&lt;/code&gt;, &lt;code&gt;@app.after_request&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;信号钩子：&lt;code&gt;flask.signals&lt;/code&gt;模块&lt;/li&gt;
&lt;li&gt;模板过滤器、模板全局函数、变量：&lt;code&gt;@app.context_processor&lt;/code&gt;, &lt;code&gt;@app.template_filter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;错误处理器：&lt;code&gt;@app.errorhandler&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;只要里面提供的扩展点，都可以打包在一个扩展中统一对外提供，唯一缺失的就是 DB model，导致 Flask 扩展不能包含 db model，这是一个很大的限制。&lt;/p&gt;
&lt;h3&gt;Django&lt;/h3&gt;
&lt;p&gt;Django 在扩展方便性上比 Flask 差一些，但它的插件模块自治性非常好。因为 Django 是以 app 为单位进行组织的，模板、静态文件、数据库模型、admin 视图，测试，都可以包含在一个 app 中，不依赖外部的组件。这样一个 app 就可以单独分拆出来到处使用。但是如果插件中有包含 middleware, logging 处理这些东西，用户还是要单独在&lt;code&gt;settings.py&lt;/code&gt;中配置，不是很方便，而且插件也必须深度绑定 Django。&lt;/p&gt;
&lt;h3&gt;Marko&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/frostming/marko&quot;&gt;Marko&lt;/a&gt;是我自己写的一个 CommonMark 的 parser 和 renderer。众所周知 CommonMark 是个 spec 极度变态的 Markdown 标准，它的 parser 没办法用 BNF+AST 的方法来实现。几乎所有的 CommonMark 库（甚至 Markdown 库）都是穷举所有元素类型，为他们分别编写 parse 函数和 render 函数来实现。我在做 Marko 之初，就希望它是一个比较容易扩展的 Markdown 库，用户能扩展：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修改已有元素的解析方法&lt;/li&gt;
&lt;li&gt;修改已有元素的渲染方法&lt;/li&gt;
&lt;li&gt;增加新的自定义元素类型&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;并能把这一坨聚合在一个包里发出。&lt;/p&gt;
&lt;p&gt;在介绍 Marko 的插件系统前，我们先看看&lt;a href=&quot;https://github.com/Python-Markdown/markdown/&quot;&gt;Python-Markdown&lt;/a&gt;的扩展方法&lt;/p&gt;
&lt;h4&gt;Python-Markdown 的扩展方法&lt;/h4&gt;
&lt;p&gt;我猜没有人给这货写过扩展吧，它的官方文档，几乎什么也没写，要研究怎么写扩展，得去看源码（从例子中学习）。经过一番抓头，得出大致有这么几个扩展点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Preprocessor&lt;/code&gt; 先扫描一遍文档，元素的解析要在这里做&lt;/li&gt;
&lt;li&gt;&lt;code&gt;InlineParser&lt;/code&gt;, &lt;code&gt;BlockParser&lt;/code&gt; ，修改解析得到的 inline 元素和 block 元素&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Treeprocessor&lt;/code&gt;，渲染 AST&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最抓头的是所有解析都得手写正则，还有各种回溯机制，相当反人类。&lt;/p&gt;
&lt;h4&gt;Marko 的扩展方法&lt;/h4&gt;
&lt;p&gt;这里先说下 Markdown 的模块划分，所有元素的匹配和解析方法，包括块级元素和行内元素，都被封装在各自的元素类中，然后所有元素类都会被加载到 Parser 类中进行解析。得到一个 AST 以后再喂给 Renderer 类，Renderer 类中对于每种元素都有一个对应的 render 方法，把所有 render 的结果字符串拼接起来就得到了最终渲染的结果。&lt;/p&gt;
&lt;p&gt;所以这里主要的扩展操作就是类的继承、替换，加上考虑到多个扩展想继承同一个类，为避免相互覆盖，我采用了基于 Mixin 的方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于元素，自定义元素类&lt;/li&gt;
&lt;li&gt;对于 parser，定义一个&lt;code&gt;ParserMixin&lt;/code&gt;类实现自定义解析&lt;/li&gt;
&lt;li&gt;对于 renderer, 定义一个&lt;code&gt;RendererMixin&lt;/code&gt;类实现自定义 render 方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后把这三者都组装在一个对象中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
class MyExtension:
    elements = [...]
    parser_mixins = [ParserMixin]
    renderer_mixins = [RendererMixin]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在入口出通过和&lt;code&gt;Python-Markdown&lt;/code&gt;相似的&lt;code&gt;extensions=[MyExtension]&lt;/code&gt;读入扩展对象，将这三个属性取出，合成最终的 parser 和 renderer:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;self.parser = type(&quot;Parser&quot;, bases=(&quot;BaseParser&quot;,) + tuple(ext.parser_mixins))
self.parser.add_elements(ext.elements)
self.renderer = type(&quot;Renderer&quot;, bases=(&quot;HTMLRenderer&quot;,) + tuple(ext.renderer_mixins))
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>如何让你的开源项目看上去像那么回事</title><link>https://frostming.com/posts/2020/03-10/open-source/</link><guid isPermaLink="false">https://frostming.com/2020/03-10/open-source/</guid><pubDate>Tue, 10 Mar 2020 06:20:05 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;题按：本文写给那些有志于加入到开源的世界的人们，主要以我的主力语言 Python 作为例子，但可适用于任何语言。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;开源并不等于免费和开放源代码而已&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;相信各位搬砖工在公司里都有面对过屎山的经历，千奇百怪的编码风格、神出鬼没的注释和「卧槽，这也行」的骚操作充斥其间，我相信就算是 FLAGM 大厂也是如此。与之相反，如果你要将你的项目开源，对编码质量有很高的要求。而除了代码，一个开源的项目还有一些杂七杂八的东西，这些可能大家并不是很注意，但却能让你的开源项目「看上去像那么回事」。&lt;/p&gt;
&lt;h3&gt;一个漂亮的 README&lt;/h3&gt;
&lt;p&gt;README 是开源项目的门面，一个优秀的 README，至少要包含以下方面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;简单介绍项目是做什么的&lt;/li&gt;
&lt;li&gt;项目有什么特点，吸引用户来用&lt;/li&gt;
&lt;li&gt;安装、编译的方法&lt;/li&gt;
&lt;li&gt;最简单的 Quick start 示例&lt;/li&gt;
&lt;li&gt;License&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;README 并不等同于文档（除非你没有专门的文档网站），它不需要详细到 API Reference，只需要几个简单，吸引人的例子，和安装使用说明。&lt;/p&gt;
&lt;p&gt;其他可以给 README 加分的东西有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Badge&lt;/strong&gt;，包括包管理、CI 状态、覆盖率、代码质量，这些都有 badge 可以用，可以到&lt;a href=&quot;https://shields.io/&quot;&gt;Shields.io&lt;/a&gt;选择你想要的，但不能放太多，6 个以下为宜。不能 README 没写多少，badge 一大坨。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一个图片或视频演示&lt;/strong&gt;，相关工具及服务有：
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://carbon.now.sh/?&quot;&gt;Carbon&lt;/a&gt;或它的命令行版本——创建漂亮的代码高亮截图&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://asciinema.org/&quot;&gt;Asciinema&lt;/a&gt;——创建终端动画视频&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nbedos.github.io/termtosvg/&quot;&gt;Termtosvg&lt;/a&gt;——录制终端动画为 svg 格式，也可以渲染成 Asciinema 的格式&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一个可供用户预览的 Demo&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;如果是应用类，可以做成网站放出&lt;/li&gt;
&lt;li&gt;如果是工具类，可以做成云 shell 的形式，有以下服务：
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://repl.it/&quot;&gt;Repl.it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ssh.cloud.google.com/cloudshell/editor&quot;&gt;Google Cloud Shell&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://colab.research.google.com/&quot;&gt;Google Colab&lt;/a&gt;——分享 Jupyter notebook&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rootnroll.com/&quot;&gt;https://rootnroll.com&lt;/a&gt;——类似 Repl.it&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devcenter.heroku.com/articles/heroku-button&quot;&gt;Heroku 一键部署&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他 README 参考资源：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://www.makeareadme.com/&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;开源社区相关文件&lt;/h3&gt;
&lt;p&gt;要把你的项目开源，还有一些杂七杂八的文件，这些 GitHub 上的&lt;a href=&quot;https://github.com/frostming/pdm/community&quot;&gt;Community&lt;/a&gt;页面都有展示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LICENSE&lt;/strong&gt;——最最最重要的，没放这个文件，别人是不能用你的源代码的。有 N 种许可证可以选，具体的差异不赘述，大部分情况我都是默认用 MIT。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code of conduct&lt;/strong&gt;——行为准则，用于约束贡献者、用户及项目相关的讨论规范，避免一些不必要的争端，这个 GitHub 也有默认模板，或者去其他开源项目抄，都可以。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CONTRIBUTING.md&lt;/strong&gt;——贡献指南，包括如何设置开发环境，如何提交 issue，PR，如何跑测试，以及代码的规范等等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Issue template/PR template&lt;/strong&gt;——Issue 和 PR 的提交模板，有太多用户不知如何提一个好的问题，经常信息不全、只言片语，就指望你为他排忧解难，怎么解？用水晶球吗？所以放一个完善的 Issue 模板，把待填的信息留空，可以很大程度避免这种情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;项目代码相关文件&lt;/h3&gt;
&lt;p&gt;一千个人有一千种编码风格，除了在贡献指南中文字约定之外，我们还需要一些强制措施来保证代码的一致性。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;持续集成&lt;/strong&gt;——非常重要，如果你的开源项目没有 CI 的话会显得相当不专业，配置一个 CI 服务，把 Badge 贴到 README 里面，一目了然，持续集成包括&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自动化测试 Testing&lt;/li&gt;
&lt;li&gt;代码检查 Linting&lt;/li&gt;
&lt;li&gt;自动发布 Release&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可选择的服务有 TravisCI, Jenkins, Appveyor, Azure Pipelines, GitHub Actions。其中（个人认为）功能最强大的是 Azure Pipelines，但最容易上手的是 GitHub Actions，在这里我最推荐 GitHub Actions。&lt;/p&gt;
&lt;p&gt;持续集成通常用来兜底，作为 PR 通过的强制指标之一。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://pre-commit.com/&quot;&gt;Pre-commit&lt;/a&gt;&lt;/strong&gt; ——主要用来代码格式化和运行 Linter，如果开发者安装了 pre-commit 钩子，这些动作会在提交前运行。虽然 Linting 经常包括在持续集成中了，但 Pre-commit 检查仍然有必要，且更快捷，能更早的发现问题，因为跑一次 CI 短则几分钟，长则能达到一小时，你肯定不想等这么久结果发现代码中有个错字吧。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;各种 Linter, formatter 的配置文件&lt;/strong&gt;，用来统一配置。我常用的此类工具有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/psf/black&quot;&gt;black&lt;/a&gt;——代码格式化&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pypi.org/project/flake8/&quot;&gt;flake8&lt;/a&gt;——代码检查&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/timothycrosley/isort&quot;&gt;isort&lt;/a&gt;——import 语句排序&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://editorconfig.org/&quot;&gt;Editorconfig&lt;/a&gt;&lt;/strong&gt; ——统一化一些编辑器的设定，包括换行符统一、编码统一、Tab/空格统一，终极争端解决器。当然前提是代码编辑器支持 Editorconfig。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于一些需要工具支持的文件，需要在 CONTRIBUTING.md 中要求开发者安装。否则不给过 PR，哼。&lt;/p&gt;
</content:encoded></item><item><title>PDM - 一款新的 Python 包管理器</title><link>https://frostming.com/posts/2020/02-28/pdm-introduction/</link><guid isPermaLink="false">https://frostming.com/2020/02-28/pdm-introduction/</guid><description>自己的轮子，自己做主</description><pubDate>Fri, 28 Feb 2020 05:56:16 GMT</pubDate><content:encoded>&lt;p&gt;去年临近跨年的某一天，一个包管理器突然在脑海中形成了蓝图。粗略地估计了一下我的编码能力，我认为这在我的能力范围之内，于是尽管年底非常忙，还要忙着晋升答辩的事情，我还是腾出空（摸鱼）写下了我的第一行代码。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/image-20200228135957046.png&quot; alt=&quot;image-20200228135957046&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个项目就是&lt;a href=&quot;https://github.com/frostming/pdm&quot;&gt;pdm&lt;/a&gt;，我给它取了一个很装逼的名字——Python Development Master。截止发文时，已经在 PyPI 上发布了 0.3.0 版本，它包含以下特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PEP 582 本地项目库目录，支持安装与运行命令，&lt;strong&gt;完全不需要虚拟环境&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;一个简单且相对快速的依赖解析器，特别是对于大的二进制包发布。&lt;/li&gt;
&lt;li&gt;兼容 PEP 517 的构建后端，用于构建发布包(源码格式与 wheel 格式)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;做一个项目，首先自己要用起来，至少对我来说，这些功能非常 Exciting，而且我随时可以根据自己的喜欢做新功能（P.S. 是的，当 Pipenv 的维护人却没有什么权限发布新版这太让人沮丧了）。如果你对这个新工具也感兴趣，可以访问&lt;a href=&quot;https://frostming.github.io/pdm/&quot;&gt;官方文档&lt;/a&gt;或是 GitHub 主页。&lt;/p&gt;
&lt;p&gt;不如就多说一些别的吧，当做是我开发这个项目的碎碎念。&lt;/p&gt;
&lt;h2&gt;把握造轮子的程度&lt;/h2&gt;
&lt;p&gt;造轮子造轮子，造法也有很多种，你可以从零件厂采购轮毂，轮胎，自己组装，也可以从冶金、找橡胶树资源开始。&lt;/p&gt;
&lt;h3&gt;1. 整体引用&lt;/h3&gt;
&lt;p&gt;前一种方法，省事，相当于你只把内部的组件打乱重组，包装成一个新的样子出来。Pipenv 即属此类，它其实是由 pip(安装器)，virtualenv(虚拟环境)，pip-tools(依赖解析)几大部分组合而成，连接调度的方式居然是通过 subprocess call，所以这里面子进程启动、输出结果解析，都是耗时的。其余的次要组件，包括依赖树显示、依赖安全性检查等，无一例外都是通过内嵌别的库实现的。这种方法，很懒，引用作者 Kenneth Reitz 本人的话，叫做&lt;strong&gt;I (re)design beautiful APIs&lt;/strong&gt;。其中一大缺点，就是要做什么 bug 修复、feature 引入，非常依赖上游库的更新，要不就是有很重的 vendor 系统，非常不自由。&lt;/p&gt;
&lt;p&gt;比如我要安装一个包，用这种方法实现出来是这个样子:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def install_requirement(requirement):
    # requirement是符合PEP508规范的依赖格式
    subprocess.check_call([pip_path, &quot;install&quot;, &quot;--no-deps&quot;, requirement])
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 使用内部 API&lt;/h3&gt;
&lt;p&gt;当你对于上游库的修改多到了一定程度，你一气之下，决定化整为零，把依赖的库拆散，只取它内部的结构和接口来做。还是同样的功能，用 pip 内部的 API 实现起来，是这个样子的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def install_requirement(requirement):
    from pip._internal.req.constructors import install_req_from_line

    ireq = install_req_from_line(requirement)
    ireq.install([&quot;--no-deps&quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看看那一串长长的 import string，人家都叫做&lt;code&gt;internal&lt;/code&gt;还带下划线了，都挡不住你要从里面 import。这就是这种方法的问题所在了：&lt;strong&gt;不稳定&lt;/strong&gt;。可能下个版本，pip 就升级了，API 会变得完全不同，那么你就要做相应的改变。&lt;/p&gt;
&lt;h3&gt;3. 自己动手，丰衣足食&lt;/h3&gt;
&lt;p&gt;你改了几个版本以后，心里暗骂了一句 pip 的祖宗，责怪它为什么老改 API，一怒之下全部推倒重来，不求别人，全都自己实现，于是这一版代码变成了这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def install_requirement(requirement):
    req = parse_install_requirement(requirement)
    download_artifact(req)
    if not req.is_wheel:
        unpack_artifact(req)
        build_wheel(req)
    copy_modules(req)
    install_scripts(req)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我省略了很多代码，这里的每一个函数，背后都是几十上百行的代码，因为 requirement 的类型是很多的，有本地的文件、有 Git 的地址，有的带 marker，有的带 extras……你要覆盖到这所有的情况，难免出 bug。这时就体现出用第三方库的优点来了：它们可能已经帮你把所有的 bug 都踩过了，并受过生产环境中的考验。&lt;/p&gt;
&lt;p&gt;所以造轮子要用哪种方法来造，是要经过仔细的考量的。用了第一种，结果发现需要定制很多；用了第三种方法，结果发现天天都在修 bug。我在开发 PDM 初始，基于对个人精力的评估，选择的是第二种方法，尽管我有一万次想丢掉 pip 这个包袱。&lt;/p&gt;
&lt;h2&gt;选择合适的 mock 策略&lt;/h2&gt;
&lt;p&gt;测试的时候往往会依赖一些外部的服务，而这些外部服务有可能 1）不可用；2）和你的代码正确性无关。这种情况下就需要 mock 技术，测试和 mock 说起来话就长了，够写一本书，我就挑一个具体的场景来说。&lt;/p&gt;
&lt;p&gt;测试一个包管理器，PyPI 是一个最重要的外部服务。Pipenv 使用的技术是模拟了一个&lt;a href=&quot;https://pypi.org/project/pytest-pypi/&quot;&gt;本地的 PyPI 服务器&lt;/a&gt;，实现了 PyPI 的接口，然后把所有指向 PyPI 的请求改为本地的 PyPI 服务。这种方法对测试代码的侵入是非常小的，你甚至只需要修改 PyPI 的 URL 为&lt;code&gt;https://127.0.0.1:{port}/simple&lt;/code&gt;就可以了。但这依然要求服务器上的文件在&lt;a href=&quot;https://github.com/sarugaku/pipenv-test-artifacts&quot;&gt;本地也有&lt;/a&gt;。这会带来额外的负担，也会拖慢测试执行的速度。现在 Pipenv 跑一遍完整的测试需要 45 min，请问谁受得了？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://webp.frostming.com/images/image-20200228151636323.png&quot; alt=&quot;image-20200228145339946&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，&lt;code&gt;find_matches(requirement)&lt;/code&gt;的作用是根据给定的依赖去 PyPI 上寻找符合条件的安装包。它会去 PyPI 的&lt;code&gt;/simple/&amp;lt;package&amp;gt;&lt;/code&gt;端点获取所有链接地址，然后封装成对象返回。其实测试这个接口，并不需要一个 PyPI 服务器，更不需要真实的安装包文件，你只需要保证返回的结果里包含你想要的数据即可。所以 PDM 拦截了这个请求，转而从一个 JSON 文件中取数据返回。这大大的加快了测试的速度。&lt;/p&gt;
&lt;p&gt;又比如，我要测试一个获取远程文件的接口，它通过&lt;code&gt;requests.get(url)&lt;/code&gt;去获取文件内容并下载到本地。测试时完全可以把这个文件放在本地，然后请求时读取这个文件内容即可，下面是一个把 requests 请求 mock 掉的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class LocalFileAdapter(requests.adapters.BaseAdapter):
    def __init__(self, base_path):
        super().__init__()
        self.base_path = base_path
        self._opened_files = []

    def send(
        self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None
    ):
        file_path = self.base_path / urlparse(request.url).path.lstrip(
            &quot;/&quot;
        )  # type: Path
        response = requests.models.Response()
        response.request = request
        if not file_path.exists():
            response.status_code = 404
            response.reason = &quot;Not Found&quot;
            response.raw = BytesIO(b&quot;Not Found&quot;)
        else:
            response.status_code = 200
            response.reason = &quot;OK&quot;
            response.raw = file_path.open(&quot;rb&quot;)
        self._opened_files.append(response.raw)
        return response

    def close(self):
        for fp in self._opened_files:
            fp.close()
        self._opened_files.clear()

# 使用
session = requests.Session()
session.mount(&apos;http://fake-url.org&apos;, LocalFileAdapter(Path(&apos;./fixtures&apos;)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个例子中是通过 mock &lt;code&gt;Adapter.send()&lt;/code&gt;方法实现的，这是一个相当底层的接口，所有经过&lt;code&gt;http://fake-url.org&lt;/code&gt;的请求都一定会通过这个方法。你当然可以通过 mock&lt;code&gt;response.get_content()&lt;/code&gt;实现相同的效果，但你无法保证上游调用者一定是调用这个方法。&lt;strong&gt;mock 得越底层，能使得你的 mock 适用范围越广。&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>百度低质回答是如何坑了你</title><link>https://frostming.com/posts/2020/02-13/search-solution/</link><guid isPermaLink="false">https://frostming.com/2020/02-13/search-solution/</guid><pubDate>Thu, 13 Feb 2020 08:02:31 GMT</pubDate><content:encoded>&lt;p&gt;昨天某个新手又抛出来个问题：为什么找不到 &lt;code&gt;django-admin&lt;/code&gt; 可执行程序？我一看这不是 Python 高频问题之一吗[^1]。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;就问他&lt;code&gt;PATH&lt;/code&gt;是怎么设置的，结果他把&lt;code&gt;django-admin&lt;/code&gt; 复制到了&lt;code&gt;site-packages/django/bin&lt;/code&gt;下面。这就相当荒谬了，&lt;code&gt;lib/site-packages&lt;/code&gt;下面放的是库文件，这里是不可能会有&lt;code&gt;bin&lt;/code&gt;存在也不会有可执行程序在这里面的，当然，你随便放在哪，只要加到&lt;code&gt;PATH&lt;/code&gt;里面了就肯定能工作。那么试问为何不把&lt;code&gt;django-admin&lt;/code&gt;原本所在位置加到&lt;code&gt;PATH&lt;/code&gt;里而要用这么蹩脚的方法呢？&lt;/p&gt;
&lt;p&gt;[^1]:
这个问题解决方法是有套路的，可以参阅我之前写的文章&lt;a href=&quot;https://frostming.com/2019/03-13/where-do-your-packages-go&quot;&gt;你的 Python 包都装到哪了？
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;答案不言而喻，肯定又是从百度搜索结果里面排名不算低的某个所谓码农站点得来的，这里并不是对百度有偏见，谷歌也不一定搜不到这些结果，但百度的排名算法多少有推波助澜的作用。&lt;/p&gt;
&lt;p&gt;除了日常喷一下这些劣质教程复制来粘贴去的做法，我也仔细的想了想，发现这种现象的存在是有背后的逻辑的。首先，这些解决方案都出自水平不高的作者，或者高手的菜鸟阶段。他们喜欢把所有遇到的具体问题的解决方法记录下来，生怕以后忘了，比如「Django 遇到 DJANGO_SETTINGS_MODULE 错误怎么办？」「如何将 Ubuntu 上的 Python 升级到 Python 3？」「安装了 Nginx 但是打不开首页怎么办？」，这些解决方案，有的可能是根据网络上的线索胡乱尝试，正好 work 的步骤而已。你又不得不佩服他们做事的认真，能把每个步骤都记录下来。这就好比上数学课，一道应用题的解法可以有很多种，有的甚至你能试几个整数就能得到答案，那么我能把这题的题解写成「尝试数字 3, 5，满足题设，此即答案」吗？显然不能。这中对于某题有用的方法，不能推及其他题目，所以考试，同样的套路，换几个数字，就不会做了。网上的这些低质的回答，就属于这种无效的解法记录。作者缺乏对问题的解决路径的归纳和提炼，所以只好遇到一个记录一个。但那些能归纳和提炼的答案呢？它们往往已经不针对某个具体问题了，标题已经抽象为「如何解决包寻找不到的问题」。另一方面，一个新手在遇到一个问题的时候，也只是把错误信息复制到搜索框里，得到的结果也肯定是那些针对具体问题的解决方法。缺乏提炼的问题，搜索到的也肯定是缺乏提炼的答案。&lt;/p&gt;
&lt;p&gt;那么这个现象如何解决呢：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;尝试观察问题的规律，搜索的时候去掉具体情况的信息，比如「Python ModuleNotFound」是一个不错的搜索关键词，比「Python Django 导入失败」要好。&lt;/li&gt;
&lt;li&gt;实在提炼不了规律，请人工筛选搜索结果来源，Github, StackOverflow 是你的首选。&lt;/li&gt;
&lt;li&gt;没有找到答案，尝试到 StackOverflow 去提问，和社区的人的交流能让你发现你的问题所在，学会下次如何提一个好的问题。&lt;/li&gt;
&lt;li&gt;不要去记录这些具体问题的解决方法&lt;a href=&quot;%E7%B1%BB%E4%BC%BC%E7%9A%84%EF%BC%8C%E6%94%B6%E9%9B%86%E4%BB%A3%E7%A0%81%E7%89%87%E6%AE%B5%E4%B9%9F%E6%B2%A1%E6%9C%89%E4%BB%80%E4%B9%88%E5%A4%AA%E5%A4%A7%E7%9A%84%E6%84%8F%E4%B9%89%EF%BC%8C%5B%E6%8D%95%E8%9B%87%E8%80%85%E8%AF%B4%E5%87%A0%E4%BD%8D%E4%B8%BB%E5%88%9B%E5%A6%82%E6%98%AF%E8%AF%B4%5D(https://pythonhunter.org/episodes/sp03)%E3%80%82&quot;&gt;^2&lt;/a&gt;，这对你的提升不大。而应该把遇到的相似问题，总结起来写一篇文章，能锻炼逻辑思维和归纳概括能力。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>社区问答中需要避免的行为</title><link>https://frostming.com/posts/2019/12-26/qa-no-action/</link><guid isPermaLink="false">https://frostming.com/2019/12-26/qa-no-action/</guid><pubDate>Thu, 26 Dec 2019 07:36:35 GMT</pubDate><content:encoded>&lt;p&gt;我日常会在 TG 群、QQ 群、微信群和知乎上解答新手问题，一贯给人的印象都是态度不是很好，冷嘲热讽。这并不是因为我鄙视菜鸟，我也曾是菜鸟。除了一些我好为人师的性格之外，更多时候我希望提问者能自己意识到问题所在，为什么错了，这比他直接得到一个答案有帮助得多。这么久在社区里摸爬滚打以来，我发现一些问答上不好的行为，不吐不快。&lt;/p&gt;
&lt;h2&gt;提问的方法&lt;/h2&gt;
&lt;p&gt;这个问题，已经老生常谈，我不想再赘述，但必须再强调一次，阅读下面的链接应该就可以了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md&quot;&gt;提问的智慧&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://coolshell.cn/articles/10804.html&quot;&gt;X-Y 问题&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其实除了提问者的问题，回答者也有不好的行为，这也是我写这篇文章的冲动所在。&lt;/p&gt;
&lt;h2&gt;瞎回答&lt;/h2&gt;
&lt;p&gt;有的时候，某水友正抽丝剥茧分析盘问，找出 bug 所在或提供一个可用解决方案，结果另一个水友刚刚打开「电视机」，就蹦出来一句「用 XXX 就好了」。这就很要命了，新手无从判断解答方案的正确与否，他会倾向于选择一个比较容易理解的、直接了当的解决方案。而且前面那个水友跟那耗了半天，什么结论也没有，后面的水友直接一句话就解决了，此时该小白就天然偏向取信后者。殊不知认真盘问才是负责任的表现，而扔出一句解决方案的很可能是错的。非但如此，还会增加小白到达成功彼岸的成本：他选择了错误的路径，等他发现不行了，他还要费精力回到原来的状态。果然是「造谣一张嘴，辟谣跑断腿」。&lt;/p&gt;
&lt;p&gt;这种行为不局限于社区中的互动，也大量存在于搜索引擎中的低质内容中，一句两句就说明白的解决方案，显然比那些长篇大论的英文文章更有吸引力，然而前者可能是作者胡乱尝试，碰巧解决，然后记录下来的产物而已。&lt;/p&gt;
&lt;h2&gt;别用你的了，用我的吧&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Q: 用 Flask 如何达到 XXX 效果？&lt;/p&gt;
&lt;p&gt;A: 用 Django 吧。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;某小白终于鼓起勇气，开始学某一框架，碰到了瓶颈，结果上来一提问，被安利了另一个框架，好嘛，白学了。这也很不负责任，我觉得要说服人用一个新的框架，接触新的知识，你得对两者都有相当的熟悉度，并且清楚两者的优劣。有两种情况我会做这样的事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;requests&lt;/code&gt; v.s. &lt;code&gt;urllib&lt;/code&gt;, 用&lt;code&gt;requests&lt;/code&gt;吧&lt;/li&gt;
&lt;li&gt;&lt;code&gt;xpath&lt;/code&gt; v.s. &lt;code&gt;BeautifulSoup4&lt;/code&gt;， 用&lt;code&gt;xpath&lt;/code&gt;吧&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其他情况，除非已有方案确实做不到，或者很难做到，我会尽量沿用提问者已经选择的方案。&lt;/p&gt;
</content:encoded></item><item><title>不想写的 2019 总结</title><link>https://frostming.com/posts/2019/12-17/2019-summary/</link><guid isPermaLink="false">https://frostming.com/2019/12-17/2019-summary/</guid><pubDate>Tue, 17 Dec 2019 01:17:32 GMT</pubDate><content:encoded>&lt;p&gt;2019 即将画上句号，犹豫了很久要不要写个总结。因为别人写总结都是获得了很大的收获，或是有很丰富的体验。而我不然，原来的小确幸已消磨殆尽，这一年只剩迁延时日。&lt;/p&gt;
&lt;h2&gt;编程&lt;/h2&gt;
&lt;p&gt;有很多技术，没用过不算真正学会，今年算是真正掌握了 Django 这个框架。原来我一直是 Flask 党的，会用 Django（特别是 DRF）以后发现这个框架也有 Flask 不具有的优势[^1]，很喜欢那种不用思考框架以外的事情的风格。以后做 Web，会多一个选择。&lt;/p&gt;
&lt;p&gt;前端技能上，在调包侠的路上走得更远了。公司做了一款前端的脚手架工具，方便不懂前端的人拖拖控件就能做出一个页面。我用了以后发现无比难受，有很多想实现的效果，要来会在 UI 库里面寻找，在众多的参数中间徘徊。两个小时以后，我果断选择了叉掉页面，打开终端 &lt;code&gt;vue create&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;今年一头扎进开源的世界里，把 GitHub 的 Profile 做得很漂亮。截止目前，创建的 PR 数 103 个，提交 814 次，而且成为了 Pipenv 的维护者。但看着这个半死不活[^2]的项目我感慨良多，深深怀疑当时我被拉进去就是填坑的。但 GitHub Profile 漂亮并不能代表什么，我们国内的都是埋头工作的风格，大佬太多但不会宣传自己，让自己被外界所知。所以我希望 Python 的社区能越来越壮大，因为我深深地热爱这里。在这里打个广告，希望大家能多来&lt;a href=&quot;#python-she-qu&quot;&gt;Python China 的社区&lt;/a&gt;逛逛。&lt;/p&gt;
&lt;p&gt;值得一提的是在 PyCon China 2019 上第一次当了回讲师，估计讲完这次往后几年都找不到再次演讲的话题了。虽有很多不足，也收到一些正向的反馈，我是不想回看自己的录像，大概能给自己打 75 分。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PPT 地址: https://shimo.im/docs/P9pJGHTRyDT6HcX8/read&lt;/li&gt;
&lt;li&gt;视频地址: https://www.bilibili.com/video/av78691555?p=8&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;生活&lt;/h2&gt;
&lt;p&gt;6 月份我获得了上天给我的礼物，我全世界最可爱的女儿。她是我工作和生活上最好的减压器。
&lt;img src=&quot;//webp.frostming.com/images/2019-12-youyou.jpg&quot; alt=&quot;youyou.jpg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然而外公的离世和母亲的患病让我突然意识到我三十岁开始的人生将与过去截然不同了。有时你明知道三十而立，明知这是人生必遭的考验，但当变故突然到来的时候，你永远都没有准备好。&lt;/p&gt;
&lt;h2&gt;2020 的愿望&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;收入更上一层楼。&lt;/li&gt;
&lt;li&gt;在开源上投入相同的时间的基础上，做出更大的成绩。&lt;/li&gt;
&lt;li&gt;参加 PyCon China 2020 主会场，做志愿者。&lt;/li&gt;
&lt;li&gt;看一场演唱会。&lt;/li&gt;
&lt;li&gt;能多看几场电影。&lt;/li&gt;
&lt;li&gt;家人都健健康康，愿 2020 不会比 2019 更坏了&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Python 社区&lt;/h2&gt;
&lt;p&gt;欢迎加入中国蟒大家庭&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pychina.org/&quot;&gt;Python China&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://python-china.org.cn/&quot;&gt;Python China 社区&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://t.me/pythonzh&quot;&gt;Python 中文 TG 群&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pythonhunter.org/&quot;&gt;捕蛇者说——Python 中文播客&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;[^1]: &lt;a href=&quot;https://testdriven.io/blog/django-vs-flask/&quot;&gt;Django vs. Flask in 2019: Which Framework to Choose&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;[^2]: &lt;a href=&quot;https://github.com/pypa/pipenv/issues/4058&quot;&gt;If this project is dead, just tell us&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Flask 博客接入第三方登录</title><link>https://frostming.com/posts/2019/11-27/oauth-login/</link><guid isPermaLink="false">https://frostming.com/2019/11-27/oauth-login/</guid><pubDate>Wed, 27 Nov 2019 06:20:59 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;在&lt;a href=&quot;https://frostming.com/2019/11-22/comment-system&quot;&gt;上一篇文章&lt;/a&gt;中我留了一部分内容，就是如何给评论登录接入第三方登录。我不希望来访问我博客的用户有太大的登录成本，否则本想留下些话的人，就会被挡在这个门槛之外。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Flask 不像 Django 一样有各种现成的组件可以选用，Flask 的各种扩展也不那么「开箱即用」。在我的博客项目中，我选用的是&lt;a href=&quot;https://authlib.org&quot;&gt;Authlib&lt;/a&gt;，它是国内的一名 Python 资深开发者&lt;a href=&quot;https://github.com/lepture&quot;&gt;@lepture&lt;/a&gt;开发的一款全面完善的 OAuth 认证库。大家可能在别的教程里会看到用的是&lt;a href=&quot;https://github.com/lepture/flask-oauthlib&quot;&gt;flask-oauthlib&lt;/a&gt;，它们的作者其实是同一人，而且在 2019 年的今天，我绝对会推荐你用 Authlib 而不是 flask-oauthlib。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;网上能搜索到的教程，有很多都已经过时，或者不那么「与时俱进」了，截止今天 Flask 已经到 1.1.1 版本了，而很多教程还停留在 0.10.x 时代[^1]。我是个喜欢与时俱进的人，我写的 Flask 相关文章，以及这个博客项目，保证都是基于最新的推荐，并会尽量保持更新。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;[^1]: 比如&lt;code&gt;Flask-Script&lt;/code&gt;这个扩展，我不推荐任何新的 Flask 项目使用，因为 Flask 从 0.11.0 开始已经内置了命令行的支持。&lt;/p&gt;
&lt;h2&gt;开发思路&lt;/h2&gt;
&lt;p&gt;首先我们要搞清楚我们需要第三方登录来做什么。很简单，获取用户的邮箱地址（用于通知）、用户头像、用户名称（用于展示）这些基本的信息。登录时，我们到对应的平台上获取令牌，然后通过此令牌去请求用户信息，存到我们的数据库里，以备后面使用。如果大家对 OAuth 不太了解的，OAuth 分为 OAuth1 协议与 OAuth2 协议，是一种开放的用户认证协议，它允许任何已注册的外部调用方(Client)，获取平台(Provider)内部的授权访问的资源。OAuth2 协议更加简化些，我预备接入的 Github 和 Google 都属于这一种协议，认证的主要过程是：
&lt;img src=&quot;//webp.frostming.com/images/2019-11-oauth.png&quot; alt=&quot;oauth.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;接入过程&lt;/h2&gt;
&lt;p&gt;Github 的 OAuth2 接入是最简单的，很多教程都选择以 Github 为例，所以我这里选择用 Google 为例。
第一步，到&lt;a href=&quot;https://console.developers.google.com/apis/credentials&quot;&gt;Google API Console&lt;/a&gt;申请 OAuth2 凭据
&lt;img src=&quot;//webp.frostming.com/images/2019-11-google1.png&quot; alt=&quot;google1.png&quot; /&gt;
选择 Web 应用，填入你的应用名称，和已获授权的重定向 URI，在上图中，当你确认授权访问以后，Google 会重定向到这个 URI 进行后续的动作。访问这个 URI 时会带上 code 的信息，一般地，这个 URI 的视图函数中应该做三件事情：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用传入的 code 去 Google 交换访问令牌&lt;/li&gt;
&lt;li&gt;存储访问令牌&lt;/li&gt;
&lt;li&gt;使用访问令牌获取用户信息&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;完成了以后你就可以看到你的&lt;strong&gt;客户端 ID&lt;/strong&gt;和&lt;strong&gt;客户端密钥&lt;/strong&gt;了。&lt;/p&gt;
&lt;h2&gt;Authlib 的使用&lt;/h2&gt;
&lt;p&gt;安装过程就不用说了，用&lt;code&gt;pip&lt;/code&gt;安装即可。先在&lt;code&gt;models.py&lt;/code&gt;中加入一个新的表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
class OAuth2Token(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(40))
    token_type = db.Column(db.String(40))
    access_token = db.Column(db.String(200))
    refresh_token = db.Column(db.String(200))
    expires_at = db.Column(db.Integer())
    user_id = db.Column(db.Integer(), db.ForeignKey(&quot;user.id&quot;))
    user = db.relationship(&quot;User&quot;, backref=db.backref(&quot;tokens&quot;, lazy=&quot;dynamic&quot;))

    def to_token(self):
        return dict(
            access_token=self.access_token,
            token_type=self.token_type,
            refresh_token=self.refresh_token,
            expires_at=self.expires_at,
        )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后创建&lt;code&gt;oauth&lt;/code&gt;对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from authlib.integrations.flask_client import OAuth

def fetch_token(name):
    token = OAuth2Token.query.filter_by(name=name, user=current_user).first()
    return token.to_token()

def update_token(name, token, refresh_token=None, access_token=None):
    if refresh_token:
        item = OAuth2Token.filter_by(name=name, refresh_token=refresh_token).first()
    elif access_token:
        item = OAuth2Token.filter_by(name=name, access_token=access_token).first()
    else:
        return
    if not item:
        return
    # update old token
    item.access_token = token[&apos;access_token&apos;]
    item.refresh_token = token.get(&apos;refresh_token&apos;)
    item.expires_at = token[&apos;expires_at&apos;]
    db.session.commit()

oauth = OAuth(fetch_token=fetch_token, update_token=update_token)

google = oauth.register(
    name=&apos;google&apos;,
    access_token_url=&apos;https://www.googleapis.com/oauth2/v4/token&apos;,
    access_token_params={&apos;grant_type&apos;: &apos;authorization_code&apos;},
    authorize_url=&apos;https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&apos;,
    authorize_params=None,
    api_base_url=&apos;https://www.googleapis.com/&apos;,
    client_kwargs={&apos;scope&apos;: &apos;email profile&apos;}
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;fetch_token&lt;/code&gt;和&lt;code&gt;update_token&lt;/code&gt;两个函数是 Authlib 需要用来获取和更新令牌用的。然后，在配置文件中加入两个配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GOOGLE_CLIENT_ID = os.getenv(&apos;GOOGLE_CLIENT_ID&apos;)
GOOGLE_CLIENT_SECRET = os.getenv(&apos;GOOGLE_CLIENT_SECRET&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为这两个配置是敏感信息，推荐从环境变量读取，不要暴露在代码库中。记得在&lt;code&gt;create_app&lt;/code&gt;中将&lt;code&gt;oauth&lt;/code&gt;对象注册到&lt;code&gt;Flask&lt;/code&gt;中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;oauth.init_app(app)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好了，现在我们可以来写视图了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def google_login():
    origin_url = request.headers[&apos;Referer&apos;]
    session[&apos;oauth_origin&apos;] = origin_url
    redirect_uri = url_for(&apos;.google_auth&apos;, _external=True)
    if not current_app.debug:
        redirect_uri = redirect_uri.replace(&apos;http://&apos;, &apos;https://&apos;)
    return google.authorize_redirect(redirect_uri)

def google_auth():
    token = google.authorize_access_token()
    # save token
    resp = google.get(&apos;oauth2/v3/userinfo&apos;)
    resp.raise_for_status()
    profile = resp.json()
    # save profile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意到我在&lt;code&gt;login&lt;/code&gt;函数中把&lt;code&gt;request.headers[&apos;Referer&apos;]&lt;/code&gt;的值保存到了会话中，这是为了登录成功后跳转会原来的页面，而中途会跳转到外部的网址，所以需要把原地址记下来。跳转 google 认证地址的 URL 中需要包含回调的地址，而这个地址必须和之前在 Google API Console 中配置的地址一致（可以允许是子页面）。现在我们就可以使用第三方登录了。&lt;/p&gt;
&lt;h2&gt;进一步简化&lt;/h2&gt;
&lt;p&gt;大家可以发现这样使用我们必须知道 Google 的认证地址、令牌地址和一些额外请求参数，虽然我们可以查阅[Google OAuth 文档]获取这些信息，但这多少也是一种负担。所以 authlib 甚至提供一个库&lt;a href=&quot;https://github.com/authlib/loginpass&quot;&gt;loginpass&lt;/a&gt;，包含几乎所有主流的 OAuth 提供方，使用 loginpass 以后，上面的三段代码可以替换成下面几行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from flask import Flask
from authlib.integrations.flask_client import OAuth
from loginpass import create_flask_blueprint, Google

app = Flask(__name__)
oauth = OAuth(app)

def handle_authorize(remote, token, user_info):
    if token:
        save_token(remote.name, token)
    if user_info:
        save_user(user_info)
        return user_page
    raise some_error

github_bp = create_flask_blueprint(Google, oauth, handle_authorize)
app.register_blueprint(github_bp, url_prefix=&apos;/google&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;我的博客即将同步至腾讯云+社区，邀请大家一同入驻：https://cloud.tencent.com/developer/support-plan?invite_code=23bvqemu5etcw&lt;/p&gt;
</content:encoded></item><item><title>给你的 Git commit 加上绿勾</title><link>https://frostming.com/posts/2019/11-25/git-commit-sign/</link><guid isPermaLink="false">https://frostming.com/2019/11-25/git-commit-sign/</guid><description>一个简单但很多人没注意的细节</description><pubDate>Mon, 25 Nov 2019 14:37:05 GMT</pubDate><content:encoded>&lt;p&gt;今天无事翻看了几个 Python 开发者的 Github，却发现大多数人的 Git commit 列表都是白茫茫一片。
&lt;img src=&quot;//webp.frostming.com/images/2019-11-no-sign.png&quot; alt=&quot;no-sign.png&quot; /&gt;
大家乍一眼可能看不出有什么问题，那么看下面这张图就明白了：
&lt;img src=&quot;//webp.frostming.com/images/2019-11-has-sign.png&quot; alt=&quot;has-sign.png&quot; /&gt;
没错，每条 commit 后面都有一个&lt;strong&gt;Verified&lt;/strong&gt;绿标，我是一个对这些东西有偏执的喜好的人，只要见过别人有，那自己也一定要有。大多数人都会去追求全站 HTTPS 的那个绿色对勾（虽然新版 Chrome 变成一把暗淡的锁让人失去许多动力），但并没有那么多人会去追求这个 commit 的绿标。之前听&lt;a href=&quot;https://pythonhunter.org&quot;&gt;捕蛇者说&lt;/a&gt;的时候也发现，有时候一些你认为习以为常的知识，很多人并不知晓。所以我觉得自己知道的东西，一定要多分享。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;勿以善小而不为，勿以知识微小而不分享&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;这个绿标是个啥？&lt;/h2&gt;
&lt;p&gt;大家可能都知道，安装 git 之后第一件事就是用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ git config set --global user.name &amp;lt;username&amp;gt;
$ git config set --global user.email &amp;lt;email&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置你的用户名和邮箱，这些信息会显示在提交历史(&lt;code&gt;git log&lt;/code&gt;)里面，表示这个提交的作者信息。但你发现了吗，你是可以随意设置邮箱和用户名的，我甚至可以设成 Linus Torvalds 的个人信息，毕竟这些东西都是公开可查的，那么难道就变成 Linus 提交的了吗？反过来，你可能工作的环境不止一个，每个环境都有不同的邮箱，工作环境用工作邮箱，个人环境用个人邮箱，那么当我在这两种环境上都提交调同一个 Github 仓库时，别人如何知道都是同一个人？&lt;/p&gt;
&lt;p&gt;这个绿标就是证明&lt;strong&gt;我是我&lt;/strong&gt;、&lt;strong&gt;别人不是我&lt;/strong&gt;的东西，这些提交其实是用个人专属的 PGP 密钥签名过的。PGP 是一种加密算法，使用非对称的密钥，而产生这种密钥的软件是 GPG(Gnu PG)。关于 PGP 和 GPG 我也不是专家只能到此为止，大家可以阅读文末的参考链接以了解更多。&lt;/p&gt;
&lt;p&gt;这个签名，起到了认证身份的作用，所以无论我用的是什么邮箱，只要带上了这个签名，那么这个提交就是我本人做出的，别人是无法伪造的。你参加开源贡献时，附上这个小小的绿标，也会显得你更加专业。&lt;/p&gt;
&lt;h2&gt;生成 GPG 密钥&lt;/h2&gt;
&lt;p&gt;一般 Linux 系统都已经自带 gpg 软件，输入&lt;code&gt;gpg --help&lt;/code&gt;可以查看你是否已经安装，如果没有安装可以用你系统的包管理器来安装。首先在终端输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ gpg --full-generate-key
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后按照提示输入信息，密钥类型使用默认的&lt;code&gt;RSA and RSA&lt;/code&gt;即可。密钥长度推荐使用默认的 4096，然后输入你的个人信息，这样密钥就会绑定到你的邮箱，要使用和 Git 提交相同的邮箱地址。最后输入一段密码，用来提取这个密钥。这样一个 GPG 密钥就生成好了，可以输入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ gpg --list-secret-keys --keyid-format LONG
/Users/hubot/.gnupg/secring.gpg
------------------------------------
sec   4096R/3AA5C34371567BD2 2016-03-10 [expires: 2017-03-10]
uid                          Hubot
ssb   4096R/42B317FD4BA89E7A 2016-03-10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来查看你的密钥，在本例中此密钥的 ID 是&lt;code&gt;3AA5C34371567BD2&lt;/code&gt;。接下来，我们需要获取公钥值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ gpg --armor --export 3AA5C34371567BD2
-----BEGIN PGP PUBLIC KEY BLOCK-----
...
-----END PGP PUBLIC KEY BLOCK-----
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将公钥的内容复制到剪贴板以备后续使用。&lt;/p&gt;
&lt;p&gt;在你的 Github 中，点击头像-Settings-SSH and GPG keys，然后点击&lt;code&gt;New GPG key&lt;/code&gt;，将复制好的公钥内容粘贴进去即可。&lt;/p&gt;
&lt;h2&gt;Git 提交启用签名&lt;/h2&gt;
&lt;p&gt;在提交时启用签名很简单，只要在&lt;code&gt;git commimt&lt;/code&gt;命令中加上&lt;code&gt;-S&lt;/code&gt;选项即可。如果 git 提示找不到 gpg 程序，很可能因为你的 gpg 可执行程序不在&lt;code&gt;PATH&lt;/code&gt;中，使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ git config set gpg.program &amp;lt;path_to_gpg&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来指定 gpg 程序位置。现在&lt;code&gt;git push&lt;/code&gt;你的提交，你就会在 commit 列表中发现提交已经加上了这个绿标了。&lt;/p&gt;
&lt;p&gt;每次提交都要加上&lt;code&gt;-S&lt;/code&gt;未免麻烦，你也可以默认启用 GPG 签名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ git config --global commit.gpgsign true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;嗯，很好，每次都会自动加上签名了，但是，你会发现签名的时候都会弹出一个 prompt 输入密码。甚至你使用 IDE 集成的 git 的时候也会弹出这么个终端，这也太烦了，有没有不用输密码的方法？我目前也只在 Mac 系统上找到了解决方法，因为这个 GPG key 的密码可以保存到 Mac 钥匙串中，你只需要安装&lt;code&gt;gpg-suite&lt;/code&gt;即可，使用 homebrew 安装起来也很简单：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ brew cask install gpg-suite
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到目前为止我们好像把 Windows 忘了，没有问题，你只需要安装一个&lt;a href=&quot;https://gpg4win.org/&quot;&gt;Gpg4win&lt;/a&gt;GUI 客户端就可以了（其实 Git for windows 会自带一个 GPG，但它只是一个命令行程序，这样对 IDE 不太友好），注意你需要确保 git 配置的 gpg 程序指向 Gpg4win 下面的 gpg(&lt;code&gt;Gpg4win的程序路径/bin/gpg.exe&lt;/code&gt;)。这个 GUI 客户端虽然不会记住密码，但起码它弹出的是一个 GUI 窗口提示输入密码，可以和 IDE 完美工作。只是在提交的时候需要输入一次密码，也不算很大的负担，反而增添了些许仪式感。&lt;/p&gt;
&lt;p&gt;一般情况下，我会在每个会提交到我的 Github 仓库的机器产都生成一个密钥，然后加到 Github 账户中。&lt;/p&gt;
&lt;h2&gt;更多关于 PGP 加密&lt;/h2&gt;
&lt;p&gt;对自己的身份严格认证，对自己的信息加密是一个很好的习惯，GPG key 除了可以做提交签名之外，也可以加解密消息，对通信进行安全加固，把公钥发给对方，别人用这个公钥加密，你收到后用私钥解密。互联网上就有这么一款产品叫 https://keybase.io ，可以分享自己的 PGP 密钥，作为自己的一种指纹信息，推荐大家都去注册一下，我的个人的指纹是&lt;a href=&quot;https://keybase.io/frostming&quot;&gt;&lt;code&gt;7B28 4C8F CC08 5EFF&lt;/code&gt;&lt;/a&gt;，我们来加密聊天吧！（然并卵，日常还不都在用微信裸奔聊天）。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.wikipedia.org/zh-hans/PGP&quot;&gt;PGP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://help.github.com/en/github/authenticating-to-github/generating-a-new-gpg-key&quot;&gt;Generating a new GPG key - GitHub Help&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://help.github.com/en/github/authenticating-to-github/adding-a-new-gpg-key-to-your-github-account&quot;&gt;Adding a new GPG key to your GitHub account - GitHub Help&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://help.github.com/en/github/authenticating-to-github/signing-commits&quot;&gt;Signing Commits - GitHub Help&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>使用 Flask 做一个评论系统</title><link>https://frostming.com/posts/2019/11-22/comment-system/</link><guid isPermaLink="false">https://frostming.com/2019/11-22/comment-system/</guid><pubDate>Fri, 22 Nov 2019 13:30:42 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;2024.1.2 更新&lt;/strong&gt;: 本文中的评论系统已经迁移到了 &lt;a href=&quot;https://artalk.js.org/&quot;&gt;Artalk&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因为我博客使用的 Disqus 代理服务下线，博客的评论系统可能有一阵子没有工作了。惭愧的是我竟然最近才发现，我的工作环境一直是没有 GFW 存在的，发现是因为有个朋友为了留言给我不惜通过赞赏 1 元钱的方式。赞赏功能也是我最近才上的功能，但我怎么是这么一个无良的博主呢，我认为一个好的评论交流环境还是非常有必要的。但是自建评论还是换用其他墙内友好的评论系统，我还是纠结了一阵的，大致上我有这么几个要求：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;主要服务墙内，Disqus 虽香但墙内用不了啊&lt;/li&gt;
&lt;li&gt;颜值，要能匹配当前博客的主色调，或者能方便地自定义皮肤&lt;/li&gt;
&lt;li&gt;评论要支持 markdown 语法&lt;/li&gt;
&lt;li&gt;评论数据要有地方可管理、归档、导入导出等&lt;/li&gt;
&lt;li&gt;外部用户使用评论的门槛要低&lt;/li&gt;
&lt;li&gt;用户收到回复时能通过他「常用的」联系方式收到通知&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;评论系统大致有这么几个选择方向：一是使用类似 Disqus 这样的三方平台，这样数据托管不用操心，但服务随时有挂掉的风险，而且外观上也不够自由；二是使用 Github Issue 作为后端的评论系统，比如&lt;a href=&quot;https://github.com/imsun/gitment&quot;&gt;Gitment&lt;/a&gt;，&lt;a href=&quot;https://utteranc.es/&quot;&gt;utterances&lt;/a&gt; 好处是你不必担心 Github 挂掉，而且不用收钱。但不方便后续打包迁移，而且我一直反对&lt;strong&gt;过度利用&lt;/strong&gt;Github；那么剩下的选择就是自己撸一个了，简单的构思评估以后我列出以下列&lt;a href=&quot;https://github.com/frostming/Flog/issues/18&quot;&gt;功能大纲&lt;/a&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;评论数据模型&lt;/li&gt;
&lt;li&gt;评论展示&lt;/li&gt;
&lt;li&gt;评论管理&lt;/li&gt;
&lt;li&gt;导入 disqus 评论&lt;/li&gt;
&lt;li&gt;新评论通知&lt;/li&gt;
&lt;li&gt;第三方登录&lt;/li&gt;
&lt;li&gt;评论导出（低优先）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;类比 Workpress 提供的评论功能，用户只需要填用户姓名和电子邮件这两个信息就够了，前者用来显示作者名，后者用来接收通知，个人网站用来推广自己，但不是必填的。我在这个基础上，希望增加第三方登录的功能，这样用户就不用填写这些信息，点一个按钮就好了。关于第三方登录的开发实现，我会留到下一篇文章中。&lt;/p&gt;
&lt;h2&gt;评论数据模型&lt;/h2&gt;
&lt;p&gt;首先是评论数据模型的设计，我的理念是够用就好，不用太多太复杂的东西，毕竟我的文章平均 0.2 条评论。所以，点赞什么的就不要了，评论删除直接删数据就好了，也不需要什么状态。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//webp.frostming.com/images/2019-11-comment_uml.png&quot; alt=&quot;comment_uml.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中分别有一个外键指向作者用户以及文章记录，User 里面会记录这个用户的 Email, 名称，头像信息。另外会有一个 parent_id 指向评论回复的对象（也是一条评论），这里有一个指向自身的外键，使用&lt;code&gt;Flask-SQLAlchemy&lt;/code&gt;写起来是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Comment(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    post_id = db.Column(db.Integer, db.ForeignKey(&quot;post.id&quot;))
    author_id = db.Column(db.Integer, db.ForeignKey(&quot;user.id&quot;))
    floor = db.Column(db.Integer)
    content = db.Column(db.Text())
    html = db.Column(db.Text())
    create_at = db.Column(db.DateTime(), default=datetime.utcnow)
    parent_id = db.Column(db.Integer, db.ForeignKey(&quot;comment.id&quot;))
    replies = db.relationship(
        &quot;Comment&quot;, backref=db.backref(&quot;parent&quot;, remote_side=[id]), lazy=&quot;dynamic&quot;
    )

    __table_args__ = (db.UniqueConstraint(&quot;post_id&quot;, &quot;floor&quot;, name=&quot;_post_floor&quot;),)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;floor&lt;/code&gt;表明评论是「第几楼」，注意这里有个限制，每篇文章楼层不能重复。&lt;/p&gt;
&lt;h2&gt;评论展示&lt;/h2&gt;
&lt;p&gt;接下来看看如何展示评论。每条评论都可能有若干回复，回复评论又有回复，所以这是一个树形的结构，最极端的，如果把所有树形都嵌套显示出来，就会像网易新闻评论盖楼那样。另一个极端，是把所有评论都展平，按回复时间排序显示，这样又会失去回复的上下文信息。还是那句话，够用就好，我选择了一条折中的方式：两层树形展示。直接评论的是第一层节点，然后回复这些评论的，和回复这些回复的，都展平成一层节点，算作这条评论的子节点。外层评论和子节点都按时间排序显示，但只有外层评论具有楼层属性。且子节点应该展示回复的是哪位作者，这样就大大减小了上下文混淆的可能（虽然我觉得我这评论的量，全显示成一层也不会怎样）。&lt;/p&gt;
&lt;p&gt;评论框编辑器使用的是&lt;a href=&quot;https://github.com/sparksuite/simplemde-markdown-editor&quot;&gt;simple-mde&lt;/a&gt;，使用起来非常简单：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;link
  rel=&quot;stylesheet&quot;
  href=&quot;https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css&quot;
/&amp;gt;
&amp;lt;script src=&quot;https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
...
&amp;lt;textarea name=&quot;content&quot;&amp;gt;&amp;lt;/textarea&amp;gt;
...
&amp;lt;script&amp;gt;
  var simplemde = new SimpleMDE();
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完事！后续可能会考虑加上 emoji 选择器。markdown 保存，后端渲染 html，前端取出展示。最后结果非常漂亮令我满意，大家可以在本篇文章下面看到效果。&lt;/p&gt;
&lt;h2&gt;评论管理&lt;/h2&gt;
&lt;p&gt;对应的，在管理员页面也加上一个评论管理页面，以及开启内置评论的开关。因为最初设计的是评论一经发出，只能删除，不能修改，所以这种页面对我这样的 CRUD 程序员来说不在话下。&lt;/p&gt;
&lt;p&gt;现在就到了激动人心的时刻了，把 Disqus 的评论数据迁移过来！我到 Disqus 页面上去看，发现 Disqus 支持导出评论数据为特定的结构，是一个 xml，只要是结构化的数据，那就问题不大了。主要分为两个部分，前半部分是 thread 的列表，表示有哪些文章开启了 Disqus 的评论，包含文章的 url 等信息（取决于你如何开启的 Disqus），后半部分是评论列表，每条评论有评论内容、作者信息、回复的上级评论 ID，还好数据模型设计得好，这些都在射程范围内。于是写了一个函数解析，导入这些数据，注意有些已删除的或者垃圾评论直接过滤掉即可，函数放在&lt;a href=&quot;https://github.com/frostming/Flog/blob/0620874d9080cfd0748007d189ae8649449ff560/flaskblog/api/views.py#L321&quot;&gt;这里&lt;/a&gt;了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//webp.frostming.com/images/2019-11-disqus_export.png&quot; alt=&quot;disqus_export.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上传文件，导入，成功，Disqus 的评论就完美迁移过来了！&lt;/p&gt;
&lt;h2&gt;评论通知&lt;/h2&gt;
&lt;p&gt;评论通知需要拿到用户的联系方式，所以表单中电子邮件是必填的，接入第三方登录时，我也要考虑哪些服务是可以获得联系方式的，目前决定是用 Github，Google 两种方式，至于新浪微博，虽然国人常用，但好像没有谁会在微博上留联系方式，所以排除，微信倒是很好，但微信的第三方登录好像很麻烦的样子，暂不考虑。所以最后就是邮件通知。那就简单了，用 Flask 的扩展&lt;a href=&quot;https://pythonhosted.org/Flask-Mail/&quot;&gt;Flask-Mail&lt;/a&gt;全都搞定，但在使用中我遇到两个坑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果在后台任务中做发送邮件的操作，注意获取&lt;code&gt;g&lt;/code&gt;对象需要应用上下文，获取请求信息需要请求上下文，而光用 Flask 提供的&lt;code&gt;copy_current_request_context&lt;/code&gt;只复制请求上下文，而会创建新的应用上下文，我写了两个函数，一个是添加应用和请求上下文到一个函数，另一个是将函数转换成后台任务:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;def with_app_context(f):
   ctx = _app_ctx_stack.top
   req_ctx = _request_ctx_stack.top.copy()

   def wrapper(*args, **kwargs):
       with ctx:
           with req_ctx:
               return f(*args, **kwargs)
   return update_wrapper(wrapper, f)

def background_task(f):
   def wrapper(*args, **kwargs):
       future = gevent.spawn(with_app_context(f), *args, **kwargs)

       def callback(result):
           exc = result.exception
           current_app.log_exception((type(exc), exc, exc.__traceback__))

       future.link_exception(with_app_context(callback))
       return future

   return update_wrapper(wrapper, f)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里后台任务用了 gevent，如果用线程方式，则改成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from concurrent.futures import ThreadPoolExecutor

def background_task(f):
    def wrapper(*args, **kwargs):
        with ThreadPoolExecutor() as pool:
            future = pool.submit(with_app_context(f), *args, **kwargs)

        def callback(result):
            exc = result.exception()
            if exc is not None:
                current_app.log_exception((type(exc), exc, exc.__traceback__))

        future.add_done_callback(with_app_context(callback))
        return future

    return update_wrapper(wrapper, f)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;腾讯云的主机默认禁掉了 25 端口，害我找了半天原因，只要自己在控制台解禁一下即可立刻生效。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;源码仓库: https://github.com/frostming/Flog&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.miguelgrinberg.com/post/implementing-user-comments-with-sqlalchemy&quot;&gt;Implementing User Comments with SQLAlchemy - miguelgrinberg.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>我的Python环境设置</title><link>https://frostming.com/posts/2019/11-18/python-setup/</link><guid isPermaLink="false">https://frostming.com/2019/11-18/python-setup/</guid><description>2020年我使用的Python工具</description><pubDate>Mon, 18 Nov 2019 09:42:47 GMT</pubDate><content:encoded>&lt;p&gt;网上看到一篇&lt;a href=&quot;https://jacobian.org/2019/nov/11/python-environment-2020/&quot;&gt;博文&lt;/a&gt;，我突然也想写一下自己正在使用的 Python 环境设置，以及对应的工具链。众众众所周知，Python 环境管理是个很大很大的坑，坑里面有无数新人 or 老司机的尸体。而 Python 环境管理的工具又五花八门，所以可能每个人的设置都不尽相同。我列出的我使用的工具链，至少最大地满足了自己的需求，但不一定满足所有人的需求。但我自认为在 Python 环境管理方面颇有心得，所以有一定的参考价值。&lt;/p&gt;
&lt;h2&gt;我的需求&lt;/h2&gt;
&lt;p&gt;照例列一下我的需求：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;我平时在三种不同的环境中使用 Python，除了公司项目规定使用 Python 3.6 以外，个人项目都是尽可能用最新版：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Python 3.6.8 + Linux（公司，公司项目）&lt;/li&gt;
&lt;li&gt;Python latest + Windows（公司，个人项目）&lt;/li&gt;
&lt;li&gt;Python latest + MacOS（在家，个人项目）&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我同时工作在多个项目上，所以隔离环境非常重要&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;除非非常必要，否则不用 docker&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我用到很多 Python 的命令行工具：black, twine, ...&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;系统上保留的 Python 数量尽可能少，但我绝不会干升级系统 Python 这种事的，所有系统 Python 是什么就是什么，我不会去碰它&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;使用的工具&lt;/h2&gt;
&lt;h3&gt;1. Python 版本管理: &lt;a href=&quot;https://github.com/uranusjr/pythonup-posix&quot;&gt;PythonUp&lt;/a&gt;(posix), None(Windows)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;为何不是 pyenv?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pyenv&lt;/code&gt; 把所有 Python 版本都分开安装，就算是 patch release。这样做可以最大可能地保证你机器上的所有虚拟环境、命令行程序都是可用的，但我会嫌 python 的版本太多了，毕竟 99.99%的情况下，Python 3.7.4 都可以平滑&lt;strong&gt;替换&lt;/strong&gt;为 Python 3.7.5 而不造成任何损失。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PythonUp&lt;/code&gt;就是这样一个工具，它同时支持 posix + windows 平台。你可以把它看成是&lt;code&gt;pyenv&lt;/code&gt;的简化版，但它是支持 minor release 层面隔离的，如果只是 patch release 升级是直接替换的。使用方法很简单：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ pythonup install 3.6
$ pythonup install 3.8
$ pythonup use 3.8
$ python3 --version
Python 3.8.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但要注意它相比&lt;code&gt;pyenv&lt;/code&gt;要少一些功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自动激活 local python 版本&lt;/li&gt;
&lt;li&gt;管理虚拟环境&lt;/li&gt;
&lt;li&gt;全局解释器名称为&lt;code&gt;python3&lt;/code&gt;，&lt;code&gt;pip3&lt;/code&gt;而不是&lt;code&gt;python&lt;/code&gt;，&lt;code&gt;pip&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Windows 呢?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我在 Windows 上没有用任何工具管理 Python 版本，因为 Python 的 Windows 安装器本身就支持替换升级（patch update），而且全局的 Python 命令行程序不会受到任何影响。而且 Windows 上的 Python 3 自带一个&lt;code&gt;py&lt;/code&gt;的版本启动器，可以方便地选择运行的 Python:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; py -2 --version
Python 2.7.15
&amp;gt; py -3 --version
Python 3.8.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以我基本也不用切换 python 版本了（&lt;code&gt;py -3&lt;/code&gt; 运行起来比&lt;code&gt;python&lt;/code&gt;还短些）&lt;/p&gt;
&lt;h3&gt;2. 安装命令行程序: &lt;a href=&quot;https://pypi.org/project/pipx/&quot;&gt;pipx&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;把命令行程序安装在隔离的环境中，不会搞乱依赖。原来有一个工具叫&lt;a href=&quot;https://pypi.org/project/pipsi&quot;&gt;&lt;code&gt;pipsi&lt;/code&gt;&lt;/a&gt;但它停止维护了，&lt;code&gt;pipx&lt;/code&gt;是活跃状态而且更加好用，强烈推荐！使用起来也很简单，只需要在原来&lt;code&gt;pip install&lt;/code&gt;安装的基础上加一个&lt;code&gt;x&lt;/code&gt;就可以了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ pipx install black
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 虚拟环境、依赖管理：&lt;a href=&quot;https://github.com/pypa/pipenv.git&quot;&gt;Pipenv&lt;/a&gt;@master 分支 + virtualenv 魔改版&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;master 分支&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Pipenv 被诟病最多的就是已经近一年没有新版发布了，使用 Github 上的 master 分支完美解决这个问题，嘿嘿，几个月使用来看，bug 已经相当少了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;virtualenv 魔改了什么?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Pipenv 是使用&lt;code&gt;virtualenv&lt;/code&gt;来创建虚拟环境的，但&lt;code&gt;virtualenv&lt;/code&gt;有几个重大缺陷，大到我忍不了所以搞了个&lt;a href=&quot;https://github.com/frostming/virtualenv-venv&quot;&gt;fork&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;virtualenv 中的 python 无法再创建虚拟环境&lt;/li&gt;
&lt;li&gt;virtualenv 指向的 python 升级则环境变成 broken 状态&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;而 Python 3 自带的 venv 能解决这些问题，不明白为什么 virtualenv 还不支持 venv，我只能 fork 一下使得 virtualenv 尽可能使用 python3 自带的 venv 来创建虚拟环境。
使用&lt;code&gt;virtualenv&lt;/code&gt;魔改版替换原版：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ pip install -I https://github.com/frostming/virtualenv-venv/releases/download/16.4.4-fork/virtualenv-16.2.0_fork-py2.py3-none-any.whl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;fork 版本的更新并不能跟上上游的更新，主要也是因为没碰到什么 bug 且目前只有我自己在用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Poetry 呢&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Poetry 确实也相当好用且有越来越多的人从 Pipenv 切换过去，但对我来说 Poetry 没解决这两个问题之前我不会切过去（也可能已经改进了，有一段时间没用过）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;更多的虚拟环境的管理：清理，删除，查看&lt;/li&gt;
&lt;li&gt;poetry 的&lt;code&gt;pyproject.toml&lt;/code&gt;还不是标准，配置文件格式还有许多问题（C 扩展定义、markers 支持等），如果切换到 poetry 会破坏兼容性导致项目只能用 poetry 开发。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Flask前后端分离实践：Todo App(3)</title><link>https://frostming.com/posts/2019/11-12/flask-vue-todo3/</link><guid isPermaLink="false">https://frostming.com/2019/11-12/flask-vue-todo3/</guid><description>CSRF防护与后端鉴权</description><pubDate>Tue, 12 Nov 2019 08:12:34 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;前序文章&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://frostming.com/2018/09-18/flask-vue-todo1&quot;&gt;Flask 前后端分离实践：Todo App(1)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://frostming.com/2018/09-27/flask-vue-todo2&quot;&gt;Flask 前后端分离实践：Todo App(2)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本文项目地址: https://github.com/frostming/flask-vue-todo&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;作者按&lt;/em&gt;&lt;/strong&gt;: 几天前我收到一封邮件，有读者说看了我的前后端分离实践的文章获益很多。然而我却丧尽天良的断更了？不行不行，我不是这样的人，所以一年后，我再补上这个系列最后一篇文章吧。&lt;/p&gt;
&lt;h2&gt;CSRF 防护&lt;/h2&gt;
&lt;p&gt;如果你们是看了 Miguel 的&lt;a href=&quot;https://www.amazon.cn/dp/B07KW12YLN/ref=sr_1_2?__mk_zh_CN=%E4%BA%9A%E9%A9%AC%E9%80%8A%E7%BD%91%E7%AB%99&amp;amp;keywords=flask+web%E5%BC%80%E5%8F%91&amp;amp;qid=1573545635&amp;amp;sr=8-2&quot;&gt;狗书&lt;/a&gt;，或是李辉大大的&lt;a href=&quot;https://www.amazon.cn/dp/B07GST8Z8M/ref=sr_1_1?__mk_zh_CN=%E4%BA%9A%E9%A9%AC%E9%80%8A%E7%BD%91%E7%AB%99&amp;amp;keywords=flask+web%E5%BC%80%E5%8F%91&amp;amp;qid=1573545635&amp;amp;sr=8-1&quot;&gt;狼书&lt;/a&gt;，一定知道我们在提交表单时，常常会附带上一个隐藏的 csrf 值，用来防止 CSRF 攻击。关于 CSRF 是什么这里就不过多介绍了，大家可以参阅&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0&quot;&gt;维基百科&lt;/a&gt;。那么我们来到前后端分离的世界，CSRF 应该如何做呢？因为是前后端分离，所以服务端产生的 CSRF 值并不能实时更新到页面上，页面的更新全都要依赖客户端去主动请求。那我是不是要每次渲染表单的时候，就去服务器取一次 CSRF token 呢？这未免太麻烦，我们完全可以减少请求的次数，请求一次，然后在客户端（浏览器）上存起来，要用的时候带上即可。&lt;/p&gt;
&lt;p&gt;在 Flask 中引入 CSRF 保护主要是用 Flask-WTF 这个扩展，但既然我们不用 WTF 去渲染表单了，那么表单的 CSRF 保护也用不上了，所幸，这个扩展还提供了一个全局 CSRF 保护方法，就是所有 view 都可以通过一个模板变量去获取 CSRF token 的值，并不仅限于表单。开启方法也很简单：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from flask_wtf.csrf import CSRFProtect

csrf = CSRFProtect(app)
# 或者使用工厂函数模式：
csrf = CSRFProtect()
def create_app():
    app = Flask(__name__)
    ...
    csrf.init_app(app)
    return app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样在模板中，可以通过&lt;code&gt;{{ csrf_token() }}&lt;/code&gt;获得 CSRF token 的值。推荐放在返回的前端页面&lt;code&gt;index.html&lt;/code&gt;的 meta 标签中，以供 ajax 方法获取&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
&amp;lt;header&amp;gt;
  &amp;lt;meta name=&quot;csrf-token&quot; content=&quot;{{ csrf_token() }}&quot; /&amp;gt;
  ...
&amp;lt;/header&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 ajax 请求中，取出这个值然后带上即可，这里展示一下如何用&lt;code&gt;axios&lt;/code&gt;实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const api = axios.create({
  headers: {
    &quot;Content-Type&quot;: &quot;application/json&quot;,
    &quot;X-CSRF-TOKEN&quot;: document
      .querySelector(&apos;meta[name=&quot;csrf-token&quot;]&apos;)
      .getAttribute(&quot;content&quot;),
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这也是我这个 todo 项目采用的方法，但这种方法有一个很大的限制：前端页面必须至少由 Flask 应用渲染一次，这只能叫做半个前后端分离。实际开发中，前端和后端可能完全是分离部署，通过 nginx 等其他 web 服务器返回的。这样一来，&lt;code&gt;{{ csrf_token() }}&lt;/code&gt;就完全没机会透给前端。不要紧，我们还可以用 Cookies 嘛。当然，这需要自己定制一下&lt;code&gt;Flask-WTF&lt;/code&gt;这个扩展，可以查看这个&lt;a href=&quot;https://gist.github.com/frostming/dbb514c8ae1e9363039e9df537988812&quot;&gt;代码示例&lt;/a&gt;。在 Django 中，默认采用的就是这种方式。&lt;/p&gt;
&lt;h2&gt;后端鉴权&lt;/h2&gt;
&lt;p&gt;好了，我们又用到了 Cookie，如果有人对上一篇还有印象的话（并没有），用户的登录态也是放在 cookie 里面的，这种方案对于一般的普通应用就足够了，我一直提倡如果某种方法够用，就不用急着使用更高级的方法。但当某些客户端不支持 cookie 的时候（比如手机 app），我们就需要新的方法了。&lt;/p&gt;
&lt;p&gt;当然，这个解决方案现在也很成熟了，就是&lt;a href=&quot;https://en.wikipedia.org/wiki/JSON_Web_Token&quot;&gt;JWT(JSON Web Token)&lt;/a&gt;。大概流程是，第一次打开页面时，请求后端，如果没登录，则返回 401 让前端跳转登录，如果是登录状态，则返还一个 Token，这个 token 自带某些用户信息，和过期时间。前端收到这个 token 则自己保存起来，保存方式可以是 cookie，也可以是 localstorage，然后后续的请求均带上这个 token，前后端之间仅仅依靠这个 token 鉴定身份，无需来回传送 cookie 或会话信息。
&lt;img src=&quot;//webp.frostming.com/images/2019-11-jwt.jpg&quot; alt=&quot;jwt.jpg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;JWT 的好处是服务端无需保存这个 token 值，token 本身就带有是否有效的信息，以及登录态的关键信息（比如 user id），而 token 是通过服务端密钥加密的，所以难以被破解。Flask 内置了一个&lt;code&gt;itsdangerous&lt;/code&gt;的库来生成这种 token，先总结一下，Flask 要做的事有：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每次请求都校验这个 token 值，若不通过则返回 401&lt;/li&gt;
&lt;li&gt;login 端点生成 token 值&lt;/li&gt;
&lt;li&gt;logout 端点清除 token 值&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@app.before_request
def validate_request():
    token = request.headers.get(&apos;X-Token&apos;)
    if not token:
        abort(401)
    user = User.verify_token(token)
    if not user:
        abort(401)
    g.current_user = user

@api.route(&apos;/user/login&apos;, methods=[&apos;POST&apos;])
def login():
    data = request.get_json()
    if not verify_auth(data.get(&apos;username&apos;), data.get(&apos;password&apos;)):
        return jsonify(
            {&apos;code&apos;: 60204, &apos;message&apos;: &apos;Account and password are incorrect.&apos;}
        )
    return jsonify({&apos;code&apos;: 20000, &apos;data&apos;: {&apos;token&apos;: g.user.generate_token().decode()}})
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;from itsdangerous import (
    TimedJSONWebSignatureSerializer as Serializer,
    BadSignature,
    SignatureExpired,
)

class User(db.Model):
    ...
    @classmethod
    def verify_auth_token(cls, token):
        s = Serializer(current_app.config[&quot;SECRET_KEY&quot;])
        try:
            data = s.loads(token)
        except (BadSignature, SignatureExpired):
            return None
        user = cls.query.get(data[&quot;id&quot;])
        return user

    def generate_token(self, expiration=24 * 60 * 60):
        s = Serializer(current_app.config[&quot;SECRET_KEY&quot;], expires_in=expiration)
        return s.dumps({&quot;id&quot;: self.id})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而前端请求 ajax 时，只需要把这个事先保存好的 token 值取出来加到请求头部&lt;code&gt;X-Token&lt;/code&gt;就可以了。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;好了，我想这三篇文章已经覆盖了前后端分离与传统 MVC 架构的主要区别和开发技巧，当然还有更多的点我没法覆盖到，欢迎到评论区或邮件骚扰我。&lt;/p&gt;
</content:encoded></item><item><title>让你的Django应用变DRY的几个最佳实践</title><link>https://frostming.com/posts/2019/10-24/django-dry/</link><guid isPermaLink="false">https://frostming.com/2019/10-24/django-dry/</guid><description>Django+Django REST Framework下的DRY实践</description><pubDate>Thu, 24 Oct 2019 13:21:16 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;目前在 Python 的 Web 框架中被应用最广泛的就是 Django 和 Django REST Framework. 这两种框架都提供了非常健壮的功能，能满足 Web 开发的各个方面。DRY 是 Don&apos;t-Repeat-Yourself 的缩写，是一种代码编写的原则，即不要重复自己的工作。我个人有些代码洁癖，凡是发现我需要复制粘贴代码的地方，就想着能怎样去除重复的工作。在日常的开发中也总结出了一些个人的实践，分享给大家。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;总的来说，要使得你的应用很 DRY，要遵循以下两个原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全局都应用的变更，收拢到一个地方配置&lt;/li&gt;
&lt;li&gt;有少数与其他不一样的行为，将多数行为定义为全局行为，将少数行为分别配置，并尽可能简化配置方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Django 和 Django REST framework（后简称 DRF）提供了海量的全局配置、局部配置，来实现上述思想，但配置项太多了，有时人们往往不知道该如何利用。&lt;/p&gt;
&lt;h2&gt;一、用户鉴权&lt;/h2&gt;
&lt;h3&gt;1. Django 的配置&lt;code&gt;AUTHENTICATION_BACKENDS&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AUTHENTICATION_BACKENDS&lt;/code&gt;控制了应用根据传入的参数校验用户是否属于合法用户（用户名是否存在？密码是否正确？）。使用时通过&lt;code&gt;django.contrib.auth.authenticate&lt;/code&gt;函数，传入想要的参数，该函数会自动选择对应的后端进行用户校验，常用的校验方式有数据库校验、配置文件校验、LDAP 校验等等。如果你想接入第三方登录，OAuth 登录，都应该自定义一个 Backend，无需继承任何基类，只需实现一个 authenticate 方法，该方法参数与&lt;code&gt;django.contrib.auth.authenticate&lt;/code&gt;的传入参数相同，返回一个用户对象，然后将这个 Backend 添加到&lt;code&gt;AUTHENTICATION_BACKENDS&lt;/code&gt;就可以了。&lt;/p&gt;
&lt;p&gt;**注意：**在使用到用户模型的时候，要使用&lt;code&gt;django.contrib.auth.get_user_model()&lt;/code&gt;而不是导入具体的 model 类，这样可以方便用&lt;code&gt;AUTH_USER_MODEL&lt;/code&gt;配置去改变用户模型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class PowerOAuthBackend:
    &quot;&quot;&quot;请求Power单点登录后跳转的验证&quot;&quot;&quot;

    def authenticate(self, request, user=None, password=None):
        if check_user_password(user, password):
            # 返回用户对象
            return get_user_model().get(username=user)
        else:
            # 用户名密码错误 403
            raise PermissionDenied()

    def get_user(self, user_id):
        # 若通过浏览器访问则需要定义次方法，获取已登录的用户对象
        # 若只有RESTful调用则跳过
        return get_user_model().objects.get(staff_id=user_id)

# 登录
def login_view(request):
    username = request.POST.get(&apos;user&apos;)
    password = request.POST.get(&apos;password&apos;)
    user = authenticate(user=username, password=password)
    # 将用户存入会话
    login(request, user)
    return redirect(&apos;/&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. DRF 的配置 &lt;code&gt;DEFAULT_AUTHENTICATION_CLASSES&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;DEFAULT_AUTHENTICATION_CLASSES&lt;/code&gt;，以及针对每个&lt;code&gt;APIView&lt;/code&gt;配置的&lt;code&gt;authentication_classes&lt;/code&gt;，是对 RESTful 请求的身份验证，通过分析请求带的身份信息判断来源方的身份，一般有以下几种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;会话鉴权（登录态）&lt;/li&gt;
&lt;li&gt;BasicAuth 鉴权&lt;/li&gt;
&lt;li&gt;Token 鉴权&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些类都包含在&lt;code&gt;rest_framework.authentication&lt;/code&gt;模块中。如果你要通过智能网关转发后端请求，则需要写一个 Authentication 类，继承自&lt;code&gt;rest_framework.authentication.BaseAuthentication&lt;/code&gt;类，其中有两个比较重要的方法，函数签名及说明如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyAuthentication(BaseAuthentication):
    def authenticate(self, request):
        # 若鉴权成功，则返回一个(user, auth)的元组
        return user, auth
        # 否则，若想交给后面的authentication处理，则返回None
        return None
        # 否则抛出401错误
        raise rest_framework.exceptions.AuthenticationFailed()

    def authenticate_header(self, request):
        # DRF会选择第一顺位的Authentication的此方法返回的结果作为WWW-Authentication头
        # 如果返回为空则会将401错误转换成403错误
        return &apos;OMS&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. DRF 的&lt;code&gt;DEFAULT_PERMISSION_CLASSES&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;如果说 Authentication 是判断「你是谁」，那么 Authorization 就是判断「你能做什么」，就好比你进入公司大楼需要用工卡（Authentication），但你有了工卡也不能随便去总裁办公室（Authorization）。在 DRF 中完成 Authorization 工作的就是&lt;code&gt;DEFAULT_PERMISSION_CLASSES&lt;/code&gt;配置项，以及针对每个&lt;code&gt;APIView&lt;/code&gt;配置的&lt;code&gt;permission_classes&lt;/code&gt;，他是用来精确控制请求放对某一资源有无权限。在 RESTful 规范中，无鉴权信息是 401 错误而无权限是 403 错误。在&lt;a href=&quot;https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/&quot;&gt;DRF 的官方文档&lt;/a&gt;中有详细例子这里就不再赘述。&lt;/p&gt;
&lt;h2&gt;二、自定义响应体&lt;/h2&gt;
&lt;p&gt;很多时候（如前端框架、开发 SDK）对响应体的格式是有要求的，我看到大多数的实现只是用一个格式化的类去填充响应信息，但这种方法有两个缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每次需要人为构造响应&lt;/li&gt;
&lt;li&gt;无法适用于 DRF 的&lt;code&gt;ModelViewSet&lt;/code&gt;，因为它自带的方法的响应是默认的，如果要挨个重载就无法利用到&lt;code&gt;ModelViewSet&lt;/code&gt;的懒人特性&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以我们需要将这种格式自定义收拢到一处，做到使用时无感知，响应自动形成期望的格式。要达成这种效果，大致有两种途径：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;写自定义中间件，修改响应格式&lt;/li&gt;
&lt;li&gt;写自定义 renderer&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里第一种途径有几处劣势：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在中间件处理时&lt;code&gt;rest_framework.response.Response&lt;/code&gt;已完成渲染，修改内部数据不起作用&lt;/li&gt;
&lt;li&gt;若重新构造一个&lt;code&gt;rest_framework.response.Response&lt;/code&gt;则会报未渲染错误，而渲染过程比较复杂&lt;/li&gt;
&lt;li&gt;若选择用&lt;code&gt;django.http.response.JSONResponse&lt;/code&gt;重新构造响应则放弃了 DRF 的自动渲染特性&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我对这些缺陷不能忍，于是想到了第二种途径，也就是自定义 renderer，它有以下好处：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;即可全局生效（&lt;code&gt;DEFAULT_RENDERER_CLASSES&lt;/code&gt;），又可针对单个&lt;code&gt;APIView&lt;/code&gt;生效，非常灵活&lt;/li&gt;
&lt;li&gt;保留了 DRF 的智能渲染特性，即浏览器请求渲染 HTML 页面，后端请求渲染 JSON 响应&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;DRF 的默认 renderer 有两个：&lt;code&gt;rest_framework.renderers.JSONRenderer&lt;/code&gt;和&lt;code&gt;rest_framework.renderers.BrowsableAPIRenderer&lt;/code&gt;。这里可以按需重载，如果浏览器和后端响应都需要，则都重载，如果只需要 JSON 响应，则重载第一个就可以了，这里两个类的重载点不一样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class JSONRenderer(renderers.JSONRenderer):

    def render(self, data, accepted_media_type=None, renderer_context=None):
        request = renderer_context[&apos;request&apos;]
        # 在此处修改data
        return super().render(data, accepted_media_type=accepted_media_type, renderer_context=renderer_context)

class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer):

    def get_content(self, renderer, data,
                    accepted_media_type, renderer_context):
        request = renderer_context[&apos;request&apos;]
        # 在此处修改data
        return super().get_content(renderer, data,
                                   accepted_media_type, renderer_context)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三、异常处理&lt;/h2&gt;
&lt;p&gt;我们经常会需要抛出异常，有些是主动抛出、有些是未捕获的异常，在这些情况下，我们都希望日志记录异常的堆栈信息，然后返回一个规范的响应（格式与上一节中一致），这样我们就需要更改异常处理。在 Django+DRF 中异常处理有两个重载点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;中间件中的&lt;code&gt;process_exception&lt;/code&gt;函数&lt;/li&gt;
&lt;li&gt;DRF 的&lt;code&gt;EXCEPTION_HANDLER&lt;/code&gt;配置&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;而其中&lt;code&gt;EXCEPTION_HANDLER&lt;/code&gt;的作用时间早于中间件，这就导致了有些 DRF 内置的异常，在到达中间件之前已经渲染为正常的响应了，这明显不是我们期望的效果，所以我们选择第二个重载点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def exception_handler(exc, context):
    # copy自DRF默认exception_handler
    if isinstance(exc, Http404):
        exc = exceptions.NotFound()
    elif isinstance(exc, PermissionDenied):
        exc = exceptions.PermissionDenied()

    if isinstance(exc, exceptions.APIException):
        # DRF内置异常
        headers = {}
        if getattr(exc, &apos;auth_header&apos;, None):
            headers[&apos;WWW-Authenticate&apos;] = exc.auth_header
        if getattr(exc, &apos;wait&apos;, None):
            headers[&apos;Retry-After&apos;] = &apos;%d&apos; % exc.wait

        if isinstance(exc.detail, (list, dict)):
            body = exc.detail
            message = str(exc)
        else:
            body = {}
            message = str(exc.detail)
        # copy结束
        # 组装响应体
        return Response({...})

    else:
        # 其他未捕获异常
        logger.error(traceback.format_exc())
        if not isinstance(exc, ApiError):
            exc = ApiError(str(exc))
        # 组装响应体
        return exc.as_response()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;美中不足的是有一大段的代码是从 DRF 默认的异常处理函数 copy 过来的，这是 DRF 为数不多的不合理设计，留了一个配置项供你改变默认行为，但却没有留出一个好的重载点。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;DRY 原则能使你的代码结构好、易维护、易扩展。在日常的开发中，要时刻反思自己的代码是否过于重复，可以精简。在 Python 中，可以说只要你想，一定能把多处一样的代码给抽取出来。只是有时候为了抽出这些代码，又产生了很多额外的代码，这是需要取舍的。相信本文中提到的三个大方向，能对你有所启发。&lt;/p&gt;
</content:encoded></item><item><title>外公的密室</title><link>https://frostming.com/posts/2019/grandpa/</link><guid isPermaLink="false">https://frostming.com/2019/grandpa/</guid><pubDate>Mon, 09 Sep 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;小时候，我有一大半的休闲时间，都是在外公家度过的。二层的小楼，红色的砖墙。阳光透过法桐的巨大树叶，洒在葡萄藤上，孕育着一颗颗碧绿的葡萄。一只聒噪的蝉，不知趴在哪里吱呀个不停。老旧的吊扇转得不快，发出阵阵嗡嗡的声音。&lt;/p&gt;
&lt;p&gt;外公的房间就在东头，二层的光线不算明亮，白墙下刷着绿漆，房间里弥漫着一种神秘的香味。那是一种收藏的气味，通常来自于一些压箱底的物事。是了，是进门右手边那个蓝色的塑料桶。这个桶是小孩们的最爱，里面经常放着一些常温经放的食物。花生瓜子，饼干辣椒饼，啧啧，我没少偷吃。&lt;/p&gt;
&lt;p&gt;外公的书桌上，是他最喜欢的一副象棋，没听京剧的时候，我经常见他一人在那里自弈。我在桌上还搜到过一本棋谱，竟也研读了一番，学会了炮二平五车九进三这种表示法，可外公仿佛很紧张我摆出象棋来，生怕丢掉一颗。可能我到现在不会下棋，都得怪他。书桌的抽屉里，还有一些剃须刀保健球之类的玩意。&lt;/p&gt;
&lt;p&gt;除了象棋外公最大的爱好就是京剧，他常往来于老年宫，和票友交流学习，房间里还有一把他的京胡。再仔细找找，我甚至还找到了一个唱片机和几张唱片，都是京剧的。京剧的那种咿咿呀呀的唱腔，最配唱片机那羸弱的，附带电流杂音的音色。&lt;/p&gt;
&lt;p&gt;坏了，被大人发现了，我只好悻悻的出来。但下次我又进去了，翻看着那些早就不新奇的东西。&lt;/p&gt;
&lt;p&gt;外公是一个老干部，也很固执，但和傅明老人的张扬不同，他不稀的和我们争。我们要看动画片不要看新闻，他只是不做一声地到他的房间里，那里有个几年前换下来的小电视。&lt;/p&gt;
&lt;p&gt;后来我上大学了，就很少去他那里了，例行的家庭聚会，他话也不多，只是知道我去了中科院读研，就一遍遍敦促我要当院士。我不好告诉他我们所千号人，也就俩院士，太难熬了，况且我也不是搞科研的料。只是维诺地说好好好，一定一定。&lt;/p&gt;
&lt;p&gt;我外公陈家，只要人聚齐了，就要在一块吃饭，并对此非常热衷。外公是个有口福的人，我很羡慕他八十多岁了，还能嚼排骨嚼出声，而且家乡菜还重味重辣。&lt;/p&gt;
&lt;p&gt;可是他走了，最后的日子里我没在家。得知他生病有一段时间了，可爸妈也一直没让我回来。本想等中秋再回去，可他终究没等到中秋。人到三十，终归要渐尝人生的苦涩，我有心理准备，但仍不免悲伤。今天也正好是我宝宝满一百天的日子，一代一代的亲情在传承，我看到宝宝天使的笑容，发觉我也会在某一天离她而去，而留下的仍需带上遗憾和前人的寄托生活下去。&lt;/p&gt;
&lt;p&gt;也该回去看看外公的密室，有没有什么新鲜的东西了。&lt;/p&gt;
&lt;p&gt;—— 2019.9.9 于北上的列车上&lt;/p&gt;
</content:encoded></item><item><title>Pip trusted_host问题记录</title><link>https://frostming.com/posts/2019/09-03/pip-trusted-host/</link><guid isPermaLink="false">https://frostming.com/2019/09-03/pip-trusted-host/</guid><pubDate>Tue, 03 Sep 2019 05:28:35 GMT</pubDate><content:encoded>&lt;h2&gt;问题定位&lt;/h2&gt;
&lt;p&gt;一日我在 Pipenv 上收到一个&lt;a href=&quot;https://github.com/pypa/pipenv/issues/3841&quot;&gt;issue&lt;/a&gt;: 用户说 Pipenv 执行的 pip 命令中&lt;code&gt;--trusted-host&lt;/code&gt;缺少了 port 部分。然后我去扒源码，结果发现有两处同样的函数：&lt;a href=&quot;https://github.com/pypa/pipenv/blob/3b9b7172293169ad5ce0b7be77e6f27e3dbcde7b/pipenv/utils.py#L266&quot;&gt;[1]&lt;/a&gt;&lt;a href=&quot;https://github.com/pypa/pipenv/blob/3b9b7172293169ad5ce0b7be77e6f27e3dbcde7b/pipenv/vendor/requirementslib/utils.py#L283&quot;&gt;[2]&lt;/a&gt;逻辑不一致。顿时感觉事情没那么简单。于是我本地搞了一个pypi server, 并用自签名支持了 https，然后用 pip 测试[^1]：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ pip install -i https://localtest.me:5001 urllib3 --trusted-host localtest.me:5001
Successful

$ pip install -i https://localtest.me:5001 urllib3 --trusted-host localtest.me
Looking in indexes: https://localtest.me:5001
Collecting urllib3
  Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by &apos;SSLError(SSLCertVerificationError(1, &apos;[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1076)&apos;))&apos;: /urllib3/
  ...
  Failed

$ pip install -i http://localtest.me:5000 urllib3 --trusted-host localtest.me:5000
Looking in indexes: http://localtest.me:5000
Collecting urllib3
  The repository located at localtest.me is not a trusted or secure host and is being ignored. If this repository is available via HTTPS we recommend you use HTTPS instead, otherwise you may silence this warning and allow it anyway with &apos;--trusted-host localtest.me&apos;.
  Could not find a version that satisfies the requirement urllib3 (from versions: )
No matching distribution found for urllib3

$ pip install -i http://localtest.me:5000 urllib3 --trusted-host localtest.me
Successful
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;惊呆，HTTPS 和 HTTP 针对&lt;code&gt;trusted-host&lt;/code&gt;带不带 port 的处理方式不一样：HTTPS 希望你带 port，而 HTTP 不需要带 port。这显然是不合理的，于是我去看 pip 的源码关于这块的处理逻辑。以下基于 pip 19.2.3 的源码。
&lt;code&gt;src/pip/_internal/download.py&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
    insecure_adapter = InsecureHTTPAdapter(max_retries=retries)
    # Save this for later use in add_insecure_host().
    self._insecure_adapter = insecure_adapter

    self.mount(&quot;https://&quot;, secure_adapter)
    self.mount(&quot;http://&quot;, insecure_adapter)

    # Enable file:// urls
    self.mount(&quot;file://&quot;, LocalFSAdapter())

    # We want to use a non-validating adapter for any requests which are
    # deemed insecure.
    for host in insecure_hosts:
        self.add_insecure_host(host)

def add_insecure_host(self, host):
    # type: (str) -&amp;gt; None
    self.mount(&apos;https://{}/&apos;.format(host), self._insecure_adapter)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;insecure_adapter&lt;/code&gt;是不检查证书的，&lt;code&gt;secure_adapter&lt;/code&gt;是检查证书的，可以看到在&lt;code&gt;add_insecure_host()&lt;/code&gt;这个函数中，是把传进来的 host 加上末尾的&lt;code&gt;/&lt;/code&gt;拼成一个 URL 来新增一个 adapter 端点的，而在 requests 中，多个 adapter 端点是依靠&lt;code&gt;startswith&lt;/code&gt;来识别是否匹配的。所以如果&lt;code&gt;trusted-host&lt;/code&gt;是&lt;code&gt;example.org&lt;/code&gt;，则只有&lt;code&gt;https://example.org/&lt;/code&gt;会被识别为信任的站点而&lt;code&gt;https://example.org:8080/&lt;/code&gt;不会。&lt;/p&gt;
&lt;p&gt;以上是仅针对 HTTPS 而言，HTTP 是无需证书检查的，它相关的逻辑在&lt;/p&gt;
&lt;p&gt;&lt;code&gt;src/pip/_internal/index.py&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def _validate_secure_origin(self, logger, location):
    # type: (Logger, Link) -&amp;gt; bool
    # Determine if this url used a secure transport mechanism
    parsed = urllib_parse.urlparse(str(location))
    origin = (parsed.scheme, parsed.hostname, parsed.port)

    # The protocol to use to see if the protocol matches.
    # Don&apos;t count the repository type as part of the protocol: in
    # cases such as &quot;git+ssh&quot;, only use &quot;ssh&quot;. (I.e., Only verify against
    # the last scheme.)
    protocol = origin[0].rsplit(&apos;+&apos;, 1)[-1]

    # Determine if our origin is a secure origin by looking through our
    # hardcoded list of secure origins, as well as any additional ones
    # configured on this PackageFinder instance.
    for secure_origin in self.iter_secure_origins():
        if protocol != secure_origin[0] and secure_origin[0] != &quot;*&quot;:
            continue

        try:
            # We need to do this decode dance to ensure that we have a
            # unicode object, even on Python 2.x.
            addr = ipaddress.ip_address(
                origin[1]
                if (
                    isinstance(origin[1], six.text_type) or
                    origin[1] is None
                )
                else origin[1].decode(&quot;utf8&quot;)
            )
            network = ipaddress.ip_network(
                secure_origin[1]
                if isinstance(secure_origin[1], six.text_type)
                # setting secure_origin[1] to proper Union[bytes, str]
                # creates problems in other places
                else secure_origin[1].decode(&quot;utf8&quot;)  # type: ignore
            )
        except ValueError:
            # We don&apos;t have both a valid address or a valid network, so
            # we&apos;ll check this origin against hostnames.
            if (origin[1] and
                    origin[1].lower() != secure_origin[1].lower() and
                    secure_origin[1] != &quot;*&quot;):
                continue
        else:
            # We have a valid address and network, so see if the address
            # is contained within the network.
            if addr not in network:
                continue

        # Check to see if the port patches
        if (origin[2] != secure_origin[2] and
                secure_origin[2] != &quot;*&quot; and
                secure_origin[2] is not None):
            continue

        # If we&apos;ve gotten here, then this origin matches the current
        # secure origin and we should return True
        return True

    # If we&apos;ve gotten to this point, then the origin isn&apos;t secure and we
    # will not accept it as a valid location to search. We will however
    # log a warning that we are ignoring it.
    logger.warning(
        &quot;The repository located at %s is not a trusted or secure host and &quot;
        &quot;is being ignored. If this repository is available via HTTPS we &quot;
        &quot;recommend you use HTTPS instead, otherwise you may silence &quot;
        &quot;this warning and allow it anyway with &apos;--trusted-host %s&apos;.&quot;,
        parsed.hostname,
        parsed.hostname,
    )

    return False
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看上去是分别匹配 scheme, hostname 和 port，没什么问题。问题在于&lt;code&gt;self.iter_secure_origins()&lt;/code&gt;这里产生的值，在同一个文件中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def iter_secure_origins(self):
    # type: () -&amp;gt; Iterator[SecureOrigin]
    for secure_origin in SECURE_ORIGINS:
        yield secure_origin
    for host in self.trusted_hosts:
        yield (&apos;*&apos;, host, &apos;*&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里没做任何处理，就把&lt;code&gt;trusted-host&lt;/code&gt;当做 hostname 丢出来了，看来这里压根没考虑过&lt;code&gt;trusted-host&lt;/code&gt;带 port 的需求。&lt;/p&gt;
&lt;h2&gt;问题修复&lt;/h2&gt;
&lt;p&gt;找到了问题所在，总结一下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTPS 需要带 port 是因为&lt;code&gt;requests.Session&lt;/code&gt;的&lt;code&gt;mount&lt;/code&gt;是依靠前缀匹配来获取对应的适配器（adapter）的，并且末尾会加上一个&lt;code&gt;/&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;HTTP 需要不带 port 是因为检查是否安全 URL 的时候，是拿目标 URL 的 hostname（不带 port）去匹配&lt;code&gt;trusted-host&lt;/code&gt;的值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以对应的修复方法就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;添加信任的端点时，如果&lt;code&gt;trusted-host&lt;/code&gt;不带 port，则需要把&lt;code&gt;https://hostname:&lt;/code&gt;也添加为无需安全检查的端点（利用前缀匹配的特性）。&lt;/li&gt;
&lt;li&gt;生成&lt;code&gt;secure_origin&lt;/code&gt;时解析传入的&lt;code&gt;trusted-host&lt;/code&gt;值，分成 hostname 与 port 部分分别匹配。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体代码可以看&lt;a href=&quot;https://github.com/pypa/pip/pull/6909&quot;&gt;我提交的 PR&lt;/a&gt;，这个 PR 已经被 merge，预计可以在下个版本中发布。&lt;/p&gt;
&lt;p&gt;[^1]: 这里我用了一个 trick，使用了&lt;a href=&quot;http://localtest.me&quot;&gt;localtest.me&lt;/a&gt;转发 localhost 的请求，因为 localhost 是永远被信任的地址，trusted-host 不起作用。&lt;/p&gt;
</content:encoded></item><item><title>Pipenv有什么问题</title><link>https://frostming.com/posts/2019/09-01/pipenv-problems/</link><guid isPermaLink="false">https://frostming.com/2019/09-01/pipenv-problems/</guid><description>又一篇Pipenv文章</description><pubDate>Sun, 01 Sep 2019 08:36:32 GMT</pubDate><content:encoded>&lt;p&gt;这不是我第一次写 Pipenv 相关的文章，也相信不是最后一次，前两篇我用的是英文，（浅陋地）分析了 Pipenv 和 Poetry 的优劣，至今仍是我博客访问量最高的文章。今天是因为在知乎上看到两位朋友写的两篇文章（链接我放在文末了），吐槽了一通以后推荐大家不要使用 Pipenv。说实话，作为核心维护者之一我是有点心酸的，因为他们说的那些问题的确都存在。在本文中我希望从一个核心维护者的角度，总结一下 Pipenv 存在的问题，作为一个告解。&lt;/p&gt;
&lt;p&gt;从我关注 Issues 列表以来，我脑中能回想起来的，抱怨频率最高的，也是最影响用户体验的，有几个问题：&lt;/p&gt;
&lt;h2&gt;1. Lock 时间长&lt;/h2&gt;
&lt;p&gt;用户经常抱怨&lt;code&gt;pipenv lock&lt;/code&gt;的时间长，特别是涉及到一些科学计算的库时，如&lt;code&gt;numpy&lt;/code&gt;, &lt;code&gt;sklearn&lt;/code&gt;, &lt;code&gt;tensorflow&lt;/code&gt;，会慢得让你怀疑人生。&lt;code&gt;pipenv lock&lt;/code&gt;其实做的就是依赖解析，而慢的原因是，Pipenv 需要下载所有的安装包来计算它们的哈希值，要命的是，像&lt;code&gt;numpy&lt;/code&gt;这种库，一个版本就有&lt;a href=&quot;https://pypi.org/project/numpy/#files&quot;&gt;17 个包&lt;/a&gt;，每个包的大小是 10M~20M 不等，总共下载的大小就有 300 多 M 左右。也有人提 PR 希望修改这个逻辑，但后来都不了了之。&lt;/p&gt;
&lt;p&gt;Issue 传送门：https://github.com/pypa/pipenv/issues/3827&lt;/p&gt;
&lt;p&gt;PR 传送门：https://github.com/pypa/pipenv/pull/3830&lt;/p&gt;
&lt;h2&gt;2. 命令及选项的结果不符合预期&lt;/h2&gt;
&lt;p&gt;李辉老师的文章里面列举了安装、卸载、更新包的问题，我这里先回复一下，其实它们都是同一个问题：&lt;code&gt;pipenv update&lt;/code&gt;不能保护其他包不被更新。并且&lt;code&gt;--keep-outdated&lt;/code&gt;和&lt;code&gt;--selective-upgrade&lt;/code&gt;这两个选项意义不明容易让用户搞错。&lt;/p&gt;
&lt;p&gt;其实&lt;code&gt;--keep-outdated&lt;/code&gt;有一次&lt;a href=&quot;https://github.com/pypa/pipenv/pull/3768&quot;&gt;大修复&lt;/a&gt;，&lt;strong&gt;只是还没有发布到新版本&lt;/strong&gt;，所以用 github 上的 master 分支是没问题的。我在这里解释一下&lt;code&gt;--keep-outdated&lt;/code&gt;和&lt;code&gt;--selective-update&lt;/code&gt;这两个选项的作用：&lt;code&gt;--keep-outdated&lt;/code&gt;意思是更新 Pipfile.lock 时，不会删除&lt;strong&gt;已经不需要&lt;/strong&gt;的依赖。这对于产生一个跨平台的 lock 文件非常有用，因为有些仅 Windows 需要的依赖，你在 Windows 上生成 Pipfile.lock 时会有，而换到 Linux 上再执行&lt;code&gt;pipenv lock&lt;/code&gt;时就没有了。这个选项时针对 Pipfile.lock 更新的，而&lt;code&gt;--selective-upgrade&lt;/code&gt;是针对安装过程的，它会控制 pip 安装包时，只在有必要的时候升级次级依赖的版本。这里又涉及到一个逻辑的不统一：用&lt;code&gt;pipenv install xxx&lt;/code&gt;安装包的时候会先调用&lt;code&gt;pip install xxx&lt;/code&gt;，并用 pip 的机制去更新依赖，再用 Pipenv lock 去锁定依赖。理想情况下，依赖解析器应该唯一，应该通过 Pipenv 解析完了以后再统一安装。&lt;/p&gt;
&lt;p&gt;除此之外，其他的一些不符合预期的命令和混乱的选项有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pipenv install&lt;/code&gt;有&lt;code&gt;--skip-lock&lt;/code&gt;, &lt;code&gt;--ignore-pipfile&lt;/code&gt;, &lt;code&gt;--deploy&lt;/code&gt;，此外还有不更新 Pipfile.lock 的&lt;code&gt;pipenv sync&lt;/code&gt;命令，有谁能一眼区分出它们各自的作用？&lt;/li&gt;
&lt;li&gt;安装普通依赖用&lt;code&gt;pipenv install&lt;/code&gt;，安装普通和开发依赖用&lt;code&gt;pipenv install --dev&lt;/code&gt;，但&lt;code&gt;pipenv lock&lt;/code&gt;永远一起解析普通和开发依赖，有没有&lt;code&gt;--dev&lt;/code&gt;都一样。然而&lt;code&gt;pipenv lock -r&lt;/code&gt;是生成普通依赖，&lt;code&gt;pipenv lock -r --dev&lt;/code&gt;是仅生成开发依赖。&lt;/li&gt;
&lt;li&gt;接上一条，&lt;code&gt;pipenv uninstall --all&lt;/code&gt;是删除当前虚拟环境中所有已安装的包，&lt;strong&gt;不更新 Pipfile&lt;/strong&gt;，而&lt;code&gt;pipenv uninstall --all-dev&lt;/code&gt;是删除所有开发的依赖，&lt;strong&gt;更新 Pipfile&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我说的这些问题，都对应着许多 Issue，我就(lan)不(de)一一列举了。&lt;/p&gt;
&lt;h2&gt;3. 无法解析依赖&lt;/h2&gt;
&lt;p&gt;这一点也是在&lt;a href=&quot;https://link.zhihu.com/?target=https://github.com/sdispater/poetry#dependency-resolution&quot;&gt;Poetry 的文档中&lt;/a&gt;作为反面教材抨击的，其根本原因是，Pipenv 不能自动回溯依赖的版本来满足依赖的限制。比方说 A 包依赖 &lt;code&gt;C&amp;lt;1.0&lt;/code&gt;，而 B 包的 1.x 版本依赖 &lt;code&gt;C&amp;lt;1.0&lt;/code&gt; 而 2.0 版本依赖 &lt;code&gt;C&amp;gt;=1.0&lt;/code&gt;，那么你在 Pipfile 中同时包含 A, B 时就会解析失败：Pipenv 只会选用 B 的最新版本，在依赖不能满足时不会尝试旧版本。&lt;/p&gt;
&lt;p&gt;Pipenv 解析依赖其实用的是&lt;a href=&quot;https://github.com/jazzband/pip-tools/&quot;&gt;piptools&lt;/a&gt;，后者不能解析的 Pipenv 也不能。好消息是 Pipenv 维护小组做了一个新的依赖解析器&lt;a href=&quot;https://github.com/sarugaku/passa&quot;&gt;passa&lt;/a&gt;，还在试验阶段，它能解决这个问题，未来会替代成为 Pipenv 的依赖解析器。&lt;/p&gt;
&lt;h2&gt;4. 怎么还不 release，以及其他的开发流程的问题&lt;/h2&gt;
&lt;p&gt;董伟明老师说到 BDFL，没错，Kenneth Reitz 曾经设计出了一套 PEEP 的流程，并把自己放在 BDFL 的位置上。但是，由于他本人对开源热情的消退，他已经实际上&lt;a href=&quot;https://github.com/pypa/pipenv/blob/master/peeps/PEEP-003.md&quot;&gt;退出了这个位置&lt;/a&gt;。但是 PEEP 的机制仍然存在：所有 Behavior change 必须先落地到 PEEP 文档上，其实 PEEP 不难写，能说清楚就行了，但是从这个机制创立至今，仅有 5 个 PEEP 被接受且&lt;strong&gt;没有外部贡献者的 PEEP&lt;/strong&gt;，这其中，又仅有 2 个涉及机制变化的 PEEP 真正落地。&lt;/p&gt;
&lt;p&gt;其实 Pipenv 的问题数量不算多，维护者的人力对比 Poetry 也不见得少，关键问题就是上述的几个严重影响用户体验的问题，或者问题修复了却迟迟不发布新版。&lt;/p&gt;
&lt;p&gt;不止一个人，也不止一次有人抱怨发布无限延期的问题，就快变成陈永仁那个「说好了三年，三年之后又三年，如今都快十年了！」的梗了，我在这里也解释一下。现在核心维护者主要有 Dan Ryan(techalchemy), Tsuping Chong(uranusjr)和我，其中只有 Dan 有 PyPI 的权限，我其实说白了就是个「比较勤奋的 Contributor」的角色。而 Dan 因为私事缠身无暇顾及开源工作。但好消息是他自称事情已处理得差不多，会慢慢跟上进度。虽然我知道催促一个维护者在开源社区中不是一个礼貌的做法，但我也理解大家的心情，以及因此而心灰意冷弃用的用户，所以我恳请大家，宽容一些，静静等待吧。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么不开放权限给其他人？比如说我。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Dan 是一个严谨的人，他希望亲自过一遍改动日志，润色完了以后再发布，所以还需要等待一些日子。他也对新特性的态度非常保守，总是害怕影响 regression，破坏已有用户的体验。具体可以看看&lt;a href=&quot;https://github.com/pypa/pipenv/pull/3830&quot;&gt;这个 PR&lt;/a&gt;里面的回复，这一点我不能认同，但也无可指责，毕竟他承担了 Kenneth Reitz 以后 90%的开发工作，其中有些部分确实非常棘手和麻烦。&lt;/p&gt;
&lt;p&gt;作为维护者之一，我的想法是，因为 master 上已经积压了太多的改动，先等 Dan 回来把这次新版本发布了，回到正轨以后，我会开始针对以上我提到的问题，编写 PEEP，引入 Deprecation 机制，不去回避 Breaking change。&lt;/p&gt;
&lt;h2&gt;5. Poetry 如何呢&lt;/h2&gt;
&lt;p&gt;最后还是提一下 Poetry 吧。Python 的工作流工具，其实无非是解决三个方面的问题：虚拟环境管理、依赖管理、打包发布。Pipenv 只包含前两项，比重是 50%:50%，而 Poetry 同时包括三项，比重是 20%:40%:40%。所以当我用惯了 Pipenv 切换到 Poetry 时会非常不习惯——它对于虚拟环境的控制太弱了：我无法知道我用的是哪个环境，路径是什么，也不能随心所欲地删除、清理、指定虚拟环境的位置。Pipenv 的依赖解析器确实存在很多问题，但 Poetry 的也离完美有一段距离。而且 Poetry 负责的打包发布部分，也不是最好的。所以我认为 Poetry 也没有大家推荐的那么好。如果 Pipenv 没有满足你的要求，那么虚拟环境管理方面我推荐&lt;a href=&quot;https://virtualenvwrapper.readthedocs.io/&quot;&gt;virtualenvwrapper&lt;/a&gt;+&lt;a href=&quot;https://www.baidu.com/link?url=ZLnHDmLvp9jeNCDgIzlPNZUbONmmIC5VaeqUuHAiHWG&amp;amp;wd=&amp;amp;eqid=8c60d2c7001f275e000000065d6b811e&quot;&gt;direnv&lt;/a&gt;（这两个的最大问题是不支持 Windows)，依赖解析方面我推荐&lt;a href=&quot;https://github.com/jazzband/pip-tools/&quot;&gt;piptools&lt;/a&gt;，打包发布还是用 setuptools。&lt;/p&gt;
&lt;h2&gt;相关阅读&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/80478490&quot;&gt;李辉：不要用 Pipenv&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/80683249&quot;&gt;董伟明：也谈「不要用 Pipenv」&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://frostming.com/2018/05-15/pipenv-vs-poetry&quot;&gt;Python packaging war: Pipenv vs. Poetry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://frostming.com/2019/01-04/pipenv-poetry&quot;&gt;A deeper look into Pipenv and Poetry&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>使用Flask搭建个人博客</title><link>https://frostming.com/posts/2019/08-11/flask-blog/</link><guid isPermaLink="false">https://frostming.com/2019/08-11/flask-blog/</guid><pubDate>Sun, 11 Aug 2019 06:00:21 GMT</pubDate><content:encoded>&lt;p&gt;我的个人博客从 Hexo 迁移到自建主机，主要是为了能自由的增减特性，和随时随地的更新博客（然而并没有）。所以考虑用 Python 的 Web 框架来写，由于我最开始是从 Flask 入门的，对它的源码也最了解，所以就选择了 Flask。总的来说，一个个人博客网站，主要包含以下几个功能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;文章的保存和展示&lt;/li&gt;
&lt;li&gt;文章的分类和标签&lt;/li&gt;
&lt;li&gt;文章的评论管理&lt;/li&gt;
&lt;li&gt;对于动态博客来说，还有博客的后台部分&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其中第 4 部分已经有&lt;a href=&quot;/2019/04-24/new-admin&quot;&gt;单独的文章&lt;/a&gt;来介绍，使用的是前后端分离的方式访问 API。而第 3 部分我暂时打算用第三方的评论系统来管理（毕竟造个轮子也没有别人强大）。至于文章编写，我当然是选用 Markdown。&lt;/p&gt;
&lt;h2&gt;代码结构&lt;/h2&gt;
&lt;p&gt;使用 Flask 来写博客，首先要考虑的是项目结构——它不像 Django 一样，有固定的推荐结构，而是给了用户很大的自由空间来组织项目的代码，总的来说，有两大流派：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;按业务划分，有点类似于 Django APP 的组织方式，我们会有 post, auth, user, comment 等部分。&lt;/li&gt;
&lt;li&gt;按模块划分，分成操作数据库的 models 部分，渲染视图的 views 部分，处理模板的部分等等。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;由于去掉了评论系统以后，博客的功能还是比较简单的，就是文章、分类、标签的管理，所以我使用了第二种组织方式，下面是我的代码结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flaskblog
├── __init__.py
├── admin.py
├── api             # API路由
├── app.py          # app对象
├── babel.cfg
├── cli.py          # app命令行
├── config.py       # 配置
├── md              # markdown解析器
├── models.py       # 数据库模型
├── templates       # HTML模板
├── templating.py   # 模板处理函数
├── translations    # 翻译文件
├── utils.py        # 通用函数
└── views.py        # 视图函数
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Flask 扩展&lt;/h2&gt;
&lt;p&gt;用 Flask 来写 Web，最重要的是选用恰当合适的扩展。因为扩展质量良莠不齐，加上有些扩展很久不维护了，以往有很多其他文章中推荐的扩展，其实都不需要了（基于 Flask 1.0+版本），本着最小使用的原则，下面是我博客中用到的扩展：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Flask-Login 处理用户登录&lt;/li&gt;
&lt;li&gt;操作数据库的 ORM 和迁移必备组合 Flask-SQLAlchemy 和 Flask-Migrate&lt;/li&gt;
&lt;li&gt;Flask-Whooshee 搜索索引&lt;/li&gt;
&lt;li&gt;Flask-Moment 本地化时间（因为时间统一以 UTC 时间保存）&lt;/li&gt;
&lt;li&gt;Flask-Assets 处理静态文件&lt;/li&gt;
&lt;li&gt;Flask-Babel 国际化&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于后台部分是只有 API 的，而博客展示部分又没有表单，所以 Flask-WTF，Flask-Bootstrap 这些都不需要了，但 Flask-Login 还是要用来做后端用户态管理；Flask-Scripts 的功能已经&lt;a href=&quot;https://flask.palletsprojects.com/en/1.1.x/cli/&quot;&gt;内置到 Flask 中&lt;/a&gt;了，所以推荐大家都弃用掉这个扩展。Flask-Assets 主要用来 Minify CSS 和 JS 文件，它会自动在静态文件的 URL 中加上一个独特的后缀，这样不用更新静态文件后每次清除缓存。&lt;/p&gt;
&lt;h2&gt;Markdown 渲染&lt;/h2&gt;
&lt;p&gt;在 Python 的世界中已经有很多 Markdown 的解析器，但它们要么有时输出不符合预期（mistune），要么自己写起扩展功能来非常痛苦（python-markdown, python-markdown2），所以我一怒之下自己造了个轮子&lt;a href=&quot;https://github.com/frostming/marko&quot;&gt;Marko&lt;/a&gt;，它默认符合 CommonMark 规范且自带 GFM 支持，还内置提供三个常用的扩展：脚注、目录生成、及中英文之间插入空格，欢迎大家提 PR 实现更多扩展。在博客项目中，我又利用 Marko 的扩展机制进行了进一步的定制：图片排版功能。使用方法是将多个图片放在一起（不换行），将渲染为多列图片。例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![](//webp.frostming.com/images/image1.jpg) ![](//webp.frostming.com/images/image2.jpg)
![](//webp.frostming.com/images/image3.jpg) ![](//webp.frostming.com/images/image4.jpg)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;渲染效果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/frostming/Flog/raw/master/resources/sample_images.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;博客源码&lt;/h2&gt;
&lt;p&gt;更多实现细节可以参阅我已公开到 Github 上的&lt;a href=&quot;https://github.com/frostming/Flog&quot;&gt;源码&lt;/a&gt;。&lt;/p&gt;
</content:encoded></item><item><title>How does it work? -- threading.Condition</title><link>https://frostming.com/posts/2019/04-30/threading-condition/</link><guid isPermaLink="false">https://frostming.com/2019/04-30/threading-condition/</guid><description>HDIW系列文章之二</description><pubDate>Tue, 30 Apr 2019 10:03:07 GMT</pubDate><content:encoded>&lt;p&gt;继两年前的&lt;a href=&quot;https://frostming.com/2017/08-28/how-does-it-work-with-metaclass&quot;&gt;上一篇文章&lt;/a&gt;之后，不靠谱博主终于想起了&lt;em&gt;How does it work&lt;/em&gt;这个坑。主要是近期也没有遇到可值得分享的「精巧」的实现。之前其实也过了一遍&lt;code&gt;threading&lt;/code&gt;模块的源码，对里面的各种锁也只是有个大概印象，并且它们之前非常像，很容易让人 confusing。这次碰到实际需要，于是仔细看了一下源码，发现还是有很多搞头的。当然，你只是使用的话照着例子用就好了不会出错，但还是值得花点工夫弄清里面的原理。&lt;/p&gt;
&lt;h2&gt;Condition 的使用示例&lt;/h2&gt;
&lt;p&gt;下面是我随便从网上搜来的代码片段&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import threading, time
class Hider(threading.Thread):
    def __init__(self, cond, name):
        super(Hider, self).__init__()
        self.cond = cond
        self.name = name
    def run(self):
        time.sleep(1) #确保先运行Seeker中的方法
        self.cond.acquire() #b
        print self.name + &apos;: 我已经把眼睛蒙上了&apos;
        self.cond.notify()
        self.cond.wait() #c
                         #f
        print self.name + &apos;: 我找到你了 ~_~&apos;
        self.cond.notify()
        self.cond.release()
                            #g
        print self.name + &apos;: 我赢了&apos;   #h
class Seeker(threading.Thread):
    def __init__(self, cond, name):
        super(Seeker, self).__init__()
        self.cond = cond
        self.name = name
    def run(self):
        self.cond.acquire()
        self.cond.wait()    #a    #释放对琐的占用，同时线程挂起在这里，直到被notify并重新占有琐。
                            #d
        print self.name + &apos;: 我已经藏好了，你快来找我吧&apos;
        self.cond.notify()
        self.cond.wait()    #e
                            #h
        self.cond.release()
        print self.name + &apos;: 被你找到了，哎~~~&apos;
cond = threading.Condition()
seeker = Seeker(cond, &apos;seeker&apos;)
hider = Hider(cond, &apos;hider&apos;)
seeker.start()
hider.start()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里用的捉迷藏，换成聊天也可以。其实就是两个线程间的同步，一应一答。一个线程执行完操作以后通知另一方并等待应答。下面我们要解决一些问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;它跟&lt;code&gt;Lock&lt;/code&gt;有什么区别？&lt;/li&gt;
&lt;li&gt;可以注意到双方动作前都&lt;code&gt;acquire&lt;/code&gt;了同一个&lt;code&gt;Condition&lt;/code&gt;，这样不阻塞吗？&lt;/li&gt;
&lt;li&gt;为什么一定要&lt;code&gt;acquire&lt;/code&gt;？我换成获取一个普通的&lt;code&gt;Lock&lt;/code&gt;行吗？&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Condition 源码分析&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Condition&lt;/code&gt;的初始化方法为&lt;code&gt;Condition(lock)&lt;/code&gt;，其中&lt;code&gt;lock&lt;/code&gt;不传的话默认是一个&lt;code&gt;RLock()&lt;/code&gt;，即可重入锁，关于锁的区别比较好理解，这里就不啰嗦了。初始完之后会把传入的锁存为属性，然后&lt;code&gt;Condition&lt;/code&gt;的&lt;code&gt;acquire&lt;/code&gt;和&lt;code&gt;release&lt;/code&gt;就只是对这个锁的获取释放而已。所以：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lock = Lock()
cond = Condition(lock)
cond.acquire()
# 换成lock.acquire()完全等价，release类似
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到此为止还看不出为何要用&lt;code&gt;Condition&lt;/code&gt;而不用&lt;code&gt;Lock&lt;/code&gt;，关键是下面两个方法&lt;code&gt;wait&lt;/code&gt;和&lt;code&gt;notify&lt;/code&gt;，我把代码完整贴出附上自己的注释：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def wait(self, timeout=None):
    if not self._is_owned():    # 必须先获取self._lock
        raise RuntimeError(&quot;cannot wait on un-acquired lock&quot;)
    waiter = _allocate_lock()       # 新建一个锁
    waiter.acquire()    # 获取刚刚新建的锁
    self._waiters.append(waiter)    # 加入waiters列表
    saved_state = self._release_save()    # 这里释放了self._lock
    gotit = False
    try:    # restore state no matter what (e.g., KeyboardInterrupt)
        if timeout is None:
            waiter.acquire()    # 再次获取新建的锁
            gotit = True
        else:
            if timeout &amp;gt; 0:
                gotit = waiter.acquire(True, timeout)   # 等待时间后返回
            else:
                gotit = waiter.acquire(False)   # timeout == 0, 立即返回
        return gotit
    finally:
        self._acquire_restore(saved_state)    # 重新恢复self._lock状态
        if not gotit:   # 如果是非阻塞的(timeout != None)
            try:
                self._waiters.remove(waiter)    # 从waiters列表删除
            except ValueError:
                pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;果然 talk is cheap, show me the code，看了代码就一目了然了。可以看到&lt;code&gt;self._lock&lt;/code&gt;（就是初始化时传入的那个锁）在第 7 行之前是占用状态的，此时其他线程不可插入，然后整个 try-block 里&lt;code&gt;self._lock&lt;/code&gt;是释放状态可被其他线程获取。通过再次获取同一个 waiter 锁达到了阻塞的效果，这样看起来就像是新加入了一个等待者在等待某个事件。等待的这个事件，就是其他线程用同一个&lt;code&gt;Condition&lt;/code&gt;实例调用的&lt;code&gt;notify&lt;/code&gt;方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def notify(self, n=1):
    if not self._is_owned():
        raise RuntimeError(&quot;cannot notify on un-acquired lock&quot;)
    all_waiters = self._waiters   # 获取所有等待的锁
    waiters_to_notify = _deque(_islice(all_waiters, n))    # 只选给定数量的等待者，如果是notify_all方法则是全部
    if not waiters_to_notify:
        return
    for waiter in waiters_to_notify:
        waiter.release()    #
        try:
            all_waiters.remove(waiter)
        except ValueError:
            pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到&lt;code&gt;notify&lt;/code&gt;方法全程都拥有锁&lt;code&gt;self._lock&lt;/code&gt;，这样保证了只有 Notify 完成之后对方才能下一步动作。调用时序如下：
&lt;img src=&quot;//webp.frostming.com/images/2019-04-Snipaste_2019-04-30_18-02-42.png&quot; alt=&quot;Snipaste_2019-04-30_18-02-42.png&quot; /&gt;
总结来说的话，就是只有&lt;code&gt;wait()&lt;/code&gt;方法能主动释放锁，而&lt;code&gt;notify()&lt;/code&gt;不能，所以 waiter 线程一定要先启动，防止发生死锁。&lt;/p&gt;
&lt;h2&gt;Event 与 Condition&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;threading&lt;/code&gt;中还有一个&lt;code&gt;Event&lt;/code&gt;，与&lt;code&gt;Condition&lt;/code&gt;非常类似。区别在于前者等待、监听某个值&lt;code&gt;is_set()&lt;/code&gt;为真，而后者只是一个通知等待的模型。并且&lt;code&gt;Event&lt;/code&gt;中监听值翻转以后，正是通过&lt;code&gt;Condition&lt;/code&gt;去通知等待者的。&lt;code&gt;Event&lt;/code&gt;变成&lt;code&gt;set&lt;/code&gt;以后，就「失效」了，要手动&lt;code&gt;clear&lt;/code&gt;一次才能继续使用用，而&lt;code&gt;Condition&lt;/code&gt;是可以无限&lt;code&gt;wait&lt;/code&gt;, &lt;code&gt;notify&lt;/code&gt;循环的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Event:
    def set(self):
        with self._cond:
            self._flag = True
            self._cond.notify_all()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实，&lt;code&gt;Condition&lt;/code&gt;也包含一个&lt;code&gt;wait_for(eval_function, timeout)&lt;/code&gt;方法，用来等待某函数的返回值为真。这个方法用起来和&lt;code&gt;Event&lt;/code&gt;的作用是很像的，你可以理解为&lt;code&gt;Event&lt;/code&gt;只是提供了一个包装好了的&lt;code&gt;Condition&lt;/code&gt;。&lt;/p&gt;
</content:encoded></item><item><title>全新后台上线</title><link>https://frostming.com/posts/2019/04-24/new-admin/</link><guid isPermaLink="false">https://frostming.com/2019/04-24/new-admin/</guid><description>Vue.js + Flask前后端分离</description><pubDate>Wed, 24 Apr 2019 03:14:48 GMT</pubDate><content:encoded>&lt;p&gt;最近几周空余时间都在做博客后台的重构，主要是因为之前匆匆上线的后台略显简陋。这一次重构就是奔着前后端分离去的，也是为了练练自己的前端技能。&lt;/p&gt;
&lt;p&gt;主体框架使用了国人的&lt;a href=&quot;https://github.com/PanJiaChen/vue-element-admin&quot;&gt;Vue Element Admin&lt;/a&gt;，框架本身提供了丰富的机制，我做了大量的裁剪，主要保留了文章列表、文章编辑等模块，其他亮点有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主题颜色实时显示更新&lt;/li&gt;
&lt;li&gt;配置更新立即生效&lt;/li&gt;
&lt;li&gt;文章配置默认隐藏，更专注于写作&lt;/li&gt;
&lt;li&gt;全局国际化，选择语言立即生效&lt;/li&gt;
&lt;li&gt;独立集成工具页面，方便集成各种三方服务，后续添加中&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;后台样式预览&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;//webp.frostming.com/images/2019-04-Snipaste_2019-04-24_11-43-43.png&quot; alt=&quot;Snipaste_2019-04-24_11-43-43.png&quot; /&gt;
&lt;img src=&quot;//webp.frostming.com/images/2019-04-Snipaste_2019-04-24_11-44-03.png&quot; alt=&quot;Snipaste_2019-04-24_11-44-03.png&quot; /&gt;
&lt;img src=&quot;//webp.frostming.com/images/2019-04-Snipaste_2019-04-24_11-44-13.png&quot; alt=&quot;Snipaste_2019-04-24_11-44-13.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;后续改进&lt;/h3&gt;
&lt;p&gt;完成了后台改造，前台改造也在计划中了，不过暂时还没必要用前后端分离的方法，主要是改下样式。&lt;/p&gt;
&lt;p&gt;就酱&lt;/p&gt;
</content:encoded></item><item><title>你的 Python 包都装到哪了？</title><link>https://frostming.com/posts/2019/03-13/where-do-your-packages-go/</link><guid isPermaLink="false">https://frostming.com/2019/03-13/where-do-your-packages-go/</guid><description>终结一切找不到包、可执行文件的问题</description><pubDate>Wed, 13 Mar 2019 09:53:19 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;写这篇文章是因为最近在 Python 社区看到，有几个求助频率非常高的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我安装了 pip 为什么运行报找不到可执行文件？&lt;/li&gt;
&lt;li&gt;import module 为什么报 &lt;code&gt;ModuleNotFound&lt;/code&gt;？&lt;/li&gt;
&lt;li&gt;为什么我用 Pycharm 能运行在 cmd 里运行不了？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;授人以鱼不如授人以渔，要解决这类问题，你得知道 Python 是如何找包的。希望看完这篇文章，能有所帮助。（主要还是下次再有人问，我就可以链接甩脸了哈哈）&lt;/p&gt;
&lt;h2&gt;Python 是如何寻找包的&lt;/h2&gt;
&lt;p&gt;现在大家的电脑上很可能不只有一个 Python，还有更多的虚拟环境，导致安装包的时候，一不小心你就忘记注意安装包的路径了。首先我们来解决找包的问题，这个问题回答起来很简单，但很多人不知道这个原理。假如你的 Python 解释器的路径是 &lt;code&gt;$path_prefix/bin/python&lt;/code&gt;，那么你启动 Python 交互环境或者用这个解释器运行脚本时，会默认寻找以下位置[^1]：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;$path_prefix/lib&lt;/code&gt;（标准库路径）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$path_prefix/lib/pythonX.Y/site-packages&lt;/code&gt;（三方库路径，X.Y 是对应 Python 的主次版本号，如 3.7, 2.6）&lt;/li&gt;
&lt;li&gt;当前工作目录（&lt;code&gt;pwd&lt;/code&gt;命令的返回结果）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里如果你用的是 Linux 上的默认 Python，&lt;code&gt;$path_prefix&lt;/code&gt; 就是 &lt;code&gt;/usr&lt;/code&gt;，如果你是自己使用默认选项编译的，&lt;code&gt;$path_prefix&lt;/code&gt; 就是 &lt;code&gt;/usr/local&lt;/code&gt;。从上面第二条可以看到不同 Python 版本的三方库路径不同，如果你把 Python 从 3.6 升级到 3.7 那么之前装的三方库都没法用了。当然你可以整个文件夹都拷贝过去，大部分情况不会出问题。&lt;/p&gt;
&lt;p&gt;[^1]: 本文示例均使用 Unix 路径习惯，如果是 Windows 系统则应当做适当改动，如 &lt;code&gt;$path_prefix/bin&lt;/code&gt; 应为 &lt;code&gt;$path_prefix/Scripts&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;几个有用的函数&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sys.executable&lt;/code&gt;：当前使用的 Python 解释器路径&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sys.path&lt;/code&gt;：当前包的搜索路径列表&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sys.prefix&lt;/code&gt;：当前使用的 &lt;code&gt;$path_prefix&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;除此之外，还可以在&lt;em&gt;命令行&lt;/em&gt;中运行 &lt;code&gt;python -m site&lt;/code&gt;，会打印出当前 Python 的一些信息，包括搜索路径列表。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; import sys
&amp;gt;&amp;gt;&amp;gt; sys.executable
&apos;/home/frostming/.pyenv/versions/3.7.2/bin/python&apos;
&amp;gt;&amp;gt;&amp;gt; sys.path
[&apos;&apos;, &apos;/home/frostming/.pyenv/versions/3.7.2/lib/python37.zip&apos;, &apos;/home/frostming/.pyenv/versions/3.7.2/lib/python3.7&apos;, &apos;/home/frostming/.pyenv/versions/3.7.2/lib/python3.7/lib-dynload&apos;, &apos;/home/frostming/.local/lib/python3.7/site-packages&apos;, &apos;/mnt/d/Workspace/pipenv&apos;, &apos;/home/frostming/.pyenv/versions/3.7.2/lib/python3.7/site-packages&apos;]
&amp;gt;&amp;gt;&amp;gt; sys.prefix
&apos;/home/frostming/.pyenv/versions/3.7.2&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用环境变量添加搜索路径&lt;/h3&gt;
&lt;p&gt;如果你的包的路径不存在上面列出的搜索路径列表里，可以把路径加到 &lt;code&gt;PYTHONPATH&lt;/code&gt; 环境变量里，多个路径用 &lt;code&gt;:&lt;/code&gt; 隔开（Windows 用 &lt;code&gt;;&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;但需注意，避免把不同 Python 版本包的路径加到 &lt;code&gt;PYTHONPATH&lt;/code&gt; 里，比如 &lt;code&gt;PYTHONPATH=/home/frostming/.local/lib/python2.7/site-packages&lt;/code&gt;，因为 &lt;code&gt;PYTHONPATH&lt;/code&gt; 中的路径是优先于默认搜索路径，如果用 Python 3 的话会有兼容性问题。事实上 &lt;code&gt;PYTHONPATH&lt;/code&gt; 里最好不要出现任何带 &lt;code&gt;site-packages&lt;/code&gt; 的路径。&lt;/p&gt;
&lt;p&gt;顺便说下 &lt;code&gt;PATH&lt;/code&gt; 是用来找&lt;strong&gt;可执行程序&lt;/strong&gt;的搜索路径，假如你在终端中运行命令 &lt;code&gt;my_cmd&lt;/code&gt;，系统会依次扫描 &lt;code&gt;PATH&lt;/code&gt; 中的路径，看 &lt;code&gt;my_cmd&lt;/code&gt; 是否存在于该路径下，所以如果提示找不到程序或命令无法识别，那你就要看路径是否加到 &lt;code&gt;PATH&lt;/code&gt; 里了。&lt;/p&gt;
&lt;h2&gt;Python 是如何安装包的&lt;/h2&gt;
&lt;p&gt;现在用安装 Python 包基本是用的 &lt;code&gt;pip&lt;/code&gt;，就算你是用 &lt;code&gt;pipenv&lt;/code&gt;，&lt;code&gt;poetry&lt;/code&gt;，底层依然是 &lt;code&gt;pip&lt;/code&gt;，一律适用。
如果你没有安装 &lt;code&gt;pip&lt;/code&gt; 请参考&lt;a href=&quot;https://pip.pypa.io/en/stable/installing/&quot;&gt;这里&lt;/a&gt;，如果安装了还无法用 &lt;code&gt;pip&lt;/code&gt; 命令请参考上一节。&lt;/p&gt;
&lt;p&gt;运行 &lt;code&gt;pip&lt;/code&gt; 有两种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pip ...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;python -m pip ...&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第一种方式和第二种方式大同小异，区别是第一种方式使用的 Python 解释器是写在 &lt;code&gt;pip&lt;/code&gt; 文件的 shebang 里的，一般情况下，如果你的 &lt;code&gt;pip&lt;/code&gt; 路径是 &lt;code&gt;$path_prefix/bin/pip&lt;/code&gt;，那么 Python 路径对应的就是 &lt;code&gt;$path_prefix/bin/python&lt;/code&gt;。如果你用的是 Unix 系统则 &lt;code&gt;cat $(which pip)&lt;/code&gt; 第一行就包含了 Python 解释器的路径。第二种方式则显式地指定了 Python 的位置。这条规则，对于所有 Python 的可执行程序都是适用的。流程如下图所示。
&lt;img src=&quot;//webp.frostming.com/images/2019-03-pip-flow2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那么，不加任何自定义配置时，使用 &lt;code&gt;pip&lt;/code&gt; 安装包就会自动安装到 &lt;code&gt;$path_prefix/lib/pythonX.Y/site-packages&lt;/code&gt; 下（&lt;code&gt;$path_prefix&lt;/code&gt; 是从上一段里得到的），可执行程序安装到 &lt;code&gt;$path_prefix/bin&lt;/code&gt; 下，如果需要在命令行直接使用 &lt;code&gt;my_cmd&lt;/code&gt; 运行，记得加到 &lt;code&gt;PATH&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;pip 中更改安装位置的选项&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--prefix PATH&lt;/code&gt;，替换 &lt;code&gt;$path_prefix&lt;/code&gt; 为给定的值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--root ROOT_PATH&lt;/code&gt;，在 &lt;code&gt;$path_prefix&lt;/code&gt; 前面加上 &lt;code&gt;ROOT_PATH&lt;/code&gt;，比如 &lt;code&gt;--root /home/frostming&lt;/code&gt;，&lt;code&gt;$path_prefix&lt;/code&gt; 就会从 &lt;code&gt;/usr&lt;/code&gt; 变成 &lt;code&gt;/home/frostming/usr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--target TARGET&lt;/code&gt;，直接指定安装位置到 &lt;code&gt;TARGET&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;虚拟环境&lt;/h2&gt;
&lt;p&gt;虚拟环境就是为了隔离不同项目的依赖包，使他们安装到不同的路径下，以防止依赖冲突的问题。理解了 Python 是如何安装包的机制之后就不难理解虚拟环境（&lt;code&gt;virtualenv&lt;/code&gt;, &lt;code&gt;venv&lt;/code&gt;模块）的原理。其实，运行&lt;code&gt;virtualenv myenv&lt;/code&gt;会复制一个新的 Python 解释器到&lt;code&gt;myenv/bin&lt;/code&gt;下，并创建好&lt;code&gt;myenv/lib&lt;/code&gt;，&lt;code&gt;myenv/lib/pythonX.Y/site-packages&lt;/code&gt;等目录（&lt;code&gt;venv&lt;/code&gt;模块不是用的复制，但结果基本一样）。执行&lt;code&gt;source myenv/bin/activate&lt;/code&gt;以后会把&lt;code&gt;myenv/bin&lt;/code&gt;塞到&lt;code&gt;PATH&lt;/code&gt;前面，让这个复制出来的 Python 解释器最优先被搜索到。这样，后续安装包时，&lt;code&gt;$path_prefix&lt;/code&gt;就会是&lt;code&gt;myenv&lt;/code&gt;了，从而实现了安装路径的隔离。&lt;/p&gt;
&lt;h2&gt;脚本运行方式对搜索路径的影响&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;本节于 2021/1/21 新增&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;从上面的介绍大家可以知道，Python 找不找得到一个包，最直接的原因是 &lt;code&gt;sys.path&lt;/code&gt;，而更进一步的原因是 &lt;code&gt;sys.executable&lt;/code&gt; 的路径。程序写完了，我们总得需要运行它，不同的运行方法却有可能影响到 &lt;code&gt;sys.path&lt;/code&gt; 而造成不同的行为，下面我们就来讨论这个问题。&lt;/p&gt;
&lt;p&gt;假设你的包结构如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├── main.py
└── my_package
    ├── __init__.py
    ├── a.py
    └── b.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;main.py&lt;/code&gt; 的内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import my_package.b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;b.py&lt;/code&gt; 的文件内容很简单：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import sys
print(&quot;I&apos;m b&quot;)
print(sys.path)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在在 &lt;code&gt;main.py&lt;/code&gt; 同级的目录下执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ python main.py
I&apos;m b
[&apos;/home/frostming/test_path&apos;, ...]  # 省略的路径是共同的，与讨论的问题无关
$ python my_package/b.py
I&apos;m b
[&apos;/home/frostming/test_path/my_package&apos;, ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;python xxx.py&lt;/code&gt; 的运行方式叫做&lt;strong&gt;直接运行&lt;/strong&gt;，这时该文件中的 &lt;code&gt;__name__&lt;/code&gt; 值会指定为 &lt;code&gt;__main__&lt;/code&gt;，IDE 中的「 Run File」「 运行脚本」用的就是这种方式。可以看到这时 &lt;code&gt;sys.path&lt;/code&gt;的第一个值是&lt;strong&gt;该脚本文件所在的目录&lt;/strong&gt;，随脚本路径而变化，记住我们执行测试始终是在 &lt;code&gt;/home/frostming/test_path&lt;/code&gt; 这个目录下。&lt;/p&gt;
&lt;p&gt;好，那么如果我们需要在 &lt;code&gt;b.py&lt;/code&gt; 中导入 &lt;code&gt;a.py&lt;/code&gt;，&lt;code&gt;a.py&lt;/code&gt; 包含简单的一行 &lt;code&gt;print(&quot;I&apos;m a&quot;)&lt;/code&gt;，那么 &lt;code&gt;b.py&lt;/code&gt; 脚本中应该怎么写呢？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Easy!, &lt;code&gt;import a&lt;/code&gt;，好，再执行一遍上面的测试&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ python main.py
ModuleNotFoundError: No module named &apos;a&apos;
$ python my_package/b.py
I&apos;m a
I&apos;m b
[&apos;/home/frostming/test_path/my_package&apos;, ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一个测试出错了，如果前面的内容你已经看过，这个报错就是意料之中了——&lt;code&gt;sys.path&lt;/code&gt; 压根没有 &lt;code&gt;a.py&lt;/code&gt; 所在的目录 &lt;code&gt;/home/frostming/test_path/my_package&lt;/code&gt;，当然找不到 &lt;code&gt;a&lt;/code&gt; 了。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;改成 &lt;code&gt;from my_package import a&lt;/code&gt;，测试就不做了，因为基于同样的分析，我们可以预测第一个运行没问题而第二个会报错找不到 &lt;code&gt;my_package&lt;/code&gt;。注意由于 &lt;code&gt;b&lt;/code&gt; 是在 &lt;code&gt;my_package&lt;/code&gt; 包中的，这时可以使用&lt;strong&gt;相对导入&lt;/strong&gt;，写 &lt;code&gt;from . import a&lt;/code&gt; 和 &lt;code&gt;from my_package import a&lt;/code&gt; 的效果是一样的。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;那么我有没有让这两次运行都不报错的方法呢？有。我们要知道，一个项目中的入口是有限的，实际上不会出现可以执行的代码既在顶层有，又在子目录里也有。我们应该把主要的运行逻辑，都放在 &lt;code&gt;main.py&lt;/code&gt; 中（不一定是这个名字，比如 Django 项目是 &lt;code&gt;manage.py&lt;/code&gt;），如果这时确实需要运行一个子目录中某脚本的代码，应该用 &lt;code&gt;python -m &amp;lt;module_name&amp;gt;&lt;/code&gt;，而 &lt;code&gt;b.py&lt;/code&gt; 中导入 &lt;code&gt;a&lt;/code&gt; 的语句应为 &lt;code&gt;from my_package import a&lt;/code&gt;，我们来看一下运行效果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ python main.py  # 和 python -m main 效果一样
I&apos;m a
I&apos;m b
[&apos;/home/frostming/test_path&apos;, ...]
$ python -m my_package.b
I&apos;m a
I&apos;m b
[&apos;/home/frostming/test_path&apos;, ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到这两次运行的 &lt;code&gt;sys.path&lt;/code&gt; 内容一致了，它的第一个值是&lt;strong&gt;当前运行所在的目录&lt;/strong&gt;。这种运行方式叫做&lt;strong&gt;以模块方式运行&lt;/strong&gt;，&lt;code&gt;python -m&lt;/code&gt; 后面的参数是（以 &lt;code&gt;.&lt;/code&gt; 分隔的）&lt;strong&gt;模块名&lt;/strong&gt;，而不是路径名。由于这种统一性，你在项目中的所有导入都可以用相同的定义方式，而不用管是在哪个脚本中。这也是为什么 Django 官方文档中推荐导入名称全部用 &lt;code&gt;myapp.models.users&lt;/code&gt; 这种形式。&lt;/p&gt;
&lt;p&gt;除此之外，以模块方式运行的时候，传入模块名的每一级父模块（或包）都会以模块形式运行，这意味着你可以在模块中使用相对导入（以直接运行方式运行不可以），并且传入的模块中&lt;code&gt;__name__&lt;/code&gt; 的值会置为 &lt;code&gt;__main__&lt;/code&gt;，你依然可以应用 &lt;code&gt;if __name__ == &quot;__main__&quot;:&lt;/code&gt; 的判断。如果 &lt;code&gt;python -m &amp;lt;module_name&amp;gt;&lt;/code&gt; 中传入的模块是个包，那么会执行包目录中的 &lt;code&gt;__main__.py&lt;/code&gt; 脚本（如果存在），此时该脚本的 &lt;code&gt;__name__&lt;/code&gt; 值为 &lt;code&gt;__main__&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;看到这里大家可以发现，关于包路径搜索最重要的就是这个&lt;code&gt;$path_prefix&lt;/code&gt;路径前缀，而这个值又是从使用的 Python 解释器路径推导出来的。所以要找到包的路径，只需要知道解释器的路径就可以了，如果遇到改变包的路径，只需要通过正确的&lt;code&gt;PATH&lt;/code&gt;设置，指定你想要的 Python 解释器即可。&lt;/p&gt;
&lt;p&gt;现在回到开头的三个问题，大家会解决了吗？在评论区写出你的排查步骤或解决方法。&lt;/p&gt;
</content:encoded></item><item><title>使用双拼输入法一周回顾</title><link>https://frostming.com/posts/2019/01-21/pinyin-wubi/</link><guid isPermaLink="false">https://frostming.com/2019/01-21/pinyin-wubi/</guid><pubDate>Mon, 21 Jan 2019 09:23:10 GMT</pubDate><content:encoded>&lt;p&gt;最近这周我一直都在用双拼作为中文输入法，没用过一次五笔。总的来说挺顺手，除了有时会误打五笔或全拼，还挺好的，速度还上不去罢了。当我脱离了使用二十年的五笔，再回头看，可以发现之前从来没意识到的问题。&lt;/p&gt;
&lt;p&gt;使用拼音类输入法，最大的好处，是海量词库加持。用拼音输入法，不是键位简单了，事实上双拼有那么多方案，其实都差不多，全都是依赖强大的词库。但用五笔，根本无法利用到大数据加持的庞大词库。这导致了即便是大厂的五笔输入法，在界面和体验上差拼音输入法好几个世代。&lt;/p&gt;
&lt;p&gt;这就涉及到一个与五笔很大的不同：拼音是靠词库和候选消除重码，五笔是靠编码组合。五笔的重码率要小得多，那些消不掉的，就没办法了，比如「老师/教师」。重码率小的最大的好处是你几乎不用看候选，脑子想到，字就上了屏，注意力不会被分散。而用拼音，你再熟悉，总是要保持着盯着候选框，特别是开启了自动调频的情况。&lt;/p&gt;
&lt;p&gt;不过还有一个隐忧：习惯了拼音以后，长此以往，怕是要提笔忘字了。&lt;/p&gt;
</content:encoded></item><item><title>A deeper look into Pipenv and Poetry</title><link>https://frostming.com/posts/en/2019/pipenv-poetry/</link><guid isPermaLink="false">https://frostming.com/en/2019/pipenv-poetry/</guid><pubDate>Fri, 04 Jan 2019 02:50:01 GMT</pubDate><content:encoded>&lt;p&gt;It is 8 months passed since I posted the &lt;a href=&quot;https://frostming.com/2018/05-15/pipenv-vs-poetry&quot;&gt;article comparing Pipenv with Poetry&lt;/a&gt;, which is the most popular article in my blog now. However, it was not a good review of the two tools, I have not even read the documentation of Poetry. In the end of last year I became a collaborator of Pipenv and util then have I realized there are so many trade-offs and, well, defects in Pipenv. In the area of software engineering, the successor always wins. The creator can&apos;t anticipate all corner cases in his prototype or original thoughts, especially for such a CLI tool that are run on millions of computers with totally different environment setup.&lt;/p&gt;
&lt;h2&gt;Main problems with Pipenv&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Introduced more files of a new format, which is not perfect also. See how many miscellaneous files there are under the project root:
&lt;img src=&quot;//webp.frostming.com/images/2019-01-pipenv-files.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;The commands and options are always &lt;a href=&quot;https://github.com/pypa/pipenv/issues/3316#issuecomment-442288953&quot;&gt;confusing and unclear&lt;/a&gt;, and the options are not fully tested to ensure the availability.&lt;/li&gt;
&lt;li&gt;Locking performance, there are &lt;a href=&quot;https://github.com/pypa/pipenv/issues?q=is%3Aissue+is%3Aopen+label%3A%22dependency+resolution%22&quot;&gt;many issues&lt;/a&gt; about dependency resolution. The rollment of &lt;a href=&quot;https://github.com/sarugaku/passa&quot;&gt;passa&lt;/a&gt; may bring a big improvement, but the timeline is still not decided yet.&lt;/li&gt;
&lt;li&gt;Regression issues burned out users&apos; patience. The test coverage is still very low, though testing against cross-platform and multi environment is a difficult and complicated mission.&lt;/li&gt;
&lt;li&gt;The maintainers are maintaining a large amount of projects backing Pipenv which need to be updated at a fairly high frequency. And it in turn brings high risk of regression. All &lt;a href=&quot;https://github.com/pypa/pipenv/blob/master/peeps/PEEP-000.md&quot;&gt;PEEPs&lt;/a&gt;(Pipenv&apos;s enhancement proposals) need BDFL&apos;s approval while Kenneth Reitz is bothered by mental healthy and takes the back seat for a long time.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I myself are using Pipenv in daily work and help maintain Pipenv, too. So I wish a better future for Pipenv.&lt;/p&gt;
&lt;h2&gt;What about Poetry&lt;/h2&gt;
&lt;p&gt;Poetry seems a better choice ya? I am also considering transfering to Poetry, but before that, I have to point out some downsides of it.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Poetry doesn&apos;t work well with PyPA&apos;s current packaging system. It is very common that people clone a VCS repository and work on it. However, you can&apos;t pip install a Poetry project without extra steps to generate a &lt;code&gt;setup.py&lt;/code&gt; from &lt;code&gt;pyproject.toml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It is weird that Poetry puts python requires in dependency section. Python requires should be placed in global settings, although it shares the same version specifiers with normal dependencies.&lt;/li&gt;
&lt;li&gt;Neither Pipenv or Poetry supports to activate a virtualenv outside of project directory. I know it is inspired by NPM but in Python world people are likely to put some scripts in places other than the project directory. It is supported by virtualenvwrapper.&lt;/li&gt;
&lt;li&gt;Poetry only works under &lt;em&gt;one&lt;/em&gt; workflow. For example, it doesn&apos;t support installing current dependencies into system Python, which is the typical workflow for developing in docker.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What I expect in the future of Python packaging&lt;/h2&gt;
&lt;p&gt;I like the idea of &lt;a href=&quot;https://poetry.eustace.io/docs/libraries/#every-project-is-a-package&quot;&gt;&quot;Every project is a package&quot;&lt;/a&gt; and the introduction of &lt;code&gt;pyproject.toml&lt;/code&gt;. It is introduced in &lt;a href=&quot;https://www.python.org/dev/peps/pep-0518/&quot;&gt;PEP-508&lt;/a&gt; but it is not finalized yet.&lt;/p&gt;
&lt;p&gt;After PEP-508 is finalized, it is better that there is an official way to define dependencies in &lt;code&gt;pyproject.toml&lt;/code&gt;, not in section like &lt;code&gt;tool.poetry&lt;/code&gt;. It may look like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[project]
name = &quot;poetry&quot;
version = &quot;0.12.10&quot;
description = &quot;Python dependency management and packaging made easy.&quot;
authors = [ &quot;Sébastien Eustace &amp;lt;sebastien@eustace.io&amp;gt;&quot; ]
license = &quot;MIT&quot;
readme = &quot;README.md&quot;
python = &quot;~2.7 || ^3.4&quot;
homepage = &quot;https://poetry.eustace.io/&quot;
repository = &quot;https://github.com/sdispater/poetry&quot;
documentation = &quot;https://poetry.eustace.io/docs&quot;
keywords = [
  &quot;packaging&quot;,
  &quot;dependency&quot;,
  &quot;poetry&quot;
]
classifiers = [
  &quot;Topic :: Software Development :: Build Tools&quot;,
  &quot;Topic :: Software Development :: Libraries :: Python Modules&quot;
]

# Requirements
[dependencies]
cleo = &quot;^0.6.7&quot;
requests = &quot;^2.18&quot;
cachy = &quot;^0.2&quot;
requests-toolbelt = &quot;^0.8.0&quot;
jsonschema = &quot;^3.0a3&quot;
pyrsistent = &quot;^0.14.2&quot;
pyparsing = &quot;^2.2&quot;
cachecontrol = { version = &quot;^0.12.4&quot;, extras = [ &quot;filecache&quot; ] }
pkginfo = &quot;^1.4&quot;
html5lib = &quot;^1.0&quot;
shellingham = &quot;^1.1&quot;
tomlkit = &quot;^0.5.1&quot;

# The typing module is not in the stdlib in Python 2.7 and 3.4
typing = { version = &quot;^3.6&quot;, python = &quot;~2.7 || ~3.4&quot; }

# Use pathlib2 for Python 2.7 and 3.4
pathlib2 = { version = &quot;^2.3&quot;, python = &quot;~2.7 || ~3.4&quot; }
# Use virtualenv for Python 2.7 since venv does not exist
virtualenv = { version = &quot;^16.0&quot;, python = &quot;~2.7&quot; }
# functools32 is needed for Python 2.7
functools32 = { version = &quot;^3.2.3&quot;, python = &quot;~2.7&quot; }

[dev-dependencies]
pytest = &quot;^3.4&quot;
pytest-cov = &quot;^2.5&quot;
mkdocs = { version = &quot;^1.0&quot;, python = &quot;~2.7.9 || ^3.4&quot; }
pymdown-extensions = &quot;^6.0&quot;
pygments = &quot;^2.2&quot;
pytest-mock = &quot;^1.9&quot;
pygments-github-lexers = &quot;^0.0.5&quot;
black = { version = &quot;^18.3-alpha.0&quot;, python = &quot;^3.6&quot; }
pre-commit = &quot;^1.10&quot;
tox = &quot;^3.0&quot;
pytest-sugar = &quot;^0.9.2&quot;

[scripts]
poetry = &quot;poetry.console:main&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I hope &lt;code&gt;pyproject.toml&lt;/code&gt; will eventually replace &lt;code&gt;setup.py&lt;/code&gt;, and in the transition period, Pip, or whatever name, should be able to read both files. The &quot;Pip&quot; should be a combination of Pipenv and Poetry and be the ultimate solution for Python packaging.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Update&lt;/h2&gt;
&lt;p&gt;Another drawbacks I found recently: Pipenv, or more precisely, virtualenv, doens&apos;t use the built-in venv module to create a VE, which may bring troubles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When the Python interpreter is updated, the VE will become stale.&lt;/li&gt;
&lt;li&gt;Some packages such as &lt;code&gt;matplotlib&lt;/code&gt; requires framework build on macOS. Virtualenv creates non-framework build even if the original one is framework-build&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I &lt;a href=&quot;https://github.com/frostming/virtualenv-venv&quot;&gt;forked the virtualenv&lt;/a&gt; with a patch to address these problems. Replace the PyPI virtualenv via:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ pip install -I https://github.com/frostming/virtualenv-venv/releases/download/16.2.0-fork/virtualenv-16.2.0_fork-py2.py3-none-any.whl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything works well now.&lt;/p&gt;
</content:encoded></item><item><title>第一次成为网红</title><link>https://frostming.com/posts/2018/11-26/first-time-popular/</link><guid isPermaLink="false">https://frostming.com/2018/11-26/first-time-popular/</guid><pubDate>Mon, 26 Nov 2018 03:31:50 GMT</pubDate><content:encoded>&lt;p&gt;最近发生了两件事情。&lt;/p&gt;
&lt;p&gt;第一件，我正式成功&lt;a href=&quot;https://github.com/pypa/pipenv&quot;&gt;Pipenv&lt;/a&gt;的官方维护者之一。2018 我才开始涉足开源，Github 上截止现在有 394 个 commit，也算是对我这些贡献的一个回报吧。还是有点小满足了虚荣心的。&lt;/p&gt;
&lt;p&gt;第二件，我在推特上发了一个关于 Python 3 f-string vs format vs %的性能比较。这是两天内推文的动态：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//webp.frostming.com/images/2018-11-twitter.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我有点惊呆了，因为我在微博上从来没有到达过这个热度，而这个推特账号是今年才建立的。其实还是要归功于我在 Pipenv 上的活跃，得以结识了一些大 V，Python 核心开发之一&lt;a href=&quot;https://twitter.com/ncoghlan_dev&quot;&gt;Nick Coghlan&lt;/a&gt;就关注了我，并且转发了这条推特。虽然马上就被另一个核心开发指出了我的错误：&lt;/p&gt;
&lt;p&gt;&amp;lt;blockquote className=&quot;twitter-tweet&quot; data-lang=&quot;zh-cn&quot;&amp;gt;
&amp;lt;p lang=&quot;en&quot; dir=&quot;ltr&quot;&amp;gt;
Try my perf module, python3 -m perf timeit --duplicate=1024 -s &apos;a=1; b=2&apos; ...
&amp;lt;br /&amp;gt;* &apos;&quot;%s + %s = %s&quot; % (a, b, a + b)&apos;: 254 ns +- 10 ns
&amp;lt;br /&amp;gt;* &apos;&quot;%d + %d = %d&quot; % (a, b, a + b)&apos;: 268 ns +- 13 ns
&amp;lt;br /&amp;gt;* &apos;f&quot;{a} + {b} = {a + b}&quot;&apos;: 273 ns +- 6 ns
&amp;lt;br /&amp;gt;* &apos;&quot;{} + {} = {}&quot;.format(a, b, a+b)&apos;: 321 ns +- 10 ns
&amp;lt;/p&amp;gt;
— Victor Stinner 🐍 (@VictorStinner) &amp;lt;a href=&quot;https://twitter.com/VictorStinner/status/1065626845541003264?ref_src=twsrc%5Etfw&quot;&amp;gt;2018年11月22日&amp;lt;/a&amp;gt;
&amp;lt;/blockquote&amp;gt;
&amp;lt;script async src=&quot;https://platform.twitter.com/widgets.js&quot; charSet=&quot;utf-8&quot;&amp;gt;&amp;lt;/script&amp;gt;
我本来就是随便发一个我的新发现，没想到影响挺大，为防止误导更多的人，我已经删除了原推。&lt;/p&gt;
</content:encoded></item><item><title>Flask前后端分离实践：Todo App(2)</title><link>https://frostming.com/posts/2018/09-27/flask-vue-todo2/</link><guid isPermaLink="false">https://frostming.com/2018/09-27/flask-vue-todo2/</guid><description>表单与登录</description><pubDate>Thu, 27 Sep 2018 03:59:16 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;前序文章&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://frostming.com/2018/09-18/flask-vue-todo1&quot;&gt;Flask 前后端分离实践：Todo App(1)&lt;/a&gt;
使用 Vue.js 搭建 Todo App&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本文项目地址: https://github.com/frostming/flask-vue-todo&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在上一篇文章里我们已经用 Flask+Vue 搭建了一个可以把数据持久化到服务器的 Todo App。那么，为了让多人一起使用这个 App，我们需要对数据按用户做隔离，这样就自然需要一个注册/登录界面。在前后端分离的架构里，我们是怎么验证用户，保持会话的呢？&lt;/p&gt;
&lt;h2&gt;用户登录&lt;/h2&gt;
&lt;p&gt;先复习一下以往用 Flask 是怎么解决这问题的，没错，通过 Flask-Login 模块，从 request 中获取用户名和密码，验证通过后用&lt;code&gt;login_user&lt;/code&gt;记录到会话中，之后的请求就会带有登录信息了。如果要退出登录，只需要调一下&lt;code&gt;logout_user&lt;/code&gt;就可以了。&lt;/p&gt;
&lt;p&gt;那么使用前后端分离以后，所有对后端的请求都是以 Ajax 的方式发送，上面的方法依然有效！区别仅仅在于，我们将请求改成 JSON 格式之后，后端是从&lt;code&gt;request.get_json()&lt;/code&gt;中获取的。为此，我们专门建立一个名为&lt;code&gt;auth&lt;/code&gt;的蓝图:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@bp.route(&apos;/login&apos;, methods=[&apos;POST&apos;])
def login():
    user_data = request.get_json()
    form = LoginForm(data=user_data)
    if form.validate():
        user = form.get_user()
        login_user(user, remember=form.remember.data)
        return jsonify({&apos;status&apos;: &apos;success&apos;, &apos;user&apos;: user.to_json()})
    return jsonify({&apos;status&apos;: &apos;error&apos;, &apos;message&apos;: form.errors}), 403
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后端只接收 POST 请求，因为 GET 都在前端那边，自然也就没有 login_view 的配置了。前端那边，axios 发请求时自动会带上 cookie，所以后端这边依然可以通过&lt;code&gt;flask_login.current_user&lt;/code&gt;拿到当前用户。&lt;/p&gt;
&lt;h2&gt;表单与验证&lt;/h2&gt;
&lt;p&gt;现在我们需要一个包含表单的登录页面，而我们知道，所有的页面都是前端渲染。所以这里 wtform 或 flask-boostrap 就不太能派上用场了。好在表单也比较简单，不是很难写。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;form action=&quot;/auth/login&quot; method=&quot;post&quot;&amp;gt;
    &amp;lt;h2&amp;gt;Login&amp;lt;/h2&amp;gt;
    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
      &amp;lt;label for=&quot;username&quot;&amp;gt;Username&amp;lt;/label&amp;gt;
      &amp;lt;input
        type=&quot;text&quot;
        id=&quot;username&quot;
        name=&quot;username&quot;
        v-model=&quot;username&quot;
        required
      /&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
      &amp;lt;label for=&quot;password&quot;&amp;gt;Password&amp;lt;/label&amp;gt;
      &amp;lt;input
        type=&quot;password&quot;
        id=&quot;password&quot;
        name=&quot;password&quot;
        v-model=&quot;password&quot;
        required
      /&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
      &amp;lt;label for=&quot;remember&quot;&amp;gt;
        &amp;lt;input
          type=&quot;checkbox&quot;
          id=&quot;remember&quot;
          name=&quot;remember&quot;
          v-model=&quot;remember&quot;
        /&amp;gt;
        Remember Me
      &amp;lt;/label&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;form-footer&quot;&amp;gt;
      &amp;lt;button type=&quot;submit&quot; name=&quot;submit&quot; class=&quot;btn&quot;&amp;gt;Submit&amp;lt;/button&amp;gt;
      &amp;lt;router-link to=&quot;/&quot; class=&quot;btn&quot;&amp;gt;Return Home&amp;lt;/router-link&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/form&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有一表单验证的工作，比如必填项，长度限制等，完全不需要后端的，可以在前端完成。我们需要写一个提交的函数，绑定到表单的 submit 动作上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default {
  methods: {
    checkForm(e) {
      e.preventDefault();
      const vm = this;
      api
        .login({
          username: this.username,
          password: this.password,
          remember: this.remember,
        })
        .then((data) =&amp;gt; {
          vm.$router.push({ path: &quot;/&quot; }, () =&amp;gt; {
            vm.success(&quot;Logged in successfully!&quot;);
          });
        })
        .catch((e) =&amp;gt; {
          const errors = e.response.data.message;
          for (const key in errors) {
            errors[key].forEach((e) =&amp;gt; vm.error(`${key}: ${e}`));
          }
        });
    },
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但有些验证工作，比如密码校验，还是要麻烦后端的，所以这里我们获取后端返回的错误（储存在&lt;code&gt;data.message&lt;/code&gt;中），然后依次渲染在页面中（这里我使用了一个 Vue 的插件&lt;a href=&quot;https://www.npmjs.com/package/vue-flash-message&quot;&gt;Vue-flask-message&lt;/a&gt;来完成）。&lt;/p&gt;
&lt;p&gt;后端验证这一块，由于没有渲染需求了，可以不用 wtform 这一套，改用&lt;a href=&quot;https://github.com/marshmallow-code/marshmallow&quot;&gt;marshmallow&lt;/a&gt;，但为了后面的方便，我还是使用了 Flask-WTF，把验证放到表单类里。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from flask_wtf import FlaskForm

class LoginForm(FlaskForm):
    username = StringField(&apos;Username&apos;, validators=[Length(max=64)])
    password = PasswordField(&apos;Password&apos;, validators=[Length(8, 16)])
    remember = BooleanField(&apos;Remember Me&apos;)

    def validate_username(self, field):
        if not self.get_user():
            raise ValidationError(&apos;Invalid username!&apos;)

    def validate_password(self, field):
        if not self.get_user():
            return
        if not self.get_user().check_password(field.data):
            raise ValidationError(&apos;Incorrect password!&apos;)

    def get_user(self):
        return User.query.filter_by(username=self.username.data).first()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成了登录部分，那么注册界面也大同小异，总结起来，大致思想是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于无需后端的验证，由前端完成。&lt;/li&gt;
&lt;li&gt;后端的验证，通过响应内容传回错误。&lt;/li&gt;
&lt;li&gt;验证错误通过 Vue-flash-message 显示到页面上。&lt;/li&gt;
&lt;li&gt;login 和 register 的视图函数仅处理 POST 请求。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Flask前后端分离实践：Todo App(1)</title><link>https://frostming.com/posts/2018/09-18/flask-vue-todo1/</link><guid isPermaLink="false">https://frostming.com/2018/09-18/flask-vue-todo1/</guid><description>使用Vue.js搭建Todo App</description><pubDate>Tue, 18 Sep 2018 07:49:20 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;前言&lt;/strong&gt;：有句老话，叫做「现在都 8102 年了，你怎么还 XXX？」。随着前端工具的越来越完善和好用，现在前端能做的东西，实在太多了。而现在主流的 Flask 教程，都是基于以往的服务端模板渲染的架构。这在 2018 年，未免有些过时和笨拙。我曾看过一个用 Flask 写的 Todo 项目，每个交互都要向服务端发送 AJAX， 甚至连动态添加 DOM 元素都交由服务端渲染好再用 jQuery 添加。本系列文章，亦将由一个 Todo App 入手，实践前后端分离的架构，进而初窥全栈开发的门径。诚然，在前后端分离的系统中，Python 作为后端并不是一个最优的选择（出门右转 Golang）。但一则我热爱 Python 和 Flask，二则别的我也不太会，所以我假定阅读本文的作者，已经看过&lt;a href=&quot;http://flask.pocoo.org/docs/1.0/&quot;&gt;Flask 的官方文档&lt;/a&gt;，或&lt;a href=&quot;https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world&quot;&gt;Miguel Grinberg 的 Flask Mega 教程&lt;/a&gt;。那么现在开始。&lt;/p&gt;
&lt;p&gt;本文项目地址: https://github.com/frostming/flask-vue-todo&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前后端分离的思路&lt;/h2&gt;
&lt;p&gt;有人要问，我为什么要前后端分离？这个说起就话长了，网上也能搜索到一些解答，不过可简要概括为以下两点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前端越来越重，很多页面交互，交由前端来实现会更加方便。我一直秉承：让专业的人做专业的事。这样事情会做得更漂亮。&lt;/li&gt;
&lt;li&gt;前后端脱耦，可以分别交给两个人（团队）去做，且不会互相牵制。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么哪些事是前端该做哪些是后端该做的呢？凡是涉及页面逻辑的部分，都是前端的工作，包括路由，渲染，页面事件等等。而只有在需要服务端的数据时，才给后端发请求。这样能大大节省网络带宽，减少网络延时的影响，一切交互都在本地，享受飞一般的感觉。特别是 Todo App，你肯定不想每加一项，勾选一个完成都要 busy 一阵吧，哪怕就是 10ms 也是无法忍受，所以 Todo App 非常适合用前后端分离来实现。当然，Todo App 也是各种前端框架的常见例子了，所以不太了解前端的各位 Pythonista 们，照着教程来一遍就差不多了，Flask 的后端仅仅需要完成两个功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将内容持久化到服务器数据库&lt;/li&gt;
&lt;li&gt;加入用户验证系统&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;建立 Vue 应用&lt;/h2&gt;
&lt;p&gt;我选用 Vue.js 作为前端框架，当然用 React.js 也是可以的，它们都有强大的工具链，但 Vue.js 的好处是它是中国人开发的，几乎所有官方库文档都有中文版哦，方便学习嘛，而且个人感觉 Vue.js 用起来也确实更爽一点。&lt;/p&gt;
&lt;h3&gt;目录结构&lt;/h3&gt;
&lt;p&gt;与传统的 Flask app 不同，前后端分离架构推荐静态文件（html, css, js 们）和 Python 文件分开存放。目录结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flask-vue-todo
├─frontend    # 存放前端文件
├─backend    # 存放python文件
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;安装依赖&lt;/h3&gt;
&lt;p&gt;首先我们需要安装一键建立 Vue 项目的命令行工具&lt;code&gt;vue-cli&lt;/code&gt;，安装方法（本文使用 Yarn 管理前端依赖，npm 大同小异）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yarn global add vue-cli
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照上述结构建立好项目之后，进入&lt;code&gt;frontend&lt;/code&gt;目录，执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vue init webpack-simple
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在一通眼花缭乱的进度条之后项目就建好了，执行&lt;code&gt;yarn run dev&lt;/code&gt;看看效果吧。&lt;/p&gt;
&lt;h2&gt;编写 Todo App&lt;/h2&gt;
&lt;p&gt;这一部分我不做重点介绍。此应用主要有以下逻辑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;输入内容按下回车时在 Todo 列表中加上一项&lt;/li&gt;
&lt;li&gt;点 Todo 项前的 checkbox 将其标为完成&lt;/li&gt;
&lt;li&gt;点 Todo 项的红叉将其删除&lt;/li&gt;
&lt;li&gt;通过 All, Undone, Completed 过滤显示的 Todo 项&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我使用了&lt;a href=&quot;https://vuex.vuejs.org/zh&quot;&gt;Vuex&lt;/a&gt;来管理应用的状态。注意把 Ajax 请求部分单独抽离到一个文件中方便管理，这时你可以先让它永远返回成功即可。为了符合之后即将使用的 axios 的 API，可以这样写请求：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// api/index.js
const mockTodos = [
  { id: 1, text: &quot;Item 1&quot;, done: false },
  { id: 2, text: &quot;Item 2&quot;, done: true },
];

function mockRequest() {
  return new Promise((resolve, reject) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      Math.random() &amp;lt; 0.85
        ? resolve(mockTodos)
        : reject(new Error(&quot;Get Todo list error!&quot;));
    }, 100);
  });
}

const api = {
  getTodos() {
    return mockRequest(&quot;/todos&quot;);
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，我在应用中做了很多美化的工作让应用显得高大上，符合 Vue.js 的 UI。&lt;/p&gt;
&lt;p&gt;再次执行&lt;code&gt;yarn run dev&lt;/code&gt;（若已执行则不必，它会自动热重载），你会看到编写完成的效果。&lt;code&gt;yarn run build&lt;/code&gt;来编译已经写好的源文件。&lt;/p&gt;
&lt;h2&gt;编写 Flask 部分&lt;/h2&gt;
&lt;p&gt;好了，现在切换到&lt;code&gt;backend&lt;/code&gt;目录，后端的应用预备作为一个 API server 来使用，为方便与前端交互，输入输出均采用 JSON 格式，Flask 中可用&lt;code&gt;flask.jsonify&lt;/code&gt;将结果转换成 JSON 的响应。告别看文档啃 Stackoverflow 爬坑，一切都是熟悉的味道，写起 Flask 来那还不上下翻飞？所有 API 请求都给它放到一个蓝图里，包含以下接口：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;获取所有 Todo 项，包括它们的完成状态&lt;/li&gt;
&lt;li&gt;更新 Todo 项&lt;/li&gt;
&lt;li&gt;删除 Todo 项&lt;/li&gt;
&lt;li&gt;新建 Todo 项&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这根本就是数据库的增删查改嘛，用上&lt;code&gt;flask-sqlalchemy&lt;/code&gt;简直不要太方便。其实这么简单的操作无需用 SQL，用一个 NonSQL 数据库会更好，但为了部署 Heroku，它提供免费的 PostgreSQL 数据库。主路由就简单了，只剩一个&lt;code&gt;index&lt;/code&gt;了，因为页面路由都交给前端了嘛，这时我们的 App 就成了一个「单页应用」(SPA)了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@app.route(&apos;/&apos;)
def index():
    return render_template(&apos;index.html&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;且慢，因为我们改换了目录结构，你必须告诉 Flask 静态文件和 html 文件的正确位置，编译好的静态文件在&lt;code&gt;frontend/dist&lt;/code&gt;中，&lt;code&gt;index.html&lt;/code&gt;在&lt;code&gt;frontend&lt;/code&gt;中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FRONTEND_FOLDER = os.path.join(os.path.dirname(os.path.dirname(__file__)), &apos;frontend&apos;)

def create_app():
    app = Flask(
        __name__, static_folder=os.path.join(FRONTEND_FOLDER, &apos;dist&apos;),
        template_folder=FRONTEND_FOLDER
    )
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对了，不要记得所有错误也都以 JSON 格式返回:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from werkzeug.exceptions import HTTPException

@app.errorhandler(HTTPException)
def handle_http_error(exc):
    return jsonify({&apos;status&apos;: &apos;error&apos;, &apos;description&apos;: exc.description}), exc.code
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好了，现在可以把前端部分中之前伪造的请求换成真的了，我就用的 Vue.js 推荐的&lt;a href=&quot;https://github.com/axios/axios&quot;&gt;axios&lt;/a&gt;，需要初始化一下，把所有请求变成 JSON 请求：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import axios from &quot;axios&quot;;

const api = axios.create({
  headers: {
    &quot;Content-Type&quot;: &quot;application/json&quot;,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;赶紧运行&lt;code&gt;FLASK_ENV=development flask run&lt;/code&gt;吧，然后你就能从&lt;a href=&quot;http://localhost:5000&quot;&gt;http://localhost:5000&lt;/a&gt;看到效果了。&lt;/p&gt;
&lt;h2&gt;关于前端开发服务器和后端开发服务器&lt;/h2&gt;
&lt;p&gt;可能有的同学已经注意到了，前端和后端都有一个开发服务器，但默认端口号不同，一个是 8080，一个是 5000。其中 8080 的开发服务器是调试前端页面用的，它仅仅包含静态文件，这时后端 API 是不可用状态的。但它有很多方便调试的功能，比如详尽的错误信息和热重载，编写前端时，用这个就够了，但 API 请求需要弄成假的。&lt;/p&gt;
&lt;p&gt;而 5000 端口的服务器是 Flask 提供的，启用了&lt;code&gt;FLASK_ENV=development&lt;/code&gt;可以打开 Flask 的&lt;code&gt;DEBUG&lt;/code&gt;模式。它也能访问主页，但那是前端已经编译好的，不支持热重载哦。当然，Flask 支持 Python 文件热重载，现在知道专业的人干专业的事的道理了吧。区别总结如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;localhost:8080&lt;/th&gt;
&lt;th&gt;localhost:5000&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;能访问页面？&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;能访问 API？&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;热重载&lt;/td&gt;
&lt;td&gt;HTML/CSS/Javascript&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;更新静态文件&lt;/td&gt;
&lt;td&gt;刷新生效&lt;/td&gt;
&lt;td&gt;先&lt;code&gt;yarn run build&lt;/code&gt;，再强制刷新&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;还有，这两个服务器，都不能在生产环境使用哦。那么，能否同时获取这两个服务器的好处呢？当然是可以了，同时启动两个服务器，然后把 Flask 启动的那个 5000 服务器单纯作为 API 服务器，从 8080 端口访问页面。这时，API 请求的 URL 就与当前地址不同了，需要显式配置请求 URL 到 5000 端口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ...
const api = axios.create({
  baseURL: &quot;http://localhost:5000&quot;,
  headers: {
    &quot;Content-Type&quot;: &quot;application/json&quot;,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好，到现在为止，我们已经成功运行了一个可以持久化到服务器数据库的 Todo App，下篇文章我们将会加入更多功能，使得 App 更加像样。&lt;/p&gt;
</content:encoded></item><item><title>Python包管理工作流</title><link>https://frostming.com/posts/2018/09-14/python-packaging-flow/</link><guid isPermaLink="false">https://frostming.com/2018/09-14/python-packaging-flow/</guid><description>从pip到virtualenv到pipenv</description><pubDate>Fri, 14 Sep 2018 02:31:14 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;凡是一个成熟的软件生态都有它的软件源和对应的包管理工具，Python 也不例外，pip 就是它的（官方推荐的）包管理工具。可能很多小伙伴都对 pip 比较熟悉了，那么使用 pip 会有什么问题呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;使用 requirements.txt 管理依赖&lt;/h2&gt;
&lt;p&gt;pip 最普通的使用方法就是&lt;code&gt;pip install &amp;lt;package_name&amp;gt;&lt;/code&gt;，如果要指定版本，可以用&lt;code&gt;pip install &amp;lt;package_name&amp;gt;==&amp;lt;version&amp;gt;&lt;/code&gt;。如果你的应用中包含很多条依赖，可以把这些依赖都写在一个&lt;code&gt;requirements.txt&lt;/code&gt;文件中，就像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Django==1.11.2
requests&amp;gt;=2.11.0
simplejson
ordereddict
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中既有指定版本号的，也有忽略版本号的。然后你就可以用：&lt;code&gt;pip install -r requirements.txt&lt;/code&gt;来安装所有依赖。&lt;code&gt;requirements.txt&lt;/code&gt;本质上是一个纯文本文件，但它还支持其他特性，比如包含另一个&lt;code&gt;requirements.txt&lt;/code&gt;，这就使得模块化成为可能：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-r web.txt
-r secure.txt
simplejson
ordereddict
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有了内部 PyPI 镜像，安装依赖将会变得非常简单。那么问题来了：如果我想要升级依赖的版本呢？&lt;/p&gt;
&lt;p&gt;对于忽略版本号的依赖，当然没问题，全新安装时，自动会选择当前最新版本，但对于指定了版本号的，则需要手动更新这些版本号，然后重新安装。&lt;/p&gt;
&lt;h2&gt;使用虚拟环境&lt;/h2&gt;
&lt;p&gt;现在升级好了，一运行，你发现其他服务挂了，这是因为其他服务可能不兼容新版的依赖。这非常有可能发生，A 应用依赖 v1.0，B 应用依赖 v2.0，你一升级，A 就用不了了。如果把 A 应用和 B 应用环境独立，装两份不同版本的依赖，不就没问题了？没错，要达成这一目的，你可以装两份 Python，然后分别使用 Python 下的 pip 安装，就会安装到不同路径，运行应用时，指定不同的 Python 路径就可以了。但这样未免太过烦琐，于是 virtualenv 大展身手的时机来了。&lt;/p&gt;
&lt;p&gt;Virtulenv 会使用当前的 Python 解释器创建出一个虚拟环境，并把 Python 解释器拷贝一份到环境中，这个拷贝，比起编译安装一个新的会省不少资源。使用时，需要事先激活这个虚拟环境，把当前的 Python 指到这个环境中的 Python：&lt;/p&gt;
&lt;h3&gt;创建虚拟环境&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;$ virtualenv venv
...
$ cd venv
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;激活环境&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;$ source venv/bin/activate
(venv)$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后续的 pip 安装、启动应用，只要在这个虚拟环境中运行即可。也可以不激活，通过绝对路径使用它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ /home/frostming/myproject/venv/bin/python server.py
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pipenv: pip + virtualenv&lt;/h2&gt;
&lt;p&gt;有了虚拟环境，依赖冲突的问题解决了，但还有一个问题仍未解决：更新版本号，如果你想更新依赖包，对于那些在&lt;code&gt;requirements.txt&lt;/code&gt;中指定了版本号的依赖，你得逐个检查是否有新版，然后更新。既然如此麻烦，那是不是全都忽略版本号就好了？非也，这会产生新的问题。你在开发机上验证完毕了，部署到生产机上，或者别的小伙伴喜欢这个应用，想在自己的机器上跑。这时使用无版本号的&lt;code&gt;requirements.txt&lt;/code&gt;安装依赖，很可能安装的版本和你开发时不一样，结果导致应用不可用。&lt;/p&gt;
&lt;p&gt;但仔细分析，&lt;code&gt;requirements.txt&lt;/code&gt;中是否指定版本号，解决的是两个维度的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无版本号是为了方便你更新依赖时自动拉取最新版本。（A 型）&lt;/li&gt;
&lt;li&gt;有版本号是为了部署和开发时的环境完全一致。（B 型）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但&lt;code&gt;requirements.txt&lt;/code&gt;只有一份，手动维护两份&lt;code&gt;requirements&lt;/code&gt;成本又过高。于是 Pipenv 就应运而生，它可以从 A 型的&lt;code&gt;requirements.txt&lt;/code&gt;（Pipenv 使用了一种新的格式 Pipfile）生成 B 型的文件，称为 Pipfile.lock，锁定当前所有依赖的版本。部署时，从 Pipfile.lock 安装，这些理念，是从其他语言的包管理工具借鉴过来的。&lt;/p&gt;
&lt;p&gt;除此之外，Pipenv 还会帮你管理虚拟环境，不用自己创建。&lt;/p&gt;
&lt;p&gt;Pipenv 的一些主要的使用方法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;pipenv --two/--three&lt;/code&gt;：使用 Python 2 或 Python 3 创建一个虚拟环境并新建 Pipfile，它会探测系统中安装的所有 Python 并自动选择对应的 Python 版本。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pipenv install&lt;/code&gt;：从当前的 Pipfile 安装所有依赖&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pipenv install --deploy&lt;/code&gt;：从 Pipfile.lock 安装所有依赖，部署用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pipenv lock&lt;/code&gt;：从当前的 Pipfile 生成 Pipfile.lock&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pipenv install &amp;lt;package_name&amp;gt;&lt;/code&gt;：安装新的依赖包、添加到 Pipfile 中，并 lock&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pipenv update&lt;/code&gt;：使用最新可用版本更新 Pipfile.lock 并安装&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pipenv shell&lt;/code&gt;：激活虚拟环境的 shell&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pipenv run &amp;lt;command&amp;gt;&lt;/code&gt;：在不激活虚拟环境时运行虚拟环境中的命令&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其他用法参考文档：https://docs.pipenv.org/&lt;/p&gt;
</content:encoded></item><item><title>Flask+Nginx博客容器化部署</title><link>https://frostming.com/posts/2018/09-11/flask-nginx-deployment/</link><guid isPermaLink="false">https://frostming.com/2018/09-11/flask-nginx-deployment/</guid><description>Flask博客部署到云服务器完整流程</description><pubDate>Tue, 11 Sep 2018 03:56:26 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;2019.03.05 更新内容&lt;/strong&gt;
0x08 HTTPS&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2019.08.09 更新内容&lt;/strong&gt;
使用构建好的 Docker 镜像&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2019.12.04 更新内容&lt;/strong&gt;
更新.env 配置内容&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2020.05.19 更新内容&lt;/strong&gt;
简化部署步骤&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我是一个爱折腾的人，2016 年才开始学会自建博客，到现在博文没写多少篇却折腾了好几回。经历了 Hexo+GitHub Page，再到 Flask+Heroku，现在终于用上了国内云服务+Nginx，感觉速度快了很多。总结起来，使用 Flask+Nginx，好处有以下几个方面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可 DIY 程度高，现在我用的自己开发的 Markdown 引擎，非常方便扩展，在此推荐一下：&lt;a href=&quot;https://github.com/frostming/marko&quot;&gt;Marko&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;依靠 Nginx 强大的反向代理，现在我终于不用到处存图片然后贴一个巨长的 URL 了，直接映射到&lt;code&gt;/images/&lt;/code&gt;下，干净整洁。&lt;/li&gt;
&lt;li&gt;HTTPS 支持，可以用云服务购买的免费证书，也可以用 Letsencrypt，甚至可以用自签名证书。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我之前部署 Flask 的网站一直都用的 virtualenv，现在既然切到云服务器，就干脆换成用 Docker 了，隔离化程度更高，我也可以用现在最新版本的 Python 了。博客系统可拆分为三个部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Flask 应用，负责处理请求，是系统的核心&lt;/li&gt;
&lt;li&gt;数据库&lt;/li&gt;
&lt;li&gt;Nginx 服务器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;三个部分分别独立为一个容器。从一个全新的云服务器开始（以 Ubuntu Server 16.04.1 为例，其余系统类似），部署步骤如下：&lt;/p&gt;
&lt;h2&gt;0x00 添加用户&lt;/h2&gt;
&lt;p&gt;使用一个非 root 的用户是一个好习惯，需要自己添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# adduser fming
# echo &quot;fming   ALL=(ALL:ALL) NOPASSWD: ALL&quot; &amp;gt;&amp;gt; /etc/sudoers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后切换到 fming 用户登录&lt;/p&gt;
&lt;h2&gt;0x01 安装 Docker&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;$ curl -fsSL https://get.docker.com -o get-docker.sh
$ sudo sh get-docker.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此脚本将自动将 Docker CE 安装到系统上，若安装失败，可尝试&lt;a href=&quot;https://docs.docker.com/install/linux/docker-ce/ubuntu/#install-docker-ce&quot;&gt;其他安装方法&lt;/a&gt;。安装完成后，需添加当前用户到 docker 组：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo usermod -aG docker $USER
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;0x02 安装 Docker-compose&lt;/h2&gt;
&lt;p&gt;Docker-compose 是一款 Docker 的工具，它能让你高效管理多个容器，否则需要加一大堆选项到 Docker 命令后。它同样提供了一个一键安装脚本（&lt;a href=&quot;https://docs.docker.com/compose/install/#install-compose&quot;&gt;其他安装方法&lt;/a&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo curl -L &quot;https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m)&quot; -o /usr/local/bin/docker-compose
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;0x03 创建本地环境配置&lt;/h2&gt;
&lt;p&gt;在代码根目录下（与&lt;code&gt;docker-compose.yml&lt;/code&gt;同级）创建&lt;code&gt;.env&lt;/code&gt;文件，主要是数据库相关的环境变量，请自行填写缺失变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SECRET_KEY=&amp;lt;服务器密钥&amp;gt;
FLASK_APP=flaskblog.app
FLASK_MAIL_USERNAME=&amp;lt;发送邮件使用的用户名&amp;gt;
FLASK_MAIL_SERVER=&amp;lt;发送邮件的SMTP/IMAP地址&amp;gt;
FLASK_MAIL_PASSWORD=&amp;lt;发送邮件的密码&amp;gt;
FLASK_MAIL_SENDER=&amp;lt;显示名称 &amp;lt;邮箱地址&amp;gt;&amp;gt;

GITHUB_CLIENT_ID=&amp;lt;GitHub OAuth2 Client ID&amp;gt;
GITHUB_CLIENT_SECRET=&amp;lt;GitHub OAuth2 Client Secret&amp;gt;
GOOGLE_CLIENT_ID=&amp;lt;Google OAuth2 Client ID&amp;gt;
GOOGLE_CLIENT_SECRET=&amp;lt;Google OAuth2 Client Secret&amp;gt;
POSTGRES_USER=xxx
POSTGRES_PASSWORD=xxx
POSTGRES_DB=flog_db
DB_SERVICE=db
DATABASE_URL=postgresql+psycopg2://xxx:xxx@db:5432/flog_db
CERTBOT_EMAIL=&amp;lt;Your Email Address&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;db&lt;/code&gt;就可以指代数据库容器的服务地址了。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;注意：&lt;code&gt;.env&lt;/code&gt;和&lt;code&gt;./nginx/cert&lt;/code&gt;（证书目录）不可提交到版本控制平台上。&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;0x04 构建静态文件&lt;/h2&gt;
&lt;p&gt;博客的后台部分用到了 Vue.js + ElementUI，需要构建静态文件，使用起来也很简单：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ cd static
$ npm i
$ npm run build:prod
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;0x05 Nginx 配置&lt;/h2&gt;
&lt;p&gt;在上一节的配置中可以看到我把 Nginx 的配置文件映射到了&lt;code&gt;./nginx/conf.d/default.conf&lt;/code&gt;中。编辑该文件，修改&lt;code&gt;servername&lt;/code&gt;为你的域名，ssl 开头的部分暂时不变，我们会在后面修改它。配置的一些说明：&lt;/p&gt;
&lt;h3&gt;主站&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;location / {
    proxy_pass http://web:5000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置静态文件缓存&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;proxy_temp_path /tmp/temp_dir;
proxy_cache_path /tmp/cache levels=1:2 keys_zone=mycache:100m inactive=1d max_size=10g;
server {
...
location /static {
    alias /opt/static;
    proxy_set_header Host $host;
    proxy_cache mycache;
    expires 30d;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我之前已经把静态文件映射到 Nginx 容器中的&lt;code&gt;/opt/static&lt;/code&gt;了。&lt;/p&gt;
&lt;h2&gt;0x06 启动容器&lt;/h2&gt;
&lt;p&gt;好了，万事俱备，现在可以启动容器了！转到仓库所在目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ docker-compose up --build -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拉取镜像，构建镜像，启动容器，一条命令足矣！一切都没有问题的话，你的网站已经跑起来了。&lt;/p&gt;
&lt;p&gt;请参考此&lt;a href=&quot;https://github.com/frostming/Flog&quot;&gt;博客的 GitHub&lt;/a&gt;获取完整配置。&lt;/p&gt;
&lt;p&gt;现在，你的博客已经启用 HTTPS 了，地址栏前面会出现一个锁标志，可以到&lt;a href=&quot;https://www.ssllabs.com/ssltest/index.html&quot;&gt;Qualys SSL Labs&lt;/a&gt;检测你的网站安全分数。
&lt;img src=&quot;//webp.frostming.com/images/2019-03-ssl-score.png&quot; alt=&quot;我的是A+&quot; title=&quot;我的是A+&quot; /&gt;&lt;/p&gt;
&lt;p&gt;更多阅读：&lt;a href=&quot;https://ksmx.me/letsencrypt-ssl-https/&quot;&gt;LET&apos;S ENCRYPT 给网站加 HTTPS 完全指南&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;0x07 初始化博客&lt;/h2&gt;
&lt;p&gt;第一次启动博客，请先到 &lt;code&gt;&amp;lt;your domain&amp;gt;/admin/#/settings&lt;/code&gt;设置你的博客。&lt;/p&gt;
</content:encoded></item><item><title>五笔已经落伍了吗？</title><link>https://frostming.com/posts/2018/07-31/wubi-input/</link><guid isPermaLink="false">https://frostming.com/2018/07-31/wubi-input/</guid><pubDate>Tue, 31 Jul 2018 08:59:17 GMT</pubDate><content:encoded>&lt;p&gt;有很多次，小伙伴到我的工位上帮忙解决问题，都会惊讶地问：「你用的是五笔啊！」这时我是自豪夹带着窘迫的复杂心理。自豪的是现在还在用五笔打字的应该很少了吧，窘迫的是这样小伙伴就不方便用我的键盘了。因为知乎上的一篇回答：&lt;a href=&quot;https://www.zhihu.com/question/20339084/answer/388152574&quot;&gt;五笔输入法落伍了吗？最终会消亡吗？&lt;/a&gt;，让我想写点关于五笔的故事。&lt;/p&gt;
&lt;p&gt;学习五笔，在上世纪九十年代末，是很时髦的事情，现在大多会五笔的，都是直接或间接受了那个时代的影响。有一阵，城里的上班族流行去上计算课，教的不是编程，是五笔打字，因为个人电脑刚刚兴起，会打字是个可以吃饭的技能。我妈作为一名人民教师，就是其中的一员。而且也就是为了练习打字，家里才痛快同意买了小霸王学习机。因为小霸王学习机的键盘上，是印着五笔字根的！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//webp.frostming.com/images/018-07-xiaobawang.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以小霸王学习机，真的是用来学习的！出于对小霸王学习机的热爱，我也开始学习打字了，因为，用小霸王学打字，是不会被骂的！当时小霸王自带的打字游戏里，都是五笔字根练习。天空中飘着一个个气球，编码正确就会被击破。所以我根本不需要背什么字根口诀，通过游戏学习，进展神速。一级字根练了没多久就失去挑战了，然后是二级字根，词组等。这时候我的打字水平，已经超过了我妈了。也不知为什么，我能坐在那录入考试卷坐一下午，不是为了打印，就是为了练习。可能这是唯一一个可以在大人面前，挨在小霸王跟前的方法了吧。也是因为童年没什么小伙伴，否则我能坐得住，他们早跳脚了。&lt;/p&gt;
&lt;p&gt;输入法，还真不能在实际使用时现学。等到网吧普及，QQ 兴起，大家就没有闲暇专门学一个有相当门槛的输入法。汉语拼音谁没学过，但你和我说王旁青头戋五一，那是个什么玩意？&lt;/p&gt;
&lt;p&gt;上面这段话，放在喵小姐身上是不成立的。她真的能在短时间内，把「第一次上网吧」「第一次用 QQ」「学习用五笔」这几件事，一口气完成了。也不知道最初她是怎么选择了五笔，毕竟和她同去的小伙伴，都是用拼音的啊。那时高考刚结束，她为了学习五笔，还向我请教，而那时我们还压根不熟啊，后来的事情，大家都知道了。我们还专门较量过打字的速度，不得不说，她的速度比我快，但我的优势在于多年的经验和对拆字法的熟悉。五笔的拆字法真的有点诡异的，「牛」独立成字，和做偏旁的时候，编码是不一样的！还有「拜」这种每次都要试很多种拆字法的字，我就没少掉进这些坑。&lt;/p&gt;
&lt;p&gt;得亏五笔学得早，现在让我学，我是肯定学不会的了，一个东西顺手以后，再去接触全新的，会相当痛苦。在智能手机上我用的是拼音，一是实在没习惯用全键盘，二是拼音输入法确实很强的。五笔输入法就差在这方面了，热词更新，强大的联想，要是开发者有精力像搜狗拼音一样做个五笔输入法，也会非常好用的吧。我觉得五笔相对拼音，有这么几点好处：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不超过四码上屏。&lt;/li&gt;
&lt;li&gt;错字率非常低，你肯定见过满屏谐音错字的文章，这如果用五笔是绝不可能的。&lt;/li&gt;
&lt;li&gt;疑难杂字，你要是不会读，用拼音就打不出来（当然现代输入法可以），但用五笔，你就可以打出来到百度搜索。&lt;/li&gt;
&lt;li&gt;通过结构，加深汉字的记忆，有助于缓解提笔忘字的问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;综上，五笔简直是为汉字而生的输入法，感谢致敬王永民先生。&lt;/p&gt;
</content:encoded></item><item><title>Python packaging war: Pipenv vs. Poetry</title><link>https://frostming.com/posts/en/2018/pipenv-vs-poetry/</link><guid isPermaLink="false">https://frostming.com/en/2018/pipenv-vs-poetry/</guid><pubDate>Tue, 15 May 2018 03:24:36 GMT</pubDate><content:encoded>&lt;p&gt;This is my second post about Python packaging. In the &lt;a href=&quot;/2017/11-19/python-packaging&quot;&gt;last post&lt;/a&gt;, I regarded &lt;strong&gt;npm&lt;/strong&gt; as my ideal packaging management tool because I had limited experience about other tools in other languages. Honestly saying, &lt;strong&gt;npm&lt;/strong&gt; is never perfect with many drawbacks in its own, but it also has many things we can learn from.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pipenv&lt;/strong&gt;, brought to the community again by &lt;a href=&quot;https://kennethreitz.org&quot;&gt;Kenneth Reitz&lt;/a&gt; on &lt;a href=&quot;https://www.youtube.com/watch?v=GBQAKldqgZs&amp;amp;feature=youtu.be&quot;&gt;PyCon 2018&lt;/a&gt;, which is also mentioned in the last post, is more than 1 year old since it was born. In the past months I have becomed a contributor of the project, during which time I gained more understanding of its philosophy and design purpose. As explained &lt;a href=&quot;https://docs.pipenv.org/advanced/#pipfile-vs-setuppy&quot;&gt;here&lt;/a&gt;, &lt;strong&gt;Pipenv&lt;/strong&gt; is designed for application deps management, rather than libraries. You still have to maintain a &lt;code&gt;setup.py&lt;/code&gt; file besides &lt;code&gt;Pipfile&lt;/code&gt; to serve as a library configuration file. From my daily experience of using this amazing project, it handles all the messing stuff of virtualenvs and installation, which saves lots of lines in &lt;code&gt;README.md&lt;/code&gt;, but it&apos;s far from perfect regarding the issue number in its issue tracker.&lt;/p&gt;
&lt;p&gt;Then I encountered a much younger project -- &lt;strong&gt;Poetry&lt;/strong&gt;, which is only 3 months old with less than 600 starts on &lt;a href=&quot;https://github.com/sdispater/poetry&quot;&gt;GitHub repo&lt;/a&gt;(compare to 11000+ stars of &lt;strong&gt;Pipenv&lt;/strong&gt;). &lt;strong&gt;Poetry&lt;/strong&gt; uses &lt;a href=&quot;https://www.python.org/dev/peps/pep-0518/&quot;&gt;standardized&lt;/a&gt; &lt;code&gt;pyproject.toml&lt;/code&gt; instead of customized &lt;code&gt;Pipfile&lt;/code&gt; as the project deps configuration file. It&apos;s more like a &lt;code&gt;packaging.json&lt;/code&gt; as in Javascript&apos;s packaging world in following ways:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It can serve for both applications and libraries, depending on whether you do &lt;em&gt;upload&lt;/em&gt; or not.&lt;/li&gt;
&lt;li&gt;Packages are prefered to be installed with non-wildcard version, with support of &lt;a href=&quot;https://poetry.eustace.io/docs/versions/&quot;&gt;multiple version specifiers&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Poetry&lt;/strong&gt; does a lot of work on deps resolution and packaging, so that &lt;code&gt;pyproject.toml&lt;/code&gt; can &lt;strong&gt;replace&lt;/strong&gt; &lt;code&gt;setup.py&lt;/code&gt;, it is monolithic. While &lt;strong&gt;Pipenv&lt;/strong&gt; is more like a wrapper built on top of pip and virtualenv(or pew). Kenneth Reitz is very good at adopting amazing tools and merge them together to be a project really powerful and easy to use(same as &lt;a href=&quot;https://pypi.org/p/requests-html&quot;&gt;requests-html&lt;/a&gt;), but SDispater, with his &lt;strong&gt;Poetry&lt;/strong&gt;, in my honest opinion, is making Python packaging much different.&lt;/p&gt;
&lt;p&gt;I am not putting down any one and raising the other, but the current status is that &lt;strong&gt;Pipenv&lt;/strong&gt; is more exposed to the community with KR&apos;s fame and great talks, and it&apos;s also adopted by PyPA. But PyPA is never an authority though the word is in its name, and standard may change. For the long time of view, let the large community choose the winner, but before it&apos;s finalized, there must be a &lt;a href=&quot;https://www.reddit.com/r/Python/comments/8jd6aq/why_is_pipenv_the_recommended_packaging_tool_by/&quot;&gt;war&lt;/a&gt; between these two tools.&lt;/p&gt;
&lt;p&gt;&amp;lt;div className=&quot;alert alert-info&quot;&amp;gt;&lt;/p&gt;
&lt;h4&gt;Update&lt;/h4&gt;
&lt;p&gt;This post was written without an intensive review of the two tools and was likely to be biased. After having been the maintainer of Pipenv for months, I wrote a &lt;a href=&quot;/2019/01-04/pipenv-poetry&quot;&gt;new post&lt;/a&gt; with a deeper look into these tools.&lt;/p&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>西方的侠</title><link>https://frostming.com/posts/2018/05-06/monte-christo/</link><guid isPermaLink="false">https://frostming.com/2018/05-06/monte-christo/</guid><description>《基督山伯爵》读后感</description><pubDate>Sun, 06 May 2018 14:04:40 GMT</pubDate><content:encoded>&lt;p&gt;百万字的《基督山伯爵》，终于读完了，作者以其高超的叙事技巧，将一个庞大架构的故事，讲得引人入胜。基督山伯爵的高超的智慧，缜密的思维和其独特的人格魅力，深深地映在我脑中，不能释怀。&lt;/p&gt;
&lt;p&gt;在我看来，基督山伯爵就像武侠小说中的一位大侠。同样是剧作者出身的大仲马，同样的以真实历史为背景，让我不禁把这部小说与金庸的武侠相比较。主人公初时身陷囹圄，却机缘巧合受到高人指点，得到一身绝学。恩怨分明，赏罚决断，隐忍刚毅，智慧高绝。他初时隐忍，一朝崛起，就卷起风雷，就像郭襄生日那天的神雕大侠，受万人瞩目，把昔日仇敌，那些曾经践踏过他的人，一个个都踩在脚下，着实大快人心。&lt;/p&gt;
&lt;p&gt;在最开始，埃德蒙‧唐戴斯就是这么一位天真的人，直到狱中的神甫为他点明这一些，点明他遭此厄运的原因，让他领悟这黑暗的世界。从此之后，他的任督二脉就被打通了，练就了一双在黑暗中视物的火眼金睛，此后将无往不利。他制定周密的计划，每一个点都精确无比，不早不晚，就在敌人最薄弱的地方出击。他又不动声色，隐身于黑暗中，谈笑间卷起惊涛骇浪。他其实什么也没做，只是挑起了敌人的罪恶之魂，让他们自我毁灭，简直是杀人不见血的典范。每一出报复，都精致地像个艺术品，自己却挥一挥衣袖，不沾一滴血，潇洒又狠决。然而他也不是一尊石佛，他也会流露感情：他做完好事，虚荣地要求别人记住自己；他面对昔日爱人，痛苦地决定牺牲自己和一切计划；面对复仇，他又怀疑自己是否正确。&lt;/p&gt;
&lt;p&gt;这样一个主人公，没法不让人仰慕倾心。他俨然一个遗世大侠，黑色披风，孤傲绝立。写到这，我又联想起另一个角色来——V，于是把他作为头图。&lt;/p&gt;
&lt;p&gt;PS：推荐译林出版周克希译本，既不过度中化显得违和，又保留了中文那独特的语言的力量，大师译作。&lt;/p&gt;
</content:encoded></item><item><title>新域名!</title><link>https://frostming.com/posts/2018/03-13/new-domain/</link><guid isPermaLink="false">https://frostming.com/2018/03-13/new-domain/</guid><pubDate>Tue, 13 Mar 2018 13:28:52 GMT</pubDate><content:encoded>&lt;p&gt;正式启用新域名，SSL enabled!旧域名已重定向。&lt;/p&gt;
&lt;p&gt;https://frostming.com&lt;/p&gt;
&lt;p&gt;折腾好久，disqus 迁移，Google 迁移。同时也踩了各种坑，&lt;a href=&quot;https://github.com/heroku/heroku-buildpack-python/issues/654&quot;&gt;heroku build pack 的坑&lt;/a&gt;，&lt;a href=&quot;https://github.com/flask-admin/flask-admin/issues/1583&quot;&gt;flask-admin 的坑&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;人生在世，哪里没坑。Bug 处处存在，就算是大厂。&lt;/p&gt;
</content:encoded></item><item><title>Codingame本周谜题「折纸曲线」解</title><link>https://frostming.com/posts/2018/02-11/paper-folding-curve/</link><guid isPermaLink="false">https://frostming.com/2018/02-11/paper-folding-curve/</guid><pubDate>Sun, 11 Feb 2018 04:52:16 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://www.codingame.com/ide/10137678f93219a29269cdd1ef831d9436717c7e&quot;&gt;Codingame.com Puzzle of the Week: Paper folding curve&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目简述&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;一张纸，一直保持向同一个方向对折，然后将折痕展成直角。从侧面看去，纸的折角会构成一个折线。&lt;/p&gt;
&lt;p&gt;从纸的一端出发到另一端，每碰到一个折角，用 1 表示向左折，0 表示向右折，则所有折角会构成一个 1 与 0 组成的序列，称为折叠序列。比如折一次是 1，折两次是 110，依次类推。&lt;/p&gt;
&lt;p&gt;给定折叠次数，要求输出折叠序列中自 s 至 e 号元素（0-index，均包含）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;朴素解法&lt;/h2&gt;
&lt;p&gt;首先，不妨假定我们每次都向左对折，因为如果是向右，我们可以从另一侧观察，则还是向左对折。然后我们来研究下序列的特点。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对折$n$次将产生$2^n$小段的纸，$2^n-1$个折痕。也说是说序列长度为$2^n-1$，不妨在第 1 位补个 1，序列变成&lt;strong&gt;1-index&lt;/strong&gt;，共有$2^n$个元素。&lt;/li&gt;
&lt;li&gt;考察对折点，对折点之前的纸和对折点之后的纸走向完全一致，即&lt;strong&gt;序列关于对折点对称&lt;/strong&gt;，但由于先进方向相反，原来是向左的对称之后变成向右。所以准确说应该是&lt;strong&gt;序列关于对折点反向对称&lt;/strong&gt;
&lt;img src=&quot;//webp.frostming.com/images/raph.jpg&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;** 序列与对折次数无关**。观察上图，折叠 2 次的折痕是 ①②③，而再折叠一次，序列 ①②③④⑤⑥⑦ 的前三项将与折叠两次的一致。就好像把纸延长了相同的长度，再把后续的折痕加在原序列后面。所以题目中「给定折叠次数」是用不上的。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;由 1 及题设，第$2^n$个元素值恒为 1。由 1+2，序列的对折点所在是其实是第$2^{n-1}$个元素。又由 2+3，序列关于$2^{n-1}$号元素对称，左半部分又关于$2^{n-2}$号元素对称，依次类推。转换成数学语言，用$f(i)$表示序列中第$i$号元素的值，则：&lt;/p&gt;
&lt;p&gt;$$
\begin{equation}
\begin{split}
f(2^n)=1,,&amp;amp;n=0,1,\ldots \\
f(2^n+x)=\sim f(2^n-x),,&amp;amp;0&amp;lt;x&amp;lt;2^n,,n&amp;gt;1
\end{split}
\end{equation}
$$&lt;/p&gt;
&lt;p&gt;其中&quot;~&quot;表示将值取反。所以如果我们已经得到了序列前$2^n$项的值，我们就能根据对称性得到$2^{n+1}$项的值，写成伪代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 写出前7项
result = &apos;1101100&apos;
while len(result) &amp;lt; e + 1
    result += &apos;1&apos; + reverse_and_flip(result)
end
print(result[s:e])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度为$8+16+\cdots=O(N)$，其中$N=2^n$为全序列长度。提交运行，前面都能 PASS，但当 s, e 都非常大时挂了，果然还是太朴素。&lt;/p&gt;
&lt;h2&gt;优化解法&lt;/h2&gt;
&lt;p&gt;我一直在思考：序列某个元素的值由前面的某个值得到，而前面的某个值又由更前面的某个值得到，最后的种子其实就是最开始的几个元素。我们浪费了很多时间在生成不需要输出的结果上。本能觉得，对某个特定元素，可能可以仅通过它的序号，在$O(1)$的复杂度上得到值。说干就干，继续找找规律。由上述的公式，我进一步发展，当$x\neq2^{n-1}$时，&lt;/p&gt;
&lt;p&gt;$$
\begin{equation}
\begin{split}
f(2^n+x)&amp;amp;=\sim f(2^n-x) \\
&amp;amp;=\sim f(2^{n-1}+(2^{n-1}-x)) \\
&amp;amp;=f(2^{n-1}-(2^{n-1}-x))=f(x)
\end{split}
\end{equation}
$$&lt;/p&gt;
&lt;p&gt;当$x=2^{n-1}$时，&lt;/p&gt;
&lt;p&gt;$$f(2^n+x)=\sim f(2^{n-1})=0$$&lt;/p&gt;
&lt;p&gt;Exciting! 如果我们把元素序号用二进制表示的话，通过以上方法，我们就能使序号快速缩小（去掉首位的 1）到一个很小的数，进而得到它的值。具体地：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数字除去末尾的 0 后，如果是以连续的 1 结尾，则值为 0&lt;/li&gt;
&lt;li&gt;否则，值为 1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;伪代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;result = &apos;&apos;
// 修正0 - index到1 - index
for i in [s + 1:e + 1]
    // i是2的幂，快速返回
    if i &amp;amp; (i - 1) == 0
        result += &apos;1&apos;
            break
    end
    // 去掉末位的0
    while i &amp;amp; 1 == 0
        i &amp;gt;&amp;gt;= 1
    end
    if i &amp;amp; 3 == 3   // 11结尾
        result += &apos;0&apos;
    else            // 01结尾
        result += &apos;1&apos;
    end
end

print(result)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度近似为$O(e-s)$。&lt;/p&gt;
&lt;p&gt;一开始从一个实际问题出发，最后得出了一个简洁的数学表达，果然是代码令我愉快。完整代码(Python)见&amp;lt;br/&amp;gt;
https://github.com/frostming/Codingame/blob/master/Puzzle-of-the-Week/paper_folding_curve.py&lt;/p&gt;
</content:encoded></item><item><title>[译]Python正则表达式拾珠</title><link>https://frostming.com/posts/2018/02-06/python-hidden-regexp/</link><guid isPermaLink="false">https://frostming.com/2018/02-06/python-hidden-regexp/</guid><description>Python&apos;s Hidden Regular Expression Gems</description><pubDate>Tue, 06 Feb 2018 09:08:14 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;原文作者：Armin Ronacher &lt;br /&gt;
原文链接：http://lucumr.pocoo.org/2015/11/18/pythons-hidden-re-gems/&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Python 标准库中有很多非常恶心的模块，但 Python 的&lt;code&gt;re&lt;/code&gt;模块不是其中之一。虽然它已经很老了而且多年未更新，它仍是我认为的众多动态语言中最好的（正则表达式模块）。&lt;/p&gt;
&lt;p&gt;对这个模块，我经常能发现有趣的东西。Python 是少有的几个，本身没有集成正则表达式的动态语言之一。虽然缺少解释器的语法支持，但从纯粹的 API 角度来说，它弥补了核心系统设计的缺憾。而同时它又非常奇特。比如它的解析器是用纯 Python 实现的，你如果去追踪它的导入过程，会发现一些奇怪的事：它把 90%的时间都花在一个&lt;code&gt;re&lt;/code&gt;的支持模块上了。&lt;/p&gt;
&lt;h2&gt;久经考验&lt;/h2&gt;
&lt;p&gt;Python 的正则表达式模块很早就存在标准库之中了。先不说 Python 3，从它有的那天起，除了中途加入了 unicode 的基础支持，就基本没变过了。直到今天（译注：本文作于 2015.11.8），它的成员枚举还是错的（对一个正则表达式的 pattern 对象使用&lt;code&gt;dir()&lt;/code&gt;看看）。&lt;/p&gt;
&lt;p&gt;然而，老模块的好处是不同的 Python 版本都一样，非常可靠。我从未因为正则表达式模块的改动而调整任何东西。对于我这种要写很多正则表达式的人来说，这是个好消息。&lt;/p&gt;
&lt;p&gt;它的设计中有个有趣的特点：它的解析器和编译器是用 Python 写的，而匹配器是用 C 写的。只要你想，你能跳过正则解析，直接把解析器的内部结构传给编译器。这没有包含在文档中，但这是可行的。&lt;/p&gt;
&lt;p&gt;除此之外，正则表达式系统中还有很多东西未见于文档或文档不足。所以我希望给大家举例说明为什么 Python 的正则表达式模块这么酷。&lt;/p&gt;
&lt;h2&gt;迭代匹配&lt;/h2&gt;
&lt;p&gt;毫无疑问，Python 正则表达式系统的最强特性之一，就是它严格区分匹配和搜索。这在其他正则表达式引擎中并不多见。具体来说，你在进行匹配时能提供一个索引值作为偏移量，匹配将基于该位置进行。&lt;/p&gt;
&lt;p&gt;具体地，这意味着你能做类似下面的事情：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; pattern = re.compile(&apos;bar&apos;)
&amp;gt;&amp;gt;&amp;gt; string = &apos;foobar&apos;
&amp;gt;&amp;gt;&amp;gt; pattern.match(string) is None
True
&amp;gt;&amp;gt;&amp;gt; pattern.match(string, 3)
&amp;lt;_sre.SRE_Match object at 0x103c9a510&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这极大地有助于实现一个语法分析器，因为你能继续使用&lt;code&gt;^&lt;/code&gt;来标明字符串的起始位置，只需要增加索引值就可以进行后续的匹配。这也意味着我们不需要自己对字符串进行切片，节省了大量内存开销和字符串拷贝操作（Python 对此并不是特别在行）。&lt;/p&gt;
&lt;p&gt;除了匹配之外，Python 还能进行搜索，它会一直向后寻找直到找到匹配字符串：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; pattern = re.compile(&apos;bar&apos;)
&amp;gt;&amp;gt;&amp;gt; pattern.search(&apos;foobar&apos;)
&amp;lt;_sre.SRE_Match object at 0x103c9a578&amp;gt;
&amp;gt;&amp;gt;&amp;gt; _.start()
3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;不匹配也是一种匹配&lt;/h2&gt;
&lt;p&gt;一个常见的问题是，如果没有匹配的字符串，会对 Python 造成很大的负担。思考下实现一个类似百科语言的分词器（比如说 markdown）。在表示格式的标识符之间，有很长的文字也需要处理。所以匹配标识符之间时，一直在寻找是否有别的标识符也需要处理。如何跳过这一过程呢？&lt;/p&gt;
&lt;p&gt;一种方法是编译一些正则表达式，放在一个列表中，再逐一检查。如果一个都不匹配则跳过一个字符：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rules = [
    (&apos;bold&apos;, re.compile(r&apos;\*\*&apos;)),
    (&apos;link&apos;, re.compile(r&apos;\[\[(.*?)\]\]&apos;)),
]

def tokenize(string):
    pos = 0
    last_end = 0
    while 1:
        if pos &amp;gt;= len(string):
            break
        for tok, rule in rules:
            match = rule.match(string, pos)
            if match is not None:
                start, end = match.span()
                if start &amp;gt; last_end:
                    yield &apos;text&apos;, string[last_end:start]
                yield tok, match.group()
                last_end = pos = match.end()
                break
        else:
            pos += 1
    if last_end &amp;lt; len(string):
        yield &apos;text&apos;, string[last_end:]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这不是一个优雅的解决方案，也不是很快速。不匹配的字符串越多，过程就越慢，因为每次只前进一个字符，这个循环是在 Python 解释器里的，处理过程也相当不灵活。对每个标识符我们只得到了匹配的字符串，如果需要加入分组就要进行一点扩展。&lt;/p&gt;
&lt;p&gt;有没有更好的方法呢？有没有可能我们能告诉正则表达式引擎，我希望它只扫描若干正则式中的任意一个？&lt;/p&gt;
&lt;p&gt;事情开始变得有趣了，这就是我们用子模式&lt;code&gt;(a|b)&lt;/code&gt;时本质上在做的事。引擎会搜索&lt;code&gt;a&lt;/code&gt;和&lt;code&gt;b&lt;/code&gt;其中之一。这样我们就能用已有的正则表达式构造一个巨大的表达式，然后再用它去匹配。这样不好的地方在于所有分组都加入进来以后非常容易把人搞晕。&lt;/p&gt;
&lt;h2&gt;初探 Scanner&lt;/h2&gt;
&lt;p&gt;有意思的来了，在过去的 15 年中，正则表达式中一直存在一个没有文档的功能：Scanner。scanner 是内置的 SRE 模式对象的一个属性，引擎通过扫描器，在找到一个匹配后继续找下一个。甚至还有一个&lt;code&gt;re.Scanner&lt;/code&gt;类（也没有文档），它基于 SRE 模式 scanner 构造，提供了一些更高一层的接口。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;re&lt;/code&gt;模块中的 scanner 对于提升「不匹配」的速度并没有多少帮助，但阅读它的源码能告诉我们它是如何实现的：基于 SRE 的基础类型。&lt;/p&gt;
&lt;p&gt;它的工作方式是接受一个正则表达式的列表和一个回调元组。对于每个匹配调用回调函数然后以此构造一个结果列表。具体实现上，它手动创建了 SRE 的模式和子模式对象（大概地说，它构造了一个更大的正则表达式，且不需要解析它）。有了这个知识，我们就能进行以下扩展：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sre_parse import Pattern, SubPattern, parse
from sre_compile import compile as sre_compile
from sre_constants import BRANCH, SUBPATTERN

class Scanner(object):

    def __init__(self, rules, flags=0):
        pattern = Pattern()
        pattern.flags = flags
        pattern.groups = len(rules) + 1

        self.rules = [name for name, _ in rules]
        self._scanner = sre_compile(SubPattern(pattern, [
            (BRANCH, (None, [SubPattern(pattern, [
                (SUBPATTERN, (group, parse(regex, flags, pattern))),
            ]) for group, (_, regex) in enumerate(rules, 1)]))
        ])).scanner

    def scan(self, string, skip=False):
        sc = self._scanner(string)

        match = None
        for match in iter(sc.search if skip else sc.match, None):
            yield self.rules[match.lastindex - 1], match

        if not skip and not match or match.end() &amp;lt; len(string):
            raise EOFError(match.end())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如何使用呢？像下面这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scanner = Scanner([
    (&apos;whitespace&apos;, r&apos;\s+&apos;),
    (&apos;plus&apos;, r&apos;\+&apos;),
    (&apos;minus&apos;, r&apos;\-&apos;),
    (&apos;mult&apos;, r&apos;\*&apos;),
    (&apos;div&apos;, r&apos;/&apos;),
    (&apos;num&apos;, r&apos;\d+&apos;),
    (&apos;paren_open&apos;, r&apos;\(&apos;),
    (&apos;paren_close&apos;, r&apos;\)&apos;),
])

for token, match in scanner.scan(&apos;(1 + 2) * 3&apos;):
    print (token, match.group())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的代码中，当不能解析一段字符时，将会抛出&lt;code&gt;EOFError&lt;/code&gt;，但如果你加入&lt;code&gt;skip=True&lt;/code&gt;，则不能解析的部分将会被跳过，这对于实现像百科解析器的东西来说非常完美。&lt;/p&gt;
&lt;h2&gt;扫描空位&lt;/h2&gt;
&lt;p&gt;我们在跳过时可以使用&lt;code&gt;match.start()&lt;/code&gt;和&lt;code&gt;match.end()&lt;/code&gt;来查看哪一部分被跳过了。所以第一个例子可以改为如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scanner = Scanner([
    (&apos;bold&apos;, r&apos;\*\*&apos;),
    (&apos;link&apos;, r&apos;\[\[(.*?)\]\]&apos;),
])

def tokenize(string):
    pos = 0
    for rule, match in self.scan(string, skip=True):
        hole = string[pos:match.start()]
        if hole:
            yield &apos;text&apos;, hole
        yield rule, match.group()
        pos = match.end()
    hole = string[pos:]
    if hole:
        yield &apos;text&apos;, hole
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;解决分组问题&lt;/h2&gt;
&lt;p&gt;还有一个很烦人的问题：分组的序号不是基于原来的正则表达式而是基于组合之后的。这会导致如果你有一个&lt;code&gt;(a|b)&lt;/code&gt;的规则，用序号来引用这个分组会得到错误的结果。我们需要一些额外的工作，在 SRE 的匹配对象上包装一个类，改变它的序号和分组名。如果你对这个感兴趣我已经在一个&lt;a href=&quot;https://github.com/mitsuhiko/python-regex-scanner&quot;&gt;github 仓库&lt;/a&gt;中基于以上方案实现了一个更加复杂的版本，包括了一个匹配包装类和一些例子来告诉你怎么用。&lt;/p&gt;
</content:encoded></item><item><title>随想</title><link>https://frostming.com/posts/2018/01-09/handnote/</link><guid isPermaLink="false">https://frostming.com/2018/01-09/handnote/</guid><pubDate>Tue, 09 Jan 2018 11:12:39 GMT</pubDate><content:encoded>&lt;p&gt;每天白天都被工作撑得很满。晚上回家还得 Coursera，一天就这么过去了。&lt;/p&gt;
&lt;p&gt;Python 还是 codecademy 学的，现在又在自学 Machine Learning。没办法，Python 看似很火，实则快过气了，都靠 AI 在撑着。我看着旁边才入门的 Golang，琢磨着自己为啥非要削尖了脑袋往码农这靠。&lt;/p&gt;
&lt;p&gt;一定要坚持学习，坚持提升自己。都没时间刷知乎了[破涕为笑]。&lt;/p&gt;
&lt;p&gt;也想过尝试不同的工作，尝试新鲜的事情。&lt;/p&gt;
&lt;p&gt;自从美国回来以后左眼视力下降很多，坐在后面都看不清演示了。美国办公环境虽好，没有适应我的人体。所幸本来左眼视力高出右眼不少，这下重新配镜也不用那么尴尬的两镜片了。&lt;/p&gt;
&lt;p&gt;看来还是要舍得给自己花钱，你穿个特步搞不好会被人嫌。&lt;/p&gt;
&lt;p&gt;$$\frac12=1.2$$&lt;/p&gt;
&lt;p&gt;第一次手机写博，于缓行的 36 路公交上&lt;/p&gt;
</content:encoded></item><item><title>Bye 2017, hello 2018</title><link>https://frostming.com/posts/2018/01-04/from-2017-to-2018/</link><guid isPermaLink="false">https://frostming.com/2018/01-04/from-2017-to-2018/</guid><description>年度照片和新年计划</description><pubDate>Thu, 04 Jan 2018 13:48:17 GMT</pubDate><content:encoded>&lt;h2&gt;2017 年的十张照片&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;//webp.frostming.com/images/2017-DSC_5512-edited.jpg&quot; alt=&quot;&quot; /&gt; &lt;img src=&quot;//webp.frostming.com/images/2017-DSC_6028-edited.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;//webp.frostming.com/images/2017-DSC_6643-edited.jpg&quot; alt=&quot;&quot; /&gt; &lt;img src=&quot;//webp.frostming.com/images/2017-DSC_7170-edited.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;//webp.frostming.com/images/2017-DSC_7426-edited.jpg&quot; alt=&quot;&quot; /&gt; &lt;img src=&quot;//webp.frostming.com/images/2017-DSC_7837-edited.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;//webp.frostming.com/images/2017-DSC_8465-edited.jpg&quot; alt=&quot;&quot; /&gt; &lt;img src=&quot;//webp.frostming.com/images/2017-DSC_8506-edited.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;//webp.frostming.com/images/2017-DSC_9195-edited.jpg&quot; alt=&quot;&quot; /&gt; &lt;img src=&quot;//webp.frostming.com/images/2017-DSC_9305-edited.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;新年计划&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;学习完 Machine Learning 的 Coursera 课程，学习 Deep Learning 课程&lt;/li&gt;
&lt;li&gt;入门 Node.js&lt;/li&gt;
&lt;li&gt;入门 Vue.js 和 webpack 相关前端技术&lt;/li&gt;
&lt;li&gt;去日本，去一次西北&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>What&apos;s the problem about Python packaging?</title><link>https://frostming.com/posts/en/2017/python-packaging/</link><guid isPermaLink="false">https://frostming.com/en/2017/python-packaging/</guid><pubDate>Sun, 19 Nov 2017 22:25:38 GMT</pubDate><content:encoded>&lt;p&gt;I have been using Python as my primary programming language for 4 years. It is elegant, easy coding and reading. I didn&apos;t notice the discomfort until I came to know about &lt;a href=&quot;https://nodejs.org&quot;&gt;Node.js&lt;/a&gt;. All you need to do to install all the dependencies is a single command &lt;code&gt;npm install&lt;/code&gt;. If you want to install another library to your project, just install it with npm, and append &lt;code&gt;--save&lt;/code&gt; to save it in the config file, usually &lt;code&gt;package.json&lt;/code&gt;. There is no trouble of polluting the environment of other projects, because the install root is totally isolated unless you add &lt;code&gt;--global&lt;/code&gt; explicitly.&lt;/p&gt;
&lt;p&gt;Now when I looked into the python packaging system again I found many inconvenience and messes, as many other articles indicates&lt;a href=&quot;https://medium.com/@alonisser/things-i-wish-pip-learned-from-npm-f712fa26f5bc&quot;&gt;^1&lt;/a&gt;. The &lt;code&gt;pip&lt;/code&gt; and &lt;code&gt;setuptools&lt;/code&gt; requires a &lt;code&gt;setup.py&lt;/code&gt; in the project root, which is similar with &lt;code&gt;package.json&lt;/code&gt;. It defines the requirements of installation together with project information. However, &lt;code&gt;pip&lt;/code&gt; always install the library and all dependencies in your current python root without the help of other tools. &lt;a href=&quot;https://virtualenv.pypa.io&quot;&gt;virtualenv&lt;/a&gt; and its followers setup an isolated virtual environment which is done by altering the python root when activated.&lt;/p&gt;
&lt;p&gt;The problem is solved, right? I don&apos;t think so, when it comes to deployment. I have been developing and running some web applications written in python web framework, Flask and Django. The deployment is quite complicated and easy to fail. Dynamic languages require an almost same runtime environment installed on the target machine. Same dependencies, same 3rd party libs, etc. Any change of requirement version may cause a failure in deployment. &lt;a href=&quot;https://golang.org/&quot;&gt;Go language&lt;/a&gt;, which I learned recently, is another polar. It brings me brand new experiences of deployment and let me scream: &quot;That is the way!&quot;. With statically compiled binary, all things to do in deployment is just a copy command, and it can work on any machine. But I don&apos;t like the way Go package organizes -- every modularized file locates under the same directory, which makes the hierarchy unclear.&lt;/p&gt;
&lt;p&gt;This is what I need Python learn from node.js and go. Fortunately, &lt;a href=&quot;https://kennethreitz.org&quot;&gt;Kenneth Reitz&lt;/a&gt; developed a superb library for Python packaging: &lt;a href=&quot;https://docs.pipenv.org/&quot;&gt;Pipenv&lt;/a&gt;. It is the most modern packaging tools in my opinion, which brings me almost the same experience as npm. Life is short, throw away pip, easy-install from now on and use Pipenv!&lt;/p&gt;
</content:encoded></item><item><title>浮生三藩</title><link>https://frostming.com/posts/2017/11-05/fu-sheng-san-fan/</link><guid isPermaLink="false">https://frostming.com/2017/11-05/fu-sheng-san-fan/</guid><description>旧金山的阴与晴</description><pubDate>Sun, 05 Nov 2017 07:47:27 GMT</pubDate><content:encoded>&lt;h2&gt;1&lt;/h2&gt;
&lt;p&gt;来美帝村里呆了一周，这个周末终于决定进趟城。开车的高队是个新司机，但我们都非常相信他。在村里通勤时，公路都特别宽敞，坡度很小，车少人少。去程果然也比较顺遂，一个多小时就到了目的地。找地停车是个问题，我们搜索着路边一切 P 的标志，但一开始怎么也找不到。后面知道那个蓝底白 P 的标志是三藩的路边停车位，每个车位旁都有一个桩，可以自助支付。但我觉得，一切不能用「扫二维码」解决的都不能算自助服务。绕了一圈，终于找到一个 Public Parking，于是我第一回见到了，看守员能帮你停车的停车场，你只用把钥匙交给他就不用管了，价格也不贵， 8 小时 15 美元，这个服务，到位。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//webp.frostming.com/images/f8b801406349648f3649c75c4ee5b2ce.png&quot; alt=&quot;&quot; /&gt; &lt;img src=&quot;//webp.frostming.com/images/f2b3f4a3a2399dffda6e13d91153294.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;//webp.frostming.com/images/996df9e53efe709ae907f087a7634b1b.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;2&lt;/h2&gt;
&lt;p&gt;我们朝着渔人码头步行，一眼望见一条笔直地通向云端的公路，就像《盗梦空间》的海报里一样魔幻。刚下过雨的街道行人寥落，天空灰得像（划掉）是哭过。我们边走边讨论了坡道起步以及 30 度侧方位停车的难易程度。却发现其实很多车都是停在这样的路上，任性的外国人甚至直接横停在路边，让人不禁担心一辆车侧向翻滚可能导致的多米诺骨牌效应。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//webp.frostming.com/images/e30273853b2dcd821d1fb8a7a3e4c00b.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在这样的路上徒步跟爬山没什么区别，塘朗山步道的坡度也不过如此。等到了峰顶，身上的寒冷早已被驱除地一干二净。&lt;/p&gt;
&lt;h2&gt;3&lt;/h2&gt;
&lt;p&gt;渔人码头是一个景点，是景点就会有套路，初来乍到的我们先后被两个人套路了。前一个是一般的求捐，几句寒暄就迫不及待进入正题，我们一走了之。第二个是个操着东北口音的大叔，他热心地告诉我们每年十月的第一周是舰艇周，那时来码头可以免费登航母，和战斗机飞行表演。相谈甚欢时他却悄悄亮出了自己的身份——轮子。我们一声叹息，深觉防不胜防。&lt;/p&gt;
&lt;p&gt;码头还有很多海狮在休息，接受游人的参观。他们挪动的肥胖而并不笨拙的身体，叫声此起彼伏。&lt;/p&gt;
&lt;p&gt;上午过后肚中饥饿，我们返回唐人街吃了一家湖南菜。来美国这一周就没吃西餐，虽然觉得不能天天都还吃中餐，可谁让西餐贵呢。这边的华人区，跟国内没什么两样，到处都是闲聊的大爷和买菜的大妈。依这看来，就算一辈子也不会说几个英文词，在这里生活也完全没有大碍。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//webp.frostming.com/images/fc0244fde704b71220618e57015e04fb.png&quot; alt=&quot;&quot; /&gt; &lt;img src=&quot;//webp.frostming.com/images/ba717b6c15b8a884b45771d5753635b3.png&quot; alt=&quot;&quot; title=&quot;唐人街「天下为公」的牌坊&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;4&lt;/h2&gt;
&lt;p&gt;吃完饭天也完全放晴，加州的阳光晃眼，紫外线强烈，天也蓝的没有王法。汽车的挡风玻璃反映着蓝天白云，加上墙面的颜色、鲜艳的大巴和电车，轻易就是一张水彩画。途中还有一个小插曲：一辆车缓慢地前行，可能是在找路，后面一辆车紧贴着，聒躁地按着喇叭，末了还探出头来吼了一声脏话。「吼」字绝不夸张，那个音量已经不是「骂」。目睹了这一切的高队表示开车压力山大，毕竟我们都没有防弹衣，鬼知道人车里有没有 AK。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//webp.frostming.com/images/5707d5f5df780f55a1845076b51e7dd5.png&quot; alt=&quot;&quot; /&gt; &lt;img src=&quot;//webp.frostming.com/images/257adfdaccaa09fa7dce0ad396996033.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;//webp.frostming.com/images/10cea07ea19900daec686be81f3ae883.png&quot; alt=&quot;&quot; /&gt; &lt;img src=&quot;//webp.frostming.com/images/2017-DSC_9305-edited.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;金门大桥是一个能让我想起国内景点的地方，游人如织。这座建立于 1930 年代的大桥已经在海峡上屹立了 80 多年。下过雨的天空澄澈透明，大桥清晰可见，直跨南北。值得一提的是桥头挂有一块铭牌，纪念一位叫 Gauri Govil 的两岁女童，她因坠入桥梁的间隙中而丧生。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//webp.frostming.com/images/1cd93d36850fcb63428d94696666b7de.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;//webp.frostming.com/images/2694a65aedc64ad2e1c6f93f5745d369.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不等太阳落山，我们就回程了。色彩鲜艳，是异域的风情，阳光明媚，是加州的特色。旧金山它从不旧，正如金门大桥也不是金色的。&lt;/p&gt;
</content:encoded></item><item><title>概率也会欺骗你</title><link>https://frostming.com/posts/2017/09-20/gai-lu-ye-hui-qi-pian-ni/</link><guid isPermaLink="false">https://frostming.com/2017/09-20/gai-lu-ye-hui-qi-pian-ni/</guid><description>一个违反直觉的概率问题</description><pubDate>Wed, 20 Sep 2017 05:00:27 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;有一道包含四个选项的单选题，你发现你完全不知道该怎么做。在不考虑「三短一长选一长」「遇到不会就选 C」的玄学答题法时，你有一个机会去掉一个错误选项，然后就只能随机瞎选。请问这时你选到正确答案的概率是多少？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;老师，这道题我会答，是 1/3！&lt;/p&gt;
&lt;p&gt;恭喜你答对了！没去掉错误选项时，答对概率是 1/4，去掉就只剩三个选项了，所以是 1/3。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//webp.frostming.com/images/37f55be882bb516ce6cc0220a555d5a4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;规则变一下，你先在四个选项中选一个，然后老师在剩下的三个选项中去掉一个错误答案，请问你要不要更改答案？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这就要看改与不改，正确的概率分别是多少了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果不改，因为老师总能去掉一个错误答案，这件事有没有对你没有任何影响（因为你没改答案）。所以正确率仍为 1/4&lt;/li&gt;
&lt;li&gt;如果改，没去掉选项前剩下选项的正确率之和为 3/4，去掉一个错误选项，其他两个选项的正确率为： 3/4 ÷ 2 = 3/8。所以改答案更优。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;//webp.frostming.com/images/433c308720da881ef8cbca9daecb517b.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;哈？&lt;/p&gt;
&lt;p&gt;不懂没关系，我们换个极端的情况，有 100 个选项，你选了 1 个，老师去掉了错误的 98 个，问你要不要改成没去掉的那一个？&lt;/p&gt;
&lt;p&gt;老师你当我傻比吗？你没去掉那一个，正确率是 99%！当然要改。&lt;/p&gt;
&lt;p&gt;一个是先去掉再选，一个是先选再去掉，这里面有什么区别？&lt;/p&gt;
&lt;p&gt;有区别，老师去掉错误答案时，是告诉你一个信息：去掉的选项，是 100% 错误的。这样你的概率空间就缩小了。100 个选项情况下，你选某一个时，概率空间仍是完整的，100 选 1。你选的正确率是 1%，剩下的 99 个选项平分 99% 正确率（每个 1%）。老师去掉了 9 8 个错误选项，是告诉你这 98 个选项的正确率都是 0%。所以那个没去掉的选项就独得 99% 正确率。&lt;/p&gt;
&lt;p&gt;**有没有信息，能影响一件事情的概率分布。**比如一个 6 位数的密码，你什么信息也不知道，和你已知前 5 位数，猜对的概率有天壤之别。再看下一题：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;已知老王有两个孩子，&lt;strong&gt;老大是男孩&lt;/strong&gt;，请问老二也是男孩的概率是多少？&lt;/li&gt;
&lt;li&gt;已知老王有两个孩子，&lt;strong&gt;其中有一个是男孩&lt;/strong&gt;，请问另一个也是男孩的概率是多少？&lt;/li&gt;
&lt;li&gt;已知老王有两个孩子，&lt;strong&gt;老大是周二出生的男孩&lt;/strong&gt;，请问老二也是男孩的概率是多少？&lt;/li&gt;
&lt;li&gt;已知老王有两个孩子，&lt;strong&gt;其中有一个是周二出生的男孩&lt;/strong&gt;，请问另一个也是男孩的概率是多少？&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=1642018225,3441348339&amp;amp;fm=27&amp;amp;gp=0.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;冷静！先把刀放下，我先揭晓一下正确答案：1/2, 1/3, 1/2, 13/27。&lt;/p&gt;
&lt;p&gt;第一题，很简单，独立同分布，老大是男是女不影响老二的性别。1/2&lt;/p&gt;
&lt;p&gt;第二题和第一题区别在哪呢？想像一下出题人出第一题的时候，他只需要看老大的性别就可以跑来问你了，可能老二他见都没见到。但第二题呢，他可能观察了两个孩子的性别，然后告诉你其中有一个是男孩，问你另一个的性别。我们不知道这孩子是男是女，也不知道他是老大老二，它的概率空间比第一题要大了。有可能这个孩子是老大，是男孩，那老二是男是女都无所谓（根据题设）。也有可能这个孩子是老二，是女孩，那老大就必须是男孩。所以我们需要列出两个孩子性别的联合分布：男男，男女，女男，女女。女女这个情况已经被观察者去掉了，所以剩下三种情况，另一个也是男孩（男男）的概率是 1/3。&lt;/p&gt;
&lt;p&gt;当然，第一题也可以列联合分布来做，去掉的是（女男、女女），剩下两种情况，男男的概率是 1/2。可以发现，给定其中一个孩子的性别，另一个孩子性别的分布没有变化，仍是 1/2，这就是独立分布的意义。&lt;/p&gt;
&lt;p&gt;第三题，同样，出题人没有给出任何第二个孩子的信息，第二个孩子的性别是独立的， 1/2。就算你说破大天，说老大是大队长三好学生钢琴十级，仍然没有 P 用。老二是男孩子的概率还是 1/2。&lt;/p&gt;
&lt;p&gt;第四题，出题人问的是另一个孩子的性别，又没问星期几出生，你告诉我老大是周二出生有什么用，Who TM cares? No, no, no（摇手指）。这就是多余的信息起到的微妙的作用。出题人也要观察两个孩子，问他娘，老王的媳妇他们是周几出生的，才能告诉你这个信息。有三个维度：排行、性别、周几出生，概率空间又扩大了。我们用 B 表示男孩子，G 表示女孩，周几出生用 数字 表示，比如 2B 是周二出生的男孩。列出所有包含 2B 的情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;| 2B 1B | 2B 2B | 2B 3B | 2B 4B | 2B 5B | 2B 6B | 2B 7B |
| 2B 1G | 2B 2G | 2B 3G | 2B 4G | 2B 5G | 2B 6G | 2B 7G |
| 1G 2B | 2G 2B | 3G 2B | 4G 2B | 5G 2B | 6G 2B | 7G 2B |
| 1B 2B | 3B 2B | 4B 2B | 5B 2B | 6B 2B | 7B 2B |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中两个男孩的情况（第一行和最后一行）占总数的 13/27，略小于 1/2。可以想像，条件给的越精确，这个数会越接近 1/2。&lt;/p&gt;
&lt;p&gt;现在大家都喜欢把「薛定谔的猫」当段子讲，其实它的原理跟上面的例子一样：**因为我们的观察，获得了信息，导致了概率分布的改变。**原来是一半生一半死，一观察，就变成 100% 的生或死了。&lt;/p&gt;
</content:encoded></item><item><title>动态博客的后台定制</title><link>https://frostming.com/posts/2017/09-16/dong-tai-bo-ke-de-hou-tai-ding-zhi/</link><guid isPermaLink="false">https://frostming.com/2017/09-16/dong-tai-bo-ke-de-hou-tai-ding-zhi/</guid><description>自定义 Flask-Admin 的表单控件</description><pubDate>Sat, 16 Sep 2017 13:12:09 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://juejin.im/entry/59bfd19a5188256c4b72456a/detail&quot;&gt;&lt;img src=&quot;https://badge.juejin.im/entry/59bfd19a5188256c4b72456a/likes.svg?style=flat&quot; alt=&quot;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;搭建动态博客的初衷就是想随时随地，只要一个浏览器，就能更新博客。那么就需要一个后台来管理文章，包含文章编辑器，和各种表单控件。&lt;/p&gt;
&lt;h2&gt;编辑器&lt;/h2&gt;
&lt;p&gt;先来解决文本编辑器的问题，CKEditor 功能强大，但只是一个富文本编辑器。对于已经习惯 Markdown 写作的我来说，只管写，排版渲染就交给浏览器去做。找了很多内嵌 Markdown 编辑器，既要外观匹配，还要最好带预览功能。最终我选择了 &lt;a href=&quot;https://simplemde.com/&quot;&gt;Simple MDE&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;使用方法非常简单，引入 CSS, Javascript 文件后，只需要一句话就搞定了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
  var simplemde = new SimpleMDE({ element: document.getElementById(&quot;MyID&quot;) });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体到 Flask-Admin，只需重载&lt;code&gt;admin/model/edit.html&lt;/code&gt;和&lt;code&gt;admin/model/create.html&lt;/code&gt;模板文件，在其中加入对应 HTML 代码，然后在&lt;code&gt;ModelView&lt;/code&gt;中分别指定&lt;code&gt;create_template&lt;/code&gt;和&lt;code&gt;edit_template&lt;/code&gt;就行了。外观如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//webp.frostming.com/images/f6aca0a2dcd505bec32431b8def9980c.png&quot; alt=&quot;&quot; /&gt; &lt;img src=&quot;//webp.frostming.com/images/1e68d9b76e35928fcf4e833ecd3e7786.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我已经事先把 Flask-Admin 的基模板给换成了 bootstrap4。这个编辑器全屏模式下支持分栏预览，非常惊艳。&lt;/p&gt;
&lt;h2&gt;Tag 与 Category 输入框&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Tag&lt;/code&gt;与&lt;code&gt;Category&lt;/code&gt;是&lt;code&gt;Post&lt;/code&gt;的两个属性，其中一个是多对多关系，另一个是一对多关系。Flask-Admin 原生支持这两种类型的属性输入框，但有以下不足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基于 Select2 3.x，不支持自由输入的选择框（tags）。&lt;/li&gt;
&lt;li&gt;无法动态添加不存在的项到数据库中。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;针对以上两点开始我们的定制。首先将要加载自由输入的选择框打上 HTML 标记，在&lt;code&gt;ModelView&lt;/code&gt;中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;form_widget_args = {
    &apos;tags&apos;: {&apos;data-role&apos;: &apos;select2-free&apos;},
    &apos;category&apos;: {&apos;data-role&apos;: &apos;select2-free&apos;},
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重载&lt;code&gt;edit.html&lt;/code&gt;和&lt;code&gt;create.html&lt;/code&gt;，引入 select2 4.0.x 的文件，以及以下 Javascript 代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$(&apos;[data-role=select2-free]&apos;).each(function(){ $(this).select2({tags: true});
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在可以自由输入了，还需要动态添加。查看 Flask-Admin 的源码，对应这两种域的表单分别定义为&lt;code&gt;QuerySelectField&lt;/code&gt;与&lt;code&gt;QuerySelectMultiField&lt;/code&gt;，它们被 hardcode 在&lt;code&gt;AdminModelConverter._model_select_field&lt;/code&gt;里面，而&lt;code&gt;AdminModelConverter&lt;/code&gt;在&lt;code&gt;ModelView&lt;/code&gt;中被指定。所以我们要重载&lt;code&gt;QuerySelectField&lt;/code&gt;的行为，则需要继承&lt;code&gt;AdminModelConverter&lt;/code&gt;，重载下面的&lt;code&gt;_model_select_field&lt;/code&gt;方法，再将其加载到我们自定义的&lt;code&gt;ModelView&lt;/code&gt;就可以了，示意图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//webp.frostming.com/images/d82620114808d9527afacfda0d2e3b17.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了自定一个&lt;code&gt;SelectField&lt;/code&gt;，重载了三个类，真是大费周章。在重载的&lt;code&gt;QuerySelectField&lt;/code&gt;里，我们需要实现以下逻辑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先寻找匹配的 model 对象，并绑定到&lt;code&gt;form.data&lt;/code&gt;里（未重载之前的行为）&lt;/li&gt;
&lt;li&gt;剩下的未匹配的选择项，为它们创建 model 对象，并绑定到&lt;code&gt;form.data&lt;/code&gt;里。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class AutoAddSelectField(QuerySelectField):
    def __init__(self, model_factory, *args, **kwargs):
        super(AutoAddSelectField, self).__init__(*args, **kwargs)
        self.model_factory = model_factory

    def _get_data(self):
        if self._formdata is not None:
            for pk, obj in self._get_object_list():
                if pk == self._formdata:
                    self._set_data(obj)
                    break
            else:
                obj = self.model_factory(self._formdata)
                self._set_data(obj)
        return self._data

    def _set_data(self, data):
        self._data = data
        self._formdata = None

    data = property(_get_data, _set_data)

    def pre_validate(self, form):
        pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们要在初始化时传入 model 的创建方法，并取消了有效性检查。&lt;code&gt;QuerySelectMultiField&lt;/code&gt;也大同小异了。最终效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//webp.frostming.com/images/Untitled.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;美中不足&lt;/h2&gt;
&lt;p&gt;动态添加做好了，那么删除呢？想像一下这个使用场景，你修改文章，把一个标签删除了，这个标签已经没有任何文章使用，那你肯定不希望它再出现在标签列表里吧？SQLAlchemy 中有&lt;code&gt;cascade&lt;/code&gt;属性，用来指定&lt;code&gt;parent&lt;/code&gt;改变时&lt;code&gt;child&lt;/code&gt;的行为，但不符合我们的要求，因为我们要的是一对多和多对多关系中「多」的一方变化时另一方的行为。于是我们需要监听&lt;code&gt;before_flush&lt;/code&gt;信号，检查当前&lt;code&gt;session&lt;/code&gt;中的对象并做对应处理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def auto_delete_orphans(attr):
    target_class = attr.parent.class_

    @sa.event.listens_for(sa.orm.Session, &apos;after_flush&apos;)
    def delete_orphan_listener(session, ctx):
        session.query(target_class).filter(~attr.any())\
                                   .delete(synchronize_session=False)

auto_delete_orphans(Tag.posts)
auto_delete_orphans(Category.posts)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>How does it work - with_metaclass</title><link>https://frostming.com/posts/2017/08-28/how-does-it-work-with-metaclass/</link><guid isPermaLink="false">https://frostming.com/2017/08-28/how-does-it-work-with-metaclass/</guid><pubDate>Mon, 28 Aug 2017 19:57:59 GMT</pubDate><content:encoded>&lt;p&gt;我在看源代码的时候，经常蹦出这一句：How does it work! 竟然有这种操作？本系列文章，试图剖析代码中发生的魔法。顺便作为自己的阅读笔记，以作提高。&lt;/p&gt;
&lt;p&gt;先简单介绍下 Python 中的元类(metaclass)。元类就是创建类的类，对于元类来说，类是它的实例，&lt;code&gt;isinstance(cls, metaclass)&lt;/code&gt;将返回&lt;code&gt;True&lt;/code&gt;。Python 中的所有类，都是&lt;code&gt;type&lt;/code&gt;的实例，换句话说，&lt;code&gt;type&lt;/code&gt;是元类的基类。使用&lt;code&gt;type&lt;/code&gt;创建一个类的方法如下：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; type(&apos;MyClass&apos;, (), {})
&amp;lt;class &apos;__main__.MyClass&apos;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;type&lt;/code&gt;接受三个参数，第一个参数是类名称，第二个参数是继承的基类的元组，第三个参数是类的命名空间。上例中，我们创建了一个无基类（直接继承&lt;code&gt;object&lt;/code&gt;），无初始命名空间的类&lt;code&gt;MyClass&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;注：使用&lt;code&gt;type&lt;/code&gt;创建的类和使用元类的类，都是新式类&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;使用元类后，该类将由定义的元类实例化来创建。定义的方法在 Python 2 与 Python 3 中有所不同：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Python 2:
class MyClass(object):
    __metaclass__ = MyMeta

# Python 3:
class MyClass(metaclass=MyMeta):
    pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你的项目需要兼容 Python 2 和 Python 3，就需要使用一种方法，同时支持 Python 2 和 Python 3。元类有两个基本特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;元类实例化得到类&lt;/li&gt;
&lt;li&gt;元类能被子类继承&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据这两个特性，我们不难得到解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用元类实例化得到一个临时类&lt;/li&gt;
&lt;li&gt;定义类时继承这个临时类&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们可以写出一个&lt;code&gt;with_metaclass&lt;/code&gt;函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def with_metaclass(meta, *bases):
    &quot;&quot;&quot;Compatible metaclass

    :param meta: the metaclass
    :param *bases: base classes
    &quot;&quot;&quot;
    return meta(&apos;temp_class&apos;, bases, {})

# Testing:
class TestMeta(type):
    def __new__(cls, name, bases, d):
        d[&apos;a&apos;] = &apos;xyz&apos;
        return type.__new__(cls, name, bases, d)

class Foo(object):pass

class Bar(with_metaclass(TestMeta, Foo)): pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们就创建了一个以&lt;code&gt;TestMeta&lt;/code&gt;为元类，继承&lt;code&gt;Foo&lt;/code&gt;的类&lt;code&gt;Bar&lt;/code&gt;。验证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; Bar.a
&apos;xyz&apos;
&amp;gt;&amp;gt;&amp;gt; Bar.__mro__
(&amp;lt;class &apos;__main__.Bar&apos;&amp;gt;, &amp;lt;class &apos;__main__.temp_class&apos;&amp;gt;, &amp;lt;class &apos;__main__.Foo&apos;&amp;gt;, &amp;lt;class &apos;object&apos;&amp;gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一切正常，但我们看到在&lt;code&gt;Bar&lt;/code&gt;的 mro 里混进了一个临时类&lt;code&gt;temp_class&lt;/code&gt;，你忽略它吧，有时会很麻烦。作为完美主义者，我想寻找一种解决办法，不要在 mro 中引入多余的类。&lt;/p&gt;
&lt;p&gt;Python 的&lt;code&gt;six&lt;/code&gt;模块专门为解决 Python 2to3 兼容问题而生，模块里带有一个&lt;code&gt;with_metaclass&lt;/code&gt;函数，我们来看它是怎么实现的：（为了 debug，添加了一个 print 语句）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def with_metaclass(meta, *bases):
    class metaclass(type):
        def __new__(cls, name, this_bases, d):
            print(cls, &quot;new is called&quot;)
            return meta(name, bases, d)
    return type.__new__(metaclass, &apos;temp_class&apos;, (), {})

# Testing:
class TestMeta(type):
    def __new__(cls, name, bases, d):
        d[&apos;a&apos;] = &apos;xyz&apos;
        print(cls, &quot;new is called&quot;)
        return type.__new__(cls, name, bases, d)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一时看不懂？没关系，我们来用用看，为了看清楚过程，我们分成两步执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; temp = with_metaclass(TestMeta, Foo)
&amp;gt;&amp;gt;&amp;gt; class Bar(temp): pass
...
&amp;lt;class &apos;__main__.with_metaclass.&amp;lt;locals&amp;gt;.metaclass&apos;&amp;gt; new is called
&amp;lt;class &apos;__main__.TestMeta&apos;&amp;gt; new is called
&amp;gt;&amp;gt;&amp;gt; Bar.a
&apos;xyz&apos;
&amp;gt;&amp;gt;&amp;gt; Bar.__mro__
(&amp;lt;class &apos;__main__.Bar&apos;&amp;gt;, &amp;lt;class &apos;__main__.Foo&apos;&amp;gt;, &amp;lt;class &apos;object&apos;&amp;gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们明明生成了一个临时类&lt;code&gt;temp_class&lt;/code&gt;，但后来竟然消失了！下面来仔细分析函数的运行过程。首先我们看到，执行第一步生成临时类时，两个&lt;code&gt;__new__&lt;/code&gt;都没有调用，而第二步定义类时，两个&lt;code&gt;__new__&lt;/code&gt;都调用了。奥秘就在函数的返回语句&lt;code&gt;return type.__new__(metaclass, &apos;temp_class&apos;, (), {})&lt;/code&gt;，它创建了一个临时类，具有如下属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;名称为&lt;code&gt;temp_class&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;是函数内部类&lt;code&gt;metaclass&lt;/code&gt;的实例，它的元类是&lt;code&gt;metaclass&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;没有基类&lt;/li&gt;
&lt;li&gt;创建时仅调用了&lt;code&gt;type&lt;/code&gt;的&lt;code&gt;__new__&lt;/code&gt;的方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这是一个**&lt;code&gt;metaclass&lt;/code&gt;实例的不完全版本**。接下来，定义&lt;code&gt;Bar&lt;/code&gt;时，&lt;code&gt;Bar&lt;/code&gt;得到继承的元类&lt;code&gt;metaclass&lt;/code&gt;，过程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;实例化&lt;code&gt;metaclass&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;调用&lt;code&gt;metaclass.__new__&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;返回&lt;code&gt;meta(name, bases, d)&lt;/code&gt;， &lt;code&gt;meta=TestMeta&lt;/code&gt;，&lt;code&gt;bases=(Foo,)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;调用&lt;code&gt;TestMeta.__new__&lt;/code&gt;实例化得到&lt;code&gt;Bar&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;Bar&lt;/code&gt;的基类由第 3 步得到，于是就去除了&lt;code&gt;temp_class&lt;/code&gt;，这其实用到了闭包，&lt;code&gt;with_metaclass&lt;/code&gt;返回的临时类中，本身无任何属性，但包含了元类和基类的所有信息，并在下一步定义类时将所有信息解包出来。&lt;/p&gt;
&lt;p&gt;以上就是&lt;code&gt;with_metaclass&lt;/code&gt;源代码的解析，通过这篇文章，相信能加深元类与闭包的理解。&lt;/p&gt;
</content:encoded></item><item><title>西游记中有趣的细节(2)</title><link>https://frostming.com/posts/2017/xi-you-ji-2/</link><guid isPermaLink="false">https://frostming.com/2017/xi-you-ji-2/</guid><description>重新认识孙悟空</description><pubDate>Tue, 25 Jul 2017 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;唐僧其人&lt;/h2&gt;
&lt;p&gt;说完了孙悟空，来说说其他三人。玄奘法师跋山涉水不畏艰险取得真经，是个了不起的人物。然而纵观全书，他给我的印象，不是一个英雄，更像一个普通人，和我们一样，有懦弱，有犹豫，有恐惧的普通人。&lt;/p&gt;
&lt;p&gt;他每到深山老林，阴森诡异之处，都害怕有妖怪以至足不得行，要靠孙悟空的宽慰；他护短，总是偏袒好吃懒做的猪八戒而数落兢兢业业的孙悟空；他身陷女儿国，几乎差点就没有经受住考验；他在大节上也不总是能坚持住。在五庄观一节，徒弟闯祸，自知理亏难逃责罚，就想趁夜一走了之。他作为师父，竟然没有担当，并未阻止这种行为。这要换金庸笔下的大侠，必然一人做事一人当，徒弟做事师父当。&lt;/p&gt;
&lt;p&gt;再比如，师徒四人到敕建宝林寺，唐僧前去借宿，遭到冷眼相待，他怎么说的：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;长老闻言，满眼垂泪道：「可怜!可怜!这才是『人离乡贱』!我弟子从小儿出家，做了和尚，又不曾拜忏吃荤生歹意，看经怀怒坏禅心；又不曾丢瓦抛砖伤佛殿，阿罗脸上剥真金。噫，可怜啊!不知是那世里触伤天地，教我今生常遇不良人!和尚，你不留我们宿便罢了，怎么又说这等惫懒话，教我们在前道廊下去『蹲』？此话不与行者说还好，若说了，那猴子进来，一顿铁棒，把孤拐都打断你的！」&lt;/p&gt;
&lt;p&gt;&lt;em&gt;——第三十六回 心猿正处诸缘伏 劈破傍门见月明&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;你没看错，这出自唐僧之口。平时慈悲为怀的大唐高僧，何尝不是仰仗了一个好勇斗狠的大徒弟呢？孙悟空好打好杀又不是一天两天，若不过分，就睁只眼闭只眼，还拿来吓唬别人，太过分了才大骂一通赶回花果山。&lt;/p&gt;
&lt;p&gt;这不正是一个普通人，心存侥幸，怕被抓住担不起责任，有便捷的方法时，虽略有不正，也只能默许。&lt;/p&gt;
&lt;h2&gt;不按套路起妖名&lt;/h2&gt;
&lt;p&gt;《西游记》中还有很多妖怪的名字让人忍俊不禁，比如：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;时有六个小妖，是他知己的精灵，封为健将，都有名字：一个叫做云里雾，一个叫做雾里云，一个叫做急如火，一个叫做快如风，一个叫做兴烘掀，一个叫做掀烘兴。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;——第四十一回 心猿遭火败 木母被魔擒&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;兴烘掀和掀烘兴是什么鬼啊，怕不是脸在键盘上滚出来的吧。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;那怪物战战兢兢，口叫：「饶命！」遂从实供道：「我两个是乱石山碧波潭万圣龙王差来巡塔的。他叫做奔波儿灞，我叫做灞波儿奔。他是鲇鱼怪，我是黑鱼精。」&lt;/p&gt;
&lt;p&gt;&lt;em&gt;——第六十二回 涤垢洗心惟扫塔 缚魔归正乃修身&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这两个很有名了，可怜两条鱼，被打成了肉泥，应该可以做成鱼肉丸。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;……即使个定身法，把两个狼头精定住。眼睁睁，口也难开；直挺挺，双脚站住。又将他扳翻倒，揭衣搜捡，果是有二十两银子，着一条搭包儿打在腰间裙带上，又各挂着一个粉漆牌儿，一个上写着「刁钻古怪」，一个上写着「古怪刁钻」。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;——第八十九回 黄狮精虚设钉钯宴 金木土计闹豹头山&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;狼头精：我就是这样古怪刁钻！&lt;/p&gt;
&lt;p&gt;吴老师特别喜欢这种翻来倒去的名字，起名字真是任性！一个六百年前的古人写出这样童心的名字，有种反差的萌。&lt;/p&gt;
&lt;h2&gt;损人不带脏字&lt;/h2&gt;
&lt;p&gt;试看几例：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;八戒道：「好乖儿子！正是这等说！仔细看钯！」妖邪道：「你原来是半路上出家的和尚！」八戒道：「我的儿，你真个有些灵感，怎么就晓得我是半路出家的？」妖邪道：「你会使钯，想是雇在那里种园，把他钉钯拐将来也。」八戒道：「儿子，我这钯，不是那筑地之钯。你看：（夸一通兵器）……」&lt;/p&gt;
&lt;p&gt;&lt;em&gt;——第四十九回 三藏有穴沉水宅 观音救难现鱼篮&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;八戒被黑了一番，逼得他把兵器大夸一通，还不解气，转头就是以其人之道还治其人之身：&lt;/p&gt;
&lt;p&gt;八戒使钉钯架住道：「你这泼物，原来也是半路上成精的邪魔！」那怪道：「你怎么认得我是半路上成精的？」八戒道：「你会使铜锤，想是雇在那个银匠家扯炉，被你得了手，偷将出来的。」妖邪道：「这不是打银之锤。你看：（夸一通兵器）……」&lt;/p&gt;
&lt;p&gt;沙僧也加入战斗，却自讨了个没趣，同样的格式，再来一次：&lt;/p&gt;
&lt;p&gt;沙和尚见他两个攀话，忍不住近前高叫道：「那怪物!休得浪言!古人云：『口说无凭，做出便见。』不要走!且吃我一杖！」妖邪使锤杆架住道：「你也是半路里出家的和尚。」沙僧道：「你怎么认得？」妖邪道：「你这个模样，像一个磨博士出身。」沙僧道：“如何认得我像个磨博士？「妖邪道：“你不是磨博士，怎么会使赶面杖？」沙僧骂道：「你这孽障，是也不曾见！（夸一通兵器）……」&lt;/p&gt;
&lt;p&gt;筑地你妹啊，扯炉你妹啊，赶面杖你妹啊！吴老师你能不能正经一点？妖怪啊，打架了啊，严肃一点好不？&lt;/p&gt;
&lt;h2&gt;斗嘴的乐趣&lt;/h2&gt;
&lt;p&gt;看《西游记》最大的乐趣之一是三兄弟间的谈话，挖苦讽刺，生动幽默。这是《西游记》胜于其余三本四大名著之处。三个都是莽撞人，配上一个禁欲系的冷面人，天生具有喜剧效果。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;老者道：「既有徒弟，何不同来？」教：「请，请，我舍下有处安歇。」三藏回头，叫声「徒弟，这里来。」&lt;/p&gt;
&lt;p&gt;那行者本来性急，八戒生来粗鲁，沙僧却也莽撞，三个人听得师父招呼，牵着马，挑着担，不问好歹，一阵风，闯将进去。那老者看见，唬得跌倒在地，口里只说是「妖怪来了!妖怪来了！」&lt;/p&gt;
&lt;p&gt;&lt;em&gt;——第四十七回 圣僧夜阻通天水 金木垂慈救小童&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这一段，画面感扑面而来，三个没有礼数的长相奇怪的家伙，一听有人叫，不问好歹，哗地冲进来，把老人家吓得半死。哈哈哈哈哈。来看看他们怎么斗嘴的：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;进前行处，忽见有一城池相近。三藏勒马叫：「徒弟们，你看那是甚么去处？」行者道：「师父原来不识字，亏你怎么领唐王旨意离朝也！」三藏道：「我自幼为僧，千经万典皆通，怎么说我不识字？」行者道：「既识字，怎么那城头上杏黄旗，明书三个大字，就不认得，却问是甚去处何也？」三藏喝道：「这泼猴胡说！那旗被风吹得乱摆，纵有字也看不明白！」行者道：「老孙偏怎看见？」八戒、沙僧道：「师父，莫听师兄捣鬼。这般遥望，城池尚不明白，如何就见是甚字号？」行者道：「却不是『朱紫国』三字？」三藏道：「朱紫国必是西邦王位，却要倒换关文。」行者道：「不消讲了。」&lt;/p&gt;
&lt;p&gt;——&lt;em&gt;第六十八回 朱紫国唐僧论前世 孙行者施为三折肱&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;泼猴子，师父问你，你就回答三个字就完了呗。怎么比唐僧还啰嗦。&lt;/p&gt;
</content:encoded></item><item><title>西游记中有趣的细节(1)</title><link>https://frostming.com/posts/2017/xi-you-ji-1/</link><guid isPermaLink="false">https://frostming.com/2017/xi-you-ji-1/</guid><description>重新认识孙悟空</description><pubDate>Thu, 20 Jul 2017 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;从很小起就被各种西游的影视作品、文学作品所浸染，近来西游记更成了最大的 IP，各种电影层出不穷。86 版《西游记》的导演杨洁也于今年逝世，这部电视剧无疑是非常优秀的，许多人对于西游记的故事、师徒四人的印象都来自于此。但，还有更多的细节，是藏在原著中的，我们未必熟悉。&lt;/p&gt;
&lt;h2&gt;孙悟空的转变&lt;/h2&gt;
&lt;p&gt;在小说的前十回，孙悟空桀骜不驯，「天要压我，我劈开这天，地要压我，我踏碎这地。」（《悟空传》语），唯独对授业恩师才客气点。《悟空传》便是拓写了这段这期的孙悟空，把它塑造成反抗权威，孤胆奋战的英雄，看的人中二之魂熊熊燃烧。其实，孙悟空一直都想进「体制内」，它大闹天宫的基本诉求，是想在天庭做一个大官。而天庭一开始给他个弼马温，后来终于承认「齐天大圣」的合法性了，却是个无实权的虚职，于是他就不乐意了。这也解释了为何他在观音告知他有赎罪成佛的机会时，他欣然而往，对唐僧毕恭毕敬，一点也不像当年那个妖猴之王。&lt;/p&gt;
&lt;p&gt;而吴承恩，更是借着孙悟空之口，阐述了一些佛法和禅意，硬是给唐僧当了几回老师。比如：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;正欢喜处，忽见一座高山阻路。唐僧勒马道：「徒弟们，你看这面前山势崔巍，切须仔细！」行者笑道：「放心，放心！保你无事！」三藏道：「休言无事。我看那山峰挺立，远远的有些凶气，暴云飞出，渐觉惊惶，满身麻木，神思不安。」行者笑道：「你把乌巢禅师的《多心经》早已忘了？」三藏道：「我记得。」行者道：「你虽记得，这有四句颂子，你却忘了哩。」三藏道：「那四句？」行者道——&lt;/p&gt;
&lt;p&gt;佛在灵山莫远求，灵山只在汝心头。人人有个灵山塔，好向灵山塔下修。&lt;/p&gt;
&lt;p&gt;三藏道：「徒弟，我岂不知？若依此四句，千经万典，也只是修心。」行者道：「不消说了。心净孤明独照，心存万境皆清。差错些儿成惰懈，千年万载不成功。但要一片志诚，雷音只在跟下。似你这般恐惧惊惶，神思不安，大道远矣，雷音亦远矣。且莫胡疑，随我去。」那长老闻言，心神顿爽，万虑皆休。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;——第八十五回 心猿妒木母 魔主计吞禅&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;心中无挂碍，妖邪只等闲。孙悟空的「佛性」，俨然要比唐僧高。&lt;/p&gt;
&lt;h2&gt;孙悟空的战斗力&lt;/h2&gt;
&lt;p&gt;「猴哥，猴哥，你真了不得」，猴哥到底有多了不得？其实，从降生之初，到大闹天宫，也就是五百年光景，就算他天赋异禀，由灵气孕育而生，这个道行，放在妖怪中还行，放到仙班一比较，就泯然众人了。你看玉皇大帝：「他自幼修持，苦历过一千七百五十劫。每劫该十二万九千六百年。」我数学不好，谁来算算他多少岁？怕不是三叠纪人士吧？太上老君也同样，在取经途中，「不小心」放走个小童坐骑，带上两件宝物，就能让孙悟空束手无策，老君实力深不可测。&lt;/p&gt;
&lt;p&gt;还有那些有背景的妖怪：黄风怪，偷个灯油喝，吹风吹得猴子不能自理。黄袍怪，资深天仙，只能杀他儿子泄愤。无背景的草根呢？红孩儿，是让孙悟空最狼狈的，他爹牛魔王更不得了，孙悟空请出了全书最强阵容来制服这位曾经的兄弟：四大金刚、金头揭谛、六甲六丁、护教伽蓝与过往众神。牵牛的是哪吒三太子，拿镜的是托塔李天王，大师兄执着芭蕉扇，二师兄并土地随后，其余的都是护卫神兵。&lt;/p&gt;
&lt;p&gt;万恶之灵九头虫，观之可怖，费了老大劲，由孙悟空、猪八戒、二郎神联手击败。要说大 BOSS，还属九灵元圣:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;你看他身无披挂，手不拈兵，大踏步走到前边，只闻得孙行者吆喝哩。他就大开了洞门，不答话，径奔行者。行者使铁棒当头支住，沙僧轮宝杖就打。那老妖把头摇一摇，左右八个头，一齐张开口，把行者、沙僧轻轻的又衔于洞内。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;——第九十回 师狮授受同归一 盗道缠禅静九灵&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这是怎样的实力，轻松秒杀？不愧是太乙天尊的坐骑。&lt;/p&gt;
&lt;p&gt;除了妖怪外，还有一个狠角色，那便是五庄观的镇元大仙。孙悟空打坏他家果树，偷奸耍滑想一走了之，被一招「袖里乾坤」治得只好去求人医树，他走了三处地方，蓬莱仙岛、东方朔、南海观音，三处仙人都提到镇元子是「地仙之祖」，都说惹不起。福禄寿三星说道：「那镇元子乃地仙之祖，我等乃神仙之宗。你虽得了天仙，还是太乙散数，未入真流，你怎么脱得他手？」最后孙悟空和他行八拜之交，真是抱了个大腿。&lt;/p&gt;
&lt;p&gt;哀哉孙悟空神通广大，还只是个「太乙散仙」，大抵不是科班出身，无法融入修仙学术界。你以为他真的让天庭诸神无奈他何？天庭只是低估了他，只把它当平常作乱的妖物处理。其实他是个得了仙道的妖。&lt;/p&gt;
&lt;h2&gt;影帝孙悟空&lt;/h2&gt;
&lt;p&gt;其实孙悟空的本事，从来不在硬桥硬马，上阵斗武，而在于他的机灵巧变，坑蒙拐骗。他耍滑的本领，绝对无人能敌。如标题，他最擅长就是变作他人模样，做些偷偷摸摸的事。我认为前半部最精彩的章节，莫过于平顶山和金角银角斗法一段，有来有回，跌宕起伏，甚是精彩。&lt;/p&gt;
&lt;p&gt;他先是变了个大葫芦，骗了妖怪的真葫芦和净瓶。又打死巴山虎与倚海龙，变作他俩模样，去请老奶奶。为了骗老奶奶，一身傲骨的孙悟空愣是磕了好几个头，可以说相当敬业。再把老奶奶与老轿的都打死，变成老奶奶，赚到幌金绳，回到平顶山。一连串令人窒息的操作！&lt;/p&gt;
&lt;p&gt;可惜孙悟空没看过宝物的使用说明，被幌金绳制服，三样宝物又回到妖怪手里。读者都大呼可惜，但他还有后招：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;那大圣口里与八戒说话，眼里却抹着那些妖怪。见他在里边吃酒，有几个小妖拿盘拿盏，执壶酾酒，不住的两头乱跑，关防的略松了些儿。他见面前无人，就弄神通，顺出棒来，吹口仙气，叫：「变！」即变做一个纯钢的锉儿，扳过那颈项的圈子，三五锉，锉做两段；扳开锉口，脱将出来，拔了一根毫毛，叫变做一个假身，拴在那里，真身却幌一幌，变做个小妖，立在旁边。八戒又在梁上喊道：「不好了，不好了！拴的是假货，吊的是正身！」老魔停杯便问：「那猪八戒吆喝的是什么？」行者已变做小妖，上前道：「猪八戒撺道孙行者教变化走了罢，他不肯走，在那里吆喝哩。」二魔道：「还说猪八戒老实，原来这等不老实！该打二十多嘴棍！」&lt;/p&gt;
&lt;p&gt;&lt;em&gt;——第三十四回 魔王巧算困心猿 大圣腾那骗宝贝&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;演！接着演！&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;行者仍站在跟前，要偷他宝贝，真个甚有见识：走上厅，对那怪扯个腿子道：「大王，你看那孙行者拴在柱上，左右爬蹉，磨坏那根金绳，得一根粗壮些的绳子换将下来才好。」老魔道：「说得是。」即将腰间的狮蛮带解下，递与行者。行者接了带，把假妆的行者拴住，换下那条绳子，一窝儿窝儿笼在袖内，又拔一根毫毛，吹口仙气，变作一根假幌金绳，双手送与那怪。那怪只因贪酒，那曾细看，就便收下。这个是大圣腾那弄本事，毫毛又换幌金绳。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;——第三十四回 魔王巧算困心猿 大圣腾那骗宝贝&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;形势就逆转了，而孙悟空眼花缭乱的操作之时，其他西游众呢？完全就是他一个人的独角戏。演戏，必须人情练达，深入生活，能见人说人话见鬼说鬼话。孙悟空艺成之后，曾遍游四大部洲，这种本事，大概是那时练就的吧。&lt;/p&gt;
</content:encoded></item><item><title>一万首的MP3，一万首疯狂的爱</title><link>https://frostming.com/posts/2017/05-24/yi-mo-shou-de-mp3-yi-mo-shou-feng-kuang-de-ai/</link><guid isPermaLink="false">https://frostming.com/2017/05-24/yi-mo-shou-de-mp3-yi-mo-shou-feng-kuang-de-ai/</guid><pubDate>Wed, 24 May 2017 19:34:48 GMT</pubDate><content:encoded>&lt;p&gt;当今的互联网世界，就是腾讯和阿里两大阵营的对抗，音乐领域也不例外。自从版权纠纷尘埃落定以来，腾讯阵营囊括了 QQ 音乐、网易云音乐、酷狗及酷我，而阿里阵营则为虾米音乐与天天动听。要想能听到所有的歌曲，两个阵营都必须下载至少一个 APP。其中用户量最大的当属网易云音乐、QQ 音乐和虾米音乐。我获取了三个音乐 APP 的榜单数据，试着分析其用户喜好、曲库构成的异同。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;1. 网易云音乐&lt;/h2&gt;
&lt;p&gt;网易云音乐算是在用户中有着相当口碑的产品，用户构成应该大概跟豆瓣差不多，也是我最常用的音乐 APP。为了获取歌曲热度总榜，评论数是一个可以衡量的指标。在此要感谢网上分享的歌单 &lt;a href=&quot;http://music.163.com/playlist?id=455717860&quot;&gt;&lt;strong&gt;评论过五万的歌曲（知乎）&lt;/strong&gt;&lt;/a&gt; 省去了不少事。直接 Show me the data!
&lt;img src=&quot;http://o7u6qrlad.bkt.clouddn.com/cd69df92aa7692bed5ef1e734c0d70d7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《晴天》以 140 多万的评论数领跑全榜（截止 5/21）并且有要超过 150 万的势头。这首发布于 2003 年《叶惠美》专辑的主打抒情歌，同时也是我最爱的周杰伦的抒情慢歌，无疑在网易用户心中有着极高的地位。直到现在，一听到它的副歌旋律，都能轻易把我带回到那些有着周董陪伴的夏天。然而一转眼，已经过去 14 年了。这种初恋的味道，只有周董的歌能给。随手截几条评论吧。
&lt;img src=&quot;http://o7u6qrlad.bkt.clouddn.com/comment1.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;http://o7u6qrlad.bkt.clouddn.com/comment2.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;http://o7u6qrlad.bkt.clouddn.com/comment3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;许嵩以一首《雅俗共赏》位列第二，虽然我不是很喜欢，但许嵩还是有能力的。&lt;/p&gt;
&lt;p&gt;同一首歌的两个版本（《Fade》《Faded》）同时进入 TOP20，Alan Walker，来自挪威的电音王子。&lt;/p&gt;
&lt;p&gt;令人瞩目的是薛之谦，TOP10 独占三席，TOP20 更有 8 首入围，强，无敌。只是有一个问题，《暧昧》作为发布才一个多月的新歌，评论数何以超过《演员》？就算是网易云音乐首发，也让人不得不怀疑刷评论的嫌疑。&lt;/p&gt;
&lt;p&gt;让我们换一个维度，看看 TOP100 入围歌曲数，谁最多呢？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://o7u6qrlad.bkt.clouddn.com/neteasetop100.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一点都「不意外」的薛之谦夺得第一，作为后唱片时代的代表，薛之谦是以网易云为主要发布平台的。周杰伦与许嵩同样为 6 首。以「小周董」标签出道的许嵩，也闯出了自己的一片天。&lt;/p&gt;
&lt;h2&gt;2. QQ 音乐&lt;/h2&gt;
&lt;p&gt;接下来再来看看 QQ 音乐。QQ 音乐的界面设计是三家中最好的，腾讯的 UI 设计师不是盖的。。但 QQ 音乐一直给我的印象都是三巨头（许嵩、徐良、汪苏泷）和 TFBOYS，总之都不是我喜欢的类型。事实是否如此呢？由于 QQ 音乐很晚才有评论功能，它的特色是歌曲弹幕，我们可以通过歌曲弹幕数量来衡量它的歌曲热度。抓了前十页的热门歌单的共 11483 首歌曲后，照例统计得到弹幕 TOP100 的歌曲，歌单请戳：&lt;a href=&quot;https://y.qq.com/n/yqq/playlist/1163842744.html&quot;&gt;&lt;strong&gt;弹幕 TOP100&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://o7u6qrlad.bkt.clouddn.com/qqtop20.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这回排名第一的是《刚好遇见你》，在网易的榜单中排名第七。周杰伦的歌入围并不多，但一首新专辑中的《告白气球》成绩不错。原因在我看来可能是进入后唱片时代以来，歌手都将网络音乐平台作为宣传、打歌的主要战场了。至于 TFBOYS，毕竟是年轻人的偶像，能在榜单中占据 3 席也并不奇怪。此外，《暧昧》排在《演员》之后，这就比较合理了嘛。&lt;/p&gt;
&lt;p&gt;再来看看歌曲数量统计：
&lt;img src=&quot;http://o7u6qrlad.bkt.clouddn.com/qqtop100.png&quot; alt=&quot;&quot; /&gt;
看完这个排名，我只想说 QQ 音乐的用户跟我有代沟。前 100 的歌曲大都是动漫、影视主题曲和 TFBOYS。排在第三的 RADWIMPS 其实是《你的名字。》的电影原声带。曾经的三巨头早已不见踪影，汪苏泷勉强入围两首，QQ 音乐已经是 TFBOYS 的天下，而这还不包括三位成员（王、千、源）发的单曲。
&lt;img src=&quot;http://o7u6qrlad.bkt.clouddn.com/ab3716c6a8e75ccb39c94cb10f789d9c.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;3. 虾米音乐&lt;/h2&gt;
&lt;p&gt;最后来看虾米音乐。虽然虾米在版权之争中损失惨重，但它却有许多独占的版权，比如田馥甄、五月天、李宗盛和大多滚石系的歌手。所以虾米更像是这些歌手粉丝的自留地。它的榜单构成与上面两家有很大的不同。虾米音乐自带榜单和播放量数据，不用全网爬了，简单粗暴。它的 APP 也有一些特别的功能，比如新歌新碟关注提醒，离线音乐包等。但我必须得吐槽一下，它的网页界面设计得就是渣，跟上两家不能比。
&lt;img src=&quot;http://o7u6qrlad.bkt.clouddn.com/xiamitop20.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;你问我这回歌单在哪里？出门左转点「排行榜」就是。这里是按播放量数据排序的，不知道为什么和官网榜单顺序有些许出入。《小幸运》一骑绝尘，好吧当时我也是为了听《小幸运》才下载了虾米的 APP 的。Hebe 为虾米音乐绝对是立下了汗马功劳。两首五月天占据三、四名，别家没得听五月天，你还不许我在虾米狂听吗？还有雷打不动的《刚好遇见你》，雷打不动的《演员》，雷打不动的《Faded》，雷打不动的《成都》。恭喜他们获奖！让我们为这四位歌手献上膝盖。值得注意的是，两道薛之谦的歌都只是 Live 版，说明虾米是没有拿到谦谦的正版专辑版权的。
&lt;img src=&quot;http://o7u6qrlad.bkt.clouddn.com/xiamitop100.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;以上是入围大于 1 首歌的歌手们，数目都不是很大。可以看出虾米音乐 TOP100 的歌手相当多样，上图中的歌手只覆盖了 25 首歌（网易云和 QQ 音乐的这个数字分别是 43 和 48）。如果算上两首五月天的合唱歌曲的话，这个排行五月天会以微弱优势胜出。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;通过以上统计，我个人还是会倾向使用网易云音乐，虾米音乐作为版权的补充，依然会保留着。至于 QQ 音乐，叔叔年纪大了，欣赏不了。&lt;/p&gt;
</content:encoded></item><item><title>Flask 实现远程日志实时监控</title><link>https://frostming.com/posts/2017/04-05/flask-shi-xian-yuan-cheng-ri-zhi-shi-shi-jian-kong/</link><guid isPermaLink="false">https://frostming.com/2017/04-05/flask-shi-xian-yuan-cheng-ri-zhi-shi-shi-jian-kong/</guid><pubDate>Wed, 05 Apr 2017 19:31:31 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://juejin.im/entry/58e5a36ea22b9d00588859d3/detail&quot;&gt;&lt;img src=&quot;https://badge.juejin.im/entry/58e5a36ea22b9d00588859d3/likes.svg?style=flat&quot; alt=&quot;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;更新于 2019.11.18&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;去除业务相关逻辑&lt;/li&gt;
&lt;li&gt;示例代码仓库在 https://github.com/frostming/flask-webconsole-example&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在自动化运维系统中，常常需要监控日志，这些日志是不断更新的。本文提供了一种实时日志监控的 Python 实现。主要实现以下功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抓取远程机器的终端输出到服务器上。&lt;/li&gt;
&lt;li&gt;将服务器的日志更新实时显示到客户端网页上。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文中示例基于 Python 以及 Flask。&lt;/p&gt;
&lt;p&gt;主要依赖：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://flask.pocoo.org/&quot;&gt;Flask&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/&quot;&gt;Redis&lt;/a&gt; 及其 Python &lt;a href=&quot;https://github.com/andymccurdy/redis-py&quot;&gt;客户端&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.paramiko.org/&quot;&gt;paramiko&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;总体来说要完成实时监控日志的功能需要分为两个方面：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;实时读取远程输出&lt;/li&gt;
&lt;li&gt;将输出实时显示到页面上&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;获取远程输出&lt;/h2&gt;
&lt;p&gt;那么下面要解决的问题是如何从远程机器上获取终端输出并添加到日志队列中。在 Python 中，SSH 连接相关的库是 paramiko，于是我自然就想用下面的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;client = paramiko.SSHClient()
client.load_system_host_keys()
client.connect(host)
stdin, stdout, stderr = client.exec_command(command)
for line in stdout:
    print(line)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样是挺好的，但是很多时候日志输出时杂糅了标准输出与错误输出的，我希望能有一种方法，检测到有新输出则显示输出，有新错误则显示错误，就像 Terminal 里面那样。所幸我们可以利用更低一级的 channel 对象来实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def do_run_command(host, username, password, command, key):
    client = paramiko.SSHClient()
    hostname, port = host.split(&apos;:&apos;)
    client.load_system_host_keys()
    try:
        client.connect(hostname, port, username, password)
        stdin, stdout, stderr = client.exec_command(command)
        channel = stdout.channel
        pending = err_pending = None
        while not channel.closed or channel.recv_ready() or channel.recv_stderr_ready():
            readq, _, _ = select.select([channel], [], [], 1)
            for c in readq:
                if c.recv_ready():
                    chunk = c.recv(len(c.in_buffer))
                    if pending is not None:
                        chunk = pending + chunk
                    lines = chunk.splitlines()
                    if lines and lines[-1] and lines[-1][-1] == chunk[-1]:
                        pending = lines.pop()
                    else:
                        pending = None
                    [push_log(line.decode(), key) for line in lines]
                if c.recv_stderr_ready():
                    chunk = c.recv_stderr(len(c.in_stderr_buffer))
                    if err_pending is not None:
                        chunk = err_pending + chunk
                    lines = chunk.splitlines()
                    if lines and lines[-1] and lines[-1][-1] == chunk[-1]:
                        err_pending = lines.pop()
                    else:
                        err_pending = None
                    [push_log(line.decode(), key) for line in lines]
    finally:
        client.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里使用了 select 来控制 IO，另外需要说明的是循环条件：当所有输出都读取完毕时&lt;code&gt;channel.closed&lt;/code&gt;为&lt;code&gt;True&lt;/code&gt;，而&lt;code&gt;exit_status_ready()&lt;/code&gt;是当进程运行结束时就为真了，此时输出不一定都读完了。&lt;code&gt;pending&lt;/code&gt;和&lt;code&gt;chunk&lt;/code&gt;是用来整行读取的。&lt;/p&gt;
&lt;h2&gt;日志实时更新&lt;/h2&gt;
&lt;p&gt;下面我们需要实现一种网页显示，当用户访问时，显示当前日志，若日志有更新，只要网页还打开，无需刷新，日志就是实时更新到网页上。另外，还需要考虑到有多个客户端连接的情况，日志应该是同步更新的。&lt;/p&gt;
&lt;p&gt;对于一般的 HTTP 连接，客户端一次请求完毕后立即得到响应，若不重新请求就无法得到新的响应，服务器是被动的。要实现这种客户端的子更新，大致有三种方法：AJAX, SSE 和 Websocket。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AJAX 就是客户端自动定时发请求，定时间隔事先指定，不是真正的实时。&lt;/li&gt;
&lt;li&gt;SSE 其实是一种长连接，只能实现服务器向客户端主动发送消息。&lt;/li&gt;
&lt;li&gt;Websocket 是服务器与客户端之间的全双工通道，需要后端的软件支持。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;权衡以上三者，SSE 是能满足我的要求的代价最小的选择。它的原理是客户端建立一个事件监听器，监听指定 URL 的消息，在服务器端，这个 URL 返回的响应必须是一个流类型。只要将响应体设为一个生成器，并设置头部为&lt;code&gt;mimetype=&apos;text/event-stream&apos;&lt;/code&gt;就行了。在&lt;code&gt;Flask&lt;/code&gt;上，已经有封装好的扩展&lt;code&gt;Flask-SSE&lt;/code&gt;，直接安装使用就行了。&lt;code&gt;Flask-SSE&lt;/code&gt;是通过 Redis 的 Pubsub 实现的消息队列。然而，只有在连接建立以后发送的数据才能收到。只并建立事件监听接受新的日志即可。代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
(function () {
  document.getElementById(&quot;post-form&quot;).onsubmit = function(e) {
    e.preventDefault();
    var parentNode = document.getElementById(&apos;log-container&apos;);
    parentNode.innerHTML = &quot;&quot;;
    var data = new FormData(document.getElementById(&apos;post-form&apos;));
    fetch(&apos;/run&apos;, {
      method: &apos;POST&apos;,
      body: data
    }).then(resp =&amp;gt; resp.json()).then(data =&amp;gt; {
      var source = new EventSource(&apos;/stream?channel=&apos; + data.uid);
      source.addEventListener(&apos;message&apos;, function(event) {
        var res = JSON.parse(event.data);
        var pre = document.createElement(&apos;pre&apos;);
        pre.innerText = res.message;
        parentNode.appendChild(pre);
      });
    });
  }
})();
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相应地，添加日志时就要同时发送消息到 Pubsub：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def push_log(message, channel):
    sse.publish({&apos;message&apos;: message}, &apos;message&apos;, channel=channel)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;几个注意事项&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;若远程脚本使用 python 运行时，需要带上&lt;code&gt;-u&lt;/code&gt;选项，否则&lt;code&gt;print&lt;/code&gt;的输出不会立即吐出，而是有缓冲。&lt;/li&gt;
&lt;li&gt;redis 的 pubsub 只会收到&lt;strong&gt;连接建立之后&lt;/strong&gt;的消息，可能会造成消息丢失。可以在 pubsub 之外，另外持久化一份消息到 redis 中，显示时，消息则由「redis 中取出的消息」+ 「监听收到的新消息」组成。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;参考链接：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://flask-sse.readthedocs.io/en/latest/quickstart.html&quot;&gt;http://flask-sse.readthedocs.io/en/latest/quickstart.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://stackoverflow.com/a/32758464&quot;&gt;http://stackoverflow.com/a/32758464&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>SQLite 爬坑记</title><link>https://frostming.com/posts/2017/03-30/sqlite-pa-keng-ji/</link><guid isPermaLink="false">https://frostming.com/2017/03-30/sqlite-pa-keng-ji/</guid><pubDate>Thu, 30 Mar 2017 20:13:46 GMT</pubDate><content:encoded>&lt;p&gt;作为从零开始的 Web 开发人员，在项目开发中总是遇到这样那样的坑，其中数据库的坑最多。由于在功能完善过程中需要变换频繁，不可避免地要更改 DB Schema，不过我都是能不改尽量不改。逃不过时，只能硬着头皮刚。&lt;/p&gt;
&lt;p&gt;故事是这样的，我要把两个表中的某两列的类型由字符型改成列表。在数据库值类型中就是 BLOB，ORM 中叫做 PickleType。数据库使用 SQLite，ORM 使用 SQLAlchemy，并使用基于 Alembic 的自动化迁移工具，于是就开始了。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;Round 1&lt;/h2&gt;
&lt;p&gt;直接开搞&lt;/p&gt;
&lt;p&gt;migrate。。。咦？怎么脚本没生成？Google 之，Alembic 不能探测类型变化。&lt;/p&gt;
&lt;p&gt;OK，我手动写个好了吧，upgrade。。。报错！ALTER TABLE 不支持改变类型。&lt;/p&gt;
&lt;h2&gt;Round 2&lt;/h2&gt;
&lt;p&gt;好在这两列也是新加不久，并不十分重要，于是我想到了，我删了再加可以了吧？&lt;/p&gt;
&lt;p&gt;downgrade。。。报错！drop column 也不支持。&lt;/p&gt;
&lt;h2&gt;Round 3&lt;/h2&gt;
&lt;p&gt;看来只能放弃自动化迁移了，Google 一番，找到一个 drop column 的 workaround:复制一个去掉该列的新表，并覆盖原表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;create table temp as select col1,col2... from old_table;
drop table old_table;
alter table temp rename to old_table;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时候再 migrate，正确生成了脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;add_column ......
add_column ......
create_foreign_key ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;嗯。。看上去不错，最后一行是。。新表没有带上外键信息。&lt;/p&gt;
&lt;p&gt;upgrade。。。报错！create_foreign_key 失败！SQLite 也不支持，无语了，不愧是 Lite，怎么不去屎？&lt;/p&gt;
&lt;p&gt;进数据库看看，新的列已经加上了，查了一下已有的关联列，没啥问题啊？&lt;/p&gt;
&lt;p&gt;LEAVE IT ALONE!管它了，跑起来，新增一行数据，Beng shaka laka！原来缺少外键信息已有数据没问题，新增就出问题，还加了一行死数据，删不掉还（没有生成主键）。&lt;/p&gt;
&lt;h2&gt;Round 4&lt;/h2&gt;
&lt;p&gt;从备份恢复数据库。Google 外键问题，得到答案是别无他法，只能重新建表再复制数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;alter table old_table rename to temp;
create table old_table (...);
insert into old_table select col1,col2,... from temp;
drop table temp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重新建立 migration 文件夹，运行测试，一切正常。&lt;/p&gt;
&lt;h2&gt;总结：&lt;/h2&gt;
&lt;p&gt;备份备份备份，折腾数据库之前一定要备份！不然就等着哭吧。代码用版本控制管理，数据库备份，我才有底气胡搞一通。&lt;/p&gt;
&lt;p&gt;特别感谢 Google 以及 StackOverflow 提供的帮助。&lt;/p&gt;
&lt;p&gt;珍爱生命，请用 MySQL。&lt;/p&gt;
</content:encoded></item><item><title>2017年初杂记</title><link>https://frostming.com/posts/2017/01-19/2017-notes/</link><guid isPermaLink="false">https://frostming.com/2017/01-19/2017-notes/</guid><pubDate>Thu, 19 Jan 2017 20:50:43 GMT</pubDate><content:encoded>&lt;p&gt;2016 年就这样浑浑噩噩地过去了，犹记得年初时豪情壮志，要去拍星空，要把看过的电影书籍一一记录下来，最后都流产了，真是懒癌 + 拖延症晚期了。&lt;/p&gt;
&lt;p&gt;买相机也有两年了，快门数将将到一万，却始终没有拍到让自己满意的，得意的作品。年初也去了趟珠海，出的片却差强人意。圆明新园的题材是我喜欢的，当天天气也非常作美，整个画面的色彩，氛围都非常到位，但是非常遗憾的是，没有对上焦。由此获得的教训是，按下快门前，一定要检查相片各状态都到达了最佳。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//webp.frostming.com/images/99aeadf07d9556af65f4b0eb4daf936.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;//webp.frostming.com/images/6eea1a60b93301d4bf936e0d6316f08.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在情侣路上的灯塔边拍的一组，非常适合做成小清新风格，然而却在最重要的问题上折了：曝光不足，强行提亮暗部的画质捉急。由此获得的教训是，曝光要准确，拍完一定要看直方图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//webp.frostming.com/images/daa5d98a62c0d04bdeabb11e4b3c4fb.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;烟花表演当晚拍摄环境可谓恶劣，前前后后都是人，所幸有几张能看的，算是差强人意之中的「强」吧。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//webp.frostming.com/images/901e1eab043ba09ea666c2789657a0c.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;//webp.frostming.com/images/908111cd78c2f7ecf926e2c8880aa3a.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其实拍了那么几百张照片，能挑出十几张能看的，这是非常正常的事情，但是那种将美景错过的遗憾，却无法弥补。我要为了心目中的那幅画面而继续努力，而不是面向朋友圈拍照，为了强行凑出一个九宫格，放低对自己的要求。&lt;/p&gt;
&lt;p&gt;说到这里，也引发了一个哲学的思考：如果自己单独一人能达到的效果是 100%，那么多加入一个人，效果就要扣 20%。一个人，可以去爬楼，出门一趟单纯为了踩点，可以为了一张照片的完美而反复调整参数。而多加一个人，就不可能这样做，你只能为了赶路，为了顾忌对方的情绪，而匆匆按下快门，来不及检查曝光，来不及手动对焦。这 20%，就是你与 TA 之间情谊的代价，20% 就在那里，但这个改进却做不到。&lt;/p&gt;
&lt;p&gt;元旦和女友及丈母娘去东门逛街，本来我也只用在旁边看包玩手机就好了，但是到了要帮我挑衣服的时候，就有麻烦了。在一座商场里的挑选范围也有限，试了几件，都不是我理想中合意的那款，碰到一件 80% 满意的，我说就它了。我实在不能再挑挑拣拣，甚至最后还没买成，80% 满意也不错了对么，但它离 100% 还有 20% 呢？这 20% 就是女友和丈母娘对我的爱（是不是恶心了点）。&lt;/p&gt;
&lt;p&gt;新的一年，目标同样远大，只希望自己能平衡好这 20%，让自己不遗憾，就够了。&lt;/p&gt;
</content:encoded></item><item><title>历史惊人的重合</title><link>https://frostming.com/posts/2016/12-27/coincidence-of-history/</link><guid isPermaLink="false">https://frostming.com/2016/12-27/coincidence-of-history/</guid><pubDate>Tue, 27 Dec 2016 20:14:35 GMT</pubDate><content:encoded>&lt;p&gt;刘老三生于楚国沛县，父亲名唤刘太公，仗义游侠，老不正经。却得张良萧何辅佐，兼有韩信，以三尺剑得天下。儿子谥惠帝，毫无存在感，由北方代地的王子（文帝）继位，之后有景帝休养生息，重孙武帝大开大阖。&lt;/p&gt;
&lt;p&gt;朱重八生于濠州凤阳，祖籍沛县，父亲名作朱五四，布衣和尚，街头乞食。文有刘温李善长，武有徐达常遇春，以布衣得大宝。儿子谥惠帝，年幼无威信，由北方燕地的王子（成祖）继位，之后仁宗与民休息，重孙宣宗最像朱棣（可惜不长命）。&lt;/p&gt;
&lt;p&gt;历史简直惊人的相似！&lt;/p&gt;
</content:encoded></item><item><title>学习的快感</title><link>https://frostming.com/posts/2016/10-15/xue-xi-de-kuai-gan/</link><guid isPermaLink="false">https://frostming.com/2016/10-15/xue-xi-de-kuai-gan/</guid><pubDate>Sat, 15 Oct 2016 21:39:46 GMT</pubDate><content:encoded>&lt;p&gt;最近两周在写一个 web application，有了之前捣鼓个人博客的经验，又狠狠地提升了一次自己的前端技能。于是我居然从对前端一无所知到现在能写一写 javascript 脚本了。自己找第三方库，看文档，Google，解决碰到一个一个坑，所有遇到的难题都找到了相当完美的解决方案。看着自己从零一点一点拼起来的应用调通运行，成就感真是无与伦比。&lt;/p&gt;
&lt;p&gt;这种沉浸的体验，不是经常能遇到的。我一般不轻易开始一件事，一旦开始了，就无法容忍它绵延太长时间，我需要短期看到效果，否则我的激情恐怕就退却了。于是就不得不提升优先级，别的事都多少搁下了。&lt;/p&gt;
&lt;p&gt;先前在知乎上看到一篇文章，说的是「不要被兴趣牵着鼻子走」。反思自己，恐怕自己就犯了这种错误（如果姑且认同该文的观点）。一件熟悉的、无挑战的、重要的工作与一件新颖的、能提升技能的、低优先级工作摆在我面前，我总是不能控制的选择了后者。但有得有失，我也因此进境颇快。&lt;/p&gt;
&lt;p&gt;直到大学毕业我都只会用一点 C，原则是如果老东西能用，就还用老的，新的东西还得再学，还得再看一堆书籍资料。后来接触了 Python，「哇，怎么会这么好用」，相见恨晚。现在又学到了前端，HTML+CSS+javascript 写的东西立即就能在任意一个浏览器的看到，还比费尽心思写的桌面 GUI 好看。我对这种美学结合了 Geek 性的东西毫无抵抗力，连 Python 都有点比下去了。更别说还有一堆大神写的好用无比的库，写网页比之十年前轻松太多了，实在要感谢现在这个好时代。&lt;/p&gt;
&lt;p&gt;这与老一辈人不愿接触新鲜事物何其相像，其实只要着手去学了，就会「哇，怎么会这么好用」，然后把旧的东西弃之如敝屣。我为那些终日声色犬马找不到人生目标的人感到遗憾，这个世界上值得学的东西何止一箩筐，且其中的绝大部分，并不算难，只是难在开始。就算日夜不停学，只怕几辈子都不够，殆矣殆矣，我命不长，只好挑感兴趣的学。&lt;/p&gt;
&lt;p&gt;愿我始终能体会到学习的快感。&lt;/p&gt;
</content:encoded></item><item><title>个人网站宣告上线</title><link>https://frostming.com/posts/2016/09-10/ge-ren-wang-zhan-xuan-gao-shang-xian/</link><guid isPermaLink="false">https://frostming.com/2016/09-10/ge-ren-wang-zhan-xuan-gao-shang-xian/</guid><pubDate>Sat, 10 Sep 2016 15:27:58 GMT</pubDate><content:encoded>&lt;p&gt;生命不息，折腾不止。绕了一大圈，还是回到了 NexT 主题，在此特别感谢 &lt;a href=&quot;http://iissnan.com/&quot;&gt;IIssNan&lt;/a&gt; 做的这么赞的主题。&lt;/p&gt;
&lt;p&gt;自己也尝试过折腾主题，原先对前端望而却步，但当我真的学进去了，才发现这个世界上有模板语言，前端框架，有 F12，万物都为你准备好了，其实并没有那么难。喏，一个半成品&lt;a href=&quot;https://github.com/frostming/hexo-theme-landing-page&quot;&gt;在这里&lt;/a&gt;。说好看也好看，但自己的要求太高，而技术水平又跟不上，还是用别人成熟的主题吧，毕竟是第一受欢迎的 Hexo 主题，作者维护还是很负责的，甚至文档页面都完爆别人几条街。&lt;/p&gt;
&lt;p&gt;折腾了一圈也不算毫无收获，至少我对 JavaScript、CSS 和 CSS 预处理语言的了解不再是零。这不，我一直希望自己的网站可以有一个超大落地图作封面，辅以简洁大气的文字，当当当当，如头图所示，个人还是非常满意的，框架参考了 &lt;a href=&quot;http://iissnan.com/&quot;&gt;IIssNan&lt;/a&gt; 同学的个人主页，字体布局则来自 &lt;a href=&quot;https://startbootstrap.com/template-overviews/grayscale/&quot;&gt;Grayscale&lt;/a&gt;。为了和博客模板的亮色背景匹配，我对 Grayscale 进行了反色。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://startbootstrap.com/img/templates/grayscale.jpg&quot; alt=&quot;Grayscale&quot; /&gt;&lt;/p&gt;
&lt;p&gt;域名用的是阿里云的万网域名，稳定可靠，.win 域名很便宜，一年 5 块钱，我一口气买了 3 年。&lt;/p&gt;
&lt;p&gt;那么，重新出发吧。&lt;/p&gt;
</content:encoded></item><item><title>如何编写向前兼容的 Python 代码</title><link>https://frostming.com/posts/2016/08-23/ru-he-bian-xie-xiang-qian-jian-rong-de-python-dai-ma/</link><guid isPermaLink="false">https://frostming.com/2016/08-23/ru-he-bian-xie-xiang-qian-jian-rong-de-python-dai-ma/</guid><pubDate>Tue, 23 Aug 2016 15:13:23 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;a href=&quot;https://juejin.im/entry/592553dfda2f60005d7be514/detail&quot;&amp;gt;
&amp;lt;img src=&quot;https://badge.juejin.im/entry/592553dfda2f60005d7be514/likes.svg?style=flat&quot; /&amp;gt;
&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文翻译自 &lt;a href=&quot;http://lucumr.pocoo.org/about/&quot;&gt;Armin Ronacher&lt;/a&gt; 的文章 &lt;a href=&quot;http://lucumr.pocoo.org/2011/1/22/forwards-compatible-python/&quot;&gt;Writing Forwards Compatible Python Code&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于网络应用来说，目前最安全的做法是仍然坚持使用 Python 2.x，即使是新的项目。一个简单的原因是现在 Python 3 还不支持足够多的库，而将已有的库移植到 Python 3 上是一个巨大的工作。当所有人都在抱怨升级到 Python 3 是如此艰难和痛苦的时候，我们如何才能让这件事变得容易一点呢？&lt;/p&gt;
&lt;p&gt;对于一个顶层应用来说，如果它的依赖库移植后行为一致，把它升级到 Python 3 就不难了。其实升级到 Python 3 从来都不应该是一件痛苦的事。因此，本文尝试列举一些编写新的代码时应该和不应该做的事。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;以 2.6 为基准&lt;/h2&gt;
&lt;p&gt;如果你要编写一个新项目，就从 Python 2.6 或 2.7 开始，它们有许多升级到 Python 3 的便利。如果你不打算支持旧版本的 Python 你已经可以使用许多 Python 3 中的新特性了，只要在代码中打开就行了。&lt;/p&gt;
&lt;p&gt;你应该使用的一些 &lt;code&gt;__future__&lt;/code&gt; 中的特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;division&lt;/code&gt; 我必须承认我非常讨厌 Python 2 中的 future division。当我审核代码时我需要不停地跳到文件开头来检查用的是哪种除法机制。然而这是 Python 3 中的默认除法机制，所以你需要使用它。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;absolute_import&lt;/code&gt; 最重要的特性。当你在 foo 包内部时，&lt;code&gt;from xml import bar&lt;/code&gt; 不再导入一个 &lt;code&gt;foo.xml&lt;/code&gt; 的模块，你需要改为 &lt;code&gt;from .xml import bar&lt;/code&gt;。更加清晰明了，帮助很大。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;至于函数形式的 &lt;code&gt;print&lt;/code&gt; 导入，为了代码清晰，我不建议使用它。因为所有的编辑器会将&lt;code&gt;print&lt;/code&gt; 作为关键字高亮，这此让人产生困惑。如果一件事情在不同的文件里表现不一致我们最好尽可能避免它。好在用 2to3 工具可以很方便地转换，所以我们完全没必要从 future 中导入它。&lt;/p&gt;
&lt;p&gt;最好不要从 future 中导入 &lt;code&gt;unicode_literals&lt;/code&gt;，尽管它看上去很吸引人。原因很简单，许多 API 在不同地方支持的字符串类型是不同的，&lt;code&gt;unicode_literals&lt;/code&gt; 会产生反作用。诚然，这个导入在某些情况下很有用，但它更多地受制于底层的接口（库），且由于它是 Python 2.6 的特性，有许多库支持这个导入。不需要导入 &lt;code&gt;unicode_literals&lt;/code&gt; 你就能使用 &lt;code&gt;b&apos;foo&apos;&lt;/code&gt; 这样的写法，两种方法都是可用的并且对 2to3 工具很有帮助。&lt;/p&gt;
&lt;h2&gt;文件输入输出与 Unicode&lt;/h2&gt;
&lt;p&gt;文件的输入输出在 Python 3 中改变很大。你终于不用在为新项目开发 API 时费尽心力处理文件 unicode 编码的问题了。&lt;/p&gt;
&lt;p&gt;当你处理文本数据时，使用 &lt;a href=&quot;http://docs.python.org/library/codecs.html&quot;&gt;codecs.open&lt;/a&gt; 来打开文件。默认使用 utf-8 编码除非显式地定义或者只对 unicode 字符串操作。若你决定使用二进制输入输出，打开文件时记得用 &lt;code&gt;&apos;rb&apos;&lt;/code&gt; 而不是 &lt;code&gt;&apos;r&apos;&lt;/code&gt; 标志。这对于适当的 Windows 支持来说是必要的。&lt;/p&gt;
&lt;p&gt;当你处理字节型数据时，使用 &lt;code&gt;b&apos;foo&apos;&lt;/code&gt; 将字符串标为字节型，这样 2to3 就不会将它转换为 unicode。注意以下 Python 2.6：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; b&apos;foo&apos;
&apos;foo&apos;
&amp;gt;&amp;gt;&amp;gt; b&apos;foo&apos;[0]
&apos;f&apos;
&amp;gt;&amp;gt;&amp;gt; b&apos;foo&apos; + u&apos;bar&apos;
u&apos;foobar&apos;
&amp;gt;&amp;gt;&amp;gt; list(b&apos;foo&apos;)
[&apos;f&apos;, &apos;o&apos;, &apos;o&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与 Python 3 对待字节型字符串的区别：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; b&apos;foo&apos;[0]
102
&amp;gt;&amp;gt;&amp;gt; b&apos;foo&apos; + &apos;bar&apos;
Traceback (most recent call last):
  File &quot;&amp;lt;stdin&amp;gt;&quot;, line 1, in &amp;lt;module&amp;gt;
TypeError: can&apos;t concat bytes to str
&amp;gt;&amp;gt;&amp;gt; list(b&apos;foo&apos;)
[102, 111, 111]
Traceback (most recent call last):
  File &quot;&amp;lt;stdin&amp;gt;&quot;, line 1, in &amp;lt;module&amp;gt;
TypeError: can&apos;t concat bytes to str
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了达成与 Python 2.6 同样的效果，你可以这样做：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; b&apos;foo&apos;[0:0 + 1]
b&apos;f&apos;
&amp;gt;&amp;gt;&amp;gt; b&apos;foo&apos; + &apos;bar&apos;.encode(&apos;latin1&apos;)
b&apos;foobar&apos;
&amp;gt;&amp;gt;&amp;gt; to_charlist = lambda x: [x[c:c + 1] for c in range(len(x))]
&amp;gt;&amp;gt;&amp;gt; to_charlist(b&apos;foo&apos;)
[b&apos;f&apos;, b&apos;o&apos;, b&apos;o&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此代码在 2.6 和 3.x 上均能正常工作。&lt;/p&gt;
&lt;h2&gt;安全好过道歉&lt;/h2&gt;
&lt;p&gt;在很多事情上 2to3 并不能达到预期效果。一部分是 2to3 可能有 BUG 的地方，另外的则是因为 2to3 不能很好的预测你的代码的目的。&lt;/p&gt;
&lt;h3&gt;str 相关的递归错误&lt;/h3&gt;
&lt;p&gt;在 Python 2 中很多人像下面这样写代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Foo(object):
    def __str__(self):
        return unicode(self).encode(&apos;utf-8&apos;)
    def __unicode__(self):
        return u&apos;Hello World&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2to3 预设你的 API 不兼容 unicode ，会将它转换成下面这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Foo(object):
    def __str__(self):
        return str(self).encode(&apos;utf-8&apos;)
    def __unicode__(self):
        return &apos;Hello World&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就有错误了。首先 &lt;code&gt;__unicode__&lt;/code&gt; 不能在 Python 3 中使用，其次当你对 &lt;code&gt;Foo&lt;/code&gt; 的一个实例调用 &lt;code&gt;str()&lt;/code&gt; 方法时，&lt;code&gt;__str__&lt;/code&gt; 将调用自身而由于无限递归触发一个 RuntimeError。这个错误可以通过自定义 2to3 修改器解决，也可以写一个简单的辅助类来检查是否是 Python 3：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import sys

class UnicodeMixin(object):
    if sys.version_info &amp;gt; (3, 0):
        __str__ = lambda x: x.__unicode__()
    else:
        __str__ = lambda x: unicode(x).encode(&apos;utf-8&apos;)

class Foo(UnicodeMixin):
    def __unicode__(self):
        return u&apos;Hello World&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用这种方法你的对象在 Python 3 中仍然有一个 &lt;code&gt;__unicode__&lt;/code&gt; 属性，但却不会有任何损害。当你想去掉 Python 2 支持时你只需遍历 &lt;code&gt;UnicodeMixin&lt;/code&gt; 的所有派生类，将 &lt;code&gt;__unicode__&lt;/code&gt; 重命名为 &lt;code&gt;__str__&lt;/code&gt;，然后再删掉辅助类。&lt;/p&gt;
&lt;h3&gt;字符串比较&lt;/h3&gt;
&lt;p&gt;这个问题会稍微棘手一点，在 Python 2 中下面这段代码是正确的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; &apos;foo&apos; == u&apos;foo&apos;
True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Python 3 中却并非如此：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; b&apos;foo&apos; == &apos;foo&apos;
False
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更糟糕的是 Python 2 不会抛出一个比较的警告（即使打开了 Python-3-warnings），Python 3 也不会。那么你如何找到问题所在呢？我写了一个名为 &lt;a href=&quot;http://pypi.python.org/pypi/unicode-nazi&quot;&gt;unicode-nazi&lt;/a&gt; 的小型辅助模块。只要导入该模块，当你试图同时操作 unicode 和 bytes 型字符串时会自动抛出警告：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; import unicodenazi
&amp;gt;&amp;gt;&amp;gt; u&apos;foo&apos; == &apos;foo&apos;
__main__:1: UnicodeWarning: Implicit conversion of str to unicode
True
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;字符串是什么？&lt;/h2&gt;
&lt;p&gt;下面这张表列举了一些字节型字符串，和它们在 Python 3 中将变成什么：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;Python 3 中的类型（unicode == str）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;标识&lt;/td&gt;
&lt;td&gt;unicode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;文档字符串&lt;/td&gt;
&lt;td&gt;unicode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;__repr__&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;unicode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;字典的字符键&lt;/td&gt;
&lt;td&gt;unicode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WSGI 的环境变量键&lt;/td&gt;
&lt;td&gt;unicode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP 的 header 值，WSGI 的 环境变量值&lt;/td&gt;
&lt;td&gt;unicode，在 3.1 中仅限于 ASCII，在 3.2 中仅限于 latin1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URL&lt;/td&gt;
&lt;td&gt;unicode，部分 API 也接受字节。需要特别注意的是，为了使用所有标准库函数，URL 需要编码为 utf-8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;文件名&lt;/td&gt;
&lt;td&gt;unicode 或者字节，大部分 API 接受两者但不支持隐式转换。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;二进制内容&lt;/td&gt;
&lt;td&gt;字节或字节序列。注意第二种类型是可变的，所以你要清醒认识到你的字符串对象是可变的。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python 代码&lt;/td&gt;
&lt;td&gt;unicode，在交给 exec 执行前你需要自行解码。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Latin1 很特别&lt;/h2&gt;
&lt;p&gt;在某些地方（比如 WSGI）unicode 字符串必须是 latin1 的子集。这是因为 HTTP 协议并未指定编码方式，为了保证安全，假定为使用 latin1 。假如你要同时控制通信的两端（比如 cookies）你当然可以使用 utf-8 编码。那么问题来了：如果请求头只能是 latin1 编码时是怎么工作的呢？在且仅在 Python 3 中你需要用一些小伎俩：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return cookie_value.encode(&apos;utf-8&apos;).decode(&apos;latin1&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你只是反 unicode 字符串伪编码为 utf-8。WSGI 层会将它重新编码为 latin1 并将这个错误的 utf-8 字符串传输出去，你只要在接收端也做一个反向的变换就可以了。&lt;/p&gt;
&lt;p&gt;这虽然很丑陋，但这就是 utf-8 在请求头中的工作方式，而且也只有 cookie 头受此影响，反正 cookie 头也不是很可靠。&lt;/p&gt;
&lt;p&gt;在 WSGI 还剩下的问题就只有 PATH_INFO / SCRIPT_NAME 元组了，你的框架运行在 Python 3 时应该解决这个问题。&lt;/p&gt;
</content:encoded></item><item><title>Python 有序字典的实现</title><link>https://frostming.com/posts/2016/07-07/python-you-xu-zi-dian-de-shi-xian/</link><guid isPermaLink="false">https://frostming.com/2016/07-07/python-you-xu-zi-dian-de-shi-xian/</guid><pubDate>Thu, 07 Jul 2016 20:16:20 GMT</pubDate><content:encoded>&lt;p&gt;最近在看 requests 源码的时候看到作者使用了 urllib3 中自己实现的&lt;code&gt;OrderedDict&lt;/code&gt;类，收获颇多。自己实现一个数据结构往往是最需要算法和优化的地方，各种语法糖黑科技，相当的 Pythonic，看这种代码实在是一种享受。如果要我自己实现的话，自己会想到用一个有序存储的对象（如列表）去 hack 内部的实现，但这样有几个缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;列表的插入、删除操作性能不如字典，复杂度是 O(N) 量级的。&lt;/li&gt;
&lt;li&gt;自定义类需要继承于&lt;code&gt;dict&lt;/code&gt;，没有利用继承的方法特性。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;来看看大神是怎么实现的吧。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;__init__&lt;/code&gt;方法&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class OrderedDict(dict):
    def __init__(self, *args, **kwds):
        if len(args) &amp;gt; 1:
            raise TypeError(&apos;expected at most 1 arguments, got %d&apos; % len(args))
        try:
            self.__root
        except AttributeError:
            self.__root = root = []                     # sentinel node
            root[:] = [root, root, None]
            self.__map = {}
        self.__update(*args, **kwds)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;a href=&quot;http://frostming.github.io/2016/06/13/python-list/&quot;&gt;上一篇文章&lt;/a&gt;中说到一些关于列表的坑，说到不要用&lt;code&gt;a=b=[]&lt;/code&gt;这样的语句来初始化，其实也不全然，我们来看 7-8 行做了什么。第 7 行使&lt;code&gt;self.__root&lt;/code&gt;和&lt;code&gt;root&lt;/code&gt;同时指向一个空列表，相关于给&lt;code&gt;self.__root&lt;/code&gt;起了一个短别名，关键是第 8 行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; root[:] = [root, root, None]
&amp;gt;&amp;gt;&amp;gt; root
[[...], [...], None]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;什么鬼？没见过&lt;code&gt;[...]&lt;/code&gt;这种的啊，我来看看&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; root[0]
[[...], [...], None]
&amp;gt;&amp;gt;&amp;gt; root[0] is root
True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What? 自己是自己的元素？简直是从前有座山山上有座庙啊，子子孙孙无穷尽啊。到底发生了什么事？Python 中万物皆指针，而&lt;code&gt;root[:]=...&lt;/code&gt;的赋值是不改变指针指向的地址而是改变指向地址的内容。右边第一个和第二个元素是指向自己的指针，这样就构造了一个我中有我的列表。
&lt;img src=&quot;//webp.frostming.com/images/1e6f8e56cb6cea791e53c29742da76c9.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;再看命名，明白了，这是一个&lt;strong&gt;双向链表&lt;/strong&gt;！列表的前两个元素分别指向上一个结点和下一个结点，第三个元素是结点的值。只用两行就初始化了一个链表，学到了。另外还初始化了一个字典，暂时不知道有什么用。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;__setitem__&lt;/code&gt;方法&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
    &apos;od.__setitem__(i, y) &amp;lt;==&amp;gt; od[i]=y&apos;
    # Setting a new item creates a new link which goes at the end of the linked
    # list, and the inherited dictionary is updated with the new key/value pair.
    if key not in self:
        root = self.__root
        last = root[0]
        last[1] = root[0] = self.__map[key] = [last, root, key]
    dict_setitem(self, key, value)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键的部分到了，这个魔法方法加了第三个参数来方便子类扩展。函数体部分，画一个图就明白了。
&lt;img src=&quot;//webp.frostming.com/images/dc7661ce1072a03bfe45c6b33647def2.png&quot; alt=&quot;&quot; /&gt;!&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;root&lt;/code&gt;的上一个结点就是末结点，保存为&lt;code&gt;last&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;创建一个新结点，它的上结点和下结点分别设为&lt;code&gt;last&lt;/code&gt;和&lt;code&gt;root&lt;/code&gt;，结点的值为字典的键。&lt;/li&gt;
&lt;li&gt;将&lt;code&gt;last&lt;/code&gt;的下结点和&lt;code&gt;root&lt;/code&gt;的上结点指向该结点。&lt;/li&gt;
&lt;li&gt;将结点加入&lt;code&gt;__map&lt;/code&gt;并加入字典。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样创建就结点就变成了新的末结点了。从此也可看出，&lt;code&gt;root&lt;/code&gt;是一个守护结点，本身并不存储值，但会简化算法。&lt;code&gt;__map&lt;/code&gt; 是结点的哈希表，避免了从头开始寻找所需的结点。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;__delitem__&lt;/code&gt;方法&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;def __delitem__(self, key, dict_delitem=dict.__delitem__):
    &apos;od.__delitem__(y) &amp;lt;==&amp;gt; del od[y]&apos;
    # Deleting an existing item uses self.__map to find the link which is
    # then removed by updating the links in the predecessor and successor nodes.
    dict_delitem(self, key)
    link_prev, link_next, key = self.__map.pop(key)
    link_prev[1] = link_next
    link_next[0] = link_prev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除结点时，从哈希表中弹出该结点，然后将它的上结点和下结点相连，并从字典中删除。&lt;/p&gt;
&lt;p&gt;实现了这三个方法，剩下的就好办了，&lt;code&gt;__iter__&lt;/code&gt;只需从头开始遍历链表并取出键值就可以了。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;实现有序字典的关键在于选取一个合适的数据结构来存储顺序信息，这里作者使用了双向链表，然后把结点哈希。这样进行插入、删除操作的时间复杂度为 O(1) ，与&lt;code&gt;dict&lt;/code&gt;类型一致，代价就是 O(2n) 的空间复杂度。&lt;/p&gt;
</content:encoded></item><item><title>记一件关于高考的小事</title><link>https://frostming.com/posts/2016/gaokao/</link><guid isPermaLink="false">https://frostming.com/2016/gaokao/</guid><pubDate>Mon, 27 Jun 2016 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;原文载于知乎: https://www.zhihu.com/question/24047129/answer/105544625 &lt;br /&gt;
特辑录于此。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我爸是我们高中的语文老师，虽然他并没有教过我，但是作为一个尖子生，特别是身为教师子女的尖子生，会有一些额外的压力。高考前最后一次模考，我甚至拿到了全市第一，这在我爸眼里看来，我就是应该清华北大的人，觉得我一点点成绩的掉落都是不应该的。然而我自己知道我自己，我是那种不勤奋苦读的学生，高考前几天还能在网吧大干 War 3，隐隐的害怕会辜负父亲的期望。&lt;/p&gt;
&lt;p&gt;于是高考到来那天，从来考试都如履平地的我分外紧张，可能也带着兴奋，前一天晚上都没怎么睡好。想来也是心理素质不是特别过硬，下午考数学时，感觉头脑不好用，有一道大题几乎没有思路，另一道也没有把握，这在我考数学时是极少的。特别是考完之后，听着周围的人说这次数学挺简单的，我就心跳加速手心发凉，暗想我可能要在阴沟翻船了。&lt;/p&gt;
&lt;p&gt;回到家面对父母的询问我也只是说还可以，内心却翻江倒海。晚上十点熄灯睡觉后，满脑子都是那道大题的公式符号，和我写的答案。于是翻身起床开灯，在草稿纸上演算，明知这没有什么意义，却仿佛只为了内心的救赎。算了几道没把握的题之后，发现与考场答的相距甚远，双手都有些颤抖。我是不甘心的，于是开始做最后那道我没有思路的大题，然而几个方向没走通之后没了头绪，只能重新关灯睡觉。突然有了一个想法，又开灯演算，又无果而终。如此几次之后被我爸注意到了，他走到我房间跟我说：「你到现在还没做出来，还有什么遗憾呢？」见我还是不放弃，就说：「要不我给你打个电话问问？」旋即拨了手机，说道：「W 老师（我的班主任，数学老师），你在学生公寓吗？你帮我问一下周同学（数学小王子），数学做得怎么样？」「哦，好，我知道了，谢谢。」然后他告诉我，Z同学好多题都没做出来哦。不得不说这个消息很安慰我，我平复了许多，于是关灯睡觉了。第二天的理综，我发挥良好。&lt;/p&gt;
&lt;p&gt;等到高考成绩出来，我的数学不出所料的跪了，比班上平均分还低 2 分，但是其他科目发挥正常，理综拔群，虽然没有上清北，也进了一所 985。现在想来也没有遗憾了，如果人生重来，那次数学再让我考一次，可能也不会考得更高。至于Z同学，他的数学当然没有跪，我才醒悟，我爸那晚的那个电话，根本就没有拨出去，他演了一出戏，却把我从崩溃的边缘拉了回来。&lt;/p&gt;
&lt;p&gt;到现在我依然感激，平时严厉的父亲，在人生最大考之时，却给了我宽容和温暖。&lt;/p&gt;
</content:encoded></item><item><title>六月夏至</title><link>https://frostming.com/posts/2016/06-15/summer-of-june/</link><guid isPermaLink="false">https://frostming.com/2016/06-15/summer-of-june/</guid><pubDate>Wed, 15 Jun 2016 21:22:23 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;blockquote&amp;gt;&lt;/p&gt;
&lt;p&gt;七岁的那一年，抓住那只蝉，以为能抓住夏天&lt;/p&gt;
&lt;p&gt;&amp;lt;cite&amp;gt;- 五月天《如烟》&amp;lt;/cite&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/blockquote&amp;gt;&lt;/p&gt;
&lt;p&gt;对于六月的好感，从小时候起，就根植在我的记忆里。&lt;/p&gt;
&lt;p&gt;暑假就要来了，虽然已经告别学生时代多年，这个时节依然令我躁动。就像一周之中，最喜欢周五的晚上，它不同于周六的尽情狂欢，那是一种对于周末的未知与兴奋。六月，端午节可以放半天假到外婆家吃几枚粽子，中考可以放四天假，高考完可以去网吧包夜，期末考试完可以享受整整两个月的暑假。空气中弥漫的是西瓜的香，知了的聒躁，和七里香的旋律：「窗外的麻雀，在电线杆上多嘴，你说这一句，很有夏天的感觉」。&lt;/p&gt;
&lt;p&gt;我的家乡是一个江西南部的小县城，端午节前后这段日子，老人称作「龙舟水」，雷鸣电闪，大雨瓢泼，路上积水是少不了的。于是我经常淌着浅至脚踝，深过膝盖的积水前行，伞是不太顶用的。与好友约好大战魔兽，涉水一路到网吧，辛酸又艰难，竟也有些快乐。&lt;/p&gt;
&lt;p&gt;外面大汗淋漓，回到家全家都缩进空调屋里，边吃着饭，边回顾着《我爱我家》，那时候的电视剧，朴素又温馨，实足的包袱让我们不时发出阵阵笑声。&lt;/p&gt;
&lt;p&gt;这三个片段平实无奇，却构成了我对六月，对夏天的独特记忆。&lt;/p&gt;
</content:encoded></item><item><title>Python 列表小技巧</title><link>https://frostming.com/posts/2016/06-13/python-lie-biao-xiao-ji-qiao/</link><guid isPermaLink="false">https://frostming.com/2016/06-13/python-lie-biao-xiao-ji-qiao/</guid><description>关于列表的一些可能遇到的坑</description><pubDate>Mon, 13 Jun 2016 13:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Python 中的列表和字典一样，都是可变数据类型，与字符串和整型相比，它具有一些独特的特性。在平常使用中， 也会经常遇到一些坑，本文试着举一些例子并说明。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;列表的拷贝&lt;/h2&gt;
&lt;h3&gt;直接赋值&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; a = [1,2,3]
&amp;gt;&amp;gt;&amp;gt; b = a
&amp;gt;&amp;gt;&amp;gt; a is b
True
&amp;gt;&amp;gt;&amp;gt; a[0]=5
&amp;gt;&amp;gt;&amp;gt; a
[5, 2, 3]
&amp;gt;&amp;gt;&amp;gt; b
[5, 2, 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在此例中，直接通过赋值将&lt;code&gt;a&lt;/code&gt;赋给了&lt;code&gt;b&lt;/code&gt;，此时，仅仅是为该列表增加了一个引用&lt;code&gt;b&lt;/code&gt;，&lt;code&gt;a&lt;/code&gt;与&lt;code&gt;b&lt;/code&gt;指向内存中同一个区域，通过&lt;code&gt;a&lt;/code&gt;改变列表的值也同时影响&lt;code&gt;b&lt;/code&gt;。请注意，这里有一个坑，很多人在初始化语句中写&lt;code&gt;a = b = []&lt;/code&gt;，这是错误的，会导致任意一个变动都会在&lt;code&gt;a&lt;/code&gt;与&lt;code&gt;b&lt;/code&gt;中同步，而且会很难 debug。正确写法应该是分别初始化。&lt;/p&gt;
&lt;h3&gt;使用&lt;code&gt;list&lt;/code&gt;工厂函数&lt;/h3&gt;
&lt;p&gt;为了创建一个&lt;code&gt;a&lt;/code&gt;的拷贝，可以使用&lt;code&gt;list&lt;/code&gt;工厂函数，这也是&lt;em&gt;Python Cookbook&lt;/em&gt;中的推荐做法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; a = [1,2,3]
&amp;gt;&amp;gt;&amp;gt; b = list(a)
&amp;gt;&amp;gt;&amp;gt; a is b
False
&amp;gt;&amp;gt;&amp;gt; a[0]=5
&amp;gt;&amp;gt;&amp;gt; a
[5, 2, 3]
&amp;gt;&amp;gt;&amp;gt; b
[1, 2, 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完美，&lt;code&gt;a&lt;/code&gt;和&lt;code&gt;b&lt;/code&gt;是两个不同的列表了！除了使用工厂函数，切片也可以达到同样的效果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; b = a[:]
&amp;gt;&amp;gt;&amp;gt; b is a
False
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用&lt;code&gt;copy&lt;/code&gt;模块&lt;/h3&gt;
&lt;p&gt;一切看起来都很美好，真的是这样吗？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; a = [1,[1,2],3]
&amp;gt;&amp;gt;&amp;gt; b = list(a)
&amp;gt;&amp;gt;&amp;gt; a[0] = 5
&amp;gt;&amp;gt;&amp;gt; a[1][1] = 5
&amp;gt;&amp;gt;&amp;gt; a
[5, [1, 5], 3]
&amp;gt;&amp;gt;&amp;gt; b
[1, [1, 5], 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What?!&lt;code&gt;b&lt;/code&gt;的第二个元素子列表中的值还是被改变了！原来，&lt;code&gt;list&lt;/code&gt;和&lt;code&gt;[:]&lt;/code&gt;都是在内存中创建了一个新的对象并赋给了&lt;code&gt;b&lt;/code&gt;，但是子列表仍然只有一份。也就是说，只复制了「一层」。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，python 中自带了一个&lt;code&gt;copy&lt;/code&gt;模块专门做拷贝的事情，使用模块下的&lt;code&gt;deepcopy&lt;/code&gt;函数来深层次拷贝一个对象，调用它试试看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; import copy
&amp;gt;&amp;gt;&amp;gt; b = copy.deepcopy(a)
&amp;gt;&amp;gt;&amp;gt; a[0] = 5
&amp;gt;&amp;gt;&amp;gt; a[1][1] = 5
&amp;gt;&amp;gt;&amp;gt; a
[5, [1, 5], 3]
&amp;gt;&amp;gt;&amp;gt; b
[1, [1, 2], 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;妈妈再也不用担心我的列表交叉影响的问题了！&lt;/p&gt;
&lt;h2&gt;列表作为函数参数&lt;/h2&gt;
&lt;h3&gt;参数的默认值&lt;/h3&gt;
&lt;p&gt;python 的函数参数传递方法都是引用传递，而不是值传递，对于列表与字典这种可变类型就要特别小心了，可能会出现以下的错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; def foo(a=[]):
...     a.append(1)
...     print a
...
&amp;gt;&amp;gt;&amp;gt; foo()
[1]
&amp;gt;&amp;gt;&amp;gt; foo()
[1, 1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;a&lt;/code&gt;列表会保存上次调用之后的内容！因为这个列表在内存中创建以后就一直存在，参数&lt;code&gt;a&lt;/code&gt;默认指向这个对象。所以，&lt;strong&gt;要避免使用列表或字典作为函数的默认参数&lt;/strong&gt;。使用下面的方法代替，只多一行，而且非常 pythonic：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def foo(a=None, b=None):
    a = a or []
    b = b or {}
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;更改传入列表的内容。&lt;/h3&gt;
&lt;p&gt;由于列表是可变的，你可以在函数体内增删元素，更改元素的值，从而影响到原列表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; def foo(array):
...     array.append(1)
...
&amp;gt;&amp;gt;&amp;gt; a=[0]
&amp;gt;&amp;gt;&amp;gt; foo(a)
&amp;gt;&amp;gt;&amp;gt; a
[0, 1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而有些时候，我们希望整体更新列表，比如去重操作&lt;code&gt;array = list(set(array)&lt;/code&gt;，这时用上面的方法就不行了，因为这里创建了一个新的列表&lt;code&gt;list(set(array))&lt;/code&gt;并将其引用重新赋给了&lt;code&gt;array&lt;/code&gt;，而函数内的局部变量&lt;code&gt;array&lt;/code&gt;的更改是无法影响全局变量的，这与上一例不同的时上个例子并没有改变&lt;code&gt;array&lt;/code&gt;的值，只是改变了&lt;code&gt;array&lt;/code&gt;&lt;strong&gt;指向的对象&lt;/strong&gt;的值。&lt;/p&gt;
&lt;p&gt;这时候，我们又要搬出切片了。只需要改成&lt;code&gt;array[:] = list(set(array))&lt;/code&gt;就可以了！因为切片本质上是对&lt;code&gt;array&lt;/code&gt;中元素的操作，意思是把&lt;code&gt;list(set(array))&lt;/code&gt;赋给&lt;code&gt;array&lt;/code&gt;中的所有元素。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; def unique(array):
...     array[:]=list(set(array))
...
&amp;gt;&amp;gt;&amp;gt; a = [1, 2, 2, 3]
&amp;gt;&amp;gt;&amp;gt; unique(a)
&amp;gt;&amp;gt;&amp;gt; a
[1, 2, 3]
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Requests源码阅读v0.8.0</title><link>https://frostming.com/posts/2016/06-03/requestsyuan-ma-yue-du-v0-8-0/</link><guid isPermaLink="false">https://frostming.com/2016/06-03/requestsyuan-ma-yue-du-v0-8-0/</guid><pubDate>Fri, 03 Jun 2016 18:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;工作两年了，一直用 python 写一些 API 之类的东西，自动化框架也有涉及，却一直感觉对个人技能提升缓慢。决定开这个坑，是之前看到&lt;a href=&quot;https://github.com/wangshunping/read_requests&quot;&gt;@wangshunping&lt;/a&gt;的&lt;strong&gt;read requests&lt;/strong&gt;，生动有趣，可惜 0.8.0 之后没有更新了。待我稍稍有了一点看源码的动力，就想接着下去写。真是漫漫长路啊，4409 个 commit，1000 多个 PR，更何况还有珠玉在前，实在没有把握能把这块硬骨头给啃下来，写一点是一点吧。作为 python 的小学生，一些错误在所难免，希望大家指出，互相讨论。
下面就开始吧！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;目标&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;0.8.0 (2011-11-13)
++++++++++++++++++

* Keep-alive support!
* Complete removal of Urllib2
* Complete removal of Poster
* Complete removal of CookieJars
* New ConnectionError raising
* Safe_mode for error catching
* prefetch parameter for request methods
* OPTION method
* Async pool size throttling
* File uploads send real names
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;源码阅读&lt;/h2&gt;
&lt;h3&gt;v0.7.1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0.7.1 (2011-10-23)
++++++++++++++++++

* Move away from urllib2 authentication handling.
* Fully Remove AuthManager, AuthObject, &amp;amp;c.
* New tuple-based auth system with handler callbacks.
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;移除&lt;code&gt;urllib2&lt;/code&gt;的 authentication 处理&lt;/li&gt;
&lt;li&gt;完全移除&lt;code&gt;AuthManager&lt;/code&gt;, &lt;code&gt;AuthObject&lt;/code&gt;和。。。&amp;amp;c？&lt;/li&gt;
&lt;li&gt;新的元组形式的&lt;code&gt;auth&lt;/code&gt;机制和处理器回调函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1. 移除&lt;code&gt;urllib2&lt;/code&gt;的 authentication 处理&lt;/h4&gt;
&lt;p&gt;添加一个&lt;code&gt;auth.py&lt;/code&gt;文件，加入了自己实现的 auth 处理器，包含&lt;code&gt;http_basic&lt;/code&gt;和&lt;code&gt;http_digest&lt;/code&gt;，分别对应 Headers 中&lt;code&gt;Autohorization&lt;/code&gt;以&lt;code&gt;Basic&lt;/code&gt;和&lt;code&gt;Digest&lt;/code&gt;开头的情形。&lt;/p&gt;
&lt;h4&gt;2. 完全删除&lt;code&gt;AuthManager&lt;/code&gt;, &lt;code&gt;AuthObject&lt;/code&gt;和。。。&amp;amp;c？&lt;/h4&gt;
&lt;p&gt;由于接口改用了 session，于是就没有必要使用&lt;code&gt;AuthManager&lt;/code&gt;储存认证信息。使用自己实现的处理器，完全删除&lt;code&gt;models.py&lt;/code&gt;中相关的代码。&lt;/p&gt;
&lt;h4&gt;3. 新的元组形式的&lt;code&gt;auth&lt;/code&gt;机制和处理器回调函数。&lt;/h4&gt;
&lt;p&gt;现在：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;self.auth = auth_dispatch(auth)

if self.auth:
    auth_func, auth_args = self.auth
    r = auth_func(self, *auth_args)
    self.__dict__.update(r.__dict__)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;def dispatch(t):
    &quot;&quot;&quot;Given an auth tuple, return an expanded version.&quot;&quot;&quot;

    if not t:
        return t
    else:
        t = list(t)

    # Make sure they&apos;re passing in something.
    assert len(t) &amp;gt;= 2

    # If only two items are passed in, assume HTTPBasic.
    if (len(t) == 2):
        t.insert(0, &apos;basic&apos;)

    # Allow built-in string referenced auths.
    if isinstance(t[0], basestring):
        if t[0] in (&apos;basic&apos;, &apos;forced_basic&apos;):
            t[0] = http_basic
        elif t[0] in (&apos;digest&apos;,):
            t[0] = http_digest

    # Return a custom callable.
    return (t[0], tuple(t[1:]))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过&lt;code&gt;dispatch&lt;/code&gt;函数，若传入二元元组，则默认前面加上&lt;code&gt;&apos;basic&apos;&lt;/code&gt;，使用&lt;code&gt;http_basic&lt;/code&gt;处理，否则需要指定处理类型。支持自定义处理器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def pizza_auth(r, username):
    &quot;&quot;&quot;Attaches HTTP Pizza Authentication to the given Request object.
    &quot;&quot;&quot;
    r.headers[&apos;X-Pizza&apos;] = username

    return r

Then, we can make a request using our Pizza Auth::

&amp;gt;&amp;gt;&amp;gt; requests.get(&apos;http://pizzabin.org/admin&apos;, auth=(pizza_auth, &apos;kenneth&apos;))
&amp;lt;Response [200]&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;v0.7.2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0.7.2 (2011-10-23)
++++++++++++++++++

* PATCH Fix.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修正 BUG（略）&lt;/p&gt;
&lt;h3&gt;v0.7.3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0.7.3 (2011-10-23)
++++++++++++++++++

* Digest Auth fix.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修正 Digest Auth 的 BUG
主要是删除了一些 debug 的 print 语句，估计当时作者脑子也不清醒了，我还注意到他改了一个文件头的&quot;~&quot;的长度，是有够无聊的！0.7.1 到 0.7.3 都在一个多小时内完成，小伙子动力很足啊！&lt;/p&gt;
&lt;h3&gt;v0.7.4&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0.7.4 (2011-10-26)
++++++++++++++++++

* Sesion Hooks fix.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主要是一些代码的美化和小 BUG，给&lt;code&gt;session&lt;/code&gt;加了一个&lt;code&gt;keep_alive&lt;/code&gt;参数，暂时还没用上，应该是为以后做准备。&lt;/p&gt;
&lt;h3&gt;v0.7.5&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0.7.5 (2001-11-04)
++++++++++++++++++

* Response.content = None if there was an invalid repsonse.
* Redirection auth handling.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;咦？日期穿越了 10 年？哈哈，什么时候会改呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果是无效响应则&lt;code&gt;content = None&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;重定向认证处理&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1. 无效响应&lt;code&gt;content = None&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;加入一个 Error Handling:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try:
    self._content = self.raw.read()
except AttributeError:
    return None
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 重定向认证处理&lt;/h4&gt;
&lt;p&gt;一个 BUG，原来是用 dispatch 后的 auth 构造新的 Request 会导致错误，现在使用&lt;code&gt;self._auth&lt;/code&gt;保存原始 auth 并传入新的 Request 对象。&lt;/p&gt;
&lt;h3&gt;v0.7.6&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0.7.6 (2011-11-07)
++++++++++++++++++

* Digest authentication bugfix (attach query data to path)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Digest 认证的 BUG 修复（在路径后附上 query）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;path = urlparse(r.request.url).path
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p_parsed = urlparse(r.request.url)
path = p_parsed.path + p_parsed.query
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我注意到日期问题已经修复了：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Updated your 2001, to 2011... unless you went back in time ;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个幽默。&lt;/p&gt;
&lt;h3&gt;v0.8.0&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0.8.0 (2011-11-13)
++++++++++++++++++

* Keep-alive support!
* Complete removal of Urllib2
* Complete removal of Poster
* Complete removal of CookieJars
* New ConnectionError raising
* Safe_mode for error catching
* prefetch parameter for request methods
* OPTION method
* Async pool size throttling
* File uploads send real names
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;支持&lt;code&gt;keep_alive&lt;/code&gt;参数（填坑来了）&lt;/li&gt;
&lt;li&gt;完全抛弃&lt;code&gt;urllib2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;完全抛弃&lt;code&gt;Poster&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;完全抛弃&lt;code&gt;CookieJars&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;新的&lt;code&gt;ConnectionError&lt;/code&gt;抛出&lt;/li&gt;
&lt;li&gt;安全的处理异常机制。&lt;/li&gt;
&lt;li&gt;为请求方法加入&lt;code&gt;prefetch&lt;/code&gt;参数&lt;/li&gt;
&lt;li&gt;新的&lt;code&gt;OPTION&lt;/code&gt;方法&lt;/li&gt;
&lt;li&gt;节省 Async 池的大小&lt;/li&gt;
&lt;li&gt;上传文件发送真实文件名&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1. 支持&lt;code&gt;keep_alive&lt;/code&gt;参数&lt;/h4&gt;
&lt;p&gt;作者在 v0.8.0 全面转向&lt;code&gt;urllib3&lt;/code&gt;，这是个第三方的轮子，它相对于&lt;code&gt;urllib2&lt;/code&gt;最大的改进是可以重用 HTTP 连接，不用每个 request 都新建一个连接了。这样大大加快了大量 request 时的响应速度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;self.poolmanager = PoolManager(
    num_pools=self.config.get(&apos;pool_connections&apos;),
    maxsize=self.config.get(&apos;pool_maxsize&apos;)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;proxy = self.proxies.get(_p.scheme)

if proxy:
    conn = poolmanager.proxy_from_url(url)
else:
    # Check to see if keep_alive is allowed.
    if self.config.get(&apos;keep_alive&apos;):
        conn = self._poolmanager.connection_from_url(url)
    else:
        conn = connectionpool.connection_from_url(url)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;keep_alive&lt;/code&gt;是默认打开的，在&lt;code&gt;urllib3&lt;/code&gt;中维护了一个连接池，当对某个 url 进行请求时，会从连接池中取出该连接，然后发送请求时直接调用此连接的子方法。&lt;/p&gt;
&lt;h4&gt;2. 完全抛弃&lt;code&gt;urllib2&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;删除了&lt;code&gt;models.py&lt;/code&gt;中用来发送请求的&lt;code&gt;build_opener&lt;/code&gt;函数，使用&lt;code&gt;urllib3&lt;/code&gt;的&lt;code&gt;conn.urlopen&lt;/code&gt;方法。&lt;/p&gt;
&lt;h4&gt;3.完全抛弃&lt;code&gt;Poster&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;同上，用一个轮子换了另一个轮子。。&lt;/p&gt;
&lt;h4&gt;4. 完全抛弃&lt;code&gt;CookieJars&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;上测试&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def test_session_persistent_cookies(self):

    s = requests.session()

    # Internally dispatched cookies are sent.
    _c = {&apos;kenneth&apos;: &apos;reitz&apos;, &apos;bessie&apos;: &apos;monke&apos;}
    r = s.get(httpbin(&apos;cookies&apos;), cookies=_c)
    r = s.get(httpbin(&apos;cookies&apos;))

    # Those cookies persist transparently.
    c = json.loads(r.content).get(&apos;cookies&apos;)
    assert c == _c

    # Double check.
    r = s.get(httpbin(&apos;cookies&apos;), cookies={})
    c = json.loads(r.content).get(&apos;cookies&apos;)
    assert c == _c

    # Remove a cookie by setting it&apos;s value to None.
    r = s.get(httpbin(&apos;cookies&apos;), cookies={&apos;bessie&apos;: None})
    c = json.loads(r.content).get(&apos;cookies&apos;)
    del _c[&apos;bessie&apos;]
    assert c == _c

    # Test session-level cookies.
    s = requests.session(cookies=_c)
    r = s.get(httpbin(&apos;cookies&apos;))
    c = json.loads(r.content).get(&apos;cookies&apos;)
    assert c == _c

    # Have the server set a cookie.
    r = s.get(httpbin(&apos;cookies&apos;, &apos;set&apos;, &apos;k&apos;, &apos;v&apos;), allow_redirects=True)
    c = json.loads(r.content).get(&apos;cookies&apos;)

    assert &apos;k&apos; in c

    # And server-set cookie persistience.
    r = s.get(httpbin(&apos;cookies&apos;))
    c = json.loads(r.content).get(&apos;cookies&apos;)

    assert &apos;k&apos; in c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;处理响应的 cookie:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if &apos;set-cookie&apos; in response.headers:
    cookie_header = response.headers[&apos;set-cookie&apos;]

    c = SimpleCookie()
    c.load(cookie_header)

    for k,v in c.items():
        cookies.update({k: v.value})

# Save cookies in Response.
response.cookies = cookies
cookies = self.cookies
self.cookies.update(r.cookies)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发送请求时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if self.cookies:

    # Skip if &apos;cookie&apos; header is explicitly set.
    if &apos;cookie&apos; not in self.headers:

        # Simple cookie with our dict.
        c = SimpleCookie()
        for (k, v) in self.cookies.items():
            c[k] = v

        # Turn it into a header.
        cookie_header = c.output(header=&apos;&apos;).strip()

        # Attach Cookie header to request.
        self.headers[&apos;Cookie&apos;] = cookie_header
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用了标准库里的&lt;code&gt;SimpleCookie&lt;/code&gt;处理和生成 cookie，而读取 cookie 全部都是字典类型。其实这些都是为了新的&lt;code&gt;urllib3&lt;/code&gt;接口而服务的，从原来的各种 Handler 改成&lt;code&gt;conn.urlopen&lt;/code&gt;以后原来的东西都相应的变化。&lt;/p&gt;
&lt;h4&gt;5. 新的&lt;code&gt;ConnectionError&lt;/code&gt;&lt;/h4&gt;
&lt;h4&gt;6. 安全模式&lt;/h4&gt;
&lt;p&gt;直接看代码吧：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;except MaxRetryError, e:
    if not self.config.get(&apos;safe_mode&apos;, False):
        raise ConnectionError(e)
    else:
        r = None

except (_SSLError, _HTTPError), e:
    if not self.config.get(&apos;safe_mode&apos;, False):
        raise Timeout(&apos;Request timed out.&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所谓安全模式就是不抛出异常。&lt;/p&gt;
&lt;h4&gt;7. 新的&lt;code&gt;prefetch&lt;/code&gt;参数&lt;/h4&gt;
&lt;p&gt;也是&lt;code&gt;urllib3&lt;/code&gt;支持的参数，当为&lt;code&gt;True&lt;/code&gt;时，在发送请求时就读取响应内容，否则跟原来一样调用&lt;code&gt;content&lt;/code&gt;方法时读取。至于这个有什么用我还不是太懂，因为我发现当&lt;code&gt;prefetch=True&lt;/code&gt;时读取&lt;code&gt;content&lt;/code&gt;会出错并且无法获取响应内容，疑似 BUG，先放在这里。&lt;/p&gt;
&lt;h4&gt;8. &lt;code&gt;OPTION&lt;/code&gt;请求方法&lt;/h4&gt;
&lt;p&gt;Option 是一种 HTTP 的请求类型，返回当前 url 支持的全部方法。&lt;/p&gt;
&lt;h4&gt;9. 节省 async 池的大小&lt;/h4&gt;
&lt;p&gt;原来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jobs = [gevent.spawn(send, r) for r in requests]
gevent.joinall(jobs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if size:
    pool = Pool(size)
    pool.map(send, requests)
    pool.join()
else:
    jobs = [gevent.spawn(send, r) for r in requests]
    gevent.joinall(jobs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大概就是传入一个&lt;code&gt;size&lt;/code&gt;参数，所有的异步请求都在这个有限大小的池里处理，嗯，又是池，真是一个好用的东西。&lt;/p&gt;
&lt;h4&gt;10. 上传文件时包含真实文件名&lt;/h4&gt;
&lt;p&gt;看代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def guess_filename(obj):
    &quot;&quot;&quot;Tries to guess the filename of the given object.&quot;&quot;&quot;
    name = getattr(obj, &apos;name&apos;, None)
    if name and name[0] != &apos;&amp;lt;&apos; and name[-1] != &apos;&amp;gt;&apos;:
        return name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;嗯，怎么得到真实文件名？靠猜啊，没有就拉倒。&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;呼，终于整完了，v0.8.0 包含一个大的重构，我这个累的啊。第一次写这种东西，感觉不是很满意，代码太多了自己的试验不太够，总的也就能理解 80% 左右吧。不管怎样，谢谢大家的阅读，欢迎交流。&lt;/p&gt;
</content:encoded></item><item><title>在博客与笔记中使用Markdown</title><link>https://frostming.com/posts/2016/05-25/zai-bo-ke-yu-bi-ji-zhong-shi-yong-markdown/</link><guid isPermaLink="false">https://frostming.com/2016/05-25/zai-bo-ke-yu-bi-ji-zhong-shi-yong-markdown/</guid><pubDate>Wed, 25 May 2016 19:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;博客的搭建&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;前段时间在 StackOverflow 与 Quora 上我接触到了 Markdown 标记语言，瞬时就被这种易用、美观、高逼格的东西所俘获，顿时深感之前在 QQ 空间之类的平台上写博的体验之差，往往调格式就要耗费很多的时间。于是就有了迁移到另一个博客平台的想法，用过的产品有：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://www.jianshu.com&quot;&gt;简书&lt;/a&gt;：包含社交功能的 Markdown 博客网站。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zybuluo.com/mdeditor&quot;&gt;CmdMarkdown&lt;/a&gt;：简单纯粹，功能强大，丰富语法支持，但是自带样式我不是很喜欢。&lt;/li&gt;
&lt;li&gt;PyCharm 的 Markdown 插件：HTML 样式简陋，高级功能需付费。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些我都不是特别满意，于是便萌生了干脆建一个个人博客的想法。通过多方考查，得到一个比较好的解决方案：Hexo + Github page。Github page 是基于静态页面的免费个人网站，而 Hexo 刚好就是基于 node.js 的静态博客，并且原生支持 Markdown 还有海量美观的模板。[^1x]&lt;/p&gt;
&lt;p&gt;[^1x]: &lt;a href=&quot;http://www.jianshu.com/p/465830080ea9&quot;&gt;HEXO+Github,搭建属于自己的博客 - 简书&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;文章云存储&lt;/h2&gt;
&lt;p&gt;博客建好以后，那么问题来了：如何随时随地地把想法记录下来以待日后放进博客？这就需要一个云同步的平台，有以下几种选择：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将 markdown 文件托管到 GitHub&lt;/li&gt;
&lt;li&gt;使用笔记应用存取 Markdown 文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;目前很多 markdown 编辑器都支持保存到 github 或者笔记应用，最终我选择了后者。原因是我只在家用的笔记本上配置了博客的环境，所以只能在家里更新博客。而且我总归需要一个笔记应用来存放笔记。&lt;/p&gt;
&lt;h2&gt;笔记应用&lt;/h2&gt;
&lt;p&gt;考察了 Onenote，有道云，印象笔记之后我最终选择了印象笔记。首先是因为界面美观，其次是支持丰富的扩展，在 Chrome 上的 &lt;a href=&quot;https://www.yinxiang.com/webclipper/&quot;&gt;印象剪藏&lt;/a&gt;也是相当好用，而相比而言有道云虽然界面简洁大方，但 Chrome 的扩展就大为不及了。&lt;/p&gt;
&lt;h2&gt;Markdown 编辑器&lt;/h2&gt;
&lt;p&gt;那么下一步就是选用合适的编辑器了，有以下几点要求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要有网页端&lt;/li&gt;
&lt;li&gt;支持保存到 Evernote&lt;/li&gt;
&lt;li&gt;双栏预览功能&lt;/li&gt;
&lt;li&gt;语法支持不能太少&lt;/li&gt;
&lt;li&gt;界面不能太难看&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;综合以上考虑，&lt;a href=&quot;http://soft.xiaoshujiang.com/&quot;&gt;小书匠&lt;/a&gt;无疑是一个很好的选择，全平台支持，并且有网页版，支持保存到 dropbox, github, evernote 等。还自带图床，简直不要更赞。
&lt;img src=&quot;//webp.frostming.com/images/bind.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;好了，一切都搞定了，赶紧来试一下吧，把文章同步到印象笔记后，文章末尾会附上一个 md 源文件的链接，这样你在任何一台电脑上只要下载这个文件再导入小书匠就可以继续编辑了。&lt;/p&gt;
</content:encoded></item></channel></rss>