☰
九九表打印的精致之道:用折行内注提升代码可读性
2026/10/6 17:25:26 网站建设 项目流程

九九表的打印,十个人里有九个写过。但如果你问一句"打印得够不够精致",大部分人会愣住——能跑、结果对、看得清,不就完了吗?我以前也这么想,直到有一次在代码评审里被老同事问住:"你这三重循环挤成一坨,每行的输出逻辑全靠人眼硬看,万一哪天要改输出格式,你打算怎么下手?"正是从那次之后,我开始认真琢磨"折行内注"这件事,也才有了这套"幼稚版"的精致打印方案。

这篇文章想跟你聊的,不是九九表本身,而是怎么借九九表这个最简单的题目,把"折行内注"这套代码排版与注释的习惯练到手。它适合刚学编程不久、想把代码写得更有条理的新手,也适合写了好几年代码但始终不太注重输出格式的老兵。读完你会发现,一张九九表能被拆出不少讲究,而这些讲究放到真实项目里,几乎每天都能用上。

1. 为什么一张九九表值得谈"精致"二字

很多人觉得九九表简单,无非是两层循环、一个乘法、一个换行。但"能打印"和"打印得精致"之间,差的恰恰是代码功底里最容易被人忽略的那部分:对输出结构的理解和对阅读体验的在意。

1.1 从"能跑"到"经得起看"

先看最朴素的三行代码:

for i in range(1, 10): for j in range(1, 10): print(f"{j}x{i}={i*j}", end=" ") print()

这段代码在功能上完全正确。它输出了一个9行9列的完整乘法表,每个算式之间用空格隔开,行末换行。功能正确。

但你把输出结果往屏幕上一贴,问题全出来了:对角线两边的算式都有,但左边下半三角和右上半三角堆在一起,列与列之间没有稳定的间距,2x2=4和3x3=9挤在相邻位置时,人对"竖排有没有对齐"完全没有任何感知。

说得直白一点:这段代码的输出结果,是一堆算式,不是一张表。

"精致"这件事,本质上是在回答三个问题:

  • 每一列该占多宽,才能让所有算式在竖向上对齐?
  • 算式的排列是保持完整的 9x9 矩阵,还是只保留有意义的半三角?
  • 换行的位置、每个算式之间的分隔符,能不能传达结构信息?

我见过不少初学者把前两个问题混为一谈,结果就是:明明只想打印下三角,却把上三角也打出来,然后说"反正能看"。能看和好看之间,差的就这十几行代码的思考量。

1.2 九九表的信息结构决定了排版策略

九九表在数学上是典型的三角矩阵。i从1到9表示被乘数(或者说行号),j从1到i表示乘数,每一行的算式数量是逐行递增的。这意味着如果你要打印完整的9x9矩阵,第1行只有1个算式,第9行有9个算式,列位置天然参差不齐。

所以精致的第一个选择是:只打印下三角。也就是行号i从1到9,列号j从1到i。这样输出的每一行从左往右排列,行末天然对齐,视觉上就是一张干净的三角形表。

第二个选择是乘积位数的处理。乘法表里最大的乘积是9*9=81,两位数;最小的乘积是1*1=1,一位数。如果不做宽度控制,一位数的算式和两位数的算式混排在一起,列间距会忽宽忽窄。解决办法是给每个算式固定一个宽度,比如固定占6个字符宽度,不足的部分用空格补齐。

这两个选择一旦定下来,"打印"就从"输出结果"变成了"排版设计",后面所有的代码细节都在为这个设计服务。

2. "折行内注"到底指什么:拆开揉碎讲清楚

"折行内注"这个词不算行业标准术语,更像是一种约定俗成的叫法。从字面拆,就是"折行"加"内注"。它包含两层意思,一层落在输出层面,一层落在源码层面。

2.1 输出层面的折行:换行点本身就是结构

在打印九九表时,什么时候换行不是随便定的。传统写法里,内层循环结束就print()换行,行数由外层循环控制。这个换行点,就是九九表"行"的边界。如果把打印过程想象成画画,换行就是落笔位置的规划——下一行从哪里开始画,决定了整张画的轮廓。

