Skip to content
话题
Python
Python Switch Case:详解 match-case 语句

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 多年的限制。几乎所有其他主流语言都有 switchcase 结构,但 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: hello

match 关键字会只对主体表达式求值一次。每个 case 都定义一种模式。Python 自上而下测试各个模式,并执行第一个匹配项。通配符 _ 是默认分支——它可以匹配任何内容。

match-case 与 switch-case:关键区别

如果你来自 C、Java、JavaScript 或 Go,下面是最重要的区别:

特性Python match-caseC/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 Found

2. 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 4

7. 守卫条件

为任意模式添加 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 mode

3.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-case3.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 中 TrueFalse 都是 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/elifmatch-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:。请始终把它放在最后。

相关文章

📚