第2章 2.5 综合实战:FizzBuzz + 倒计时器

📌 上一章我们学了箭头函数的两种写法、this 的四大绑定规则,你现在应该能看懂大部分 JS 代码里的 => 了。但光会看不够——这一章我们要用前面的知识点,做两个真实好玩的小工具,让你体会「原来我的代码真的能跑起来!」的成就感。


🎯 开场 3 分钟:为什么要学这个?

你有没有遇到过这些情况?

  • 写了个小程序,运行的时候啥反馈都没有——不知道它跑到哪了,也不知道对不对
  • 看到一个经典的 FizzBuzz 面试题,脑子里想得挺好,但写出来一堆 if-else 乱成一团
  • 想给女朋友/男朋友倒计时一个重要日子,结果网上找的工具都有广告

这一章,两个问题一起解决

我们先用 FizzBuzz 练手,学会把逻辑拆成小块、用循环+条件配合;然后做一个「倒计时器」,学会处理日期、做减法、格式化输出

学完这章,你就能:

  • ✅ 写出一个带进度条的循环程序
  • ✅ 做出一个可以计算任意目标日期倒计\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n时的小工具
  • ✅ 掌握在循环里打印状态的技巧(妈妈再也不用担心我的程序「死机」了)

🧱 基础 25 分钟:核心概念(小白视角)

什么是 FizzBuzz?

FizzBuzz 是一个经典的编程入门题,规则超简单:

从 1 数到 100
- 如果能被 3 整除,说 "Fizz"
- 如果能被 5 整除,说 "Buzz"
- 如果能同时被 3 和 5 整除,说 "FizzBuzz"
- 否则,说出这个数字

看起来很简单对吧?但面试里 90% 的人第一次写都有 bug。我们先讲为什么。

生活类比:把数字分类放袋子