很多初学版本在处理换行时是"顺手"的:内层循环完事就换行,从不考虑这一行为什么在这里结束、下一行为什么从那里开始。

精致的做法是在代码里把换行意图讲清楚。比如这样组织内层循环:

for i in range(1, 10): # 每行从 j=1 开始,到 j=i 结束,形成下三角 for j in range(1, i + 1): print(f"{j}x{i}={i*j}", end=" ") print() # 行尾换行:一行结束,进入下一行

当然,只看这个例子还看不出大差别。真正的差异在多行表达式、多条件拼接、长参数列表这些场景里才会凸显。九九表的意义在于:用最小的代码量,把"换行是有意图的"这个意识种下去。

2.2 源码层面的折行:在换行处塞注释

折行内注的第二层,也是更核心的一层,是指写代码时把超过一行的长结构拆成多行,并在折行点附近直接加上注释,解释这一整块逻辑的用途,而不是把注释统一堆在代码块开头或行尾。

举个例子。假设你要在九九表里加入一个"每组算式之间用制表符分隔"的需求,朴素写法是这样:

for i in range(1, 10): for j in range(1, i + 1): result = i * j print(f"{j}x{i}={result}", end="\t") print()

这段代码的折行点是"循环控制行"和"打印行"。但如果有一天你要改成"结果不足两位补0"、要改用中文字符填充、要在每行末尾追加行号,这个结构的扩展性就暴露问题了——所有逻辑全挤在print一行里,改一处就要动整行。

折行内注的改造思路是:把每一个动作拆成单独一行,在折行处写明意图。

for i in range(1, 10): for j in range(1, i + 1): expr = f"{j}x{i}" product = i * j # 列宽统一为8位,不足补空格,确保竖向对齐 print(f"{expr}={product:<8}", end="") print()

看到区别了吗?expr和product这两个中间变量把"拼接表达式"和"计算结果"拆开,注释紧贴在print的上一行,直接解释"为什么要补空格"这一关键决策。这种注释方式就是折行内注的雏形——在代码需要折行的地方,把拆出来的部分注上一笔。

2.3 和普通注释的两点关键差异

我刚接触这个概念的时候也犯过嘀咕:折行内注不就是"在代码中间加注释"吗?跟普通注释有什么区别?

区别在位置和粒度。

普通注释的位置有两种:代码块上方的说明性注释,以及行尾的提示性注释。前者的毛病是"距离太远"——它解释的是整个块的目的,但读者看到一个具体的折行点时,往往已经忘了头顶那行注释;后者的毛病是"行尾空间有限",稍微写长一点就撑到屏幕边缘,而且同样只挂在某一行上。

折行内注的位置则在折行点本身,也就是代码从一行拆成多行的那个拐角处。它紧贴着被拆分的逻辑块,粒度比块注释细、比行尾注释完整。遇到"这行为什么要断开""断开后这里在算什么"这类疑问,读者不需要往代码块顶部找答案,低头就能看到。

九九表打印里,最典型的折行内注场景就是我上面写的那种:内层循环的边界range(1, i + 1)后面补一句"形成下三角",列宽补位那里补一句"确保竖向对齐"。这两句注释如果写在函数开头,基本是废话;写在折行处,就是关键时刻的提示。

3. 从朴素打印到折行内注:一步步改造九九表

前面讲了理念,这节上实操。我带你把九九表的打印从最朴素的版本,一路改到"折行内注"加持的精致版本。每一步都告诉你为什么这么改。

3.1 第一版:双循环裸奔

for i in range(1, 10): for j in range(1, 10): print(f"{j}x{i}={i*j}", end=" ") print()

这版的三个问题值得拎出来说。

第一,内层循环打了完整的9列,上三角和下三角都有,信息冗余。读者一眼扫过去,左右两侧的算式完全对称,视觉重心被分散。正如前面分析的,作为一张"表",它应该只显示有效区域——下三角。

第二,end=" "的固定空格导致列宽完全由算式本身的长度决定。1x1=1后面接空格,9x9=81后面也接空格,前者总宽5个字符,后者总宽7个字符,下一列的起点就跟着飘。整张表列线是歪的。

