ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python循环嵌套入门:直角三角形、九九乘法表与金字塔实战解析

Python循环嵌套入门:直角三角形、九九乘法表与金字塔实战解析

不是我吹,这三道题几乎是我见过最适合用来理解循环嵌套的入门训练,没有之一。很多初学者拿到这类题目第一反应是“这有什么实际用处”,但等你真正把直角三角形、九九乘法表、金字塔一个个用 for 循环打出来之后,你会突然发现:原来所谓的“看懂代码”和“写得出代码”之间,差的恰恰就是这种把问题拆成行和列的能力。

这篇文章我会从零开始,把这三个图形题拆开揉碎来讲。不绕弯子,不堆概念,每一段代码都配上当时我为什么会这么写的思考过程,以及最容易踩的坑。不管你是一点基础都没有的纯小白,还是已经能看懂代码但一动手就卡壳的初学者,这篇都值得你跟着敲一遍。

1. 为什么要死磕图形题:循环嵌套就是一条“流水线”

1.1 一个被低估的入门训练场

先聊个比较现实的问题:为什么几乎所有的 Python 教程都会拿这几个图形当练习题?不是因为它们简单,而是因为它们正好卡在了一个非常关键的技能点上:你能不能把一个看起来只有一行一行的输出,拆解成“外层循环控制什么、内层循环控制什么”。

我见过不少已经学完函数、列表、字典的同学,回头让他打印一个直角三角形,他居然会纠结半天。问题出在哪里?出在他从来没有真正动手把这些基础的循环结构玩明白,后面的东西全是在空中楼阁上盖房子。

打印图形这件事,本质上就是让计算机按照指定规律输出一堆字符。它的输入是你脑子里的规律,它的输出是屏幕上的符号。你要做的就是把这个规律翻译成 for 循环的语言。这个翻译能力,往后写爬虫、写数据处理脚本、写自动化工具,全都要用。

1.2 看穿嵌套循环:外层管行,内层管列

把复杂的先放一边,我们来想一个最基本的问题:for 循环里的循环变量,到底是干嘛用的?

如果你打印过 1 到 10 这些数字,你会发现循环变量的值在每次迭代中都会变化。而图形题的关键,恰恰就是让这个“变化的值”去参与字符数量的计算,从而形成不同的形状。

for i in range(5): print(i)

输出是:

0 1 2 3 4

这里的i行是买菜的清单,range(5)就是口袋里的五块钱。每一轮循环,i就变成了一个具体的数字。现在我们把问题升级一下:如果每一行打印的星号数量等于i的值,会怎么样?

for i in range(5): print('*' * i)

输出是:

* ** *** ****

看到没有,第一行是 0 个星号,因为第一轮i = 0;第二行是 1 个星号,因为第二轮i = 1。这就是所谓“外层循环管行数,内层循环管列数”的核心逻辑。不过上面这种写法,只利用了外层循环的变量。真正考验人的是两个循环嵌套在一起,内层循环也要跟着外层循环的节奏变化。

我习惯把外层循环理解成“流水线的传送带”,每传送过来一个工件(也就是每一行),内层循环就是“工人”,负责往这个工件上装指定数量的零件(也就是每一列)。外层每转一圈,内层就得完整地跑一遍,这个流程在脑子里一定要转得过来。

2. 直角三角形:先让内层循环学会“见风使舵”

2.1 最经典的左上直角:从 1 个星号到 n 个星号

我们先定个标准:打印一个 5 行高的直角三角形,从第一行 1 个星号,到第五行 5 个星号,全部左对齐。

n = 5 for i in range(1, n + 1): for j in range(i): print('*', end='') print()

输出:

* ** *** **** *****

这里有几个值得注意的细节。

第一个是range(1, n + 1)。为什么是n + 1?因为range的终止值是不包含的,你要打印 1 到 5 行,就得写到n + 1才能把 5 囊括进去。这个边界问题,初学者十个人里至少有五个会在这里翻车。

第二个是for j in range(i)。注意,内层循环的次数不是固定的,它跟外层循环变量i直接绑定了。第一轮i = 1,内层循环只有 1 次,打印 1 个星号;第三轮i = 3,内层循环跑 3 次,打印 3 个星号。这也就是我标题里说的“见风使舵”——内层循环的循环次数,每一步都在跟着外层变。

