☰
有趣代码的沉淀:从映射表到递归、区间合并与装饰器缓存
2026/10/16 5:09:57 网站建设 项目流程

1. 先聊聊我对“有趣的代码”的判断标准

入行编程这些年,我见过不少让我眼前一亮的代码。老实说,真正能让我说出“这段代码有趣”的,往往不是它用了多冷门的语法,也不是它短得只剩一两行,而是它在解决问题时选中的那个角度,恰好和我的惯性思维不一样。看到这种代码,我会先愣一下,然后顺着逻辑一行一行推演,最后通常冒出一个念头:原来还能这样写。

这种体验多了以后,我开始有意识地收集这类代码。在工作笔记里单独建了一个目录,专门保存那些让我觉得“有点意思”的片段。时间久了,它们慢慢变成一套判断思路:一段代码到底值不值得保存,我会看三个条件。第一,要能解决实际问题。一段只为了表现技巧、没法放进真实工程里的代码,只能算玩具,谈不上有意思。第二,它要能被推演。输入任何边界值,都能在脑子里模拟出结果走向,不是“跑一下竟然对了”的那种碰运气。第三,它要有一定的可迁移性。代码背后的思想换个场景还能用,这才值得反复品味。

我翻看自己标过“有趣”的片段,发现它们大多集中在几个类型:用数据映射替代分支判断、用递归处理嵌套结构、利用排序和单调性压缩复杂度、用函数包装统一附加行为。这几个方向不是孤立的技巧,它们背后都指向同一件事:怎样让代码变得更接近某个抽象。这篇文章我会挑出几段有代表性的,复盘它们当时的问题背景、前后写法的差异,以及各自的适用边界。

1.1 首要标准:能用,还能被讲明白

“能用”听起来门槛很低,可放在代码评审里就完全不是那么回事。我见过一些刻意追求简短的单行代码,输入稍微变一下就会报错,边界情况全靠运气。这种代码我不会说它有趣,只会觉得头疼。真正有价值的那种,是原本要写几十行才能讲清的逻辑,被压缩到一个清晰的表达里,而且输入输出有明确约定,各种边界都处理干净。

另一个标准是“能被讲明白”。你把代码交给你身边另一位同事,他扫一眼就能说出大致意图,那说明代码的可读性是合格的。反过来说,如果解释这段代码需要五分钟以上,还越说越绕,那它很可能进入了过度设计的领地。我自己有个感受:一段代码是否有趣,往往可以折算成它的讲解成本。讲得清楚是底线,讲不清楚的所谓巧思,本质上只是在制造沟通负担。

1.2 三个高频出现的“有趣”来源

观察得多了,我发现有趣代码很少是天才灵感,更多来自三个常见的改写过程:把一堆条件判断改成查表;把显式循环改成递归或高阶函数;把重复计算改成缓存。这几个方向听着都是老生常谈,可真正落到具体场景,效果差异很大。

比如把 if-else 换成字典映射这件事,有人换完以后代码更丑,为什么?因为没分清条件和动作之间到底是“决定关系”还是“区间关系”。决定关系适合查表,区间关系适合有序分支,两者一旦混用就会产生错误。递归也一样,有人用它写出优雅的分治,有人写出永远停不下来的死循环。所以说到底,“有趣的代码”不是某个写法的功劳,而是对问题本身理解到位后的自然产物。接下来我用四个具体案例,把这种差异摊开来讲。

2. 四段值得回味的“有趣代码”复盘

2.1 用一张映射表替掉十几行判断逻辑

先说一段真实场景里的代码。某个内部工具需要根据订单状态返回对应的提示文案和下一步可执行动作,状态有“待审核”“已通过”“已拒绝”“已过期”等几种,每个状态对应的文案和动作都不一样。初版代码理所当然写成了长长的 if-else:

if status == 'pending': tip = '等待审核' action = 'check' elif status == 'approved': tip = '已通过' action = 'download' elif status == 'rejected': tip = '未通过' action = 'retry' elif status == 'expired': tip = '已过期' action = 'renew' else: tip = '未知状态' action = 'contact'