第三,也是最隐蔽的问题:代码里没有任何注释,循环的意图、打印的格式设计全靠人脑临时解读。你现在看觉得简单,过一个月再回来看,可能要想几秒钟才反应过来"为什么 j 是从1到9,i 是从1到9,结果还留了空格"。

3.2 第二版:确定结构,只打印下三角

第一个影响输出的决策是只打印下三角。这个决策同时决定了循环边界:内层循环的上限必须从固定的9改成外层变量i。

for i in range(1, 10): for j in range(1, i + 1): print(f"{j}x{i}={i*j}", end=" ") print()

改动只有一处:range(1, 10)改成range(1, i + 1)。但这一处的意义值得展开说。

range(1, i + 1)是九九表三角结构的核心表达。i是当前行号,j的合法取值范围是从1到自己。第1行只有1x1=1,第9行有9个算式。这样输出的每一行,行末恰好对齐,形成一个左对齐的直角三角形。

这里值得养成一个习惯:循环边界里出现的+1是有含义的。range是左闭右开,要包含i本身就得写成i + 1。很多新手在这里会直接写range(1, i),结果发现每一行都少最后一个算式,然后开始怀疑人生。折行内注如果写在这一行,我会这么注:

for j in range(1, i + 1): # range左闭右开,+1才能取到j=i

这就是典型的"在折行点(边界表达式)注明关键语义"的做法。

3.3 第三版:列宽统一,先解决竖向对齐

下三角版本比完整矩阵清爽多了,但列宽问题依然在。要精致,必须给每个算式固定占位宽度。Python里最常用的是格式化字符串的宽度控制。

for i in range(1, 10): for j in range(1, i + 1): print(f"{j}x{i}={i*j:<4}", end="") print()

:<4的含义是:这个字段左对齐,总宽度至少4个字符,不足补空格。但注意,这里我用了end="",也就是算式之间不加空格,全靠:<4撑出来的空白做间隔。这样每个算式的占用宽度完全一致,下一列从固定位置开始,整张表的竖向对齐就成立了。

很多人会问:为什么是4,不是5或者6?因为9x9=81是7个字符,<4指的是=81这部分占用4位,加上前面的9x9=固定4个字符,每个算式总宽8个字符。8是表中最长算式(7字符)加1个分隔空格的余量,刚好够用。

在实际调试中,我习惯先跑一遍看效果,再把宽度微调。不同终端、不同字体下宽度感受不一样,这个只能靠实测。但方向是确定的:要么用格式化宽度,要么在字符串拼接时手动ljust,不要再用end=" "碰运气。

3.4 第四版:引入折行内注,代码自己会说话

现在是重点。前三版解决了"能打、三角、对齐",第四版解决"人能看懂"。这一版我把中间计算拆开,在关键折行处补注释,让代码读起来像一份有批注的排版说明。

for i in range(1, 10): # 内层循环范围随行号变化,控制下三角形态 for j in range(1, i + 1): expr = f"{j}x{i}" product = i * j # 固定"=乘积"部分占4位,保证各列起点对齐 print(f"{expr}={product:<4}", end="") print()

拆解一下这段代码里的三个"折行内注"。

第一个注释挂在for j行上方,解释内层循环为什么用i + 1而不是固定9。这一行是九九表三角结构的关键,不注明白,读者还得自己推导。

第二个注释挂在print行上方,解释:<4的用途是"保证各列起点对齐"。这个参数如果不解释,别人看代码时多半要翻Python文档查格式化语法。

第三个细节是中间变量expr和product的拆分。这不是为了性能,函数调用也好、循环也好,这种规模的运算根本谈不上性能。拆变量的目的是让每个打印成分都有自己的名字——表达式是expr,乘积是product,打印语句里只有组装,没有计算。将来要改格式,比如把乘号x改成*,只动expr那一行;要改乘积的补位方式,只动print那一行。每个折行点都对应一个独立可修改的维度。

这版代码的打印结果与第三版完全一致,但代码的"可读性资产"完全不同。我的经验是:打印结果一致时,读代码的人真正在看的是"你为什么这么写"。折行内注让这个"为什么"直接出现在阅读路径上。

