11
16
2020
3

Python 小版本升级是怎么 break 已有项目的

近日,Arch Linux 终于开始升级到 Python 3.9 了。很多人认为 Python 小版本升级容易搞坏兼容性,导致项目无法在新的版本上运行。事实是这样的吗?我正好借着 Arch Linux 升级 3.9 的机会,分析一下打包过程中失败的项目到底是出了什么事。

需要说明的是,我仅大致地分析了打包的报错信息,不排除分析出错,或者有额外的问题没有被看见的情况。另外我是在打包过程中随机(arbitrarily)取样,并且排除了我不能确定问题所在的案例。

以下项目测试失败是和 Python 3.9 相关的。排序是按照项目开发者的无辜程度排序的。也就是说,排序越靠前的,我越是认为项目开发者是无辜的;而像「硬编码 Python 3.9 为未发布的版本」这种完全不 future-proof 的做法,现在坏掉了真是自找的。

其中,使用的公开特性变化导致问题的有 3 个,调用私有属性或者方法、依赖非正式的文本信息的有 11 个,使用已废弃的特性的有 8 个,使用已被修复的 bug 的有 2 个,使用未来注定会出问题的信息的有 3 个。总共 27 个。

  • freecad: PyTypeObject.tp_print 没了
  • python-llfuse: PyTypeObject.tp_print 没了
  • linux-tools: PyMODINIT_FUNC 的变化导致了警告,然后被转为错误
  • python-blist: _PyObject_GC_IS_TRACKED不再在第三方库中可用(被公开 API 取代)
  • python-pyflakes: Python 语法解析报告的列位置似乎不太对,应该是受新的语法解析器的影响
  • python-pylint: Python 语法解析报告的列位置似乎不太对,应该是受新的语法解析器的影响
  • python-typing_inspect: 使用私有名称 typing._GenericAlias,结果新版本变成了 typing._SpecialGenericAlias
  • python-sphinx-autodoc-typehints: 看上去是类型标注相关的内部更改移除了 typing.Dict.__parameters__ 属性造成的
  • python-fastnumbers: 看上去是内部函数 _Py_dg_stdnan 不再被默认包含导致的问题
  • python-libcst: 类型标注相关的内部更改移除了 typing.Dict.__args__ 属性造成的
  • monkeytype: typing.Dict 的类型从 type 变成了 typing._SpecialGenericAlias
  • scrapy: 由于 typing.Optional[str] 的字符串表示由 typing.Union[str, NoneType] 变成了 typing.Optional[str] 导致 mitmproxy 运行出错,进而使得 scrapy 的测试失败
  • python-billiard: 调用的私有方法 _posixsubprocess.fork_exec 参数发生了变化
  • python-pytest-benchmark: argparse 的帮助信息格式有优化
  • python-opentracing: 自 3.7 起废弃的 asyncio.Task.current_task 被移除
  • python-engineio: 自 3.7 起废弃的 asyncio.Task.all_tasks 被移除
  • impacket: 自 3.2 起废弃的 array.array.tostring() 被移除
  • python-pybtex: 自 3.2 起废弃的 xml.etree.ElementTree.Element.getchildren 被移除
  • python-jsonpickle: 自 3.1 起废弃的 base64.decodestring 被移除
  • python-ioflo: 自 3.1 起废弃的 json.loads() 参数 encoding 被移除
  • routersploit: 自 Python 3 起废弃的 threading.Thread.isAlive 终于被移除了
  • python-socketpool: 自 Python 3 起废弃的 threading.Thread.isAlive 终于被移除了
  • python-furl: Python 3.9 修正了一处 URL 解析 bug
  • python-stem: Python 3.9 移除了错误的 unittest.mock.__version__
  • python-natsort: Python 的 Unicode 支持更新到了 13.0.0 版本,CHORASMIAN NUMBER ONE 字符被判定为数字,但是测试代码不认识,认为程序出错
  • python-pony: 对新版本的 Python 报不支持的错误
  • python-dephell-pythons: 硬编码 Python 3.9 为未发布的版本,但现在 3.9 已经发布了