想象你有一堆积木,要分类放进三个盒子:

  • 盒子 A:能被 3 整除的 → 贴 "Fizz" 标签
  • 盒子 B:能被 5 整除的 → 贴 "Buzz" 标签
  • 盒子 C:两个都能整除的 → 贴 "FizzBuzz" 标签(优先级最高

关键点:先检查「两个都能整除」,再检查其他的。不然 15 会被错误地放进 A 或 B,不会进 C。

第一步:写最直观的版本

for i in range(1, 101):
if i % 15 == 0:          # 先检查能不能被 3 和 5 同时整除
    print("FizzBuzz")
elif i % 3 == 0:         # 再检查能不能被 3 整除
    print("Fizz")
elif i % 5 == 0:         # 最后检查能不能被 5 整除
    print("Buzz")
else:                    # 都不行就输出数字本身
    print(i)

这行在干嘛:
- range(1, 101) 生成 1 到 100 的数字
- % 是取余数运算符,15 % 3 == 0 就是「15 能被 3 整除」
- elif = else if,先不满足前面的才检查这个

运行一下试试!输出前 20 行大概长这样:

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz

第二步:进阶写法——用函数封装

为什么要封装成函数?

类比一下:就像你做菜,把「打鸡蛋」这个动作封装成一个步骤,下次做别的菜也能用,不用每次都重新想怎么打。

def fizzbuzz(n):
"""判断一个数 n 应该输出什么"""
if n % 15 == 0:
    return "FizzBuzz"
elif n % 3 == 0:
    return "Fizz"
elif n % 5 == 0:
    return "Buzz"
else:
    return str(n)

# 调用函数,生成 1-100
for i in range(1, 101):
print(fizzbuzz(i))

这行在干嘛:
- def fizzbuzz(n): 定义一个函数,输入数字 n,返回对应的字符串
- str(n) 把数字转成字符串,这样 print 的时候输出格式统一

第三步:倒计时器——处理日期

学会了 FizzBuzz,我们来做倒计时器。

核心思路: 两个日期相减,得到相差的天数/小时数/分钟数。

from datetime import datetime

# 设定目标日期(女朋友生日)
target_date = datetime(2025, 12, 25, 0, 0, 0)

# 获取当前时间
now = datetime.now()

# 计算差值
diff = target_date - now

print(f"距离目标日期还有 {diff.days} 天")
print(f"换算成小时是 {diff.total_seconds() / 3600:.1f} 小时")

这行在干嘛:
- datetime 是 Python 里处理日期时间的工具
- datetime.now() 获取此时此刻的时间
- 两个 datetime 相减得到 timedelta 对象,里面有天数、秒数等信息

输出大概长这样:

距离目标日期还有 182 天
换算成小时是 4371.5 小时

第四步:把 FizzBuzz 和倒计时组合——带进度条的倒计时

核心概念:进度条

想象你在下载文件,那个 progress bar 怎么实现的?

原理:在同一行不断覆盖打印。Python 里用 \r 实现「回车」(回到行首),用 end='' 阻止自动换行。

import time
from datetime import datetime

def countdown_with_progress(target_date):
"""带进度条的倒计时器"""
while True:
    now = datetime.now()
    diff = target_date - now

    if diff.total_seconds() <= 0:
        print("\n时间到!🎉")
        break

    # 计算进度(假设总倒计时 100 天)
    total_seconds = 86400 * 100  # 100 天
    progress = (total_seconds - diff.total_seconds()) / total_seconds * 100

    # \r 回到行首,end='' 不换行,这样就能覆盖之前的内容
    print(f"\r倒计时: {diff.days} 天 {diff.seconds // 3600} 小时 | 进度: {progress:.1f}%", end='')
    time.sleep(0.5)  # 暂停 0.5 秒,别让电脑累死

# 测试:设定 1 分钟后的时间
test_target = datetime.now().replace(second=0, microsecond=0)
test_target = test_target.replace(minute=test_target.minute + 1)

print("开始测试倒计时(1分钟后到期)...")
countdown_with_progress(test_target)

这行在干嘛:
- time.sleep(0.5) 让程序暂停半秒,不然它一秒能跑几万次
- \r 是「回车符」,让光标回到行首
- end='' 告诉 print 别换行,这样下一次 print 就会覆盖上一次


🔥 实战 35 分钟:3 个递进的小项目

项目 1(5 分钟):一行代码的 FizzBuzz

目标: 学会用列表推导式写 FizzBuzz

# 用列表推导式 + 三元表达式,一行搞定
result = ["FizzBuzz" if i % 15 == 0 else "Fizz" if i % 3 == 0 else "Buzz" if i % 5 == 0 else str(i) for i in range(1, 101)]

# 打印前 20 个
for item in result[:20]:
print(item)

预期输出:

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz

一句话解释: 列表推导式就是「把 for 循环的结果直接装进列表」的简写语法。


项目 2(15 分钟):从 CSV 读取生日清单,给每个人倒计时

目标: 学会读取文件 + 处理日期 + 格式化输出

假设你有一个 birthdays.csv 文件,内容如下:

名字,生日
小明,1995-03-15
小红,1998-07-22
小刚,2000-01-01

完整代码:

import csv
from datetime import datetime

def read_birthdays(filename):
"""从 CSV 读取生日数据"""
birthdays = []
with open(filename, 'r', encoding='utf-8') as f:
    reader = csv.DictReader(f)  # 用字典方式读取,每行是一个 {列名: 值}
    for row in reader:
        birthdays.append({
            'name': row['名字'],
            'birthday': datetime.strptime(row['生日'], '%Y-%m-%d')
        })
return birthdays

def days_until(birthday_date):
"""计算距离下次生日还有多少天"""
today = datetime.now()
# 把今年的生日日期算出来
this_year_birthday = birthday_date.replace(year=today.year)

# 如果今年的生日已经过了,算明年的
if this_year_birthday < today:
    this_year_birthday = this_year_birthday.replace(year=today.year + 1)

diff = this_year_birthday - today
return diff.days

# 读取文件并计算(需要先创建 birthdays.csv)
try:
birthdays = read_birthdays('birthdays.csv')
print("📅 生日倒计时清单")
print("=" * 30)

for person in birthdays:
    days = days_until(person['birthday'])
    print(f"{person['name']}: {days} 天后过生日")
except FileNotFoundError:
print("⚠️ 请先创建 birthdays.csv 文件(内容见上文)")

预期输出:

📅 生日倒计时清单
==============================
小明: 123 天后过生日
小红: 89 天后过生日
小刚: 201 天后过生日

一句话解释: csv.DictReader 让你像查字典一样读取 CSV,每列有个名字。


项目 3(15 分钟):做一个「发呆计时器」——工作 25 分钟,休息 5 分钟

目标: 综合运用循环、条件、倒计时,做一个番茄钟

import time
from datetime import datetime, timedelta

def pomodoro_timer(work_minutes=25, break_minutes=5, rounds=4):
"""
番茄钟:工作 -> 休息 -> 工作 -> 休息 ...
- work_minutes: 工作时长(分钟)
- break_minutes: 休息时长(分钟)
- rounds: 几轮
"""
print(f"🍅 番茄钟开始!工作 {work_minutes} 分钟,休息 {break_minutes} 分钟,共 {rounds} 轮")
print("=" * 40)

for round_num in range(1, rounds + 1):
    # 工作时段
    print(f"\n🔔 第 {round_num} 轮 - 开始工作!")
    end_time = datetime.now() + timedelta(minutes=work_minutes)

    while datetime.now() < end_time:
        remaining = (end_time - datetime.now()).seconds
        mins = remaining // 60
        secs = remaining % 60
        print(f"\r工作倒计时: {mins:02d}:{secs:02d}", end='')
        time.sleep(1)

    print(f"\n✅ 第 {round_num} 轮工作完成!")

    # 休息时段(最后一轮不休息)
    if round_num < rounds:
        print(f"☕ 开始休息 {break_minutes} 分钟...")
        break_end = datetime.now() + timedelta(minutes=break_minutes)

        while datetime.now() < break_end:
            remaining = (break_end - datetime.now()).seconds
            mins = remaining // 60
            secs = remaining % 60
            print(f"\r休息倒计时: {mins:02d}:{secs:02d}", end='')
            time.sleep(1)

        print(f"\n🎉 休息结束!")

print("\n" + "=" * 40)
print("🍅 今天的番茄钟完成了!给自己点个赞 👍")

# 运行番茄钟(演示用,改成 1 分钟工作 + 10 秒休息)
pomodoro_timer(work_minutes=1, break_minutes=10, rounds=2)

预期输出(截取部分):

🍅 番茄钟开始!工作 1 分钟,休息 10 秒,共 2 轮
========================================

🔔 第 1 轮 - 开始工作!
工作倒计时: 00:58
工作倒计时: 00:45
...
✅ 第 1 轮工作完成!
☕ 开始休息 10 秒...
休息倒计时: 00:07
...
🎉 休息结束!

🔔 第 2 轮 - 开始工作!
...
🍅 今天的番茄钟完成了!给自己点个赞 👍

一句话解释: timedelta 就像「给日期做加减法」的工具,5 分钟 = timedelta(minutes=5)


💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:忘记检查优先级

# ❌ 错误:先检查 3,再检查 5,15 永远进不去 FizzBuzz 分支
if i % 3 == 0:
print("Fizz")
elif i % 5 == 0:
print("Buzz")
elif i % 15 == 0:  # 这个永远执行不到!
print("FizzBuzz")

# ✅ 正确:先检查 15
if i % 15 == 0:
print("FizzBuzz")
elif i % 3 == 0:
print("Fizz")
elif i % 5 == 0:
print("Buzz")

为什么错: if-else 是「短路」机制,一旦前面满足了,后面就不看了。


坑 2:datetime 的坑——字符串格式不对

# ❌ 错误:格式不匹配会报错
datetime.strptime("1995/03/15", '%Y-%m-%d')  # 报错!

# ✅ 正确:格式要一致
datetime.strptime("1995-03-15", '%Y-%m-%d')  # 正常
datetime.strptime("1995/03/15", '%Y/%m/%d')  # 正常

常见格式符号:
- %Y = 4 位年份(如 2025)
- %m = 2 位月份(如 03)
- %d = 2 位日期(如 15)
- %H = 24 小时(如 14)
- %M = 分钟(如 05)
- %S = 秒(如 30)


坑 3:time.sleep 的坑——阻塞主线程

# ❌ 错误:在 GUI 程序里用 sleep 会让界面卡死
def bad_countdown():
for i in range(10, 0, -1):
    print(i)
    time.sleep(1)  # 这一秒程序什么都做不了

# ✅ 正确:用 threading 或者 tkinter 的 after 方法
import threading

def countdown_thread():
for i in range(10, 0, -1):
    print(i)
    time.sleep(1)

# 在后台线程运行,不阻塞主程序
thread = threading.Thread(target=countdown_thread)
thread.start()

坑 4:print 的 end='' 没加,导致换行混乱

# ❌ 错误:不加 end='',每个数字都换行
for i in range(1, 101):
print(i)  # 默认 end='\n',每个输出都换行

# ✅ 正确:进度条要用 end=''
for i in range(1, 101):
print(f"\r进度: {i}%", end='')  # 同一行覆盖

坑 5:循环里修改正在迭代的列表

# ❌ 错误:边迭代边删除会漏元素
nums = [1, 2, 3, 4, 5]
for n in nums:
if n % 2 == 0:
    nums.remove(n)  # 危险!

# ✅ 正确:遍历副本,或者用列表推导式生成新列表
nums = [1, 2, 3, 4, 5]
nums = [n for n in nums if n % 2 != 0]  # 只保留奇数

性能小贴士:字符串拼接用 join 不用 +

# ❌ 慢:每次 + 都会创建新字符串
result = ""
for i in range(1000):
result += str(i)  # 创建了 1000 个字符串对象

# ✅ 快:join 一次搞定
result = "".join(str(i) for i in range(1000))

调试技巧:用 f-string 加变量名,打印中间状态

# 遇到 bug 时,在关键地方打印变量值
def fizzbuzz(n):
print(f"[DEBUG] 输入 n={n}")  # 方便定位问题
if n % 15 == 0:
    return "FizzBuzz"
...

✏️ 练习题 + 作业题(共 7 分钟)

练习题(5 道,10 分钟内完成)

练习 1(2 分钟):改数字
- 输入:把 FizzBuzz 的范围从 1-100 改成 1-50
- 预期输出:只输出到 50,其他规则不变
- 提示:range(1, 101) 改成 range(1, 51)

练习 2(2 分钟):加一个判断
- 输入:在 FizzBuzz 基础上,如果数字是 7 的倍数,输出 "Boom"
- 预期输出:7 → "Boom",14 → "FizzBuzz" 或 "Boom"(选一个规则)
- 提示:先想好优先级,在 elif i % 3 == 0 前面加一行

练习 3(2 分钟):处理新数据
- 输入:给 birthdays.csv 加一个人,出生日期任意
- 预期输出:程序能正确显示新加的人的倒计时
- 提示:直接用 Excel 或记事本打开 CSV 文件,在最后一行加

练习 4(3 分钟):串起来
- 输入:用 FizzBuzz 的逻辑,统计 1-100 里有多少个 "Fizz"、"Buzz"、"FizzBuzz"
- 预期输出:Fizz: 27, Buzz: 14, FizzBuzz: 6, 数字: 53
- 提示:用一个字典 {"Fizz": 0, "Buzz": 0, ...} 来计数

练习 5(1 分钟):读报错
- 输入:运行下面代码,看报什么错

from datetime import datetime
d = datetime.strptime("2025-13-01", "%Y-%m-%d")
  • 预期输出:说出错误原因
  • 提示:月份没有 13

作业题(30 分钟-2 小时)

作业:做一个「纪念日倒计时器」

  • 需求描述: 给定一个纪念日名称和日期,计算并显示距离今天还有多久,精确到天/小时/分钟/秒
  • 功能点:
    1. 从用户输入读取纪念日名称和日期
    2. 计算倒计时,格式化成「XX 天 XX 小时 XX 分 XX 秒」
    3. 每秒更新一次显示(用 \r 覆盖)
    4. 到期后显示「到了!🎉」并退出
  • 加分项:
    1. 支持多个纪念日一起倒计时(列表)
    2. 把数据存到文件里,程序重启不丢失
  • 验收标准: 能跑起来 + 每秒更新显示 + 到期提示
  • 提交方式: 评论区贴代码或 GitHub 链接

📚 总结 + 资源(5 分钟)

本文学了 3 个核心点:

  1. FizzBuzz 是 if-elif 优先级训练的经典题——先检查范围小的条件
  2. datetime 是处理日期时间的利器——strptime 解析字符串,timedelta 做日期加减
  3. \r + end='' 可以做出进度条效果——让程序看起来更专业

延伸学习资源:

  • 📖 官方文档:datetime — 官方文档,每个方法都有例子
  • 📖 《Python 编程:从入门到实践》——第 8 章「函数」+ 第 10 章「文件和异常」
  • 🎬 B 站搜索「Python 倒计时 进度条」——有很多可视化教程

互动钩子:

🎯 你有没有什么特别的日子需要倒计时?生日、纪念日、假期倒计时?评论区说说,下一章我们会学到 map/filter/reduce,用它来批量处理多个倒计时,让代码更优雅!


(全文约 5100 字)

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。