查看全课大纲(第 15 / 51 节)
  1. 1.为什么人人都该学点编程?
  2. 2.计算机与程序设计:你的电脑到底在“想”什么?
  3. 3.Python 这门语言,到底有什么魔力?
  4. 4.Python 开发环境配置:如何拥有你的第一个“编程工具箱”?
  5. 5.如何安装一个更强大的代码编辑器?
  6. 6.汇率兑换1.0:你的第一个“钱生钱”小程序
  7. 7.你的第一行代码,藏着哪些秘密?
  8. 8.汇率兑换2.0(上):让程序看懂“CNY”和“USD”
  9. 9.汇率兑换程序,如何让它“聪明”地判断币种?
  10. 10.汇率兑换程序能一直运行,直到我喊停吗?
  11. 11.汇率兑换4.0:如何把“算钱”功能打包成积木?
  12. 12.汇率兑换5.0:给你的程序穿上“结构化”的西装
  13. 13.怎么用 Python 画一个会动的五角星?
  14. 14.如何让程序自动画出不同大小的五角星?
  15. 15.函数自己调用自己,会发生什么?
  16. 16.用递归画出神奇的分形树
  17. 17.Python 怎么计算你一天躺着不动能消耗多少热量?
  18. 18.如何让程序“活”起来,持续为你服务?
  19. 19.如何让 BMR 计算器更“聪明”?
  20. 20.当程序遇到“不按套路出牌”的用户怎么办?
  21. 21.怎么用 Python 模拟一个“滚雪球”式的存钱计划?
  22. 22.如何用列表记录每一周的存款?
  23. 23.如何让程序自己数数?—— 用 for 循环解放你的双手
  24. 24.52周存钱挑战4.0:你的存款计算器能有多灵活?
  25. 25.52周存钱挑战:怎么根据日期自动查存款?
  26. 26.Python 怎么判断一年中的第几天?
  27. 27.列表 vs 元组:哪个更适合存储月份天数?
  28. 28.用集合给月份分个队,换个思路算天数
  29. 29.把月份和天数“锁”在一起:用字典给数据配个对
  30. 30.Python 怎么判断密码强弱?
  31. 31.Python 怎么给密码设置“防沉迷”系统?
  32. 32.Python 怎么把密码保存到文件里?
  33. 33.Python 怎么读取文件里的密码?
  34. 34.Python 怎么把密码检查工具“打包”起来?
  35. 35.Python 怎么把文件操作也“打包”起来?
  36. 36.计算机怎么“扔骰子”?——用Python模拟随机事件
  37. 37.如何用 Python 模拟掷两个骰子?
  38. 38.怎么用 Python 把骰子轨迹画出来?
  39. 39.骰子点数分布长啥样?用直方图一眼看穿!
  40. 40.用科学计算库让骰子程序“起飞”
  41. 41.空气质量指数怎么算?手把手教你写一个AQI计算器
  42. 42.空气质量指数计算:从零开始,让程序看懂空气
  43. 43.如何读取空气质量数据文件并找出空气质量最好的五个监测点?
  44. 44.JSON 数据怎么变成 Excel 表格?—— 让数据换个“马甲”
  45. 45.Python 怎么让程序自己判断文件类型?
  46. 46.如何让程序自己上网查空气质量?
  47. 47.用美味的汤来解析网页:Beautiful Soup 入门
  48. 48.如何让爬虫自动抓取全国所有城市的空气质量?
  49. 49.爬到的数据怎么存下来?—— 把全国空气质量数据保存为CSV文件
  50. 50.用 pandas 玩转空气质量数据:找出全国空气最好和最差的城市
  51. 51.空气质量数据怎么“洗”干净,还能画成图?

函数自己调用自己,会发生什么?

约 13 分钟

小象实战讲义 · Python 入门:8 个小项目边玩边学

还记得我们上节课画的“五角星家族”吗?我们用循环让程序一口气画了6个大小递增的五角星。代码虽然能跑,但主程序里又是循环又是更新参数,看起来有点“热闹”。这节课,我们要玩一个更酷的魔法:让函数自己调用自己,把那些“热闹”的操作都封装起来。学完这一节,你的主程序会变得超级简洁,而画图的“魔法”都藏在函数里,就像你只需要按下一个“开始”按钮,程序就会自己完成所有工作!

💡 核心导读

  • 新魔法:递归函数:认识一种“自己调用自己”的神奇函数。
  • 封装的艺术:把复杂的循环和参数更新逻辑,都塞进一个函数里,让主程序变得清爽。
  • 关键一步:终止条件:没有它,递归函数会像失控的陀螺一样转个不停,我们必须学会如何安全地“刹车”。
  • 成果预览:我们将得到一个主程序只有一行核心代码的“五角星家族”绘制程序。

