4. 更多控制流工具

除了刚介绍的 while 语句,Python 还有一些其它控制流工具。
我们将在本章中遇到它们。

4.1. if 语句

最常见的是 if 语句:

x = 42
# Please enter an integer: 42
if x < 0:
    x = 0
    print('Negative changed to zero')
elif x == 0:
    print('Zero')
elif x == 1:
    print('Single')
else:
    print('More')

更多说明:
可有零个或多个 elif 部分,else 部分也是可选的。
关键字 elifelse if 的缩写,用于避免过多缩进。
if ... elif ... 序列可以作为其它语言中 switch/case 的替代。

如果把一个值与多个常量比较检查特定类型/属性match 语句更有用(详见 4.7)。


4.2. for 语句

Python 的 for 与 C 或 Pascal 不同:
它在序列(如列表、字符串)上按顺序迭代元素:

# 度量一些字符串:
words = ['cat', 'window', 'defenestrate']
for w in words:
    print(w, len(w))

修改正在迭代的集合通常不安全。
常见策略是迭代副本或创建新集合:

# 创建示例多项集
users = {'Hans': 'active', 'Éléonore': 'inactive', '景太郎': 'active'}

# 策略:迭代一个副本
for user, status in users.copy().items():
    if status == 'inactive':
        del users[user]
print(users)
# 创建示例多项集
users = {'Hans': 'active', 'Éléonore': 'inactive', '景太郎': 'active'}

# 策略:创建一个新多项集
active_users = {}
for user, status in users.items():
    if status == 'active':
        active_users[user] = status
print(active_users)

4.3. range() 函数

内置 range() 用于生成等差序列

for i in range(5):
    print(i)

range 不包含终止值;可指定起始、步长(可为负):

print(list(range(5, 10)))        # [5, 6, 7, 8, 9]
print(list(range(0, 10, 3)))     # [0, 3, 6, 9]
print(list(range(-10, -100, -30)))  # [-10, -40, -70]

按索引迭代可结合 range()len()

a = ['Mary', 'had', 'a', 'little', 'lamb']
for i in range(len(a)):
    print(i, a[i])

通常 enumerate() 更方便。
直接打印 range 会显示其表示形式:

print(range(10))

range() 返回的是惰性可迭代对象(不一次性生成完整列表),适合内存敏感场景,也可直接用于像 sum() 这样的函数:

print(sum(range(4)))  # 0 + 1 + 2 + 3 => 6

4.4. break 和 continue 语句

break 跳出最近一层 forwhile 循环:

for n in range(2, 10):
    for x in range(2, n):
        if n % x == 0:
            print(f"{n} equals {x} * {n//x}")
            break

continue 跳到下一次循环迭代:

for num in range(2, 10):
    if num % 2:
        print("找到一个奇数",num)
        continue
    print("找到一个偶数",num)

4.5. 循环的 else 子句

forwhile 循环中,break 可能对应一个 else 子句。
若循环正常结束(未执行 break),则执行 else
例如查找质数:

