【Python入门】流程控制 + 函数:从猜数字游戏到闭包陷阱

学 Python,流程控制和函数是最基础也是最重要的。这篇文章用一个猜数字游戏带你入门,再深入闭包陷阱和装饰器——两个新手最容易踩坑的地方。

一、先看问题

还记得猜数字游戏吗?一个人心里想一个 1-100 的数字,另一个人来猜:

  • 猜小了 → “太小了!”
  • 猜大了 → “太大了!”
  • 猜对了 → “恭喜你!”

用 Python 怎么实现?只需要 if/elif/else + while 就行。

二、基础语法

1. if / elif / else

a = 1
b = 2

# 基础判断
if a < b:
    print('a小于b')
else:
    print('a大于等于b')

# 多条件判断用 elif
if a < b:
    print('a小于b')
elif a == b:
    print('a等于b')
else:
    print('a大于b')

# 简写:只有一行时可以写在一行
if a < b: print('a小于b')

# 三元表达式
print('a小于b') if a < b else print('a大于等于b')

# and:多个条件同时满足
if a < b and a > 0: print('a小于b大于0')

# or:满足其中一个即可
if a < b or a == 0: print('a小于b或a等于0')

# pass:占位,还没想好写什么时用
if a == 0: pass

⚠️ 新手常见坑:多个条件判断要用 elif,不要写多个 ifelif 匹配后不再判断后面的条件,多个 if 每个都会判断。

2. while / for + break / continue

a = 1
# while 循环:条件成立就继续执行
while a < 3:
    print(f'a = {a}')
    a += 1
else:
    print(f'循环结束,a = {a}')  # 正常完成后执行 else

# for 循环
for i in range(10, 0, -1):  # 从10到1,步长-1
    if i == 9:
        continue  # 跳过本次,继续下一次
    if i == 3:
        break     # 直接跳出循环
    print(f'i = {i}')
else:
    print('循环圆满完成')  # break 跳出时不执行,continue 会执行

💡 while/for 的 else:正常完成循环才执行 else,break 跳出则不执行。continue 不影响 else 的执行。

三、实战:猜数字游戏

第 1 版:基本实现

import random

num = random.randint(1, 100)
count = 0
while True:
    unum = int(input('请输入1到100之间的整数:').strip())
    if unum < num:
        print('太小了')
    elif unum > num:
        print('太大了')
    else:
        print(f'恭喜你在第 {count} 轮就猜对了')
        break
    count += 1

能跑,但有两个问题。

第 2 版:修复两个 bug

问题 1:第一次就猜对,显示"第 0 轮猜对了"——不符合人的直觉。

问题 2:输入字母直接崩溃——没有输入校验。

import random

num = random.randint(1, 100)
count = 1  # ← 修复1:从1开始计数
while True:
    try:    # ← 修复2:输入校验
        unum = int(input('请输入1到100之间的整数:').strip())
    except ValueError:
        print('请输入有效数字')
        continue

    if unum == num:
        print(f'恭喜你在第 {count} 轮就猜对了')
        break
    elif unum < num:
        print('太小了')
    else:
        print('太大了')
    count += 1

四、函数参数 5 种类型

类型 示例 特点
必填参数 (a, b, c) 按顺序赋值,少填一个就报错
默认参数 (a, b=1, c=2) 有默认值的可以不填
可变参数 (*args) 可以传 0 或多个参数
关键字参数 (**kv) 以键值对形式传参
命名关键字参数 (*, a, b=1) 必须指定变量名,否则 TypeError

核心原则:范围大的放后面。 组合顺序:必填 → 默认 → 可变 → 命名关键字 → 关键字。

# 示例
def func(a, b=2, *args, c, d=4, **kv):
    print(a, b, args, c, d, kv)

func(1, 3, 5, 7, c=9, e=11)
# 输出:1 3 (5, 7) 9 4 {'e': 11}

五、闭包陷阱:为什么 c1() c2() 都返回 4?

看这段代码:

def count1():
    l = []
    for i in range(1, 3):
        def s():
            return i + i
        l.append(s)
    return l

c1, c2 = count1()
print(c1(), c2())  # 预期 2, 4,实际输出:4 4

为什么? 函数 s 引用的是外层变量 i,不是 i 的值。当 count1 执行完毕,i 已经变成了 2。c1c2 调用时,取到的 i 都是 2。

修复:用默认参数绑定当前值

def count1():
    l = []
    for i in range(1, 3):
        def s(i=i):       # ← 关键:默认参数在定义时就绑定了当前 i 的值
            return i + i
        l.append(s)
    return l

c1, c2 = count1()
print(c1(), c2())  # 输出:2 4 ✅

只改了一个地方:def s():def s(i=i):,问题解决。

⚠️ 面试高频题:闭包循环变量陷阱是 Python 面试的经典题目,一定要记住。

六、装饰器:给函数加个"壳"

装饰器就是在不改变原函数代码的前提下,给函数加上额外功能。

import functools

def log(f):
    @functools.wraps(f)          # 保留原函数的 __name__ 等属性
    def w(*args, **kv):
        print(f'我要执行{f.__name__}了')
        result = f(*args, **kv)  # 把参数原样传给原函数
        print(f'我已经执行完成了')
        return result
    return w

@log   # 等价于 go = log(go)
def go(a):
    print(a)

go(1)
# 输出:
# 我要执行go了
# 1
# 我已经执行完成了

执行流程

  1. @log 相当于 go = log(go),此时 go 指向的是 log 返回的 w 函数
  2. 调用 go(1) 实际调用的是 w(1)
  3. w 先打印"我要执行了",再调用原函数 f(1),最后打印"执行完成"

💡 @functools.wraps(f) 不能省!不加的话 go.__name__ 会变成 w,可能导致依赖函数名的代码出错。

七、踩坑记录

坑 1:猜数字 count 从 0 开始

第一次就猜对显示"第 0 轮",不符合人的直觉。count 应该从 1 开始。

坑 2:闭包循环变量陷阱

返回的函数引用了外层循环变量,循环结束后变量已变。修复:用默认参数 def s(i=i): 绑定当前值。

坑 3:装饰器不要写死参数

装饰器应该用 f(*args, **kv) 通用调用,不要写死 f(a=1) ——会破坏装饰器的通用性。

八、总结

知识点 核心要点
if/elif/else 多条件用 elif,不要写多个 if
while/for + else 正常完成才执行 else,break 不执行
函数参数 5 种类型 范围大的放后面
闭包陷阱 返回函数引用外层变量,循环结束后变量已变
装饰器 给函数加"壳",用 @functools.wraps 保留原函数属性

📌 下一篇:文件读写 + 面向对象,用类管理你的数据文件。

更多推荐