而以下项目的测试失败与 Python 3.9 没有直接关系,共 26 个。其中与 Python 生态有关的有 18 个,与其它项目有关的有 4 个,依赖外部信息的有 3 个,包括一个特别搞笑的依赖夏令时是否生效的。

  • python-eventlet: 调用的 dnspython 私有方法已不存在;DNS 解析超时
  • python-markdown2: 语法高亮的结果有少许变化,不符合预期。推测是 pygments 新版本的变化
  • python-flake8-typing-imports: 似乎是 flake8 能够检测到更多的问题了
  • python-babel: 使用了已废弃的特性,测试被 pytest 拒绝
  • python-pygal: pytest 6.1.0 移除了 Metafunc 的 funcargnames 属性
  • python-flask-gravatar: 使用了已废弃的特性,测试被 pytest 拒绝
  • python-pytest-relaxed: 使用了已废弃的特性,测试被 pytest 拒绝
  • python-pytest-randomly 使用了已废弃的特性,测试被 pytest 拒绝
  • python-deprecated: 测试所预期的警告文本信息已经发生变化
  • python-dbus-signature-pyparsing: 执行时间超过了测试设定的 200ms 时限
  • python-tinycss2: flake8 风格检查未通过
  • python-pytest-runner: black 风格检查未通过
  • python-portend: black 风格检查未通过
  • python-aiohttp: @coroutine 的 DeprecationWarning 被视作错误
  • python-poetry: poetry-core 的一项数据由 dict 改为 OrderedDict,使得输出顺序与测试预期的不一致
  • python-isort: 将使用旧版本 isort 的外部项目的 import 排序视为正确,然后它还真出错了
  • python-cachecontrol: Python 2.7 相关
  • python-zc.lockfile: 测试代码把 Python 3 代码喂给了 Python 2.7。可能是该库已经不支持 2.7 了
  • python-occ-core: 依赖 OpenCASCADE 的版本更新,不被支持
  • protobuf: C 整型比较因表示范围问题而恒为假,警告转错误。是因为新版本的 gcc 比较聪明么?
  • gnome-passwordsafe: 构建系统发现有依赖缺失
  • io: C 代码引用了不存在的系统头文件
  • ceph: C++ 相关问题
  • python-distlib: 调用远程 XML-RPC 太多被限制导致预期的数据与实际错位
  • python-requests-toolbelt: 测试所需要的 HTTP 资源 404 了
  • postgresql: 夏令时结束,导致实际时区与预期对不上。「所以冬天就不要滚包啦,冬天要冬眠!」

所以在这些升级 Python 3.9 的项目中,不兼容 Python 3.9 仅仅只占一半,其中又有一半多属于「总有一天会坏掉」的类型(一大半属于「不听话」,使用没有明确文档、预期为私有的特性,少数尝试当预言家但是失败了)。最后剩下的,再一大半是使用了至少两个版本前已经说了要废弃的特性,只有三个莫名地发现自己真的被 Python 坑了,还都是 C API 部分的。

所以我对我自己的脚本顺利升级到 Python 3.9 非常有信心呢。可能有些老代码使用了已经废弃的特性,所以我也设置了环境变量 PYTHONWARNINGS=default,ignore::ResourceWarning 以便及时得到提示。

哦对了,Arch Linux 中受 Python 3.9 升级影响需要更新的软件包共有2077个,绝大部分我都没见着失败的。目前从开始升级到现在已经过去六天,还剩最后40个失败了的包。

10
3
2019
6

红黑树到底是个什么树

红黑树使用得非常多,然而由于它不在我的本科教材上,我又不用自己实现它,所以我一直不知道它是什么样的。现在想了解一下,结果发现网上我看了好几篇的中文资料都是这种介绍方式——

假设要介绍的树叫梧桐树。文章会告诉你梧桐树会长多高,树叶长什么样,分叉有什么特征,它和棕榈树有什么不同,还会贴心地放一张或者多张梧桐树的照片给你看。