这段代码倒也没有大问题,可等到要加新状态“已撤回”时,就得在中间插入一个新的 elif。每次改动都要在一长串条件里反复寻找位置,而且一旦某个状态判断顺序写错,还会出现永远进不到某个分支的情况。

后来我改成了一张配置表:

status_table = { 'pending': ('等待审核', 'check'), 'approved': ('已通过', 'download'), 'rejected': ('未通过', 'retry'), 'expired': ('已过期', 'renew'), } tip, action = status_table.get(status, ('未知状态', 'contact'))

如果你只觉得“这是把判断换成字典”,那还没完全抓到重点。这个改动真正的变化,是把“顺序判断”变成“直接检索”。对只有几个状态的小工具来说,性能差异可以忽略不计,真正收益在可维护性上:新增状态时只在表里加一行,不用关心它插在哪个位置;状态和行为的对应关系也一目了然,不再需要逐行确认。

这里有一个边界我必须提醒:字典映射适合状态值可枚举、每个值对应固定结果的情况。如果条件和结果之间带优先级、带范围区间,字典就不合适了。比如要根据得分区间判断等级,仍然要用有序分支。硬套字典反而会把简单问题弄复杂,这一点后面还会提到。

2.2 递归展平嵌套列表,保留顺序

某次数据迁移过程中,接口返回的 JSON 结构嵌套了很多层,列表里可能套列表,列表元素又有可能是普通字符串。最终入库时,我需要把所有叶子元素按原本顺序拼成一个一维数组。

第一反应当然是循环硬写,但实际操作下来很别扭。我自己维护一个栈,手动控制什么时刻推进、什么时刻弹出,边界条件很容易错,代码读起来也累。第一版循环代码大致长这样:

def flatten_iter(items): stack = [iter(items)] out = [] while stack: try: item = next(stack[-1]) if isinstance(item, list): stack.append(iter(item)) else: out.append(item) except StopIteration: stack.pop() return out

逻辑没有错,但它太机械。真正让我觉得有趣的是递归写法:

def flatten(items): out = [] for item in items: if isinstance(item, list): out.extend(flatten(item)) else: out.append(item) return out

这段代码把“处理一整层”转化为“处理一个节点”。遇到列表就脱一层壳,再调用同一个函数继续处理。嵌套列表本质上就是一棵树,叶子是普通元素,列表节点是分支,展平过程就是对树的叶子做前序收集。递归版本正好对应这个结构,所以它看起来简单,是因为它贴着问题的本质在写,而不是靠技巧硬绕。

如果不想产生中间临时列表,还可以改成生成器版本,思路一致,只是把拼接结果改成逐项产出:

def flatten_gen(items): for item in items: if isinstance(item, list): yield from flatten_gen(item) else: yield item

调用时用list(flatten_gen(data))就能得到同一份结果。这里的yield from是把内层生成器的产出逐个交到上层,避免了手动往列表里 extend 的步骤。

不过这个写法有个坑。Python 默认递归深度在 1000 层左右,如果嵌套深度很大,递归版本会直接报 RecursionError。数据源可控、深度几十层以内,用递归非常舒服;如果面对的是用户可以任意构造的输入,还是老老实实用前面那个显式栈版本,它不依赖调用栈,比较稳。

2.3 一次遍历把区间重叠全部搞定

有一组时间片段,区间可能有重叠,需要把重叠部分合并,最后得到一串互不重叠的区间。比如[1, 4]和[2, 6]应该合并成[1, 6],而[8, 10]和前面的结果没有交集,就单独保留下来。

最直接的想法是两两比较,检查任意两个区间是否重叠,然后合并。这个方案的复杂度是 O(n^2),区间数量一多就立刻变慢。更麻烦的是,两个区间合并之后可能又和第三个区间产生新重叠,需要反复迭代,代码很容易写漏。

我最后采用的是先排序,再单次遍历。排序之后,后一个区间的起点一定不小于前一个区间的起点,于是只要维护一个“当前合并后的最右端点”,就能在一次循环里把能合并的全部合并掉:

