Python 的许多“高级语法”并不难,难的是知道它们各自解决什么问题、该在什么边界内使用。本文把日常代码中最常见的一组语法放在一起:推导式、f-string、海象运算符、with、装饰器和 match-case

它们的共同目标是让代码更贴近意图,而不是单纯压缩行数。本文示例以 Python 3.12 为基准;最低版本见下表。

语法最低版本核心用途
解包 */**Python 3.0 / 3.5 / 3.9拆分序列、合并映射
列表、字典推导式Python 2.0 / 2.7从可迭代对象构造新集合
f-stringPython 3.6格式化字符串
海象运算符 :=Python 3.8在表达式中绑定名称
上下文管理器 withPython 2.5成对管理资源的获取与释放
装饰器 @decoratorPython 2.4为函数或类附加横切行为
仅位置参数 /、仅关键字参数 *Python 3.0 / 3.8约束参数的传递方式
match-casePython 3.10结构化模式匹配
生成器与 yieldPython 2.2 / 3.3按需产出、惰性流式处理
for...else / while...elsePython 2.x循环未被 break 时的兜底

解包:拆分与收集多个值

解包把一个序列或映射「摊开」成多个目标,是 Python 里最高频的小语法。它既能把容器拆成几个变量,也能在构造新容器时把多个来源合并到一起。

交换与多目标赋值

1
2
3
a, b = 1, 2
a, b = b, a # 交换,不需要临时变量
name, age, active = "Alice", 30, True

右侧的元组按位置赋给左侧的名字;个数不匹配会直接报错,这能尽早暴露形状问题。

用星号收集剩余元素

星号 * 把剩余元素收成一个列表:

1
2
3
4
5
6
7
8
9
record = [2026, 7, 26, "log", "info", "timeout"]
year, month, day, *fields = record
# year=2026, month=7, day=26, fields=['log', 'info', 'timeout']

head, *tail = range(10)
# head=0, tail=[1, 2, ..., 9]

*init, last = range(10)
# init=[0, 1, ..., 8], last=9

星号只能在端点出现一次,收集结果永远是列表,即便源是元组或字符串。

在字面量里展开多个来源

列表、集合、元组字面量里用 * 把多个可迭代对象拼到一起,不必先 extend

1
2
3
4
5
6
a = [1, 2]
b = [3, 4]

[*a, *b] # [1, 2, 3, 4]
(*a, *b) # (1, 2, 3, 4)
{*a, *b} # {1, 2, 3, 4}

合并字典:**|

字典字面量里用 ** 展开,后面的键覆盖前面:

1
2
3
4
5
defaults = {"host": "localhost", "port": 5432, "timeout": 30}
override = {"port": 6543, "ssl": True}

config = {**defaults, **override}
# {'host': 'localhost', 'port': 6543, 'timeout': 30, 'ssl': True}

Python 3.9 起还可以用 | 合并,语义相同,写法更直接:

1
config = defaults | override

函数调用时展开

调用函数时,* 把序列展开成位置参数,** 把字典展开成关键字参数:

1
2
3
4
5
6
7
def create_user(name, age, role):
...

args = ("Alice", 30)
kwargs = {"role": "admin"}
create_user(*args, **kwargs)
# 等价于 create_user("Alice", 30, role="admin")

忽略不需要的部分

不关心的位置用 _ 接住,约定上表示「丢弃」:

1
2
_, code, message = response
first, *_ = items # 只要第一个

遍历时解包尤其常用,省掉下标访问:

1
2
3
4
for index, value in enumerate(items):
...
for key, value in mapping.items():
...

列表推导式与字典推导式

推导式适合表达“遍历、筛选、转换,最后收集结果”这一类纯数据处理逻辑。基本形式是:

1
2
3
4
5
# 列表推导式
[expression for item in iterable if condition]

# 字典推导式
{key_expression: value_expression for item in iterable if condition}

用列表推导式转换和过滤

下面两段代码结果完全相同。推导式省掉了临时列表和 append,同时更清楚地突出了“取偶数的平方”这个目标。

1
2
3
4
5
6
7
8
9
10
11
numbers = range(10)

# 普通循环
squares = []
for number in numbers:
if number % 2 == 0:
squares.append(number ** 2)

# 列表推导式
squares = [number ** 2 for number in numbers if number % 2 == 0]
# [0, 4, 16, 36, 64]

如果每个元素都要保留,只是转换方式不同,条件部分可以省略:

1
2
3
names = ["alice", "bob", "carol"]
display_names = [name.title() for name in names]
# ['Alice', 'Bob', 'Carol']

在表达式中需要“二选一”时,使用条件表达式,而不是把 else 写到 for 后面:

1
2
labels = ["even" if number % 2 == 0 else "odd" for number in range(5)]
# ['even', 'odd', 'even', 'odd', 'even']

嵌套循环

后面的 for 相当于内层循环,适合展平嵌套结构或做笛卡尔积:

1
2
3
4
5
6
matrix = [[1, 2, 3], [4, 5, 6]]
flat = [elem for row in matrix for elem in row]
# [1, 2, 3, 4, 5, 6]

pairs = [(x, y) for x in range(3) for y in range(2)]
# [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)]

字典推导式:构造索引或筛选映射

字典推导式尤其适合把一组对象按某个字段建立索引:

1
2
3
4
5
6
7
8
9
10
11
12
users = [
{"id": 1, "name": "Alice", "active": True},
{"id": 2, "name": "Bob", "active": False},
{"id": 3, "name": "Carol", "active": True},
]

active_names = {
user["id"]: user["name"]
for user in users
if user["active"]
}
# {1: 'Alice', 3: 'Carol'}

也可以反转一个值唯一的映射:

1
2
3
status_codes = {"ok": 200, "not_found": 404}
code_names = {code: name for name, code in status_codes.items()}
# {200: 'ok', 404: 'not_found'}

注意:若推导式生成了重复的键,后面的值会覆盖前面的值。这是字典赋值的正常规则,而不是推导式特有的行为。

可读性的边界

两层以内、没有副作用的推导式通常很清晰;嵌套循环、多个过滤条件和复杂业务判断混在一起时,普通循环反而更容易调试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 不推荐:需要在脑中还原三个层次的逻辑
result = [
transform(team, member)
for team in teams
for member in team.members
if member.enabled
if member.score >= threshold
]

# 更适合承载复杂逻辑的写法
result = []
for team in teams:
for member in team.members:
if not member.enabled or member.score < threshold:
continue
result.append(transform(team, member))

另外,Python 3 中推导式的循环变量拥有自己的作用域,不会泄漏到外部:

1
2
[item * 2 for item in range(3)]
print(item) # NameError: name 'item' is not defined

集合推导式

1
2
3
4
5
6
duplicates = [1, 1, 2, 2, 3, 3]
unique = {x for x in duplicates}
# {1, 2, 3}

chars = {c for c in 'hello world' if c.isalpha()}
# {'h', 'e', 'l', 'o', 'w', 'r', 'd'}

生成器推导式

把方括号换成圆括号,得到的是惰性求值的生成器,不是元组。它按需产出元素,内存占用恒定:

1
2
3
4
5
6
7
8
# 列表推导式:一次性生成所有元素,O(n) 内存
squares_list = [x ** 2 for x in range(10 ** 6)] # ~40MB

# 生成器推导式:惰性计算,O(1) 内存
squares_gen = (x ** 2 for x in range(10 ** 6))

# 适合处理大文件或无限序列
error_count = sum(1 for line in open('app.log') if 'ERROR' in line)

生成器只能遍历一次,耗尽后再次迭代得到空结果。需要反复使用就转成列表。

性能对比

推导式比等效的 for 循环快 10-20%,因为迭代在 C 层完成,避免了字节码层面的循环开销。但推导式的核心价值始终是可读性,而不是速度。

常见陷阱

1. 修改迭代变量不影响原序列

1
2
3
nums = [1, 2, 3]
[x + 1 for x in nums] # 返回 [2, 3, 4]
print(nums) # 原序列不变:[1, 2, 3]

2. 不存在“元组推导”

1
2
3
4
5
gen = (x for x in range(5))
type(gen) # <class 'generator'>,不是 tuple

# 要生成元组,用 tuple()
tup = tuple(x for x in range(5))

f-string:把格式化写在字符串旁边

f-string(formatted string literal)以 fF 开头,在花括号中直接放表达式:

1
2
3
4
5
name = "Ada"
score = 93.5

message = f"{name} 的得分是 {score:.1f}"
# 'Ada 的得分是 93.5'

它可以执行任意合法表达式,但建议只放简短、无副作用的表达式:

1
2
3
4
5
filename = "report.csv"
size = 12_345_678

print(f"文件 {filename!r} 的大小为 {size:,} 字节")
# 文件 'report.csv' 的大小为 12,345,678 字节

格式说明符的结构

冒号 : 后面是格式说明符(format specifier)。完整形式不必死记,常用部分是对齐、宽度、精度和类型:

1
{表达式:填充字符 对齐方式 符号 宽度 分组 .精度 类型}

其中对齐方式为 <(左)、>(右)、^(居中);数值常用类型为 d(十进制整数)、f(定点小数)、e(科学计数法)、%(百分比)、b(二进制)、o(八进制)和 x/X(十六进制)。字符串通常只需要对齐和宽度。

对齐、宽度与填充

宽度表示输出的最小字符数,默认用空格填充。把填充字符写在对齐符号前即可自定义填充效果:

1
2
3
4
5
6
7
8
name = "Ada"
order_id = 42

f"|{name:<8}|" # '|Ada |',左对齐
f"|{name:>8}|" # '| Ada|',右对齐
f"|{name:^8}|" # '| Ada |',居中
f"|{name:.^8}|" # '|..Ada...|',用 . 填充并居中
f"订单号:{order_id:06d}" # '订单号:000042'

这类格式尤其适合生成等宽的命令行表格:

1
2
3
4
5
6
7
products = [("keyboard", 199.0), ("mouse", 59.9)]

for product, price in products:
print(f"{product:<12} ¥{price:>8.2f}")

# keyboard ¥ 199.00
# mouse ¥ 59.90

数值格式化

数值可以添加千位分隔符、显式正负号、精度和进制前缀:

1
2
3
4
5
6
7
8
9
10
11
amount = 12_345.6789
ratio = 0.12345
number = 255

f"{amount:,.2f}" # '12,345.68':千位分隔,保留两位小数
f"{amount:_.2f}" # '12_345.68':使用下划线分组
f"{amount:+.1f}" # '+12345.7':正数也显示 +
f"{ratio:.2%}" # '12.35%':自动乘以 100 并追加 %
f"{number:b}" # '11111111':二进制
f"{number:#x}" # '0xff':带 0x 前缀的十六进制
f"{number:#06X}" # '0X00FF':前缀、零填充与大写十六进制

浮点数的 .nf 表示小数点后的位数;.ng 则表示有效数字位数,适合数值量级差异较大时的紧凑展示:

1
2
3
4
5
value = 1234.56789

f"{value:.3f}" # '1234.568'
f"{value:.4g}" # '1235'
f"{value:.3e}" # '1.235e+03'

日期、时间与动态宽度

日期时间对象可以在冒号后直接使用 strftime 格式;宽度和精度也可以由另一个表达式动态指定:

1
2
3
4
5
6
7
8
9
10
11
12
from datetime import datetime


created_at = datetime(2026, 7, 26, 9, 30)
name = "Ada"
width = 10
price = 1234.5678
precision = 3

f"时间:{created_at:%Y-%m-%d %H:%M}" # '时间:2026-07-26 09:30'
f"|{name:^{width}}|" # '| Ada |'
f"{price:.{precision}f}" # '1234.568'

动态格式适合输出列宽由配置决定、或允许调用方控制显示精度的场景;如果格式本身变得难读,应先构造 format_spec 变量再使用。

!s!r!a 转换

在冒号前可使用转换标记:!s 调用 str()!r 调用 repr()!a 调用 ascii()。最常用的是 !r,它能显示字符串两侧的引号和转义字符,便于排查空格、换行等问题。

1
2
3
4
5
value = "café\n"

f"{value!s}" # 'café\n'(实际输出含换行)
f"{value!r}" # "'café\\n'"
f"{value!a}" # "'caf\\xe9\\n'"

从 Python 3.8 起,调试时还可以使用 = 直接带上变量名:

1
2
3
4
5
user_id = 42
latency_ms = 18.7

print(f"{user_id=}, {latency_ms=:.1f}")
# user_id=42, latency_ms=18.7

花括号本身需要写两次:

1
2
template = f"{{\"user_id\": {user_id}}}"
# '{"user_id": 42}'

f-string 很适合日志消息和展示文本;但不要把 SQL、Shell 命令等不可信输入拼接进 f-string。对于数据库查询,应始终使用驱动提供的参数化接口。

海象运算符 :=

边计算,边保存结果。它会先计算右侧表达式,把结果保存到左侧名称,再把这个结果作为整个表达式的值继续使用。它最适合避免“为了判断而计算一次、使用时又计算一次”的重复工作。

在循环条件中读取数据

这是最直观也最推荐的用法:读到空字节串时结束循环。

1
2
while chunk := file.read(8192):
process(chunk)

等价的普通写法需要把读取动作写两次:

1
2
3
4
chunk = file.read(8192)
while chunk:
process(chunk)
chunk = file.read(8192)

缓存条件判断中的结果

1
2
3
4
5
import re

if match := re.search(r"order-(\d+)", "order-1024"):
order_id = int(match.group(1))
print(order_id) # 1024

在推导式中也可以保留一次昂贵计算的结果,避免重复运算:

1
2
3
4
5
6
def normalize(raw: str) -> str:
return raw.strip().lower()

raw_names = [" Alice ", "", " BOB "]
names = [name for raw in raw_names if (name := normalize(raw))]
# ['alice', 'bob']

推导式中的 := 绑定的是外层作用域的名称,不是推导式循环变量的私有作用域;也不能重新绑定该推导式的迭代变量。不要依赖这种副作用来传递复杂状态。

何时不用

:= 不应该用来制造“聪明”的一行代码。若赋值与外层条件没有紧密关系,或需要解释作用域和执行顺序,就使用普通赋值。为了避免优先级误读,在 ifwhile 和推导式之外通常应加括号:

1
2
3
# 清晰:明确把比较结果绑定到 is_valid
if (is_valid := token.is_valid()):
use(token)

上下文管理器:用 with 保证资源释放

文件、锁、数据库连接和临时目录等资源都有“获取后必须释放”的需求。with 会在代码块结束时调用上下文管理器的清理逻辑;即使块内抛出了异常,清理也仍会执行。

1
2
3
4
5
6
from pathlib import Path

path = Path("settings.json")
with path.open("r", encoding="utf-8") as file:
content = file.read()
# 离开 with 块后,文件已经关闭

它大致相当于下面的 try...finally,但实际的异常处理还可由 __exit__ 自定义:

1
2
3
4
5
file = path.open("r", encoding="utf-8")
try:
content = file.read()
finally:
file.close()

同时管理多个资源

可以在一个 with 中打开多个资源。它们会按与创建相反的顺序退出:

1
2
3
4
5
6
with (
open("input.txt", encoding="utf-8") as source,
open("output.txt", "w", encoding="utf-8") as target,
):
for line in source:
target.write(line.upper())

编写自己的上下文管理器

对于轻量的“前置动作 + 清理动作”,contextlib.contextmanager 很合适。yield 前是进入时执行的代码,yield 后是退出时执行的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from collections.abc import Generator
from contextlib import contextmanager
from time import perf_counter


@contextmanager
def timer(label: str) -> Generator[None, None, None]:
started_at = perf_counter()
try:
yield
finally:
elapsed = perf_counter() - started_at
print(f"{label} 耗时 {elapsed:.3f}s")


with timer("导入数据"):
import_data()

不要在上下文管理器中悄悄吞掉异常。只有在你能明确恢复、并且调用者期望继续执行时,才应该在退出逻辑中处理异常。

装饰器:为函数附加行为

装饰器接收一个函数(或类),返回一个替代对象。@decorator 是下面赋值语句的语法糖:

1
2
3
4
5
@trace
def calculate_total(price: float, count: int) -> float:
return price * count

# 等价于:calculate_total = trace(calculate_total)

一个可靠的函数装饰器

装饰器通常用来实现日志、计时、权限检查、缓存和重试等横切逻辑。包装函数时必须使用 functools.wraps,以保留原函数的名称、文档和其他元数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from collections.abc import Callable
from functools import wraps
from time import perf_counter
from typing import ParamSpec, TypeVar

P = ParamSpec("P")
T = TypeVar("T")


def timed(func: Callable[P, T]) -> Callable[P, T]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
started_at = perf_counter()
try:
return func(*args, **kwargs)
finally:
elapsed = perf_counter() - started_at
print(f"{func.__name__} 耗时 {elapsed:.3f}s")

return wrapper


@timed
def calculate_total(price: float, count: int) -> float:
return price * count

没有 @wraps 时,calculate_total.__name__ 会变成 wrapper,这会干扰日志、调试器、测试框架和依赖函数元数据的框架。

带参数的装饰器

当装饰器本身也需要参数时,需要再包一层工厂函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from collections.abc import Callable
from functools import wraps
from typing import ParamSpec, TypeVar

P = ParamSpec("P")
T = TypeVar("T")


def require_role(role: str) -> Callable[[Callable[P, T]], Callable[P, T]]:
def decorate(func: Callable[P, T]) -> Callable[P, T]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
if current_user.role != role:
raise PermissionError(f"需要角色:{role}")
return func(*args, **kwargs)

return wrapper

return decorate


@require_role("admin")
def delete_user(user_id: int) -> None:
...

装饰器从下到上应用:

1
2
3
4
5
6
@first
@second
def task() -> None:
...

# 等价于:task = first(second(task))

实践中,装饰器应只处理与业务无关的通用能力。若核心业务流程被层层装饰器遮住,或需要依赖复杂的隐式上下文,显式调用普通函数往往更易维护。

仅位置参数 / 与仅关键字参数 *

函数签名里,/ 之前的参数只能按位置传,* 之后的参数只能按关键字传:

1
2
def greet(name, /, *, greeting="你好"):
print(f"{greeting}{name}")
1
2
3
4
greet("Alice")                # 合法
greet("Alice", greeting="嗨") # 合法
greet(name="Alice") # 报错:name 是仅位置参数
greet("Alice", "嗨") # 报错:greeting 是仅关键字参数

/ 锁定参数名

把参数设为仅位置,意味着它的名字只是实现细节,调用方不能依赖。以后重命名或调整顺序,都不会破坏按位置调用的代码。标准库的 len(obj)int("42") 都是这样设计的。

* 强制写出参数名

参数一多,调用处一连串位置值很难读。加 * 让调用方必须写出名字,可读性立刻提升:

1
2
3
4
def create_order(*, item_id, quantity, address, coupon=None):
...

create_order(item_id=1024, quantity=2, address="...", coupon="NEW10")

单独的 * 不接收参数,只起分界作用。

完整的签名分段

一个签名可以同时出现两个分界符,把参数分成三段:

1
2
def f(pos_only, /, normal, *, kw_only):
...
  • pos_only:仅位置
  • normal:位置或关键字皆可
  • kw_only:仅关键字

实践中不必每个函数都上。/ 用于想保留改名自由的公开 API;* 用于参数多、靠位置容易混淆的函数。

match-case:按结构匹配数据

match-case 是 Python 3.10 引入的结构化模式匹配。它不仅能比较一个值,还能解构序列、映射和对象,并把其中的部分值绑定到变量。

字面量、或模式和守卫条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def http_status_message(status: int) -> str:
match status:
case 200 | 201:
return "成功"
case 400:
return "请求错误"
case 401 | 403:
return "没有权限"
case 404:
return "资源不存在"
case code if 500 <= code < 600:
return "服务端错误"
case _:
return "未知状态"

case code if ... 中的 if 称为守卫(guard):模式先匹配并绑定 code,随后再判断条件。

匹配序列与映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def describe_point(point: object) -> str:
match point:
case [0, 0]:
return "原点"
case [x, 0]:
return f"x 轴上的点 ({x}, 0)"
case [0, y]:
return f"y 轴上的点 (0, {y})"
case [x, y]:
return f"平面点 ({x}, {y})"
case _:
return "不是二维点"


def handle_event(event: dict[str, object]) -> str:
match event:
case {"type": "user.created", "user_id": user_id}:
return f"创建用户 {user_id}"
case {"type": "order.paid", "order_id": order_id, "amount": amount}:
return f"订单 {order_id} 已支付 {amount}"
case {"type": event_type}:
return f"未处理事件:{event_type}"
case _:
return "无效事件"

映射模式只要求列出的键存在,允许输入中还有额外的键。若要取得其余键,可以使用 **rest

1
2
3
match event:
case {"type": "user.updated", "user_id": user_id, **changes}:
print(user_id, changes)

匹配类与数据类

数据类会自动提供适合模式匹配的 __match_args__,因此很适合表示结构明确的消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from dataclasses import dataclass


@dataclass
class Resize:
width: int
height: int


def describe_resize(command: object) -> str:
match command:
case Resize(width, height) if width <= 0 or height <= 0:
return "尺寸必须为正数"
case Resize(width, height):
return f"调整为 {width} × {height}"
case _:
return "不是调整尺寸命令"

最大陷阱:名称捕获不是常量比较

在模式中,单独写一个名称意味着“捕获任意值并绑定给它”,并不表示与同名变量比较。因此下面的写法会匹配任何值,通常也是语法错误,因为它使后续分支不可达:

1
2
3
4
# 不要这样写
# match status:
# case OK:
# ...

要匹配常量,请使用字面量、枚举成员或带限定名的常量:

1
2
3
4
5
6
7
8
9
10
11
12
13
from enum import Enum


class Status(Enum):
OK = 200
NOT_FOUND = 404


match status:
case Status.OK:
print("成功")
case Status.NOT_FOUND:
print("未找到")

match-case 适合协议消息、命令对象、JSON 形状和有限状态的分派;若只是对少数常量做简单映射,字典或 if...elif 有时更直接。不要为了使用新语法而把普通条件判断强行改成模式匹配。

生成器与 yield:按需产出数据

生成器函数用 yield 产出值,而不是 return。调用它不会立即执行函数体,而是返回一个生成器对象;每次取一个值,函数才往下执行到下一个 yield

一次只产出一个,不必先建整张表

处理大文件或无限序列时,一次性 list 出来会撑爆内存。生成器按需产出,内存占用恒定:

1
2
3
4
5
6
7
8
def read_lines(path):
with open(path, encoding="utf-8") as file:
for line in file:
yield line.rstrip("\n")

for line in read_lines("access.log"):
if "ERROR" in line:
print(line)

生成器推导式:一行写完简单生成器

把列表推导式的方括号换成圆括号,就得到生成器。它不预先算出所有结果,适合只需遍历一次的中间步骤:

1
2
3
4
5
6
7
8
9
numbers = range(1_000_000)

# 列表推导式:先在内存里造出 100 万个元素
squares_list = [n * n for n in numbers]

# 生成器:边遍历边算,几乎不占内存
squares_gen = (n * n for n in numbers)

total = sum(squares_gen)

生成器只能遍历一次,耗尽后再次迭代得到空结果。需要反复使用就转成列表。

yield from 委托给子生成器

在生成器里再嵌一个生成器时,yield from 把子生成器的值逐个透传出来,省掉一层 for ... yield

1
2
3
4
5
6
7
8
9
def flatten(nested):
for item in nested:
if isinstance(item, list):
yield from flatten(item)
else:
yield item

list(flatten([1, [2, [3, 4]], 5]))
# [1, 2, 3, 4, 5]

用生成器串成管道

把多个生成器接起来,每一步处理完一个元素就交给下一步,整体内存占用是常数:

1
2
3
4
5
6
7
8
9
10
11
12
def parse(lines):
for line in lines:
yield line.split(",")

def filter_valid(rows):
for row in rows:
if len(row) == 3:
yield row

rows = filter_valid(parse(read_lines("data.csv")))
for row in rows:
process(row)

生成器适合「流式、只需一次」的场景;如果需要随机访问、反复遍历或切片,转成列表更直接。

for...elsewhile...else:少用,且当心

forwhile 后面可以接 else,但它的语义和直觉相反:循环正常结束(没有被 break)时才执行 else 块。

1
2
3
4
5
6
for item in items:
if item == target:
print("找到了")
break
else:
print("没找到")

这相当于「查找失败」的兜底,等价于用一个标志变量:

1
2
3
4
5
6
7
found = False
for item in items:
if item == target:
found = True
break
if not found:
print("没找到")

为什么建议少用

else 在这里几乎不传递「否则」的暗示,多数读者第一眼会以为是「循环没执行」或「条件为假」时才跑。这种语义落差是真实 bug 的来源。

更清晰的写法是把查找逻辑包成函数,用 return 表达「找到」,用函数结尾表达「没找到」:

1
2
3
4
5
def find(items, target):
for item in items:
if item == target:
return item
return None

读到带 else 的循环时,先确认它依赖的是「未被 break」这个条件;自己写代码时,优先用函数提前返回或显式标志位,把意图写得更直白。

选择语法的快速清单

需求优先选择关键提醒
拆分、收集或合并数据解包 */**字典合并时后者覆盖前者
过滤、转换并收集数据列表/字典推导式复杂嵌套改用普通循环
拼接展示或日志文本f-string不拼接不可信的 SQL、Shell 输入
在判断时保留计算结果:=只用于能提升可读性的紧凑场景
自动释放文件、锁、连接with清理逻辑必须覆盖异常路径
附加计时、鉴权、缓存等通用能力装饰器使用 functools.wraps 保留元数据
约束参数的传递方式/*仅位置参数无法按名调用
根据数据的形状、类型或字段分派match-case区分常量模式和名称捕获
流式处理大文件或无限序列生成器 yield只能遍历一次,耗尽即空
循环查找失败的兜底for...else语义反直觉,优先用函数提前返回

这些语法的价值不在于“写得更花”,而在于把重复的样板代码压缩掉,让读者第一眼看到业务意图。判断是否使用它们的标准很简单:删除语法技巧后,代码是否更长但更清楚?如果是,那么技巧就不该成为默认选择。