然而红黑树不是梧桐树啊!计算机科学也不是植物学啊!它是为特定目的人造的!所以你倒是说说它为什么会被设计成这个样子啊……

然后我去看了一眼英文维基百科上的介绍,才看了第一段就恍然大悟。

A red–black tree is a kind of self-balancing binary search tree in computer science. Each node of the binary tree has an extra bit, and that bit is often interpreted as the color (red or black) of the node. These color bits are used to ensure the tree remains approximately balanced during insertions and deletions.

意思是,红黑树是一种自平衡二叉树。每个节点有一个额外的比特,通常叫它颜色,并且是红的或者黑的。所以红黑树之所以叫红黑树,只是发明者为了方便叙述给加了点颜色,就跟数学里那些个着色问题(以及物理里夸克的颜色和味道)一样。为什么要加这些颜色呢?它们是用来保证树大致平衡的。

神奇了,为什么加了这么个颜色就能保证了?

Balance is preserved by painting each node of the tree with one of two colors in a way that satisfies certain properties, which collectively constrain how unbalanced the tree can become in the worst case. When the tree is modified, the new tree is subsequently rearranged and repainted to restore the coloring properties. The properties are designed in such a way that this rearranging and recoloring can be performed efficiently.

哦,是用特定的方法着色的,并且树被修改的时候会重新着色。神奇的算法使得这些操作非常高效。也算是用空间换时间了。

The leaf nodes of red–black trees do not contain data. These leaves need not be explicit in computer memory—a null child pointer can encode the fact that this child is a leaf—but it simplifies some algorithms for operating on red–black trees if the leaves really are explicit nodes.

红黑树的叶子节点总是黑色的,并且不包含额外的信息。所以实现的时候实际上不需要显式地存在,或者用同一个节点就好。它的存在只是为了简化算法描述而已。

红黑树的性质:

  1. 每个节点不是红色就是黑色。多出来一比特的信息,当然只有两种选择了。
  2. 根节点是黑色。
  3. 叶子节点是黑色。
  4. 红色节点的子节点是黑色。这是为什么非要加个不包含任何信息的黑色叶子节点了。不然弄出来红色叶子节点有点麻烦了。
  5. 任意节点到它下方的叶子节点,路径上的黑色节点数是相同的。再一次体现了黑色叶子节点的用处。

这些性质保证了,从根节点到所有叶子节点,最远的路径最多是最近的路径两倍远。不会特别远,甚至是退化成链表了。

然后是树上的操作。

查找就不用说了。二叉树的查找算法而已。

插入的时候,首先把新节点涂成红色,按二叉树的方式插进去,取代某个叶子节点,并自己长出两片叶子。然后修复一下。修复方案如下:

  1. 如果这是个根节点,那么涂黑就完事了。
  2. 如果它的上级节点是黑色,黑色节点下接红色节点,没事,不需要更多操作了。
  3. 如果它的上级节点是红色,并且它的上级邻节点是红色,这就坏事了,违反了性质4。那就把它们涂黑。可这样多了一层黑节点,又会违反性质5。那就把上上级节点涂红(由性质4,上上级节点之前肯定是黑色)。可如果上上级节点是根节点,又违反了性质2。不过上上级节点带的这部分子树肯定是没问题的,那就把整个上上级节点带的这棵子树当整体,递归修复一下好了。
  4. 如果它的上级节点是红色,并且它的上级邻节点是黑色,这还是破坏了性质4。而这个时候如果把上级节点涂黑,那么上上级节点不管涂不涂红,一边加了一个黑节点而另一边没有加,它这个子树本身就已经坏掉了,所以得想另外的办法。
    1. 第一步,如果上级节点在左边,而新加节点在右边,那就左旋。如果上级节点在右边,而新加节点在左边,那就右旋。其它情况不用动。由于是两个上下级红色节点在旋转,这样并不会再破坏任何性质。
    2. 第二步,两个红色节点和可以被取代的叶子节点已经准备好了,现在把上上级节点(也就是动过的子树的上级节点)用子树的根换掉,原来的挂到上一步空出来的叶子节点那里,并且把新根涂黑,把一开始那个碍事的黑色节点涂红。这样,两个红色节点就成了同一个黑色节点的子节点。性质4得到了保持,涂色过程只是把黑色上移取代之前的红色,一边的路径上少了个红色节点,另一边多了个红色节点而已,也不会破坏性质5。

