Python 的许多“高级语法”并不难,难的是知道它们各自解决什么问题、该在什么边界内使用。本文把日常代码中最常见的一组语法放在一起:推导式、f-string、海象运算符、with、装饰器和 match-case。
它们的共同目标是让代码更贴近意图,而不是单纯压缩行数。本文示例以 Python 3.12 为基准;最低版本见下表。
语法 最低版本 核心用途 解包 */** Python 3.0 / 3.5 / 3.9 拆分序列、合并映射 列表、字典推导式 Python 2.0 / 2.7 从可迭代对象构造新集合 f-string Python 3.6 格式化字符串 海象运算符 := Python 3.8 在表达式中绑定名称 上下文管理器 with Python 2.5 成对管理资源的获取与释放 装饰器 @decorator Python 2.4 为函数或类附加横切行为 仅位置参数 /、仅关键字参数 * Python 3.0 / 3.8 约束参数的传递方式 match-casePython 3.10 结构化模式匹配 生成器与 yield Python 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 head, *tail = range (10 ) *init, last = range (10 )
星号只能在端点出现一次,收集结果永远是列表,即便源是元组或字符串。
在字面量里展开多个来源 列表、集合、元组字面量里用 * 把多个可迭代对象拼到一起,不必先 extend:
1 2 3 4 5 6 a = [1 , 2 ] b = [3 , 4 ] [*a, *b] (*a, *b) {*a, *b}
合并字典:** 与 | 字典字面量里用 ** 展开,后面的键覆盖前面:
1 2 3 4 5 defaults = {"host" : "localhost" , "port" : 5432 , "timeout" : 30 } override = {"port" : 6543 , "ssl" : True } config = {**defaults, **override}
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)
忽略不需要的部分 不关心的位置用 _ 接住,约定上表示「丢弃」:
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 ]
如果每个元素都要保留,只是转换方式不同,条件部分可以省略:
1 2 3 names = ["alice" , "bob" , "carol" ] display_names = [name.title() for name in names]
在表达式中需要“二选一”时,使用条件表达式,而不是把 else 写到 for 后面:
1 2 labels = ["even" if number % 2 == 0 else "odd" for number in range (5 )]
嵌套循环 后面的 for 相当于内层循环,适合展平嵌套结构或做笛卡尔积:
1 2 3 4 5 6 matrix = [[1 , 2 , 3 ], [4 , 5 , 6 ]] flat = [elem for row in matrix for elem in row] pairs = [(x, y) for x in range (3 ) for y in range (2 )]
字典推导式:构造索引或筛选映射 字典推导式尤其适合把一组对象按某个字段建立索引:
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 2 3 status_codes = {"ok" : 200 , "not_found" : 404 } code_names = {code: name for name, code in status_codes.items()}
注意:若推导式生成了重复的键,后面的值会覆盖前面的值。这是字典赋值的正常规则,而不是推导式特有的行为。
可读性的边界 两层以内、没有副作用的推导式通常很清晰;嵌套循环、多个过滤条件和复杂业务判断混在一起时,普通循环反而更容易调试。
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)
集合推导式 1 2 3 4 5 6 duplicates = [1 , 1 , 2 , 2 , 3 , 3 ] unique = {x for x in duplicates} chars = {c for c in 'hello world' if c.isalpha()}
生成器推导式 把方括号换成圆括号,得到的是惰性求值的生成器,不是元组。它按需产出元素,内存占用恒定:
1 2 3 4 5 6 7 8 squares_list = [x ** 2 for x in range (10 ** 6 )] 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] print (nums)
2. 不存在“元组推导”
1 2 3 4 5 gen = (x for x in range (5 ))type (gen) tup = tuple (x for x in range (5 ))
f-string:把格式化写在字符串旁边 f-string(formatted string literal)以 f 或 F 开头,在花括号中直接放表达式:
1 2 3 4 5 name = "Ada" score = 93.5 message = f"{name} 的得分是 {score:.1 f} "
它可以执行任意合法表达式,但建议只放简短、无副作用的表达式:
1 2 3 4 5 filename = "report.csv" size = 12_345_678 print (f"文件 {filename!r} 的大小为 {size:,} 字节" )
格式说明符的结构 冒号 : 后面是格式说明符(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 } |" f"|{name:>8 } |" f"|{name:^8 } |" f"|{name:.^8 } |" f"订单号:{order_id:06d} "
这类格式尤其适合生成等宽的命令行表格:
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.2 f} " )
数值格式化 数值可以添加千位分隔符、显式正负号、精度和进制前缀:
1 2 3 4 5 6 7 8 9 10 11 amount = 12_345.6789 ratio = 0.12345 number = 255 f"{amount:,.2 f} " f"{amount:_.2 f} " f"{amount:+.1 f} " f"{ratio:.2 %} " f"{number:b} " f"{number:#x} " f"{number:#06X} "
浮点数的 .nf 表示小数点后的位数;.ng 则表示有效数字位数,适合数值量级差异较大时的紧凑展示:
1 2 3 4 5 value = 1234.56789 f"{value:.3 f} " f"{value:.4 g} " f"{value:.3 e} "
日期、时间与动态宽度 日期时间对象可以在冒号后直接使用 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} " f"|{name:^{width} }|" f"{price:.{precision} f}"
动态格式适合输出列宽由配置决定、或允许调用方控制显示精度的场景;如果格式本身变得难读,应先构造 format_spec 变量再使用。
!s、!r 与 !a 转换在冒号前可使用转换标记:!s 调用 str(),!r 调用 repr(),!a 调用 ascii()。最常用的是 !r,它能显示字符串两侧的引号和转义字符,便于排查空格、换行等问题。
1 2 3 4 5 value = "café\n" f"{value!s} " f"{value!r} " f"{value!a} "
从 Python 3.8 起,调试时还可以使用 = 直接带上变量名:
1 2 3 4 5 user_id = 42 latency_ms = 18.7 print (f"{user_id=} , {latency_ms=:.1 f} " )
花括号本身需要写两次:
1 2 template = f"{{\"user_id\": {user_id} }}"
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 reif match := re.search(r"order-(\d+)" , "order-1024" ): order_id = int (match .group(1 )) print (order_id)
在推导式中也可以保留一次昂贵计算的结果,避免重复运算:
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))]
推导式中的 := 绑定的是外层作用域 的名称,不是推导式循环变量的私有作用域;也不能重新绑定该推导式的迭代变量。不要依赖这种副作用来传递复杂状态。
何时不用 := 不应该用来制造“聪明”的一行代码。若赋值与外层条件没有紧密关系,或需要解释作用域和执行顺序,就使用普通赋值。为了避免优先级误读,在 if、while 和推导式之外通常应加括号:
1 2 3 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()
它大致相当于下面的 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 Generatorfrom contextlib import contextmanagerfrom 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:.3 f} s" )with timer("导入数据" ): import_data()
不要在上下文管理器中悄悄吞掉异常。只有在你能明确恢复、并且调用者期望继续执行时,才应该在退出逻辑中处理异常。
装饰器:为函数附加行为 装饰器接收一个函数(或类),返回一个替代对象。@decorator 是下面赋值语句的语法糖:
1 2 3 4 5 @trace def calculate_total (price: float , count: int ) -> float : return price * count
一个可靠的函数装饰器 装饰器通常用来实现日志、计时、权限检查、缓存和重试等横切逻辑。包装函数时必须使用 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 wrapsfrom time import perf_counterfrom 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:.3 f} 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 wrapsfrom 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 : ...
实践中,装饰器应只处理与业务无关的通用能力。若核心业务流程被层层装饰器遮住,或需要依赖复杂的隐式上下文,显式调用普通函数往往更易维护。
仅位置参数 / 与仅关键字参数 * 函数签名里,/ 之前的参数只能按位置传,* 之后的参数只能按关键字传:
1 2 def greet (name, /, *, greeting="你好" ): print (f"{greeting} ,{name} " )
1 2 3 4 greet("Alice" ) greet("Alice" , greeting="嗨" ) greet(name="Alice" ) greet("Alice" , "嗨" )
用 / 锁定参数名 把参数设为仅位置,意味着它的名字只是实现细节,调用方不能依赖。以后重命名或调整顺序,都不会破坏按位置调用的代码。标准库的 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 5 6 7 8 9 10 11 12 13 from enum import Enumclass 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 ) 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 itemlist (flatten([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...else 与 while...else:少用,且当心for 和 while 后面可以接 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语义反直觉,优先用函数提前返回
这些语法的价值不在于“写得更花”,而在于把重复的样板代码压缩掉,让读者第一眼看到业务意图。判断是否使用它们的标准很简单:删除语法技巧后,代码是否更长但更清楚?如果是,那么技巧就不该成为默认选择。