Python Switch Case:详解 match-case 语句
更新于
你正在用 Python 编写一个命令处理器。这个函数接收一个字符串,比如 "quit"、"save"、"load" 或 "help",并且需要针对每个命令执行不同逻辑。在 JavaScript 或 C 中,你会使用 switch 语句。而在 Python 中,你通常会写一串 if/elif/else 块——两个命令要五行,六个命令要十五行,十二个命令要三十行。这个函数会变成一堵比较语句组成的墙:读起来费劲、容易写错顺序,而且扩展起来很痛苦。
如果你的搜索关键词是 python switch case,可以先从这里开始。关于更深入的结构化模式匹配参考,请看 Python Match Case。如果你想了解更简单的字典分发方案,请看 Python Switch Case。
这在 Python 中曾经是真实存在了 30 多年的限制。几乎所有其他主流语言都有 switch 或 case 结构,但 Python 一直依赖 if/elif 链和字典分发。Guido van Rossum 多次拒绝 switch-case 提案,理由是 if/elif 已经足够。
这一点在 Python 3.10 中随着 PEP 634 (opens in a new tab) 的出现而改变。它引入了 match-case 语句——这是 Python 对 switch-case 的回答,但能力强大得多。它不仅能匹配值,还能解构对象、匹配模式、绑定变量并应用条件。它是结构化模式匹配,而不只是 switch 的替代品。
快速语法:30 秒看懂 match-case
def handle_command(command):
match command:
case "quit":
print("Exiting program")
case "save":
print("Saving file")
case "help":
print("Showing help")
case _:
print(f"Unknown command: {command}")
handle_command("save") # Saving file
handle_command("hello") # Unknown command: hellomatch 关键字会只对主体表达式求值一次。每个 case 都定义一种模式。Python 自上而下测试各个模式,并执行第一个匹配项。通配符 _ 是默认分支——它可以匹配任何内容。
match-case 与 switch-case:关键区别
如果你来自 C、Java、JavaScript 或 Go,下面是最重要的区别:
| 特性 | Python match-case | C/Java switch-case |
|---|---|---|
| 贯穿执行(fall-through) | 没有(每个 case 都独立) | 有(需要 break) |
| 模式类型 | 字面量、序列、映射、类、OR、守卫 | 主要是字面量 |
| 解构 | 内置支持(从匹配结构中绑定变量) | 不支持 |
| 变量绑定 | 从匹配模式中捕获值 | 不支持 |
| 默认分支 | case _: | default: |
| 表达式/语句 | 语句(无返回值) | 语句 |
| 最低 Python 版本 | 3.10+ | N/A |
最重要的区别是:没有贯穿执行。每个 case 块都是隔离的。你永远不需要写 break,也不会意外执行到下一个 case。
模式类型:match-case 能做什么
Python 的 match-case 支持七种模式类型。这也是它比传统 switch 语句强大得多的原因。
1. 字面量模式
匹配精确值——字符串、数字、布尔值、None:
def classify_status(code):
match code:
case 200:
return "OK"
case 301:
return "Moved Permanently"
case 404:
return "Not Found"
case 500:
return "Internal Server Error"
case _:
return f"Unknown status: {code}"
print(classify_status(404)) # Not Found2. OR 模式(多个值)
使用 | 运算符在一个 case 中匹配多个值:
def classify_http_status(code):
match code:
case 200 | 201 | 204:
return "Success"
case 301 | 302 | 307 | 308:
return "Redirect"
case 400 | 401 | 403 | 404:
return "Client Error"
case 500 | 502 | 503:
return "Server Error"
case _:
return "Unknown"这替代了常见的 if code in (200, 201, 204): 这种写法。
3. 捕获模式(变量绑定)
case 中的裸名称会把匹配到的值捕获到一个变量中:
def process_value(data):
match data:
case 0:
print("Zero")
case value:
print(f"Got non-zero value: {value}")
process_value(42) # Got non-zero value: 42重要陷阱:裸名称永远是捕获,而不是与某个已有变量比较。这是最常见的错误:
# WRONG — 这不会与 expected_code 比较
expected_code = 200
match response_code:
case expected_code: # 这里会把 response_code 捕获到 expected_code!
print("Match")
# RIGHT — 使用 guard 或字面量
match response_code:
case code if code == expected_code:
print("Match")如果要匹配常量,请使用带点名称(如 http.HTTPStatus.OK)或 guard 条件。
4. 序列模式(列表和元组)
直接在模式中解构列表和元组:
def process_point(point):
match point:
case (0, 0):
return "Origin"
case (x, 0):
return f"On x-axis at {x}"
case (0, y):
return f"On y-axis at {y}"
case (x, y):
return f"Point at ({x}, {y})"
print(process_point((3, 0))) # On x-axis at 3
print(process_point((2, 5))) # Point at (2, 5)你可以使用 *rest 捕获剩余元素:
def first_and_rest(items):
match items:
case []:
return "Empty list"
case [single]:
return f"Single item: {single}"
case [first, *rest]:
return f"First: {first}, remaining: {rest}"
print(first_and_rest([1, 2, 3, 4])) # First: 1, remaining: [2, 3, 4]5. 映射模式(字典)
匹配字典中的键并解构值:
def handle_event(event):
match event:
case {"type": "click", "x": x, "y": y}:
print(f"Click at ({x}, {y})")
case {"type": "keypress", "key": key}:
print(f"Key pressed: {key}")
case {"type": "scroll", "direction": direction}:
print(f"Scroll {direction}")
case _:
print("Unknown event")
handle_event({"type": "click", "x": 100, "y": 200})
# Click at (100, 200)
handle_event({"type": "keypress", "key": "Enter", "modifiers": ["Ctrl"]})
# Key pressed: Enter (extra keys are ignored)如果字典中包含指定键,映射模式就会匹配;额外的键会被静默忽略。这使它非常适合处理 JSON 数据或 API 响应。
6. 类模式
匹配类实例并提取属性:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
@dataclass
class Circle:
center: Point
radius: float
@dataclass
class Rectangle:
corner: Point
width: float
height: float
def describe_shape(shape):
match shape:
case Circle(center=Point(x=0, y=0), radius=r):
return f"Circle at origin with radius {r}"
case Circle(center=center, radius=r):
return f"Circle at ({center.x}, {center.y}) with radius {r}"
case Rectangle(width=w, height=h) if w == h:
return f"Square with side {w}"
case Rectangle(width=w, height=h):
return f"Rectangle {w}x{h}"
print(describe_shape(Circle(Point(0, 0), 5)))
# Circle at origin with radius 5
print(describe_shape(Rectangle(Point(1, 1), 4, 4)))
# Square with side 47. 守卫条件
为任意模式添加 if 条件进行额外筛选:
def classify_number(n):
match n:
case n if n < 0:
return "Negative"
case 0:
return "Zero"
case n if n % 2 == 0:
return "Positive even"
case _:
return "Positive odd"
print(classify_number(-5)) # Negative
print(classify_number(4)) # Positive even
print(classify_number(7)) # Positive odd守卫允许你添加超出模式表达能力的任意条件。程序会先完成模式匹配,再计算守卫条件。
真实案例:JSON API 响应解析器
match-case 最实用的场景之一,就是解析 API 响应或 JSON 数据:
import json
def process_api_response(response: dict) -> str:
match response:
case {"status": "success", "data": {"users": [*users]}}:
return f"Found {len(users)} users"
case {"status": "success", "data": {"user": {"name": name, "email": email}}}:
return f"User: {name} ({email})"
case {"status": "error", "code": 401}:
return "Authentication required"
case {"status": "error", "code": code, "message": msg}:
return f"Error {code}: {msg}"
case {"status": "error"}:
return "Unknown error"
case _:
return "Unrecognized response format"
# Test with different responses
print(process_api_response({
"status": "success",
"data": {"users": ["Alice", "Bob", "Charlie"]}
}))
# Found 3 users
print(process_api_response({
"status": "error",
"code": 429,
"message": "Rate limit exceeded"
}))
# Error 429: Rate limit exceeded这用 match-case 替代了深层嵌套的 if/elif 链、isinstance() 检查以及字典 .get() 调用。match-case 版本直接清晰地表达了所期望的数据结构。
真实案例:命令行参数解析器
import sys
def parse_args(args: list[str]):
match args:
case [program]:
print(f"Usage: {program} <command> [options]")
case [_, "init", project_name]:
print(f"Initializing project: {project_name}")
case [_, "init"]:
print("Error: project name required")
case [_, "build", "--release"]:
print("Building in release mode")
case [_, "build"]:
print("Building in debug mode")
case [_, "test", *test_files] if test_files:
print(f"Running tests: {', '.join(test_files)}")
case [_, "test"]:
print("Running all tests")
case [_, unknown, *_]:
print(f"Unknown command: {unknown}")
parse_args(["app", "init", "my-project"]) # Initializing project: my-project
parse_args(["app", "test", "a.py", "b.py"]) # Running tests: a.py, b.py
parse_args(["app", "build", "--release"]) # Building in release mode3.10 之前的替代方案:没有 match-case 的 switch-case
如果你使用的是 Python 3.9 或更早版本,或者 match-case 对于简单值匹配来说显得过于强大,那么下面这些方案是成熟的替代方式。
if/elif/else 链
最直接的方法:
def handle_command(command):
if command == "quit":
print("Exiting program")
elif command == "save":
print("Saving file")
elif command == "load":
print("Loading file")
elif command == "help":
print("Showing help")
else:
print(f"Unknown command: {command}")适用场景:少于 5-6 个分支且逻辑简单时。
字典分发
用字典把值映射到函数:
def cmd_quit():
print("Exiting program")
def cmd_save():
print("Saving file")
def cmd_help():
print("Showing help")
commands = {
"quit": cmd_quit,
"save": cmd_save,
"help": cmd_help,
}
def handle_command(command):
action = commands.get(command)
if action:
action()
else:
print(f"Unknown command: {command}")适用场景:当你需要把值映射到简单动作时,尤其是 case 很多的时候。查找是 O(1),而 if/elif 是 O(n)。
带参数的字典分发
def calculate(operation, a, b):
ops = {
"+": lambda a, b: a + b,
"-": lambda a, b: a - b,
"*": lambda a, b: a * b,
"/": lambda a, b: a / b if b != 0 else float("inf"),
}
func = ops.get(operation)
if func is None:
raise ValueError(f"Unknown operation: {operation}")
return func(a, b)
print(calculate("+", 10, 3)) # 13
print(calculate("/", 10, 0)) # inf基于 Enum 的分发
适用于类型化、具有自解释性的代码:
from enum import Enum
class Direction(Enum):
NORTH = "north"
SOUTH = "south"
EAST = "east"
WEST = "west"
def move(direction: Direction, x: int, y: int) -> tuple[int, int]:
offsets = {
Direction.NORTH: (0, 1),
Direction.SOUTH: (0, -1),
Direction.EAST: (1, 0),
Direction.WEST: (-1, 0),
}
dx, dy = offsets[direction]
return (x + dx, y + dy)
print(move(Direction.NORTH, 0, 0)) # (0, 1)对比:该选哪种方案
| 方案 | Python 版本 | 最适合 | 复杂度 | 性能 |
|---|---|---|---|---|
match-case | 3.10+ | 复杂模式、解构、类型匹配 | 表达力强 | 与 if/elif 相当 |
if/elif/else | 所有版本 | 简单值比较、少于 6 个分支 | 简单 | O(n) 线性扫描 |
| 字典分发 | 所有版本 | 很多分支、值 → 函数映射 | 中等 | O(1) 查找 |
| 字典 + lambda | 所有版本 | 很多分支、值 → 表达式映射 | 中等 | O(1) 查找 |
| Enum 分发 | 所有版本 | 类型化常量、穷尽匹配 | 中等 | O(1) 查找 |
经验法则:
- 当你需要解构、类型匹配或复杂模式时,使用 match-case
- 当只是少量简单比较时,使用 if/elif
- 当有很多简单的“值 → 动作”映射时,使用 字典分发
- 当值集合固定且需要类型约束时,使用 enum 分发
常见模式与技巧
基于类型的分发
def serialize(value):
match value:
case bool(): # 必须放在 int 前面 —— bool 是 int 的子类
return "true" if value else "false"
case int() | float():
return str(value)
case str():
return f'"{value}"'
case list():
items = ", ".join(serialize(v) for v in value)
return f"[{items}]"
case dict():
pairs = ", ".join(
f'{serialize(k)}: {serialize(v)}' for k, v in value.items()
)
return "{" + pairs + "}"
case None:
return "null"
case _:
raise TypeError(f"Cannot serialize {type(value)}")
print(serialize({"name": "Alice", "scores": [95, 87, 92], "active": True}))
# {"name": "Alice", "scores": [95, 87, 92], "active": true}重要:bool() 必须放在 int() 前面,因为在 Python 中 True 和 False 都是 int 的实例。
嵌套模式匹配
def evaluate(expr):
"""Simple math expression evaluator."""
match expr:
case int(n) | float(n):
return n
case ("+", left, right):
return evaluate(left) + evaluate(right)
case ("-", left, right):
return evaluate(left) - evaluate(right)
case ("*", left, right):
return evaluate(left) * evaluate(right)
case ("/", left, right):
divisor = evaluate(right)
if divisor == 0:
raise ZeroDivisionError("Division by zero")
return evaluate(left) / divisor
case ("neg", operand):
return -evaluate(operand)
case _:
raise ValueError(f"Invalid expression: {expr}")
# (3 + 4) * 2
result = evaluate(("*", ("+", 3, 4), 2))
print(result) # 14使用 match-case 构建状态机
def tokenize(text):
"""Simple tokenizer using match-case as a state machine."""
tokens = []
i = 0
while i < len(text):
match text[i]:
case ' ' | '\t' | '\n':
i += 1 # Skip whitespace
case '+' | '-' | '*' | '/':
tokens.append(("OP", text[i]))
i += 1
case '(' | ')':
tokens.append(("PAREN", text[i]))
i += 1
case c if c.isdigit():
j = i
while j < len(text) and text[j].isdigit():
j += 1
tokens.append(("NUM", int(text[i:j])))
i = j
case c if c.isalpha():
j = i
while j < len(text) and text[j].isalnum():
j += 1
tokens.append(("ID", text[i:j]))
i = j
case c:
raise SyntaxError(f"Unexpected character: {c}")
return tokens
print(tokenize("x + 42 * (y - 3)"))
# [('ID', 'x'), ('OP', '+'), ('NUM', 42), ('OP', '*'), ('PAREN', '('),
# ('ID', 'y'), ('OP', '-'), ('NUM', 3), ('PAREN', ')')]性能:match-case vs if/elif vs dict
对于简单值匹配,这三种方式都已经足够快,实际使用中差异通常可以忽略。下面是在匹配 10 个字符串值时的基准结果:
import timeit
commands = ["cmd_0", "cmd_1", "cmd_2", "cmd_3", "cmd_4",
"cmd_5", "cmd_6", "cmd_7", "cmd_8", "cmd_9"]
# if/elif approach
def if_elif_dispatch(cmd):
if cmd == "cmd_0": return 0
elif cmd == "cmd_1": return 1
elif cmd == "cmd_2": return 2
elif cmd == "cmd_3": return 3
elif cmd == "cmd_4": return 4
elif cmd == "cmd_5": return 5
elif cmd == "cmd_6": return 6
elif cmd == "cmd_7": return 7
elif cmd == "cmd_8": return 8
elif cmd == "cmd_9": return 9
else: return -1
# dict dispatch approach
dispatch_dict = {f"cmd_{i}": i for i in range(10)}
def dict_dispatch(cmd):
return dispatch_dict.get(cmd, -1)
# match-case approach
def match_dispatch(cmd):
match cmd:
case "cmd_0": return 0
case "cmd_1": return 1
case "cmd_2": return 2
case "cmd_3": return 3
case "cmd_4": return 4
case "cmd_5": return 5
case "cmd_6": return 6
case "cmd_7": return 7
case "cmd_8": return 8
case "cmd_9": return 9
case _: return -1| 方案 | 最佳情况(第一个匹配) | 最差情况(最后一个匹配) | 未命中 |
|---|---|---|---|
| if/elif | ~80ns | ~400ns | ~420ns |
| 字典分发 | ~60ns | ~60ns | ~65ns |
| match-case | ~90ns | ~450ns | ~470ns |
核心结论:字典分发是 O(1) 常数时间,不受匹配位置影响。if/elif 和 match-case 都是 O(n) 线性扫描。对于少于大约 20 个分支的情况,性能差异几乎可以忽略。应当根据可读性来选,而不是微基准结果。
常见错误及避免方法
错误 1:在 Python 3.10 之前使用 match-case
# 这会在 Python 3.9 及更早版本中导致 SyntaxError
match command: # SyntaxError: invalid syntax
case "quit":
pass检查你的 Python 版本:python --version。如果你使用的是 3.9 或更早版本,请使用 if/elif 或字典分发。
错误 2:拿变量做比较
# WRONG — 这是捕获,不是比较
STATUS_OK = 200
match response_code:
case STATUS_OK: # 这会永远匹配,并覆盖 STATUS_OK!
print("OK")
# RIGHT — 使用带点名称
class Status:
OK = 200
NOT_FOUND = 404
match response_code:
case Status.OK:
print("OK")
case Status.NOT_FOUND:
print("Not Found")
# RIGHT — 使用 guard
match response_code:
case code if code == STATUS_OK:
print("OK")错误 3:忘记写通配符默认分支
# DANGEROUS — 未匹配值会静默无操作
match command:
case "save":
save_file()
case "load":
load_file()
# No default — "delete" silently falls through with no action
# SAFE — 始终包含默认分支
match command:
case "save":
save_file()
case "load":
load_file()
case _:
raise ValueError(f"Unknown command: {command}")错误 4:重叠模式顺序不对
# WRONG — 第一个 case 会捕获一切
match value:
case x: # 捕获任何值 — 永远匹配!
print(f"Got: {x}")
case 42: # 永远不会执行
print("The answer")
# RIGHT — 先写具体模式,通用模式放最后
match value:
case 42:
print("The answer")
case x:
print(f"Got: {x}")在数据科学中使用 match-case
如果你从事数据处理流水线工作,match-case 对处理不同数据格式和清洗操作很有帮助:
import csv
from pathlib import Path
def load_data(source):
"""Load data from different source types."""
match source:
case str() as path if path.endswith(".csv"):
with open(path) as f:
return list(csv.DictReader(f))
case str() as path if path.endswith(".json"):
import json
with open(path) as f:
return json.load(f)
case list() as records:
return records
case dict() as single_record:
return [single_record]
case _:
raise TypeError(f"Unsupported data source: {type(source)}")如果你想进行交互式数据探索,像 PyGWalker (opens in a new tab) 这样的工具可以让你无需编写绘图代码就直接可视化 DataFrame——当你想快速检查处理流水线的输出数据时尤其有用。
如果你正在构建使用 match-case 做复杂分支的数据处理脚本,RunCell (opens in a new tab) 提供了一个 AI 驱动的 Jupyter 环境,你可以用真实数据样本交互式测试模式匹配。
match-case 与 dataclass 和命名元组
Python 的 match-case 与 dataclass 和 named tuple 配合得尤其好:
from dataclasses import dataclass
from typing import Optional
@dataclass
class LogEntry:
level: str
message: str
error: Optional[Exception] = None
def handle_log(entry: LogEntry):
match entry:
case LogEntry(level="CRITICAL", error=err) if err is not None:
send_alert(f"CRITICAL with error: {err}")
restart_service()
case LogEntry(level="CRITICAL", message=msg):
send_alert(f"CRITICAL: {msg}")
case LogEntry(level="ERROR", error=err) if err is not None:
log_to_file(f"ERROR: {err}")
case LogEntry(level="WARNING" | "ERROR", message=msg):
log_to_file(msg)
case LogEntry(level="INFO" | "DEBUG"):
pass # Ignore low-priority logs常见问题
Python 有 switch 语句吗?
Python 3.10 及更高版本提供了 match-case 语句,它是 Python 版本的 switch-case。它超越了传统 switch 的能力——支持结构化模式匹配,你可以解构对象、匹配模式并绑定变量。对于 Python 3.9 及更早版本,请使用 if/elif/else 链或字典分发作为替代方案。
match-case 和 switch-case 有什么区别?
Python 的 match-case 没有贯穿执行(不需要 break),支持模式匹配(序列、映射、类),可以从匹配模式中解构并绑定变量,还支持带 if 条件的守卫。C/Java 中的传统 switch-case 只能匹配字面值。
match-case 比 Python 里的 if/elif 更快吗?
对于简单值匹配,它们的表现通常差不多——两者都是线性扫描。对于大量分支,字典分发会更快(O(1))。当分支少于 20 个时,性能差异几乎可以忽略。应当根据可读性和模式复杂度来选择,而不是性能。
我能在 Python 3.9 中使用 match-case 吗?
不能。match-case 语句要求 Python 3.10 或更高版本。对于更早的版本,请使用 if/elif/else 链或字典分发。
为什么我的 match-case 变量比较不起作用?
像 case x: 这样的裸名称会把任意值捕获到 x 中——它不会与已有变量比较。要与某个变量比较,请使用 guard 条件(case val if val == x:)或带点名称(case MyClass.CONSTANT:)。
match-case 中的下划线 _ 是什么?
_ 通配符模式可以匹配任何值,但不会把它绑定到变量。它是默认分支——类似 switch 语句中的 default:。请始终把它放在最后。
相关文章
- Python Try Except: Error Handling Guide
- Python Type Hints: Complete Tutorial
- Python Decorators Explained
- Python Collections Module Guide
- Python Enumerate Function
- Python Assert Statement