CPython开发实战:实现None感知运算符?.和??

前几天在群里吹牛的时候,有位群友突然提到了PEP505,并表现的非常激动。这份PEP也引起的群友广泛关注,并表示支持这份草案。

PEP505是关于Python的None感知运算符(None-aware operator)的。

这种运算符在其它语言中,尤其是动态语言中,非常常见。它的用法是判断一个对象是否为空,如果是空则无视,否则将继续执行。拿Dart举例:

dart 复制代码
void getUserAge(String username) async {
  final request = UserRequest(username);
  final response = await request.get();
  User user = new User.fromResponse(response);

  // delightfully shorter null check
  this.userAge = user?.age ?? 18; 
  // etc.
}

这个例子中如果usernull则返回18,否则返回userage字段。这样做可以保证userAge永远不会为null

这份PEP还细分了三种None感知运算符:None合并运算符(??)、None感知属性获取运算符(?.)和None感知索引运算符(?[])。

None合并运算符(??) :这是一个二元运算符,如果前值为None则返回后值,否则返回前值。比如:

css 复制代码
a = b ?? c

如果b为None则a被赋值为c,否则为b。

None感知属性获取运算符(?.) :用在对象的后面,如果该对象不为None,则获取它的属性。PEP内没有解释对象为None的情况,我的理解是必须配合None合并运算符使用。比如:

css 复制代码
a = b?.c ?? d

如果b为None,则a赋值为d,否则为b的c成员。当然b的c成员也可能为None,甚至触发AttributeError异常。

None感知索引运算符(?[]) :用在可下标对象的后面,如果该对象不为None,则获取它的下标。PEP内同样也没有解释对象为None的情况,我的理解是也必须配合None合并运算符使用。比如:

css 复制代码
a = b?[4] ?? c

如果b为None,则a赋值为c,否则为b[4]。同样这里也有可能触发IndexError异常。

然后,这份PEP列出了语法规则,并详细的解释了三种运算符。最后给出了几个例子,表明接受这份PEP的必要性。

看完这份PEP,我决定简单的实现一下。功能不会像PEP内那么复杂,也不会涉及CPython的后端。

准备工作在之前的一系列文章中阐明了,可以参考该系列文章。后续步骤的原理部分同样也可以在这系列文章中找到答案。

1. 在Grammar/Tokens文件中添加以下代码

arduino 复制代码
QUESTDOT                '?.'
DUALQUEST               '??'

本次实战只实现None合并运算符和None感知属性获取运算符,所以需要添加两个符号。

2. 在Grammar/python.gram文件中在primary语法规则下添加以下代码

css 复制代码
    | a=primary '?.' b=NAME '??' c=primary { _PyAST_IfExp(a, _PyAST_Attribute(a, b->v.Name.id, Load, EXTRA), c, EXTRA) }

现在的primary语法规则是这样的:

css 复制代码
primary[expr_ty]:
    | a=primary '.' b=NAME { _PyAST_Attribute(a, b->v.Name.id, Load, EXTRA) }
    | a=primary '?.' b=NAME '??' c=primary { _PyAST_IfExp(a, _PyAST_Attribute(a, b->v.Name.id, Load, EXTRA), c, EXTRA) }
    | a=primary b=genexp { _PyAST_Call(a, CHECK(asdl_expr_seq*, (asdl_expr_seq*)_PyPegen_singleton_seq(p, b)), NULL, EXTRA) }
    | a=primary '(' b=[arguments] ')' {
        _PyAST_Call(a,
                 (b) ? ((expr_ty) b)->v.Call.args : NULL,
                 (b) ? ((expr_ty) b)->v.Call.keywords : NULL,
                 EXTRA) }
    | a=primary '[' b=slices ']' { _PyAST_Subscript(a, b, Load, EXTRA) }
    | atom

在Python中,primary元素是指最小不可分割的表达式,它具体表现为对象的属性(obj.something.something)、对象的下标(obj[something])、对象的调用(obj(something))或者仅仅是该对象(obj)。因此,None感知运算符在这里添加位最优。

None感知运算符本质上是Python的语法糖。为了方便,本次实战不会重新定义AST结构,而是复用Python的If表达式的AST。

实际上,None感知运算符可以通过If表达式转变,比如:

css 复制代码
a ?. b ?? c

可以完全用If表达式描述:

css 复制代码
a.b if a else c

因此,语法规则的实现部分直接调用IfExp的函数_PyAST_IfExp。该函数接受四个参数,第一个为test部分,即a;第二个为body部分,即a.b;第三个为else部分,即c;第四个为extra,代表代码上下文变量,包括代码行号,列号等。

3.在命令行中运行PCBuild/build.bat生成Python程序

该命令将会根据语法规则生成相应的语法分析器,并编译生成Python程序。生成的Python程序在PCbuild目录内。

接下来简单的测试一下:

可以看到a为有bar方法的对象Foo,b为None。如果执行a?.bar??'default value'则会返回a.bar,而如果执行 b?.bar??'default value'则会返回default value。结果是符合预期的。

现在回到PEP505。这份PEP其实已经deferred了!

原因没有直接说,但是在这版discuss上有core dev透露了部分信息。

  1. 问号(?)是仅剩最后几个ASCII符号了,是否值得用有待商榷。

这个理由没有直接回答defer的原因,反而转移到另一个话题,就是None在Python中是否足够特殊,以至于需要单独的运算符处理。不同的人持不同观点,这个很难有定论。

但是话又说回来,Python已经有Optional这个typing的先例了。某种意义上来讲,core dev承认None的特殊性。

  1. 这篇PEP,包括PEP532,设计过于复杂。Python的发展应该基于已有的语言规则,而不是重新定义一套规则。

PEP532是关于判断空值的魔术方法的提议,可以配合PEP505使用,但这种方式确实复杂。我只是利用了Python的If表达式简单的实现了这个功能,如果要完整实现PEP532和PEP505肯定不只是前端改造,还会涉及AST结构,字节码,PyTypeObject结构的修改。这种修改可能会影响运行性能,引入BUG,造成易用性等问题。作为一向保守的core dev肯定不会接受那么大的改动的。

相关推荐
denghai邓海1 分钟前
红黑树删除之向上调整
python·b+树
封步宇AIGC27 分钟前
量化交易系统开发-实时行情自动化交易-3.4.1.2.A股交易数据
人工智能·python·机器学习·数据挖掘
何曾参静谧27 分钟前
「Py」Python基础篇 之 Python都可以做哪些自动化?
开发语言·python·自动化
Prejudices31 分钟前
C++如何调用Python脚本
开发语言·c++·python
我狠狠地刷刷刷刷刷44 分钟前
中文分词模拟器
开发语言·python·算法
Jam-Young1 小时前
Python的装饰器
开发语言·python
man20171 小时前
【2024最新】基于springboot+vue的闲一品交易平台lw+ppt
vue.js·spring boot·后端
Mr.咕咕1 小时前
Django 搭建数据管理web——商品管理
前端·python·django
hlsd#1 小时前
关于 SpringBoot 时间处理的总结
java·spring boot·后端
路在脚下@1 小时前
Spring Boot 的核心原理和工作机制
java·spring boot·后端