3.5 再进一步:函数封装与参数化

如果你想把这段代码用在真实项目里(哪怕只是写个小工具),我建议再包一层函数,把列宽、范围等参数暴露出来。

def print_multiplication_table(size=9, col_width=4): for i in range(1, size + 1): for j in range(1, i + 1): expr = f"{j}x{i}" product = i * j # 列宽参数化后,终端字体变化时可直接调整 print(f"{expr}={product:<{col_width}}", end="") print()

变化有三处:9被参数size取代,4被参数col_width取代,注释也同步改成"终端字体变化时可直接调整"。

这就是折行内注在函数场景下的又一体现:每个参数的用途,在它第一次被使用的地方就近注释。

4. 精致打印的细节参数:空白、换行与最终校验

到了这一节,代码已经写得七七八八,但"精致"还有最后一公里:输出环境里的那些坑。我踩过的、见过的,都列在这里。

4.1 等宽字体与全角空格的陷阱

九九表的对齐依赖一个前置条件:输出终端使用等宽字体。绝大多数代码编辑器、终端模拟器默认就是等宽字体,所以这个条件基本满足。但有两个例外值得注意。

第一个例外是某些富文本终端或IDE的内嵌控制台,默认字体可能是比例字体。同样的空格,在不同宽度字符后面显示出的实际距离不同。如果你的九九表在某个环境里打印出来歪了,先查字体,不要急着改代码。

第二个例外是中文全角空格。Python里如果你用chr(12288)(全角空格)来补位,它的显示宽度通常是普通半角空格的两倍。在需要混合中文注释和英文输出时,全角空格很容易让对齐逻辑失算。我的建议是:补位一律用半角空格或格式化宽度,不要手动拼全角空格。

# 反面教材:用全角空格补位,列宽会因字体产生偏移 print(f"{expr}={product}" + "\u3000", end="")

这段代码在大部分终端里都会破坏对齐,而且不同终端表现还不一样,排查起来非常难受。

4.2 左对齐、右对齐还是居中:按信息类型选

格式化宽度时,<是左对齐,>是右对齐,^是居中。九九表应该用哪个?

我的建议是左对齐。原因很简单:九九表的每个算式从左往右读,j x i部分在左、乘积在右,左对齐能让j x i的起点在同一竖线上,读者眼睛从左往右扫时路径最自然是直线。如果右对齐,算式起点参差,阅读时要不断地调整视线起点。居中就更不推荐,视觉重心摇摆不定。

但在真实项目的报表打印场景里,情况刚好相反。列名、金额这类需要上下对比的数据,通常右对齐——因为个位、小数点天然对齐,人眼比较数值大小最快。九九表这种"行主导"的文本,左对齐才是对的。这个差异本身就是一种排版经验:对齐方式跟着信息的阅读方式走,不跟习惯走。

4.3 换行点与尾随空格的清理

第三版代码里我用了end="",靠宽度补位撑出分隔。这样做的副产品是每一行的末尾会多出一串空格——因为最后一个算式的product:<4也会补满4位。这些尾随空格在终端里看不出来,但如果你把输出重定向到文件,或者用diff对比两份输出,尾随空格就是干扰项。

清理办法很简单:在内层循环里判断是否为当前行最后一个算式,是的话就不补位。

for i in range(1, 10): for j in range(1, i + 1): expr = f"{j}x{i}" product = i * j if j == i: # 行末算式,不补位,避免尾随空格 print(f"{expr}={product}") else: print(f"{expr}={product:<4}", end="") # 注意:行末已通过print()自带换行,这里不需要额外print()

这个版本的输出在文件层面是干净的,没有一行带尾随空格。代价是分支判断多了几行。如果你是纯控制台查看,不加这个判断也无所谓;如果要重定向到文件、参与自动化对比,建议加上。

其实还有一个更优雅的解法:先构造整行的字符串列表,再用" ".join()拼起来,最后一次性打印。这样尾随空格天然不存在。

for i in range(1, 10): row = [] for j in range(1, i + 1): expr = f"{j}x{i}" product = i * j row.append(f"{expr}={product}") print(" ".join(row))