def merge_intervals(intervals): if not intervals: return [] intervals = sorted(intervals, key=lambda x: x[0]) merged = [[intervals[0][0], intervals[0][1]]] for start, end in intervals[1:]: if start <= merged[-1][1]: merged[-1][1] = max(merged[-1][1], end) else: merged.append([start, end]) return merged

这段代码真正有意思的不是那个排序动作,而是背后的“单调性”思想。一旦排序完成,后续区间只可能往已合并区间的右侧延伸,不会回头。于是每个新区间只需要和已合并结果的最后一个区间比较就够了。这种“让数据变得可推断”的思路,在滑动窗口、二分查找、双指针等更复杂的例子里都能看到同一个影子。

我后来还拿同一个思路延伸过另一个问题:一组区间里,最多有多少个区间同时重叠?只需要在处理时维护一个活动计数,遇到起点加一,遇到终点减一,在一次有序遍历里就能算出最大值。从一段基础代码出发,换几个条件就能得到一串变化的题目,这对我来说是很强的学习方式。

2.4 用装饰器给函数悄悄加上缓存

最后这段代码和算法关系不大,但非常实用。某个函数被高频调用,入参中大量重复,每次重新计算结果其实是浪费。最初我想的是在函数内部维护一个全局字典,手动做缓存判断,可这样一来业务逻辑和缓存逻辑混在一起,看着很乱。

更好的做法是用装饰器做一个不侵入原函数的外层包装:

import functools def memoize(func): cache = {} @functools.wraps(func) def wrapper(*args, **kwargs): key = args + tuple(sorted(kwargs.items())) if key not in cache: cache[key] = func(*args, **kwargs) return cache[key] return wrapper

使用时只需要在函数上面加一行@memoize,原函数内部一行都不用改。缓存逻辑和业务逻辑被彻底分开,两个部分各自保持简单,这是这段代码让我觉得舒服的地方。

再往深看,它体现了“函数是一等对象”的威力:函数能像普通数据一样被传来传去、被包装、被替换。这个思想是整个装饰器模式的基石。学会用这种眼神看代码,便会发现很多框架里的切面、代理、中间件,本质上就是在做类似的事,只是包装的复杂度更高。

这里有几个细节要提醒:memoize 的参数键必须可哈希,如果入参包含不可哈希对象就要自己设计键;如果缓存的是类的方法,还要考虑实例生命周期对内存的影响。总之它不是一个无脑通用的工具,用之前要想清楚场景。

3. 筛选有趣代码的正确姿势,以及我踩过的坑

上面的四段代码都给我留下过正面印象,但寻找“有趣代码”的过程中我也踩过不少坑。最典型的一种,是把自己的虚荣心包装成技巧。比如某次想证明“一行生成器能解决所有事情”,我把一段逻辑硬塞进一个表达式里,写完之后确实很爽,可第二天调试时报错堆栈完全没法读,最后老老实实拆回多行。

另一个坑是盲目追求通用方案。看到一个设计很漂亮,就想着把所有场景都改造成那个模式。实际上,一段代码值不值得用,永远要看具体场景。同一个递归写法在一个输入可控的项目里是优雅,在另一个用户可随意构造数据的项目里就是灾难。判断“有趣代码”最重要的不是代码本身,而是它所在的环境是否允许它这样运行。

3.1 不要为了漂亮牺牲可读性

有一回处理任务依赖关系,我见到一段同事写的代码:三层嵌套循环,中间夹着两个推导式,整段压缩成三行。第一眼确实觉得厉害,可两天后要改需求时,对着这三行推了十分钟,还是得一步步拆开验证。

后来我们把那段代码拆回普通循环,再抽出一个函数来承担核心过滤条件。行数多了一些,但评审会上大家很快就看明白了。那次之后我意识到,“有趣的代码”一定以能被别人读懂为前提。如果只有原作者觉得巧妙,其他人阅读时都在受苦,那它其实是在制造技术债。给代码“瘦身”和“增肌”之间,要找一个合适的平衡点。

3.2 数据规模决定写法的边界

还有一次是嵌套配置解析。我在脚本里用了递归解析配置层级,本地测试一切正常,因为测试数据深度不超过几层。结果上线后偶尔报错,排查下来发现线上某条配置的嵌套层级超过了递归深度限制。

