第 3 章 · 控制流与函数

Python 的控制流和 C/Java 同构(if/for/while),但有三处「身份差异」:缩进即语法、for 是遍历不是计数、函数参数系统复杂到需要专节。本章把这三处讲透,顺带拿下 Python 最有辨识度的语法:推导式(comprehension)。

3.1 缩进即语法:洁癖还是 genius?

C/Java 用花括号划块,Python 用缩进(4 空格,PEP 8 标准):

score = 85
if score >= 90:
    print("优秀")
elif score >= 60:
    print("及格")          # 缩进层级 = 代码块
else:
    print("不及格")

for i in range(5):          # range(5) = 0,1,2,3,4(含头不含尾,和切片一致)
    print(i)

冒号开启块,缩进定义块,混用 Tab 和空格直接报错(TabError)。反对者说「强制缩进是多管闲事」,支持者说「反正大家都得缩进,不如让机器管」;工程事实是:Python 代码的全局可读性确实高于花括号语言(软件工程课第 7 章「代码质量」的先赚一步)。

for 循环的正确心智:for 是「对每个元素」(foreach),不是「数数」。要下标用 enumerate:

for i, ch in enumerate("abc"):      # (0,'a'), (1,'b'), (2,'c')
    print(i, ch)
for a, b in zip([1,2], ["x","y"]):  # 并行遍历:(1,'x'), (2,'y')
    print(a, b)

while 同 C;break/continue 同 C;Python 独有 for-else(循环正常跑完(没 break)则执行 else,搜索类代码「找到了 break,没找到才走 else」很顺手)。

3.2 推导式:一行顶五行

Python 最有辨识度的语法,把「循环 + 过滤 + 收集」压成一行:

nums = [1, 2, 3, 4, 5, 6]

squares = [n * n for n in nums]                    # [1,4,9,16,25,36]
evens = [n for n in nums if n % 2 == 0]            # [2,4,6](带过滤)
matrix = [[1,2],[3,4]]
flat = [x for row in matrix for x in row]          # [1,2,3,4](双层,先外后内)
sq_map = {n: n*n for n in nums}                    # 字典推导式 {1:1, 2:4, ...}
unique = {x % 3 for x in nums}                     # 集合推导式 {0, 1, 2}
lazy = (n * n for n in nums)                       # 生成器表达式:不占内存(第 8 章)

读法:[表达式 for 变量 in 可迭代 if 条件],「对每个满足条件的元素,算出表达式,收集成列表」。它不只是省行数:「目的(要什么)在前,过程(怎么遍历)在后」,可读性高于等价 for 循环——这是 Pythonic 的第一课。纪律:推导式超过两层嵌套就该拆回 for(可读性优先)。

3.3 函数:定义、文档与作用域

def bmi(weight, height):
    """计算 BMI。docstring:第一行写干什么。"""
    return weight / height ** 2

print(bmi(70, 1.75))        # 22.86
print(bmi.__doc__)          # 文档字符串可查
help(bmi)                   # REPL 里看帮助

函数用 def 定义,return 返回(可返回多个值!实为打包元组:return lo, hi,调用处 lo, hi = f() 解包——Java 课第 6 章「单返回值 + 出参」的 Python 答案:直接多返回)。docstring 是函数的说明书(第 7 章 help() 的数据源),养成习惯。作用域规则 LEGB(Local → Enclosing → Global → Built-in,由内向外找);函数内读全局变量可以,赋值需要 global 声明(否则 Python 当你新建了局部变量——常见坑)。

3.4 函数参数的全貌:Python 的复杂点

Python 的参数系统是它最「丰富」的部分,五件套:

def f(a, b=2, *args, **kwargs):     # 位置必选 | 默认值 | 可变位置 | 可变关键字
    ...

f(1)                    # a=1
f(1, 5)                 # a=1, b=5
f(1, 5, 6, 7)           # args=(6, 7) 收集成元组
f(1, x=9, y=8)          # kwargs={'x':9, 'y':8} 收集成字典
f(*[1,2], **{"z": 3})   # 反向:列表/字典拆开当参数
  • 默认参数:省略即用默认(Java 课第 6 章「没有默认参数靠重载」,Python 一步到位);
  • *args:任意个位置参数,函数内是元组(计组课第 11 章「变长参数」的 Python 版);
  • **kwargs:任意个关键字参数,函数内是字典(写包装器、传配置的利器);
  • 关键字实参:调用时 f(b=5, a=1) 指名传,可读性爆炸(「布尔参数地雷」f(True, False) 的解毒剂)。

可变默认参数大坑单独立牌:

def add(item, lst=[]):        # 危险!默认列表在 def 时创建一次
    lst.append(item)
    return lst
add(1)   # [1]
add(2)   # [1, 2]  ← 不是 [2]!两次调用共享同一个列表

def add_ok(item, lst=None):   # 正确姿势:None 哨兵
    if lst is None:
        lst = []
    lst.append(item)
    return lst

原因在第 2 章「名字贴」:默认值是 def 时求值一次的对象,函数对象一直贴着它。规则:默认值永远用不可变对象(数字、字符串、None)。

3.5 一等公民:函数也是值

函数可以赋值、传参、当返回值(第 2 章「皆对象」的兑现):

def shout(s): return s.upper()
def whisper(s): return s.lower()

def speak(style):                  # 高阶函数:函数当参数/返回值
    return {"loud": shout, "quiet": whisper}[style]

speak("loud")("hi")                # 'HI'

这是第 8 章(高阶函数、装饰器)和第 11 章 GUI(回调)的地基。Java 课第 11 章的 Lambda 在 Python 里就是普通函数(本来就是对象)。

常见错误

  • range 的边界。range(1, 5) 是 1~4;含头不含尾的全语言统一规则(切片同款)。
  • 可变默认参数(3.4 的立牌案件,面试高频)。
  • for 循环里改被遍历的列表。结构变化导致跳元素;要边遍历边删,遍历副本(for x in lst[:]:)或用列表推导式重建。
  • 缩进混用 Tab 与空格。本地能跑、别人机器报错;编辑器配好「Tab 转 4 空格」一劳永逸。

练习

  1. 动手:用推导式完成:1~100 中能被 3 整除的数的平方;把字符串列表 ["apple", "banana"] 变成 {"apple": 5, "banana": 6}(字典推导式,len)。
  2. 实现:def stats(nums) 一次性返回 (最小值, 最大值, 平均值),用元组多返回;对比 Java 版(需要出参或建类)的代码量。
  3. 动手:复现 3.4 的可变默认参数坑,打印 id(lst) 验证两次调用同一对象;用 None 哨兵修复。
  4. 思考:「for-else」在 C/Java 里没有。它解决的真实场景是什么(搜索循环的「未找到」分支)?设想一个滥用它的反例,讨论「语言特性增加了表达力也增加了误用面」的权衡。