同步发表于个人 blogPython 3.10 | Match Statement - 更灵活的 switch

大纲

  • 概述
  • 前言
  • Guards
  • Irrefutable Case Blocks
  • Patterns
    • OR Patterns
    • AS Patterns
    • Literal Patterns
    • Capture Patterns
    • Wildcard Patterns
    • Value Patterns
    • Group Patterns
    • Sequence Patterns
    • Mapping Patterns
    • Class Patterns
  • 结语
  • References

  • 概述

    在我当初开始学 Python 时,发现没有 switch 可用,令我感到有些惊讶,还好从 Python 3.10 开始, Python 有了自己的 switch —— Structural Pattern Matching。

    所以什么是 Structural Pattern Matching ? 与 C++ 的 switch 相比,我觉得它更类似于 C# 的 Pattern Matching。举个简单的例子:

    is_perform = False

    match ("anon", "soyorin"):
    # Mismatch
    case \'tomorin\':
    print(\'组一辈子乐团\')

    # Mismatch: soyorin != rana
    case ("anon", "rana"):
    print(\'有趣的女人\')

    # Successful match, but guard fails
    case ("anon", "soyorin") if is_perform:
    print(\'为什么要演奏春日影!\')

    # Matches and binds y to "soyorin"
    case ("anon", y):
    print(f\'爱音,{y} 强到靠北\')

    # Pattern not attempted
    case _:
    print(\'我还是会继续下去\')

    # 爱音,soyorin 强到靠北

    📌 Note与 C++ 不同,Python 的 match 没有 fallthrough。所以一个 case 完成就会离开 match,而不会继续执行下一个 case。

    前言

    文件中把 match 后的部分 ("anon", "soyorin"):,称为 subject_expr,为了方便理解,我们叫他 match value。

    Guards

    case <pattern> if <expression>:

    如果我们在 pattern 成功 match 后,想进一步检查,可以在 pattern 后面加上 if ,也就是范例中的 case ("anon", "soyorin") if is_perform:,这种用法称为 Guard 。

    执行流程大致为

  • Pattern 成功 match,执行 Guard
  • Pattern Mismatch,不执行 Guard
  • Guard 结果为 True → 该 case 执行
  • Guard 结果为 False → 跳过该 case,继续检查下一个 case
  • Irrefutable Case Blocks

    指的是一定 match 的情况,类似于 C++ 的 default,但只能出现在 最后一个 case,且整个 match 只能有 一个 这样的 case。至于甚么 pattern 符合?可以参考 https://docs.python.org/3/reference/compound_stmts.html#irrefutable-case-blocks

    举例来说:

    match 2:
    case 1:
    print("value is 1")
    case x:
    print("Irrefutable Case Blocks")

    # Irrefutable Case Blocks

    Patterns

    OR Patterns

    就如同字面含意,就是个 or,pattern 会逐一尝试直到其中一个成功为止。

    举个简单例子:

    match 1:
    case 1 | 2 | 3:
    print("value is 1 or 2 or 3")

    # value is 1 or 2 or 3

    AS Patterns

    前面 or 用的很开心,那我们如何取的原本的 value 呢?此时我们可以用 as 来取得前面 match 到的值,也就是case <pattern> as <name>:,在 pattern 成功的情况下,match value 会 bind 到 name 上,name = <match value>。

    接续前面的例子

    match 1:
    case 1 | 2 | 3 as x:
    print(f"value is {x}")

    # value is 1

    Literal Patterns

    前面我们已经用了不少,用来比对 Python 中的 Literals,如int、string、None、bool等等,

    简单来说,如果 if <match value> == <Literal> 就会比对成功,若是遇到 Singletons ,如None, True ,False 则会透过 is 来比对

    Capture Patterns

    用来将比对的值 bind 到变数上,

    在 pattern 中,name 只能被 bind 一次

    match (1, 1):
    # SyntaxError
    case x, x:
    print(f"Matched: {x}")

    # case x, x:
    # ^
    # SyntaxError: multiple assignments to name \'x\' in pattern

    在下面例子中,在成功 match 的同时,"soyorin" 会 bind 到变数 y 上

    match ("anon", "soyorin"):
    # Matches and binds y to "soyorin"
    case ("anon", y):
    print(f\'爱音,{y} 强到靠北\')

    # 爱音,soyorin 强到靠北

    Wildcard Patterns

    _,用来比对任意值,基本上就是当 default 来用。

    比如

    match ("Raana", "soyorin"):

    # Matches and binds y to "soyorin"
    case ("anon", y):
    print(f\'爱音,{y} 强到靠北\')

    # Pattern not attempted
    case _:
    print(\'我还是会继续下去\')

    # 我还是会继续下去

    Value Patterns

    Value Pattern,是指可以透过 name resolution,也就是透过 . 存取的变数,例如 enum、math.pi 等。
    借由 == 来比对,<match value> == <NAME1.NAME2>。

    例子

    from enum import Enum

    class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

    match Color.RED:
    case Color.RED:
    print("RED")
    case Color.GREEN:
    print("GREEN")
    case Color.BLUE:
    print("BLUE")
    case _:
    print("unknown")

    # RED

    Group Patterns

    老实说,我觉得这不算 pattern,就是告诉你可以加 () 来加强可读性

    比如前面的例子

    match 1:
    # case 1 | 2 | 3 as x:
    case (1 | 2 | 3) as x:
    print(f"value is {x}")

    # value is 1

    Sequence Patterns

    用来比对 "Sequence",如 List 和 Tuple 等。
    不过,str、bytes、bytearray 并不会被当作 Sequence Patterns。

    📌 Note

    • Python 不区分 (...) 和 [...],两者作用相同。
    • 另需注意的是 (3 | 4) 会是 group pattern,但 [3 | 4] 依旧是 sequence pattern。

    举体的比对方式大致是

    • 固定长度
    • Match value 是 Sequence
    • len(value) == len(patterns)
    • 从左到右依序比对
    • 可变长度 (如[first, *middle, last])
    • Match value 是 Sequence
    • Sequence 长度小于扣除非 *(star pattern) 的数量 → 比对失败
    • 比对前面非 *(star pattern) 的部分(如同固定长度,也就是 first 部分)
    • 如果前面成功,扣除 last 部分后,收集剩余元素(变成list,对应 *middle )
    • 最后比对剩余部分,也就是 last (如同固定长度)

    看几个例子可能比较清楚

    # fixed-length

    match [10, 20, 30]:
    # note that this match can also bind names
    case [x, y, z]:
    print(f"x={x}, y={y}, z={z}")

    # x=10, y=20, z=30

    # variable-length

    match [1, 2, 3, 4, 5]:
    case [first, *middle, last]:
    print(f"first={first}, middle={middle}, last={last}")

    # first=1, middle=[2, 3, 4], last=5

    Mapping Patterns

    用来比对 "mapping",最常用的就是 dict

    与前面类似,我们可以把 **(double_star_pattern) 放在最后,收集剩余元素,另外不可以有重复的 key,否则会 SyntaxError

    举体的比对流程

  • Match value 是 mapping
  • pattern 的每个 key 有存在于 Match value
  • key 对应的 value 与 pattern 相同
  • 举个例子

    match {"name": "Bob", "age": 30, "city": "NY"}:
    case {"name": n, "age": a}:
    print(f"name={n}, age={a}")

    # name=Bob, age=30

    Class Patterns

    用于比对 class,但其比对流程相对较复杂。和 function arguments 一样,分为 positional arguments 和 keyword arguments 两种形式。

    比对流程

  • Match value 是否是 builtin type
  • 检查 Match value 是不是 Patterns 的 instance ,透过isinstance()进行检查。
  • 检查 Class Patterns 是否含有 arguments ,没有就直接比对成功
  • 如果有则分成 keyword 或 positional argument 两种情形

    • 只有 Keyword arguments:

    • 检查 attribute 是否存在于 Match value
    • 检查 attribute value 是否和 Pattern 相同
    • 成功则往下个 keyword
    • 如果有 Positional arguments:

    • 借由 Match value 的 __match_args__ attribute ,将 Positional arguments 转换为 Keyword arguments

    📌 Note

    • object.__match_args__,若没有定义,预设是一个 empty tuple ()
    • 部分 built-in types(如 bool、int、list、str 等),是比对接收 positional argument 后的整个 object。

    例子

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

    match Point(1, 2):
    case Point(x=1, y=y_value):
    print(f"Matched! y={y_value}")

    # Matched! y=2

    # positional argument
    class Point:
    # assigned a tuple of strings
    __match_args__ = ("x", "y")

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

    match Point(1, 2):
    # converted to keyword patterns using the __match_args__
    case Point(1, y_value):
    print(f"Matched! y={y_value}")

    # Matched! y=2

    结语

    算是把 match statement 完整介绍了一遍,希望下篇别拖更 ( ̄︶ ̄)↗ 如果有任何问题,欢迎在下面留言。

    References

    • https://docs.python.org/3/reference/compound_stmts.html#the-match-statement
    • https://peps.python.org/pep-0636/
    • https://peps.python.org/pep-0634/
    • https://stackoverflow.com/questions/46701063/why-doesnt-python-have-switch-case-update-match-case-syntax-was-added-to-pyt