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肯定不会接受那么大的改动的。

相关推荐
毕设源码_钟学姐8 分钟前
计算机毕业设计springboot宿舍管理信息系统 基于Spring Boot的高校宿舍管理平台设计与实现 Spring Boot框架下的宿舍管理系统开发
spring boot·后端·课程设计
运器1239 分钟前
【一起来学AI大模型】PyTorch DataLoader 实战指南
大数据·人工智能·pytorch·python·深度学习·ai·ai编程
音元系统12 分钟前
Copilot 在 VS Code 中的免费替代方案
python·github·copilot
超龄超能程序猿23 分钟前
(5)机器学习小白入门 YOLOv:数据需求与图像不足应对策略
人工智能·python·机器学习·numpy·pandas·scipy
方圆想当图灵1 小时前
ScheduledFutureTask 踩坑实录
后端
全栈凯哥1 小时前
16.Spring Boot 国际化完全指南
java·spring boot·后端
M1A11 小时前
Java集合框架深度解析:LinkedList vs ArrayList 的对决
java·后端
31535669132 小时前
Springboot实现一个接口加密
后端
cooldream20092 小时前
Python 包管理新时代:深入了解 `uv` 的使用与实践
python·uv·包管理器
之歆2 小时前
Python-魔术方法-创建、初始化与销毁-hash-bool-可视化-运算符重载-容器和大小-可调用对象-上下文管理-反射-描述器-二分-学习笔记
笔记·python·学习