删除就更复杂了。

  1. 如果被删除的节点有两个非叶子节点,按二叉树的方法删除节点,但是保持原来这个位置上节点的颜色。因为子树的根被保留颜色地替换了,所以这一步不会破坏性质。而少了的(被作为新子树根替换的)节点,递归这个删除算法就可以了,一直在深入,总能递归到没有两个非叶子节点的节点。
  2. 你不会想删叶子节点,也不能删它们。
  3. 所以剩下一种情况:被删除的节点最多有一个非叶子子节点。如果有的话就叫它 C 好了,没有就随便找个叶子节点当 C。那被删除的节点我们叫 M。
    1. 如果 M 是红色,那用 C 取代它即可。因为 C 肯定是黑色(而且是叶子节点)。
    2. 如果 M 是黑色,但 C 是红色,用 C 取代 M,并且把 C 涂黑。少了一个红色节点而已,没事的。
    3. 如果 M 和 C 都是黑色,这个时候 C 也一定是叶子节点。先用 C 取代 M,并叫新的 C 为 N,其上级节点为 P,P 的另一个子节点为 S。现在 N 这边少了个黑色节点,相对的,S 这边多了一个。
      1. 如果 N 是根节点(P 不存在),那么没事了。
      2. 如果 P 是黑色,并且 S 是红色,旋转一下,让 S 成为新的子树的根,并将 P 涂红。从颜色上看,只是红色从一个子节点跑到另一个,然后有棵子树从一边换到另一边了,性质不会被再度破坏。但之前被破坏的部分还没修复呢,现在是一个黑色下边有一个黑色的节点那里的路径上多了一个黑色节点,要走下边的步骤。
      3. 如果 P 和 S 都是黑色,N 那边不是被删掉了一个黑色节点吗,现在把 S 涂红,这样 P 的两边(N 和 S)都少了个黑色节点。子树没有问题,然而 P 这边的路径上少了个黑色节点。要么 P 是根节点,不用管了。要么 P 的邻节点那边的路径多了个黑色节点,情况就是一个子树,下方有一个黑色节点,并且这一边的路径上多了个黑色节点。如果子树的根是黑色,那么递归了,回去再走一遍上一步(3.3.2)就好。如果是红色,走下边的步骤。
      4. 如果 P 是红色,并且 S 和 S 的子节点都是黑色,把 P 和 S 的颜色交换一下,S 那边的路径少了个黑色节点,处理好了。
      5. 如果 P 是红色,N 位于左侧,并且 S 的左子节点是红色,右子节点是黑色。N 当然是黑色的啦。那么把 S 右旋,并且把旧 S 的左节点(新的子树根)涂黑,然后旧 S 自己(新右子节点)涂红。继续下边的步骤。
      6. 如果 P 是红色,N 位于左侧,并且 S 的左子节点是黑色,右子节点是红色,把 P 左旋一下,并把之前那个红色子节点涂黑。这样 N 那侧的路径多了个黑色节点。完成了。
      7. 如果 P 是红色,N 位于右侧。把上两步镜像一下就可以了。