回顾与思考:上节课的“热闹”主程序

我们先快速看一眼上节课(2.0版本)画五角星家族的核心代码片段:

size = 50
while size <= 100:
    draw_pentagram(size)  # 调用画一个五角星的函数
    size = size + 10      # 更新大小参数

这段代码在主程序里做了两件事:1. 循环调用函数;2. 更新size参数。想象一下,如果画图的逻辑更复杂(比如要画分形树!),主程序就会变得很长、很难维护。

这节课,我们只做一件事:把“循环调用”和“参数更新”这两件事,从主程序搬到函数内部去。这样,主程序只需要“启动”一次函数,剩下的就交给函数自己“递归”完成。

揭秘递归:函数里的“套娃”魔法

什么是递归?简单说,就是一个函数在执行过程中,调用了它自己。

这听起来有点奇怪,自己怎么调用自己?我们用一个生活化的比喻来理解:

想象你要打扫一个有很多房间的大房子。你的“打扫函数”是这样工作的:进入一个房间,先打扫这个房间,然后发现这个房间里面还有一个小房间,于是你再次调用「打扫函数」,去打扫那个小房间。小房间打扫完了,回到大房间,继续完成大房间的打扫。这个过程就是“递归”。

对应到我们的代码:

  • “进入房间打扫” 对应 “画一个固定大小的五角星”。
  • “发现小房间” 对应 “画完当前五角星后,把尺寸变大一点”。
  • “再次调用打扫函数” 对应 “函数自己调用自己,但传入新的、更大的尺寸参数”。

这样,对于主程序来说,它只需要说一句:“去把50尺寸开始的五角星家族画出来”。至于怎么画下一个、画到什么时候停,都是函数内部的事情。这就是封装,也是递归的魅力。

动手实战:打造递归版五角星绘制函数

让我们开始改造代码。我们会创建一个新的函数 draw_recursive_pentagram。

第一步:搭建函数框架 我们先写出函数的基本样子,它接收一个size参数。

def draw_recursive_pentagram(size):
    """
    递归绘制五角星
    """
    # 我们先在这里写绘制一个五角星的逻辑
    pass  # pass是占位符,表示"这里先空着"

第二步:填入绘制单个五角星的逻辑 这部分和之前完全一样,我们直接搬过来。

def draw_recursive_pentagram(size):
    """
    递归绘制五角星
    """
    # 绘制一个五角星
    count = 1
    while count <= 5:
        turtle.forward(size)
        turtle.right(144)
        count += 1

第三步:实现“递归”的核心——自己调用自己 画完当前这个五角星后,我们不是结束,而是要准备画下一个更大的。

  1. 更新参数:让size增加10,为画下一个做准备。
  2. 判断是否继续:如果新的size还没有超过100,我们就再次调用自己(draw_recursive_pentagram),并把新的size传进去。
def draw_recursive_pentagram(size):
    """
    递归绘制五角星
    """
    # 绘制一个五角星
    count = 1
    while count <= 5:
        turtle.forward(size)
        turtle.right(144)
        count += 1

    # 递归部分:准备画下一个
    # 1. 更新参数
    size += 10
    # 2. 判断是否达到终止条件,如果没达到,就自己调用自己
    if size <= 100:
        draw_recursive_pentagram(size)

看!if size <= 100: 这一行就是至关重要的“终止条件”。它保证了当size增加到110时,函数不会再调用自己,递归过程就此停止。如果没有这个条件,函数就会无限地自己调用自己,直到程序崩溃。

第四步:简化主程序 现在,我们惊艳的主程序来了!它只需要初始化画笔,然后调用一次我们的递归函数即可。

import turtle

def draw_recursive_pentagram(size):
    """
    递归绘制五角星
    """
    count = 1
    while count <= 5:
        turtle.forward(size)
        turtle.right(144)
        count += 1

    size += 10
    if size <= 100:
        draw_recursive_pentagram(size)

# 主程序
turtle.penup()
turtle.backward(200)
turtle.pendown()
turtle.pensize(2)
turtle.pencolor('red')

# 魔法启动!只需要调用一次
draw_recursive_pentagram(50)

# 注意:实际运行时,需要点击图形窗口才能关闭。
# 为了你能直接运行看效果,这里省略了 turtle.exitonclick()。
# 完整可运行代码请参考本章的课程资源附件。

运行一下,是不是和上节课的效果一模一样?但对比一下主程序,是不是干净利落多了?所有“如何画出一个家族”的细节,都被完美地隐藏在了 draw_recursive_pentagram 这个函数里。

递归是如何一步步运行的?(选读)

