首页 > 编程语言 >【流畅的Python】2.6 序列模式匹配

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

时间:2023-11-22 20:33:54浏览次数:38  
标签:case body env Python evaluate match exp 2.6 模式匹配

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 .

标签:case,body,env,Python,evaluate,match,exp,2.6,模式匹配
From: https://www.cnblogs.com/xiaoma2018/p/fluent_python_2rd_2_6.html

相关文章

  • python tkinter text用法
    >>>fromtkinterimport*>>>root=Tk()>>>text=Text(root,width=40,height=10)  #创建一个text文本框。长度是40pixel高度是10pixel>>>text.pack()                     #排版>>>text.config(wrap='word......
  • 详解Python单下划线和双下划线
    一、单下划线1.命名约定。在Python中,单下划线作为命名约定,用于表示某个变量、方法或属性是内部使用的或具有特定含义,但不希望在外部直接访问。2.临时变量。在一些情况下,我们可能只需要临时使用某个变量,而不关心它的具体值。此时,可以使用单下划线作为变量名,表示它是一个无关紧要......
  • python爬虫多个页面数据代码详解
    下面是一个简单的Python网页爬虫程序,可以用于爬取多个页面的数据:importrequestsfrombs4importBeautifulSoup#获取网页内容的函数defget_html(url):try:r=requests.get(url)r.raise_for_status()r.encoding=r.apparent_encoding......
  • python tkinter treeview 操作示例
    1.建立Treeviewfromtkinterimport*fromtkinter.ttkimport*root=Tk()#建立Treeviewcolumns=(('ID',50),('S_ID',50),('S_NAME',120),('B_NAME',120),('Date_Taken',100),......
  • python字典中删除键值的方法
    一、pop()方法删除keyPython字典是一种无序的映射数据类型,通过键值对的形式进行存储,可以使用键来快速找到对应的值。在某些情况下,我们可能需要在字典中删除某个键,这时候就可以使用Python字典提供的pop()方法。pop()方法用于删除字典中指定的键,并返回该键对应的值。使用该方法时......
  • python中四种方法提升数据处理的速度
    在数据科学计算、机器学习、以及深度学习领域,Python是最受欢迎的语言。Python在数据科学领域,有非常丰富的包可以选择,numpy、scipy、pandas、scikit-learn、matplotlib。但这些库都仅仅受限于单机运算,当数据量很大时,比如50GB甚至500GB的数据集,这些库的处理能力都显得捉襟见肘,打......
  • python中常见函数
    filter,reduce,和map是Python中用于对集合数据进行处理和转换的内置函数。它们分别用于筛选、归约和映射集合中的元素。filter函数:filter(function,iterable)用于筛选集合中的元素。它接受一个函数function和一个可迭代的对象iterable,并返回一个包含iterable中满足......
  • python多线程中一种错误的写法
    直接先上错误代码:importmultiprocessingdeffirst_way():init=3defprocess_function(item):result=item*initreturnresultdata=[1,2,3,4,5,6,7,8,9,10]pool=multiprocessing.Pool(processes=4)#创建一个......
  • Python基础知识
    一、先置知识1、标识符标识符由字母、数字、下划线组成。所有标识符可以包括英文、数字以及下划线(_),但不能以数字开头。标识符是区分大小写的。以下划线开头的标识符是有特殊意义的。以单下划线开头_foo的代表不能直接访问的类属性,需通过类提供的接口进行访问,不能用**fr......
  • 如何在Python中向一个集合添加值
    用Set.add()函数向一个集合只添加一个值从数学上讲,集合是一个在逻辑上有联系的不同对象的集合。在Python中,集合是一个内置的数据类型,它是无索引的和不可变的。这意味着我们可以通过一些特定的索引来访问集合项,而且我们不能修改集合内的现有数据。我们可以通过在Python中创建一个......