第三个是end=''。print默认在结尾会加一个换行符,如果我们直接写print('*'),那不管你怎么循环,每个星号都会单独占一行。end=''的意思就是告诉 Python:“打完之后别换行,我等会儿还要接着在同一行打印下一个星号。”直到整个内层循环结束,我们再用一个不带参数的print()来换行,开始下一行。

提示:很多教材里会把end=''写成end="",效果完全一样。但你要记住,end参数后面跟的是一个字符串,你甚至可以写end=' ',也就是用空格结尾,这在后面的九九乘法表里会用到。

2.2 换一个方向:右下直角和右对齐

如果你觉得左上角看腻了,我们把它翻个面,做一个左下角有直角、星号整体靠右的三角形。其实思路也很简单:每一行先打印一些空格,再打印星号。

n = 5 for i in range(1, n + 1): for k in range(n - i): print(' ', end='') for j in range(i): print('*', end='') print()

输出:

* ** *** **** *****

这里的逻辑是这样的:总的宽度是 5 个字符。第i行要放i个星号,那剩下的n - i个位置就由空格来填充。所以我们在星号之前,先让内层循环用空格把前面的位置占掉,然后再打星号。

我刚开始学的时候,总觉得两个内层循环“太挤了”,代码长得不好看。后来才发现,这种写法的好处是每一步都清清楚楚:空格归空格,星号归星号。而且这种“先空格后内容”的思路,直接为后面打印金字塔做了铺垫。

2.3 内层变量没用到?试试用 j 控制步长

前面两个例子里,内层循环的变量j只是充当了一个“计数器”,我们并没有真正用到它的值。有同学就会问:那j还有什么用?

你可以这么想:直角三角形不一定是每行都从 1 个星号开始。比如我想打印一个星号数量递增、但每行递增 2 个的三角形,这就要用到j或者说内层结构里的某种递进了。我们用range(1, 2*i+1, 2)来控制星号个数也可以。不过这种写法更偏向输出奇数个星号,属于金字塔的入门版。

真正常见的内层变量用法,是九九乘法表。我建议你现在先记住这句话:如果你只想重复 N 次相同的事情,循环变量就可以不参与计算;但如果你想让每次做的事情有一点不一样,循环变量就是你手里唯一的“变化筹码”。

3. 九九乘法表:一次看透两个变量怎么互相牵制

3.1 从图形到数字,本质是换汤不换药

直角三角形你如果已经能闭眼写出来,那九九乘法表对你来说其实只剩最后一层窗户纸:把星号换成算式而已。

先看代码:

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

输出:

1 × 1 = 1 1 × 2 = 2 2 × 2 = 4 1 × 3 = 3 2 × 3 = 6 3 × 3 = 9 1 × 4 = 4 2 × 4 = 8 3 × 4 = 12 4 × 4 = 16 1 × 5 = 5 2 × 5 = 10 3 × 5 = 15 4 × 5 = 20 5 × 5 = 25 ...

你仔细看这个结构,是不是特别像一个右对齐或者左对齐的直角三角形?它的行数i从 1 到 9,每一行的列数j正好是从 1 到i。只不过,每一格的内容不再是星号,而是三个信息:被乘数j、乘数i、乘积i * j。

在这个过程中,f"{j} × {i} = {i * j}"是 f-string 格式化输出,它把循环变量的当前值和计算结果直接嵌到字符串里。这里有一个不少新手会犯的迷糊:到底谁是被乘数谁是乘数?你用笔在纸上跟着走一遍第二轮就明白了:当外层的i = 2时,内层j会依次取 1 和 2,所以打印的是1 × 2 = 2和2 × 2 = 4。也就是说,j相当于表格里的“列头”,i相当于“行号”。

3.2 对齐和换行:end 参数的玩法变了

在九九乘法表里,我用的是end='\t'而不是end=''。为什么?因为算式有长有短——1×9=9和9×9=81的字符宽度不一样,如果不用制表符对齐,整个表会扭成一团乱麻。

\t是制表符,它的作用是跳到下一个对齐位置。在大多数终端里,它能让每一列保持一个相对整齐的宽度。当然,制表符也不是万能的,如果你的终端设置了不同的 tab 宽度,对齐效果可能略有差异。想要更精确的控制,你也可以用字符串的format方法指定最小宽度,比如:

print(f"{j} × {i} = {i * j:<4}", end='')