问题的根源不是递归本身,而是我事先没有评估清楚输入规模。从那以后,我在选择算法和写法之前,会先确认三个问题:数据量级大概多少,最坏情况会不会超时或溢出,输入边界里有没有极端情况。如果是内部可控数据,优先可读性;如果是外部用户传入,就得上迭代方案或者加保护措施。

检查项说明
数据规模最坏情况有多大,会不会超时或溢出
边界输入空输入、单元素、全重复、超大值
可读性非作者本人能否看懂核心逻辑
可测试性输入输出是否容易构造断言
维护成本新需求来了,需要改动几个地方

这张表我在写复杂代码前基本都会过一遍。它花不了多少时间,但能帮我早些发现那些“看起来有趣、其实是坑”的写法。

3.3 三个稳定来源:文档、评审、重构自己

如果你也想积累更多有启发的代码,我建议从三个渠道下手。第一是标准库文档,很多成熟库的内置实现本身就是绝佳教材,读一遍能够熟悉大量已经被验证过的思路。第二是代码评审,别人提交的改动里偶尔能看到很聪明的解法,这时候不能只看结论,还要追问当时的限制条件,因为同样的结果在不同约束下,写法背后的取舍可能完全不同。第三是自己重写旧代码,把三个月前写的一个功能重新实现一遍,往往能发现之前没看出来的简化路径。

这种积累很慢,但效果持久。坚持一段时间后会发现,看到新需求时第一反应不再是立刻开写,而是先想“这个问题有没有更本质的表示方式”。目标不再是绕过代码的重用,而是去建立抽象。

4. 坚持记录“有趣代码”之后,我收获了什么

4.1 从“能跑”到“能想”

早几年写代码的目标很简单:跑通。后来写的项目多了,开始意识到跑通只是底线。代码是否容易被修改,是否经得起新需求推动,才真正影响长期效率。那些被我标为“有趣”的代码,往往不只是某次性能表现更好,而是后续维护时省了大量时间。这种体验一旦建立,就再也不想退回“能跑就行”的状态。

心态上的变化更明显。现在遇到一段看着别扭的代码,我不会立刻接受“它就是这样的”,而会停下来想一想:这种别扭来自哪里,背后有没有更自然的表达方式。每次这样多想几分钟,通常就能发现一个更好的方案,而那个方案往往就是一段有趣的代码。

4.2 读别人代码的姿势也变了

以前看到别人写出漂亮代码,第一反应是把技巧背下来。现在我会先看输入输出,再顺着边界条件推演一遍,看它如何保证在极端情况下不出错。如果推演不通,说明代码里藏着隐式假设,这种假设就需要警惕。真正有趣的代码不怕推演,每一步都应该能被解释清楚。

就拿递归展平列表来说,推演时只需要关注两件事:遇到普通元素做什么,遇到列表做什么。这两个分支都清楚,边界就清晰了。反过来,一段代码如果到处都靠着“按理说不会发生”来说明正确性,那它的趣味就会大大下降。

4.3 给代码建一个“有趣索引”

如果非要给读者留一个具体建议,我会建议建一份自己的笔记,把那些让你觉得“有意思”的代码片段存进去。格式不用复杂,记清楚三件事就够了:这段代码解决了什么问题、关键技巧是什么、如果换一种思路又会牺牲什么。

我已经坚持这样做了一年多。现在翻回那些笔记,能明显看到自己关心的问题在变化:从最初关注“怎么写更短”,慢慢变成“怎么写更稳”,再到“怎么让设计更有弹性”。这份索引真正的作用不是背下多少代码,而是逐渐训练出拆解抽象的能力。就像把 if-else 换成字典映射,单独看只是一件小事,可一旦和数据驱动、配置化这些更大思想连接起来,很多基础架构的设计思路就串起来了。

编程这件事,说到底是在和复杂度打交道。那些我喜欢的“有趣代码”,很少有凭空出现的奇招,更多是对问题多理解了一层之后自然生长的结果。能在日常工作中不断遇到这些片段,并且停下来想明白它们为什么成立,这大概就是写代码最让人着迷的地方。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询