【流畅的Python】2.6 序列模式匹配

发布时间 2023-11-22 20:30:48作者: 马儿慢些走

2.6 序列模式匹配

这一小节围绕Python 3.10推出的模式匹配功能展开,其实就是新增的match/case语句。因为本小节属于第二章“丰富的序列”,所以这里只介绍了关于序列的模式匹配。在其他章节还有关于模式匹配更多的内容:

  • 2.6 序列模式匹配
  • 3.3 使用模式匹配处理映射
  • 5.8 模式匹配类实例
  • 18.3 lis.py中的模式匹配

Python官方文档中也有相关介绍:

相关PEP:

match/case是“软关键字”(soft keywords),这也是Python 3.10版本提出的概念。

某些标识符仅在特定上下文中被保留。 它们被称为 _软关键字_。 matchcasetype 和 _ 等标识符在特定上下文中具有关键字的语义,但这种区分是在解析器层级完成的,而不是在分词的时候。
作为软关键字,它们能够在用于相应语法的同时仍然保持与用作标识符名称的现有代码的兼容性。

意思应该是在解析器(Parser)层面,能够判断代码中的match和case是否为match/case关键字,避免和之前的代码有冲突。

示例代码:(从GitHub上下载下来看比较好)


match/case语句基础

模式匹配的用法很简单,和C语言的switch/case语句很像,但是其实更贴近Rust和Scala中的模式匹配。(这里往深了去似乎是函数式编程语言的内容)

match/case语句的语法很简单:

match 匹配对象(subject):
    case 模式(pattern):
        # 执行的过程
    case _: # 通配符,一般作为默认的模式
        # 执行的过程

匹配对象没有什么特别之处,需要注意的是字符串str、字节bytes、字节数组bytearray不作为序列处理,如果需要就得用类型转换:match tuple(xxx)。匹配对象能够支持的是collections.abc.Sequence的多数实际子类或虚拟子类的实例。

但是和拆包不同的是,match/case的析构不能匹配迭代器

模式部分有一些值得注意的:

  • 通配符:_ ,和拆包时候一样,单下划线表示统配符,所以一般放在最后一个模式。放在具体的模式中匹配序列的某个变量,那就和拆包类似。
  • as关键字:模式中本身可以给匹配到的内容(比如序列的某个元素)起名字,也可以用as再起一个。比如:case [name, _, _, (lat, lon) as coord] ,这个模式匹配一个四个元素的序列,最后一个元素中包含两个元素,所以整体是一个嵌套序列,为了这个对象析构了四个新变量。
  • 序列模式可以写成元组或列表,总之都是按照序列对待的,所以模式中写成列表,匹配对象是元组没有问题。
  • 模式中可以用“|”来添加多个选项,而且也可以和序列(列表和元组)一样进行嵌套。作者没写,但是示例中体现了:case ['3' | '4', *reset] 这里表示匹配的一个序列对象,第一个元素可以是“3”或者“4”,这是嵌套用法,也可以直接用:case '3' | '4' ,表示匹配对象可以是“3”或者“4”。
  • 使用“*”进行可迭代的析构,就和可迭代的拆包一样。case ['3' | '4', *reset] 表示从第二个元素开始,序列中的元素都被析构到reset变量。
    • 和拆包一样,只能出现一次“*”,但是可以在不同位置,避免歧义,比如case [a, *b, c]
  • 模式中可以指定匹配的类型,比如 case [str(name), _, _, (float(lat), float(lon))] ,指定了序列的第一个元素是字符串,最后一个是两个浮点数组成的序列(注意,不一定要元组,序列就可)。
    • 模式中指定类型的写法和类型转换一样,但是功能不同,针对match的匹配对象可以用此写法来转换类型。
  • case语句可以添加守卫语句(guard clause,书中翻译成“卫语句”很别扭),比如:case [str(name), _, _, (float(lat), float(lon))] if lon <= 0 。守卫语句用 if 为模式匹配增加更多的限制。

有一个概念书中没说清楚:析构。这里的析构是什么意思,和拆包的关系是什么?C++中的析构是类的析构函数,对象被销毁时候调用的函数,完全不一样。这还是作者强调的和C语言switch/case语句不同的地方。根据作者的说法,析构是一种高级的拆包形式,对于Scala和Elixir这种语言比较常见。

按照自己的理解,所谓“析构”就是“解析+构造”,比简单的拆包功能更丰富了一点(2.5节介绍了序列和可迭代对象的拆包)。所谓的“模式匹配”在这里应该和“析构”是一个意思,就是match/case的这个匹配的过程。


使用模式匹配序列实现一个解释器