这里的:<4表示把结果左对齐,占至少 4 个字符宽度,不够就用空格补足。这就比\t稳一点。不过对于九九乘法表来说,end='\t'已经足够用了。

关于换行,千万不要把print()漏了。一旦漏了,你的 81 个算式会全部挤在同一行里,那场面非常灾难。如果出现这种情况,别急着怀疑逻辑,先看看是不是缺了“行尾换行”这一句。

3.3 变体练习:倒序打印、只打印偶数行

学到这里,如果你还想再多练一把,我建议你改两个方向试试。

第一个方向:把外层循环改成for i in range(9, 0, -1),让行数从 9 到 1 递减,看看三角形的方向会不会整个反过来。

第二个方向:先判断行是否为偶数,只在偶数行输出内容,奇数行直接跳过。这个练习可以让你提前接触一下“循环里嵌套if”的常见结构。

这两个练习都不需要新的代码知识,纯粹是逼着你在现有的逻辑上做调整。我强烈建议你别只是脑子里想,而是真的打开编辑器去改、去跑、去看输出。失败一次、报错一次,你记住的东西远比看十遍教程都牢靠。

4. 金字塔:先算好“看不见的空格”,再画“看得见的星号”

4.1 每行三件事:空格、星号、换行

如果说直角三角形是循环嵌套的开胃菜,那金字塔就是正餐了。因为金字塔的每一行,要同时处理两样东西:前面的空格和中间的星号。

还是先看代码:

n = 5 for i in range(1, n + 1): for k in range(n - i): print(' ', end='') for j in range(2 * i - 1): print('*', end='') print()

输出:

* *** ***** ******* *********

注意第三行的for j in range(2 * i - 1)。为什么是2 * i - 1?因为金字塔每一行星号的数量是奇数:1、3、5、7、9。这个数列的通项公式就是2 * i - 1。我们可以口算验证一下:第 1 行2*1-1=1,第 2 行2*2-1=3,第 3 行2*3-1=5。没有一处对不上。

这里我想强调一个思维习惯:看到图形先找规律,再动手写代码。很多人一上来就闷头敲键盘,结果敲到一半发现空格数量不对,又回头改,折腾半天。其实你拿张纸,把 5 行的空格数和星号数列出来:

行号 i空格数星号数
141
233
325
417
509

规律一目了然:空格数等于n - i,星号数等于2 * i - 1。这表的含金量,比你在草稿纸上瞎画半天高多了。

4.2 推导公式背后的等差数列思维

往深了说,这个题目其实在训练一种非常实用的抽象能力:把一个连续变化的形状,总结成一个可以用变量表达的计算式。

空格数n - i是一个等差数列,随着i增加依次递减;星号数2 * i - 1也是一个等差数列,随着i增加依次递增。当你以后遇到更复杂的场景,比如生成缩进的目录树、画表格线、做某种动态报表,你本质上还是在做同一件事:用公式去描述“第 N 行会长什么样”。

我还见过有人把金字塔写成这样:

print(' ' * (n - i) + '*' * (2 * i - 1))

这也是一种非常 Pythonic 的写法,字符串乘以数字可以直接得到重复 N 次的字符串。它把两个内层循环压缩到了一行,代码少了,可读性也还不错。初学者我建议先在脑子里把两个循环分开理解,等熟练了再用这种简化写法。

注意:这里用到的字符串乘法确实很方便,但也容易让新手误以为 Python 里什么都能乘。它只能用于字符串与整数相乘,别拿字符串去乘浮点数,否则会直接报错。

4.3 改动一个条件,金字塔变空心

图形题最大的乐趣就在于,你学会了基础版,稍微改一行代码就能生出各种花样。比如空心金字塔,每一行只打印最左边和最右边的星号,中间全是空格。

n = 5 for i in range(1, n + 1): for k in range(n - i): print(' ', end='') for j in range(2 * i - 1): if j == 0 or j == 2 * i - 2 or i == n: print('*', end='') else: print(' ', end='') print()

输出:

* * * * * * * *********

判断条件里的j == 0表示这一行的第一个位置,j == 2 * i - 2表示这一行的最后一个位置,i == n表示最后一行要全部用星号封底。这个练习会让你更深刻地理解j在循环内部的角色:它不光是计数器,还是一个“位置坐标”。