总之就是各种涂色和旋转,来保持二叉树和红黑树的性质不被破坏(红黑树是二叉树,所以旋转需要点技巧,不是想旋转就一定能旋转的)。看着复杂,照着前人已经想好的步骤去做其实也并不难。然而中文网络上几乎没有人解释这些步骤的目的,以至于让人理解不能 :-(

Category: 编程 | Tags: 算法 编程
9
28
2018
20

每次修 Python 代码的 bug 的时候总会想念 Rust

俗话说:由俭入奢易,由奢入俭难。

之前写 Python,老是在实现完一个特性之后,弄出来几个 AttributeError: 'NoneType' object has no attribute 或者 TypeError: list indices must be integers or slices, not str,还有 TypeError: can only concatenate str (not "int") to str 这样的错误。一看就明白自己又是哪里一不小心疏忽了,稍微修一下就好。

后来啊,我遇见了 Rust,整个流程就变了。之前写的时候,基本上都是通过手动测试来发现这种问题。为了高效、不破坏性地测试,需要控制测试的数据量,需要保证出错的时候相关的数据不会处于某种中间状态。当然在服务器上跑的脚本,我还要来来回回地传更新的脚本,或者弄个本地测试环境。而这一切,可能不过是为了跑一个成功之后再也不会用到的小程序,比如之前分析抓包数据的那次。而在 Rust 里,这些最容易犯的错误,cargo check 一下,编译器基本上能全给你指出来。所以有时候写一些小工具我也用 Rust,虽然写起来慢,但写好就能正常运行,不用反复试错,多好啊!

最近给 Arch Linux 中文社区的自动打包机器人 lilac 增加新特性。结果实现完部署之后,夜里就被 lilac 叫起来修 bug 了,还一下子就是仨……(lilac 很难本地测试,而短暂地服务中断又没多大影响,所以我都是不进行本地测试的。)

第一个 bug 是,与 dict.get 不一样,getattr 是没有默认值的。Python 里这种不一致很多,比如 configparser 里默认值要用关键字参数指定。Rust 遇到类似的情况,就会返回一个 Option。或者如果 API 决定如果不存在就 panic 的话,那么它就会直接返回我要取的值的类型,而不会包一层 Option。而我后边的代码是预期到这里可能取不到那个属性的,所以弄错了就会类型不匹配。

第二个 bug 是局部变量在一个分支上没有初始化。Rust 当然不会允许这种情况了。实际上 C 都不用担心这种问题,编译器会给出警告的,还有一些 linter 可以用。而 Python,很遗憾的是,我所使用的 pyflakes 并没有对此发出警告。我当然知道 pylint 那些。我很讨厌 pylint 和 jslint 这种不区分潜在 bug 和风格问题的 linter。我只需要工具在我可能疏忽的时候提醒我,而不需要它对我的编码风格指指点点,特别是那些指指点点往往是不对的。比如我的文件描述符变量名不叫 fd 难道要叫 fildes?

第三个 bug 是一个可能为 None 的变量我忘了先作 is not None 判断。这段代码如果初写的话我肯定是会注意到的,但是改的时候,只想着如果 pkg 里有冒号我得处理一下,就忘记了根本没有关联的包名的情况。Python 的 None,以及 C 和 C++ 的 NULL、Java 的 null、Lua 和 Ruby 的 nil、JavaScript 的 undefined 和 null,被称作是十亿美元错误,给无数程序员和用户带来了无尽的 bug。幸好这个东西在 Rust 里不存在:表达「没有值」的值没有被作为特殊值存在于几乎所有类型中,而是作为一类类型的可能的值之一。想要使用「正常」的值,就需要显式地进行类型转换,所以不可能被不小心忽略掉。顺便说一下,Go 里也有 nil 这种东西,以至于会出现这种不容易发现的 bug

Python 现在也给出了解决方案:类型注解,提供类似的类型检查。不过检查器是第三方的,也并不十分完善。等我找到机会试用过之后再来写感想啦。

Category: python | Tags: python 编程 编程语言 Rust
9
15
2017
44

放弃 you-get,转投 youtube-dl

you-get 是一个视频下载工具。我于五年零一周前(2012年9月7日)在 AUR 打包并维护其 git 版本。当时还是叫 python-you-get-git,后来根据 Arch 官方的推荐,与语言没什么关系的软件不带语言前缀,改名为 you-get-git

youtube-dl 是另一个差不多同时期出现的视频下载工具,一开始主要针对 YouTube 等跨国网站。

选择 you-get 一部分原因是当时它对国内网站的支持更好,另一方面也是支持国产。但是今天,我决定放弃 you-get 了。

五年来,我一直是支持 you-get 的。也尝试过向其贡献代码。目前已经有29个提交被合并,排名第五位。基本上都是一些很小的修改,比如编码问题、未回收的僵尸进程、标题的反转义、ffmpeg 命令的特殊字符转义、支持 python -m 调用、视频链接的解析更新和扩充、进度条的修正和优化,等等。

其实这些年来,我一直想做更多事:

  • 可选地使用 requests 库,以提高解析速度,改善用户体验
  • 支持使用 aria2c 下载视频链接
  • 支持网易云课堂的更高清晰度的未内嵌字幕的视频(pr#1002
  • 解析更准确的信息
  • 一些其它网站的解析器(比如 bilibili 的 bangumi.bilibili.com 子域)

但是,其中很多都没能完成。勉强完成的也很奇怪:明明是针对网易云课堂的解析,我还非得关心网易云音乐。一直以来,我对 you-get 的修改都是事倍功半。我也曾尝试过更深入的修改,但是牵一发而动全身,往往要改就得改很大很大一部分代码,然后完全没有办法保证其正确性。就像今天的事情一样。

我花了数小时的时间,牺牲睡眠,把命令行选项解析由 getopt 改成了 argparse(pr#2260)。促使我做此修改的原因是,我想下载 bilibili 一整个播放列表的视频。我记得 you-get 有下载整个播放列表的功能。但是我读了好几遍 help 信息,都没有找到那个选项。我记错了吗?阅读源码之后,我终于找到了那个选项。同时,我也看到了在 C 和 bash 代码里经常看到的,一长串 if/else 来解析命令行选项。翻了好几屏。

当一个相对独立的代码片断翻屏时,bug 数量会骤增。

曾经在公司里遇到过一个 case,非常直接地证明这句论断是有多么正确。那个函数刚好超过一屏数行,而在第二屏的那部分代码,有个「}」和「return」的顺序写反了。我也是拿 Vim 的匹配括号跳转功能才发现的。

当然了,不管怎样的代码,不动它是不会出新问题的。然而我动了它。回报我的是两个局部变量名忘记改了:pr#2346pr#2355

这种问题在 nvchecker 重构以支持 aiohttp 时并没有发生。为什么呢?我们有测试。如此明显的问题,只要 cover 了必然会发现。所以我可以放心大胆地重构。

you-get 呢?you-get 也有测试。我在提交 pull requests 之后有个习惯:盯着未完成的测试,直到它变绿。如果红了,赶紧看看是不是自己代码的问题,是就赶紧修掉。一些项目(比如 Tornado)的测试本地跑起来要配置环境、装不少东西,太麻烦了,所以我习惯先提了 pr,然后等 Travis-CI 的结果。可这次,测试过了。但是有两个重要的功能却并不能正常工作。

其实呢,对于这种简单的错误,通常 linter 会告诉我的。我有装 neomake,全面支持各种 linter,用起来十分惬意。但 pylint……就像 jslint 一样,我很讨厌它们。因为它们不仅检查潜在的问题,同时还检查代码风格。而代码风格这事是每个项目单独配置的,而不是开发者自己配置好,然后让自己参与的所有开源项目都遵守。不过今天我也终于知道了另一个 Python linter——pyflakes 很对我的味口:只管问题,不管风格。

总之呢,由于各种原因,重写中出了这么两个直接立刻让用户不能用的 bug。很抱歉。一般来说,出错了就改呗。更深入一些,分析一下为什么会出现这种错误,今后怎么避免同样的错误两次出现(早年向 Tornado 提交代码时,Ben Darnell 一个简单的行为教会了我一件事:修了 bug 就写个对应的测试)。但是 you-get 的协作者 rosynirvana 不按惯例来,反而要求放弃此修改。如果就如此也就算了,后续讨论中我意识到了一个真相——为什么我在 you-get 上的工作如此困难?

The best part of you-get is that it's not so pythonic so those who only know js or as3 can take part in, moving from the universal getopt to a py-domain-specific library cannot be a nice idea.

source

What library nowaday pythonists love do not really matter here because those one know js and as3 can contribute even more in this project.

source

因为 you-get 根本就是反 Pythoner 的!作为一个 Python 项目,you-get 想要吸引的是 JavaScript 和 ActionScript 3 开发者!

我很震惊。

  • 作为 Python 开发者,我已被他们刻意排斥在外。
  • 作为 JavaScript 开发者,我还是觉得 C 好难写,还是 pythonic 的代码比较好维护啊。
  • 作为 C 开发者,我倒是对这种长达数屏的作用域见怪不怪了。不过重复的逻辑,咱一般会用宏之类的手段给整成声明式的啊。

所以,我的努力注定不会有多少效果。

然后,我看了一眼 youtube-dl。其实就瞟了一眼,也没看出太多东西来,但是

  • 按 URL 进行正则匹配的,网易云音乐和网易云课堂可以分开处理了!
  • 解析器以 class 表达,有组织有纪律!不用用 Python 的语法写 C 了!

我 disown 了 AUR 和 [archlinuxcn] 里的 you-get-git 包。关闭了未完成的 issue 和 feature pr。等修复 argparse 引入的错误的 pr 被合并(不管是只修正问题还是退回到 getopt),事一了,我就删掉仓库,只保留网易云课堂的高清视频解析代码(花了我一整天的)。已安装的 you-get 暂时保留,但首选 youtube-dl,遇到问题有时间就去修一下。已经投入到 you-get 的时间是沉没成本,不必留恋。

Category: python | Tags: python 编程 软件开发
1
6
2017
3

如何快速高效地修 bug?

看到知乎上的一个问题,心血来潮,随意写写,请读者不要太较真。

看回答,有一些可操作性很强的答案。但是呢,你知道的,考试好不代表能力强,如果你只是学习别人的方法而并不理解,那么学来之后只会是东施效颦而不能融会贯通。所以呢,我也来发表一下自己的见解。

首先,你要定位 bug。这时,你需要:

  1. 注重逻辑性。不要做没有证据的结论。如果你有猜测,就去证实或者否定它。比如某次,同事代码返回的数据有问题,认为是缓存用的 Redis 有问题,返回了错误的数据。然而没人去对此猜测进行求证……我去确认了一下,Redis 收到了请求,并且响应正常。接下来,排除所有其它可能的原因之后,最后剩下的那个就是真相。真相就是,代码里有个 } 的位置放错了,因为它刚好在一屏之后的位置,所以没有人发现……(是 Vim 帮我找到的)
  2. 基本的方法论。比如二分法。比如最小化测试用例。如果你要提问,要懂得提问的智慧,不管是向搜索引擎还是向人,你都需要提出正确的问题
  3. 知识面。你写 Web 后端的话,普通的 HTTP 得懂,浏览器的开发者工具得会用。简单的 JavaScript 也有会点儿。简单地说就是,你要精于你自己主攻的部分,然后要熟悉你的上下游。再比如如果你使用 CPython 的话,你要准备一份 CPython 的源码,并且要能够流畅地阅读 C 代码。
  4. 工具。工欲善其事,必先利其器。一大堆调试用的工具,你至少得知道它们能干什么,需要的时候能用。比如 strace、lsof、gdb、git bisect,还有高级点的 sysdig、systemtap、perf 等等。当然还有一堆不是专门为调试而设计的通用工具,比如 the silver searcher 或者 ripgrep。一个快速的全文搜索工具能帮你在最短时间内找到相关的代码或者日志。你不必成为正则表达式大师,但是简单的一定要会,不然面对上千个匹配结果你要怎么办呢?Vim 有一个插件 Mark,能够同时高亮多个模式,非常利于调试期间阅读代码和日志。投入时间学习使用高效的工具,不要把时间浪费在等待和人工搜索上,也不要让自己忙于琐事而断了灵感和线索。

最后,不要不断地、毫无目的地换个环境啦,换个版本啦,换个用户啦,这样子找问题。如果这样做很有效的话,大家都去买彩票去了。

找到 bug 之后,理解它是如何产生的。当你理解之后才能真正修好它。就像你感冒了吃抗生素,根本没有用。

Category: 编程 | Tags: 编程 软件开发

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com