这里介绍的是斯坦福Peter Norvig编写的lis.py解释器(来源于他的博客),使用Python(Python2)实现Scheme语言(Lisp语言的一种方言)的一个子集。

lis.py整体分为parse和evaluate两个函数,这是解释器的基本逻辑,解析和执行,解析是解析词法和构建语法树,执行是根据语法树求值。lis.py中大量使用if-elif-else的控制流语句和拆包来实现模式匹配,现在这些代码可以用match/case进行替代。

书中列举了两个细节:

  1. 针对lambda表达式:(lambda (params...) body1 body2...) 中的省略号后缀,这个后缀的意思是这个匿名函数(lambda表达式)的参数可以有零个或多个,所以这里需要用 *params 来析构成序列,但是后方的 body 也有多个,所以不能用多个星号来析构,所以得写成这样:case ['lambda', [*params], *body] if body 。这种写法强制把params析构为列表,即使一个元素也是列表,同时body也是列表。其实和 a,[*b],*c = [1,[2],3] 的拆包是一样的。
  2. 针对函数定义的快捷句法:(define (name param...) body1 body2...) ,name表示函数名称,param表示参数,body表示函数主体(函数主体有一个或多个表达式)。书中给出的写法:case ['define', [Symbol() as name, *params], *body] if body ,似乎没什么特别,指定了嵌套序列中第二个子项的第一项必须是Symbol类型,if body 限制了body变量不为空,即“一个或多个表达式”。

if-else的控制流更符合过程语言的逻辑,match/case更符合函数式语言的逻辑,前者更像步骤,后者更像定义。所以整体来说,对于解释器中的evaluate函数,使用match/case实现的可读性更高。

if-else写的解释器evaluate函数:

# tag::EVAL_IF_TOP[]
def evaluate(exp: Expression, env: Environment) -> Any:
    "Evaluate an expression in an environment."
    if isinstance(exp, Symbol):      # variable reference
        return env[exp]
# end::EVAL_IF_TOP[]
    elif not isinstance(exp, list):  # constant literal
        return exp
# tag::EVAL_IF_MIDDLE[]
    elif exp[0] == 'quote':          # (quote exp)
        (_, x) = exp
        return x
    elif exp[0] == 'if':             # (if test conseq alt)
        (_, test, consequence, alternative) = exp
        if evaluate(test, env):
            return evaluate(consequence, env)
        else:
            return evaluate(alternative, env)
    elif exp[0] == 'lambda':         # (lambda (parm…) body…)
        (_, parms, *body) = exp
        return Procedure(parms, body, env)
    elif exp[0] == 'define':
        (_, name, value_exp) = exp
        env[name] = evaluate(value_exp, env)
# end::EVAL_IF_MIDDLE[]
    elif exp[0] == 'set!':
        (_, name, value_exp) = exp
        env.change(name, evaluate(value_exp, env))
    else:                          # (proc arg…)
        (func_exp, *args) = exp
        proc = evaluate(func_exp, env)
        args = [evaluate(arg, env) for arg in args]
        return proc(*args)

match/case写的解释器evaluate函数:

KEYWORDS = ['quote', 'if', 'lambda', 'define', 'set!']

# tag::EVAL_MATCH_TOP[]
def evaluate(exp: Expression, env: Environment) -> Any:
    "Evaluate an expression in an environment."
    match exp:
# end::EVAL_MATCH_TOP[]
        case int(x) | float(x):
            return x
        case Symbol() as name:
            return env[name]
# tag::EVAL_MATCH_MIDDLE[]
        case ['quote', x]:  # <1>
            return x
        case ['if', test, consequence, alternative]:  # <2>
            if evaluate(test, env):
                return evaluate(consequence, env)
            else:
                return evaluate(alternative, env)
        case ['lambda', [*parms], *body] if body:  # <3>
            return Procedure(parms, body, env)
        case ['define', Symbol() as name, value_exp]:  # <4>
            env[name] = evaluate(value_exp, env)
# end::EVAL_MATCH_MIDDLE[]
        case ['define', [Symbol() as name, *parms], *body] if body:
            env[name] = Procedure(parms, body, env)
        case ['set!', Symbol() as name, value_exp]:
            env.change(name, evaluate(value_exp, env))
        case [func_exp, *args] if func_exp not in KEYWORDS:
            proc = evaluate(func_exp, env)
            values = [evaluate(arg, env) for arg in args]
            return proc(*values)
# tag::EVAL_MATCH_BOTTOM[]
        case _:  # <5>
            raise SyntaxError(lispstr(exp))
# end::EVAL_MATCH_BOTTOM[]

参考材料:

  • How to Write a (Lisp) Interpreter (in Python), Peter Norvig .