join版本的可读性反而更好:先搭行,再拼行,最后打印整行。没有end参数的干扰,换行逻辑回归到最简单的print()。我个人在真实项目里更倾向这种"先构造、后输出"的风格,它在面对更复杂的输出时(比如每一行要拼接多段不同样式的文本)扩展性更强。

4.4 校验方式:肉眼之外的第二道保险

九九表的校验,大部分人都靠肉眼扫一遍。但对于一个号称"精致"的输出,肉眼是有极限的——尤其是9x9的矩阵,列宽差一两个字符,不仔细看发现不了。

我自己常用的校验方法是把输出重定向到文件,再用脚本检查每一行的列断点位置是否一致。比如把每一行按空格拆词,校验每行单词数是否等于i,再校验每一列起始字符的位置是否在同一列。

with open("table.txt", "r", encoding="utf-8") as f: lines = f.readlines() # 校验每行的算式数量是否符合下三角 for idx, line in enumerate(lines, start=1): items = line.split() assert len(items) == idx, f"第{idx}行应有{idx}个算式,实际{len(items)}个"

这段脚本不需要很复杂,核心是"用程序代替肉眼做重复性验证"。我曾见过有人改了列宽参数后忘记重跑测试,结果整张表在终端里看着是好的,放进文档里缩进全乱。这种问题只有自动化校验能拦得住。

5. 从九九表到真实项目:折行内注的实际价值

九九表只是载体。真正有价值的是"折行内注"这套习惯。它一旦养成,会在很多日常写代码的场景里持续发挥作用。

5.1 三种最适合折行内注的日常场景

第一种是打印日志。打日志时经常要拼一长串上下文,比如请求ID、用户ID、耗时、状态码。一长串f-string塞在一个logger.info()里,换行时在哪个参数后断、为什么断,没人说得清。折行内注的思路是:把每个上下文变量拆出来,在折行点注明"这里打的是哪个维度的信息"。

logger.info( "order created: user=%s, amount=%s, cost=%s", user_id, # 用户维度,用于订单归属分析 amount, # 金额维度,用于交易统计 cost # 成本维度,用于毛利计算 )

第二种是SQL或查询条件拼接。多条件筛选时,每个条件一行,行内注释写明这个条件对应前端哪个筛选器,后续维护的人改起来会非常快。

第三种是配置文件里长列表的整理。YAML、JSON里一段重复结构,折行后在字段旁注明语义,能省去大量查文档的时间。

5.2 折行内注的边界:注释不是越多越好

折行内注虽好,但要克制。我的判断标准是:只注释"不看就不知道为什么要这样写"的地方,不为显而易见的代码写注释。比如上面九九表里的for i in range(1, 10)就不需要注释,但range(1, i + 1)由于涉及左闭右开和三角结构两个隐含信息,值得注一笔。

另外,如果注释本身比代码还长,就要警惕是不是代码结构太绕了。这时候优先改代码结构,而不是靠注释救火。折行内注是给"合理但需要解释"的代码锦上添花,不是给"糟糕但能跑"的代码擦屁股。

5.3 团队协作里的注释默契

最后说一点团队层面的体会。折行内注这种风格,在评审的时候很容易赢得老同事的好感,因为它说明你在写代码时考虑到了下一个读代码的人。但这也需要全队形成默契:谁改的代码,谁负责把相关注释同步更新。最怕的是某次重构后,注释还停在旧逻辑上——那比没有注释更坑人。

我自己在真实项目里踩过一次这个坑:一个报表模块的循环边界被改了,但折行处的注释没同步,还是旧的"按月份循环"。后来排查问题时,注释把我引向了完全错误的方向,白白浪费了半小时。从那以后,我给自己立了个小规矩:折行内注跟着代码变,宁可删掉也不要留过时的。

九九表这条看似入门的小题目,其实把"输出排版"和"代码可读性"两件事都练到了。你可以试着把文中的第四版代码敲一遍,跑出结果后自己再改一改列宽、换一换分隔符,看看注释是不是还说得通。能在修改后仍能准确引导读者的注释,才是真正合格的折行内注。

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

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

立即咨询