for n in range(2, 10):
    for x in range(2, n):
        if n % x == 0:
            print(n, '=', x, '*', n//x)
            break
    else:
        # 循环到底未找到一个因数
        print(n, '是一个质数')

注意:else 属于循环而不是 if
else 在循环被 breakreturn 或异常提前终止时不会执行。


4.6. pass 语句

pass 不执行任何动作,常用于占位:

while True:
    pass  # 无限等待键盘中断 (Ctrl+C)

用于定义最小类:

class MyEmptyClass:
    pass

也可作函数或条件体的占位符:

def initlog(*args):
    pass   # 记得实现这个!

有时也用省略符 ... 作为占位。


4.7. match 语句

match 将一个表达式与若干 case 模式比较,类似某些语言的 switch,但更像模式匹配(可提取组成部分)。
只有第一个匹配的 case 会执行;
若无匹配则不执行任何分支。

最简单形式是对字面值匹配:

def http_error(status):
    match status:
        case 400:
            return "Bad request"
        case 404:
            return "Not found"
        case 418:
            return "I'm a teapot"
        case _:
            return "Something's wrong with the internet"

_ 常用作通配符
可以用 | 组合字面值:

case 401 | 403 | 404:
    return "Not allowed"

支持解包式模式绑定变量:

# point 是一个 (x, y) 元组
match point:
    case (0, 0):
        print("Origin")
    case (0, y):
        print(f"Y={y}")
    case (x, 0):
        print(f"X={x}")
    case (x, y):
        print(f"X={x}, Y={y}")
    case _:
        raise ValueError("Not a point")

对于类可以按属性匹配:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

def where_is(point):
    match point:
        case Point(x=0, y=0):
            print("Origin")
        case Point(x=0, y=y):
            print(f"Y={y}")
        case Point(x=x, y=0):
            print(f"X={x}")
        case Point():
            print("Somewhere else")
        case _:
            print("Not a point")

若类定义 __match_args__(如 ('x', 'y')),则位置参数匹配可用,等价于多种写法。
模式可任意嵌套,例如匹配由 Point 组成的列表:

class Point:
    __match_args__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

match points:
    case []:
        print("No points")
    case [Point(0, 0)]:
        print("The origin")
    case [Point(x, y)]:
        print(f"Single point {x}, {y}")
    case [Point(0, y1), Point(0, y2)]:
        print(f"Two on the Y axis at {y1}, {y2}")
    case _:
        print("Something else")

可以为模式添加守卫子句 if,先捕获值再对守卫求值:

match point:
    case Point(x, y) if x == y:
        print(f"Y=X at {x}")
    case Point(x, y):
        print(f"Not on the diagonal")

其它要点摘要:

  • 元组/列表模式可匹配任意序列(不能匹配迭代器或字符串)。
  • 支持扩展解包,例如 [x, y, *rest]
  • 映射模式例如 {"bandwidth": b, "latency": l} 从字典捕获键值,额外键会被忽略。
  • 支持 as 捕获子模式:case (Point(x1, y1), Point(x2, y2) as p2): ...
  • 单例对象 True/False/None 按 id 比较。
  • 可使用具名常量(要用带点号名称以区分捕获变量):
from enum import Enum

class Color(Enum):
    RED = 'red'
    GREEN = 'green'
    BLUE = 'blue'

color = Color('blue')

match color:
    case Color.RED:
        print("I see red!")
    case Color.GREEN:
        print("Grass is green")
    case Color.BLUE:
        print("I'm feeling the blues :(")

详见 PEP 636


4.8. 定义函数

示例:打印小于 n 的斐波那契数列:

def fib(n):    # 打印小于 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)

说明:

  • def 定义函数,第一条语句为字符串时即为文档字符串(docstring)。
  • 函数有自己的局部符号表;
    查找变量时遵循局部 -> 外层局部 -> 全局 -> 内置的顺序。
  • 函数名是指向函数对象的引用,可以赋给其它名称:
def fib(n):    # 打印小于 n 的斐波那契数列
    """Print a Fibonacci series less than n."""
    a, b = 0, 1
    while a < n:
        print(a, end=' ')
        a, b = b, a+b
    print()
    
f = fib
f(100)

即使没有 return,函数也返回 None

若希望返回结果列表:

def fib2(n):  # 返回斐波那契数组直到 n
    """Return a list containing the Fibonacci series up to n."""
    result = []
    a, b = 0, 1
    while a < n:
        result.append(a)
        a, b = b, a+b
    return result

f100 = fib2(100)
print(f100)
# f100 -> [0, 1, 1, 2, 3, ... , 89]

4.9. 函数定义详解

函数支持可变数量参数、默认值、关键字参数、仅限位置/仅限关键字等特性。

4.9.1. 默认值参数

def ask_ok(prompt, retries=4, reminder='Please try again!'):
    while True:
        reply = input(prompt)
        if reply in {'y', 'ye', 'yes'}:
            return True
        if reply in {'n', 'no', 'nop', 'nope'}:
            return False
        retries = retries - 1
        if retries < 0:
            raise ValueError('invalid user response')
        print(reminder)
        
ask_ok('Do you really want to quit?')
ask_ok('OK to overwrite the file?', 2)
ask_ok('OK to overwrite the file?', 2, 'Come on, only yes or no!')

默认值在定义处求值(仅计算一次)。
对于可变默认值(如列表),可能导致跨调用共享:
避免方式为使用 None 作为默认并在函数体内创建新对象:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

4.9.2. 关键字参数

函数可以通过关键字参数调用:

def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'):
    print("-- This parrot wouldn't", action, end=' ')
    print("if you put", voltage, "volts through it.")
    print("-- Lovely plumage, the", type)
    print("-- It's", state, "!")

parrot(1000)
parrot(voltage=1000)
parrot(voltage=1000000, action='VOOOOOM')
parrot('a million', 'bereft of life', 'jump')
parrot('a thousand', state='pushing up the daisies')

注意:
关键字参数必须跟在位置参数之后,且不能重复为同一参数赋值。

使用 **name 接收额外关键字参数(会作为字典传入),可与 *args(接收额外位置参数,产生元组)组合:

def cheeseshop(kind, *arguments, **keywords):
    print("-- Do you have any", kind, "?")
    print("-- I'm sorry, we're all out of", kind)
    for arg in arguments:
        print(arg)
    print("-" * 40)
    for kw in keywords:
        print(kw, ":", keywords[kw])

cheeseshop("Limburger", "It's very runny, sir.",
           "It's really very, VERY runny, sir.",
           shopkeeper="Michael Palin",
           client="John Cleese",
           sketch="Cheese Shop Sketch")

4.9.3. 特殊参数:位置/关键字约束(/*

你可以在函数签名中用 / 标记仅限位置参数,用 * 标记仅限关键字参数:

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
    ...

说明:

  • pos1, pos2 仅限位置;
  • pos_or_kwd 可以位置或关键字;
  • kwd1, kwd2 仅限关键字。

示例:

def standard_arg(arg):
    print(arg)

def pos_only_arg(arg, /):
    print(arg)

def kwd_only_arg(*, arg):
    print(arg)

def combined_example(pos_only, /, standard, *, kwd_only):
    print(pos_only, standard, kwd_only)

有关与 **kwds 的交互及使用场景见正文说明。

4.9.4. 任意实参列表(可变位置参数)

使用 *args 收集多余的位置参数为元组:

def write_multiple_items(file, separator, *args):
    file.write(separator.join(args))

*args 之后的任何形参只能作为仅限关键字参数。

示例:

def concat(*args, sep="/"):
    return sep.join(args)

print(concat("earth", "mars", "venus"))            # 'earth/mars/venus'
print(concat("earth", "mars", "venus", sep="."))   # 'earth.mars.venus'

4.9.5. 解包实参列表

位置参数可以用 *序列解包,用 **字典解包:

print(list(range(3, 6)))            # [3, 4, 5]
args = [3, 6]
print(list(range(*args)))           # [3, 4, 5]
def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'):
    print("-- This parrot wouldn't", action, end=' ')
    print("if you put", voltage, "volts through it.")
    print("-- Lovely plumage, the", type)
    print("-- It's", state, "!")
d = {"voltage": "four million", "state": "bleedin' demised", "action": "VOOM"}
parrot(**d)

4.9.6. Lambda 表达式

lambda 创建匿名小函数(仅能是单表达式):

def make_incrementor(n):
    return lambda x: x + n

f = make_incrementor(42)
print(f(0))  # 42
print(f(1))  # 43

常用于作为回调或键函数,例如:

pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
pairs.sort(key=lambda pair: pair[1])
print(pairs)

4.9.7. 文档字符串

文档字符串约定:

  • 第一行为简短摘要(首字母大写,句点结尾),
    若多行,则第二行为空行以分隔摘要和其余描述。
  • 文档处理工具通常会去除多行字符串中的一致缩进(根据第一非空行的缩进来判断)。

示例:

def my_function():
    """什么都不做,只是记录下来。

    真的,它没有任何作用。
    """
    pass

print(my_function.__doc__)

4.9.8. 函数注解

函数注解为可选的元数据,存放于函数的 __annotations__ 字典,不影响函数执行:

def f(ham: str, eggs: str = 'eggs') -> str:
    print("Annotations:", f.__annotations__)
    print("Arguments:", ham, eggs)
    return ham + ' and ' + eggs

print(f('spam'))

4.10. 小插曲:编码风格

编写更长、更复杂代码时应注意风格。
Python 多数项目遵循 PEP 8
要点摘要:

  • 缩进用 4 个空格(不要用制表符)。
  • 行长度不超过 79 个字符。
  • 空行分隔函数、类及较大代码块。
  • 注释最好单独1行,使用文档字符串
  • 运算符前后、逗号后留空格,但不要在括号内额外空格:a = f(1, 2) + g(3, 4)
  • 命名:
    类用 UpperCamelCase
    函数/方法用 lowercase_with_underscores
    方法第一个参数通常命名为 self
  • 编写面向国际的代码时,优先使用 UTF-8ASCII
    尽量不要在标识符中使用非 ASCII 字符。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