这种把循环变量当作坐标来用的思路,往大了说,就是二维数组遍历和图形处理的基础。你以后如果用 Python 去做框架里的绘制或者游戏地图的生成,会发现思路完全一致。

5. 我那会儿踩过的坑和调式方法

5.1 最经典的排查手段:让程序“嘴上说”它正在干什么

代码这东西,一旦跟你脑子里的预期不一致,盲猜是没有用的。我调试图形类代码时,最常用的手段非常简单粗暴:在循环里临时加打印语句,把变量的实时值打出来。

举个例子,如果你不确定金字塔的空格数量到底对不对,可以先不打印星号,而是打印一个点:

for i in range(1, n + 1): for k in range(n - i): print('.', end='') for j in range(2 * i - 1): print('*', end='') print()

看到输出之后,你就能直观地检查每一行前面的点是不是比上一行少一个。这个方法虽然原始,但在调试循环结构时,效率远高于你干瞪眼。

还有一种情况是,明明输出的行列数没错,但整体形状歪了。那多半不是逻辑问题,而是终端字体宽度问题。在中文状态下,中文字符和英文字符宽度不一样,代码里如果混进了全角空格,那更是怎么对齐都对齐不了。我建议你写这类代码时,一律用英文半角空格,别用 Tab 和空格混排,否则 Python 在缩进上会直接给你甩脸子。

5.2 我把常见报错和异常整理了一张表

为了让你少走弯路,我把初学阶段最常见的几个问题整理了一下:

问题现象原因分析解决建议
IndentationError: expected an indented block冒号后面没缩进,或者缩进混用了空格和 Tab统一使用 4 个空格缩进;检查冒号后是否换行并缩进
三角形第一行没有星号range(n)和range(1, n+1)的首个值不一样确认循环是从 0 还是从 1 开始;通过 print(i) 验证
所有星号挤在一行忘记了行尾的print(),或者end参数写错了位置检查内层循环外是否有一句独立print()负责换行
金字塔是左对齐而不是居中的空格打印数量不对,或者终端字体导致宽度偏差检查空格数是不是n - i;用显式符号验证
九九乘法表最后一行少了一半range(1, i+1)写成了range(1, i),漏掉了第 i 列记住 range 右边界不包含,列数要等于 i 就必须写成 i+1

这张表里的错误,我当年挨个都犯过。尤其是range的边界问题,哪怕到现在,我写循环时也习惯在脑子里默念一句“含前不含后”。你可以不用刻意背,踩过一次坑自然就记住了。

6. 从图形题到现实任务:这套思维可以迁移到哪

6.1 菱形、倒三角、各种组合图形

如果你把金字塔、倒三角、直角三角形都拿下了,我建议你再挑战一个组合图形:菱形。

菱形本质上就是上半部分一个金字塔,下半部分一个倒金字塔。你只需要两个大的循环块:第一个循环打印到中间最大行,第二个循环从下一行开始递减打印。要注意的是,上下两部分不能共用一套i的取值范围,否则会多打一行或者漏掉中间行。

我自己的一个经验是,组合图形的边界经常出错,所以我会先定一个“中间行”变量,比如mid = n // 2,然后让上下两部分都围绕中间行来计算。这种拆解法,在以后写分页打印、树形菜单、分级摘要这类功能时,同样非常管用。

6.2 循环嵌套在真实脚本里的影子

图形题看起来“不务正业”,但它的核心结构在真实项目里到处都是。举几个例子:

读取一个二维表格时,你经常要写for row in table:外层循环每一行,内层for cell in row:循环每一个单元格——这不就是九九乘法表的遍历结构吗?

做数据批量处理时,每一组数据都要生成一个格式化的报告文本,组内每一行都要按固定格式拼接——这不就是金字塔里“空格 + 星号”的组合逻辑吗?

哪怕是写日志文件的分隔线、命令行工具的输出表格、批量生成测试数据,你都得跟循环嵌套和字符串格式化打交道。图形题练的恰恰是这些底层能力:循环边界感、字符串拼接、格式化输出、变量在不同层级的可见性。

说到底,这几个图形套路不是用来“应付作业”的,它们是让你在写那些更复杂的循环之前,先把手感和思维磨对。用我常跟朋友说的一句话总结:不要小看这几个符号组成的形状,你能闭着眼睛把它们写出来,循环这块就算真正入门了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进