如果你对“自己调用自己”的过程感到好奇,可以跟着下面的步骤在脑子里“运行”一遍程序:

  1. 第一次调用:draw_recursive_pentagram(50)。

    • 函数开始执行,用size=50画了一个五角星。
    • 然后size变成60,判断60 <= 100为真。
    • 于是,在第一个函数还没执行完的时候,它发起了第二次调用:draw_recursive_pentagram(60)。
  2. 第二次调用:draw_recursive_pentagram(60)。

    • 这是一个全新的函数执行过程,它有自己的size参数(值为60)。
    • 它用60画一个五角星。
    • 然后size变成70,判断70 <= 100为真。
    • 发起第三次调用:draw_recursive_pentagram(70)。
  3. 依次类推… 直到 draw_recursive_pentagram(100) 被调用。

    • 它用100画一个五角星。
    • size变成110,判断110 <= 100为假。
    • 这次,它没有再调用自己。函数执行完毕,返回。
  4. 逐层返回:当draw_recursive_pentagram(100)执行完后,会返回到调用它的地方,也就是draw_recursive_pentagram(90)函数中if语句的下一行(其实后面没代码了)。draw_recursive_pentagram(90)也执行完毕,返回到draw_recursive_pentagram(80)……最终,所有的函数调用依次结束,返回到最初的主程序。

这个过程就像钻进一个 Russian Doll(俄罗斯套娃),从最大的娃钻到最小的娃,然后再一层层钻出来。

📝 动手练一练

  1. 参数实验:如果把主程序中调用的 draw_recursive_pentagram(50) 改成 draw_recursive_pentagram(80),最终会画出几个五角星?分别是多大的?
  2. 终止条件改造:如果我们想画10个五角星就停止,而不是画到size大于100停止,应该怎么修改递归函数里的终止条件?(提示:可以引入一个额外的参数来计数)
  3. “失控”的递归:作为实验(请勿长时间运行),你可以尝试把递归函数中的终止条件 if size <= 100: 暂时注释掉,然后只调用 draw_recursive_pentagram(1)。猜猜会发生什么?这说明了什么?
👉 点击查看第1题参考答案

调用 draw_recursive_pentagram(80) 后,程序会依次绘制 size 为 80, 90, 100 的五角星。当 size 更新为 110 时,不满足 <=100 的条件,递归停止。所以总共会画出3个五角星,大小分别为80, 90, 100。

👉 点击查看第2题参考答案

思路是增加一个参数 count 来记录已经画了多少个五角星。每次调用自己时,count 加1,当 count 达到10时停止。

关键代码修改如下(注意,这是一个独立的函数片段,展示了如何修改):

def draw_recursive_pentagram_v2(size, current_count):
    """
    递归绘制五角星,限制绘制数量
    """
    # 绘制一个五角星
    count = 1
    while count <= 5:
        turtle.forward(size)
        turtle.right(144)
        count += 1

    # 更新参数:大小增加,计数增加
    size += 10
    current_count += 1

    # 终止条件:画满10个就停
    if current_count < 10:
        draw_recursive_pentagram_v2(size, current_count)

# 调用时传入初始计数0
# draw_recursive_pentagram_v2(50, 0)
👉 点击查看第3题参考答案

如果注释掉终止条件,函数在画完一个五角星后,会无限制地调用自己(size 会不断增加,但判断条件永远为真)。这会导致程序不断消耗内存创建新的函数调用,最终通常会因超出最大递归深度而崩溃(Python会报 RecursionError)。

这说明:在设计递归函数时,定义一个清晰且一定能被触发的终止条件是绝对必要的,否则就是“死递归”。

本章小结

恭喜你!到本节为止,我们已经为绘制“分形树”这个终极目标,攒齐了所有关键的工具包:

  • 1.0:学会了用 turtle 库画基本图形(五角星)。
  • 2.0:学会了用循环来重复操作,画出系列图形。
  • 3.0:学会了用递归来优雅地封装复杂的重复逻辑。

递归是一个强大的思想,它能让代码变得非常简洁,尤其适合处理“自相似”的结构——比如我们下节课要画的树,树干分叉成树枝,树枝又分叉成更小的树枝,这正是递归大显身手的地方!

📋 行动清单

  • 运行本节最终的递归版代码,感受主程序“一键启动”的简洁。
  • 尝试修改终止条件(如练习2),体验控制递归过程的感觉。
  • 在脑海里模拟一遍 draw_recursive_pentagram(50) 的完整执行流程,理解“套娃”式的调用与返回。

准备好迎接最酷的分形树了吗?我们下节课,树上见!

—— 小象教研组

配套资料 · 直接下载

课件、代码、数据集等配套资料全部免费,点击即可直接下载,无需分享或解锁。

  • 第3章代码包:分形树的绘制(含全部版本 4 个 .py)
    ZIP · 2KB
    下载
看图文没弄懂?展开看本节视频讲解(约 13 分钟)
📺 正在播放小象学院官方高清录播(支持倍速与清晰度调节)