4. 更多流程控制工具
对应官方文档:4. More Control Flow Tools 一句话导读:把「让程序自己做判断、重复干活、把逻辑打包成函数」一次讲透,是全套教程里最该动手敲的一章。
这一章小节最多,村长严格按官方 4.1 到 4.10 的编号走,4.9 下面再分三层。每节给一段能直接跑的短代码,看不懂先跑一遍再回头读。
4.1 if 语句
if 后跟条件,为真就执行缩进块;elif 是 else if 的缩写,可有零个或多个,else 可选且最多一个。你可以这样理解:它像一排闸门,从上往下第一个满足的就放行,后面看都不看。
x = 42
if x < 0:
print('Negative')
elif x == 0:
print('Zero')
else:
print('More')
4.2 for 语句
Python 的 for 不是 C 那种「计数器 + 步长 + 终止条件」,而是挨个取序列里的元素,列表、字符串、字典都行。官方特别提醒:一边遍历一边改同一个集合容易出诡异结果,稳妥做法是遍历 users.copy().items() 这类副本,或另建一个新集合。
words = ['cat', 'window', 'defenestrate']
for w in words:
print(w, len(w))
4.3 range() 函数
range() 生成等差数列,但含头不含尾:range(5) 是 0 到 4,终点永远不在里面。可以给定起点和步长(步长能为负),而且它并不真造出整张表,只是个可迭代对象,所以很省内存;想带索引遍历,官方更推荐 enumerate()。
print(list(range(5)), list(range(5, 10)), list(range(0, 10, 3)))
print(list(range(-10, -100, -30)))
print(list(enumerate(['a', 'b'])))
村长划重点:
range(5)里没有 5。含头不含尾是 Python 切片的统一规矩,字符串、列表、range 全都一样,记住一次能少调半天 bug。
4.4 break 与 continue 语句
break 跳出最近一层的 for 或 while,continue 则跳过本轮剩余代码、直接进入下一轮。注意是「最近一层」,嵌套循环里别指望它一次跳光。
for n in range(2, 8):
if n % 2 == 0:
continue
if n > 5:
break
print(n, 'is odd')
4.5 循环的 else 子句
这是 Python 最容易被误解的语法:for / while 后面也能跟 else,意思是循环正常跑完、没被 break 打断时才执行,不是「条件不成立」。官方特意强调 else 属于 for,不属于 if;return 或抛异常同样会跳过它。
for n in range(2, 10):
for x in range(2, n):
if n % x == 0:
print(n, 'equals', x, '*', n // x)
break
else:
print(n, 'is a prime number')
4.6 pass 语句
pass 什么都不做,纯粹占位:语法要求这里必须有语句,但你现在还不想写逻辑。常见于没实现的函数、最小的类或异常分支;官方还说很多人用省略号 ... 当约定式占位符。
def todo():
pass
print(todo())
4.7 match 语句(模式匹配)
match 把一个值依次和多个 case 模式比对,只有第一个匹配的分支会执行;它不只是比相等,还能把元组元素、对象属性拆出来绑到变量上。case _ 是万能兜底,没有匹配时所有分支都跳过,另外还支持 | 或模式、if 守卫、类模式与映射模式。
point = (0, 5)
match point:
case (0, y):
print(f'Y={y}')
case (x, y):
print(f'X={x}, Y={y}')
case _:
print('Not a point')
4.8 定义函数
def 定义函数,函数体靠缩进,开头第一个字符串字面量会成为文档字符串。函数没有 return 也会返回 None;参数传进来的其实是对象引用,所以传列表进去在函数里改,外面看得见。
def fib(n):
"""Print a Fibonacci series less than n."""
a, b = 0, 1
while a < n:
print(a, end=' ')
a, b = b, a + b
print()
fib(2000)
4.9 深入函数定义
这一节专讲「参数怎么传」,官方说有三种形式可以组合:默认参数值、关键字参数、任意参数列表,下面 4.9.1 到 4.9.8 逐个拆开。
def demo(*args, **kwargs):
print(args, kwargs)
demo(1, 2, k=3)
4.9.1 默认参数值
默认值让函数可以少传几个参数,但它在函数定义时只求一次值。所以拿列表、字典这种可变对象当默认值,多次调用会共用同一份数据——这是最经典的坑,正确姿势是用 None 占位。
def f(a, L=None):
L = [] if L is None else L
L.append(a)
return L
print(f(1), f(2), f(3))
村长划重点:
def f(a, L=[])会让三次调用攒出[1, 2, 3],因为那个空列表只创建了一次。默认值一律别用可变对象,改写成L=None再在函数里新建。
4.9.2 关键字参数
调用时写 名字=值,顺序就不重要了;但关键字参数必须排在位置参数之后,不能重复赋值,也不能传没声明的名字。**name 用来接住多出来的关键字参数,且必须排在 *name 之后。
def parrot(voltage, state='a stiff', action='voom'):
print(voltage, action, state)
parrot(1000)
parrot(voltage=1000, action='VOOOOOM')
4.9.3 特殊参数
用 / 和 * 两个标记把形参切成三段:/ 前的只能按位置传,* 后的只能按关键字传,中间两种都行。官方说这么划分既为可读性,也为性能。
def demo(pos1, /, pos_or_kwd, *, kwd1):
print(pos1, pos_or_kwd, kwd1)
demo(1, 2, kwd1=3)
4.9.3.1 位置或关键字参数
函数里既没写 / 也没写 * 时,参数既可按位置也可按关键字传,两种写法完全等价,是最常见的默认状态。
def standard_arg(arg):
print(arg)
standard_arg(2)
standard_arg(arg=2)
4.9.3.2 仅限位置参数
把 / 放在某几个参数后面,它们就只能按位置传,写 arg=1 会直接报 TypeError。这么干通常是因为参数名不算 API 的一部分,以后想改名也不怕破坏调用方。
def pos_only_arg(arg, /):
print(arg)
pos_only_arg(1)
try:
pos_only_arg(arg=1)
except TypeError:
print('TypeError')
4.9.3.3 仅限关键字参数
在第一个「只能按关键字传」的参数前放一个光秃秃的 *,后面就必须写成 名字=值,按位置塞会报错。适合那些名字本身有含义、写清楚更好读的参数。
def kwd_only_arg(*, arg):
print(arg)
try:
kwd_only_arg(3)
except TypeError:
print('TypeError')
kwd_only_arg(arg=3)
4.9.3.4 函数示例
把三种放一起看最清楚。下面演示合法调用:pos_only 绝不能用关键字给,kwd_only 绝不能按位置给,中间的 standard 随你挑。
def combined_example(pos_only, /, standard, *, kwd_only):
print(pos_only, standard, kwd_only)
combined_example(1, 2, kwd_only=3)
combined_example(1, standard=2, kwd_only=3)
4.9.3.5 小结
官方的结论是「用哪种,看场景」:参数名没意义、要强制顺序、或想让 **kwds 里同名不冲突,用仅限位置;名字有语义、想让调用方一眼看懂、不想被位置绑死,用仅限关键字。记住「/ 在前、* 在后」这条口诀就够了。
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
return pos1, pos2, pos_or_kwd, kwd1, kwd2
print(f(1, 2, 3, kwd1=4, kwd2=5))
4.9.4 任意参数列表
*args 把多出来的位置参数收成一个元组,通常放形参最后;它后面的形参会自动变成「仅限关键字」。配合 sep 这种关键字参数,就能写出参数个数不定的工具函数。
def concat(*args, sep='/'):
return sep.join(args)
print(concat('earth', 'mars'), concat('earth', 'mars', sep='.'))
4.9.5 解包参数列表
反过来,参数已经躺在列表 / 元组里,但函数要的是一个个独立参数,就在调用处加 * 拆开;字典则用 ** 拆成关键字参数。定义处的 *args 是「收」,调用处的 *seq 是「放」,别弄反。
args = [3, 6]
print(list(range(*args)))
def parrot(voltage, state='a stiff', action='voom'):
print(voltage, action, state)
d = {'voltage': 'four million', 'state': 'demised', 'action': 'VOOM'}
parrot(**d)
村长划重点:
*放在def里是打包成元组,*放在调用里是拆包;**同理,一边收成字典、一边把字典摊成关键字参数。位置对,意思就反了。
4.9.6 lambda 表达式
lambda 造一个没有名字的小函数,语法上只允许一个表达式,本质就是普通 def 的简写。最常见是当场传给 sort(key=...) 这类高阶函数,省得为两行逻辑单独起名。
pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
pairs.sort(key=lambda pair: pair[1])
print(pairs)
print((lambda x: x + 1)(41))
4.9.7 文档字符串
函数体第一行的字符串会存进 __doc__,既是给人看的说明,也是 help() 能读到的内容。官方建议第一行写简短摘要,空一行后再展开详细段落,而且摘要别用「这个函数……」这种废话开头。
def my_function():
"""Do nothing, but document it."""
pass
print(my_function.__doc__)
4.9.8 函数注解
注解是给参数和返回值贴的可选标签,写法是 参数: 类型 和 -> 类型,全部收在函数的 __annotations__ 字典里。它只是元数据,Python 不会替你做类型检查,别误以为写了 str 传 int 就会报错。
def f(ham: str, eggs: str = 'eggs') -> str:
return ham + ' and ' + eggs
print(f('spam'), f.__annotations__)
4.10 插曲:编码风格
官方把 PEP 8 称为「大多数项目都遵守的风格指南」:4 个空格缩进、别用 Tab;每行不超过 79 字符;函数与类之间空行隔开;注释尽量单独成行;类名用 UpperCamelCase、函数名用 lowercase_with_underscores、方法首参写 self;运算符两边和逗号后加空格,括号内侧不加,例如 a = f(1, 2) + g(3, 4)。
class Calculator:
def run(self, x, y):
return x + y
print(Calculator().run(1, 2))
本章小结
if/elif/else做分支,for直接遍历序列;range()含头不含尾,enumerate()更适合带索引遍历。break跳出最近一层循环,continue跳到下一轮;循环的else只在没被break时执行;pass是占位符。match/case按顺序只命中第一个模式,还能拆包绑定变量,case _兜底。- 函数参数花样最多:默认值只求值一次(别用可变对象)、
/与*划分传参方式、*args收、*seq放、lambda 写小函数、注解只是元数据。 - 写完顺手对齐 PEP 8:4 空格、79 字符、命名规范、留白与 docstring,代码是给人读的。