前几天在群里吹牛的时候,有位群友突然提到了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.
}
这个例子中如果user
为null
则返回18,否则返回user
的age
字段。这样做可以保证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透露了部分信息。
- 问号(?)是仅剩最后几个ASCII符号了,是否值得用有待商榷。
这个理由没有直接回答defer的原因,反而转移到另一个话题,就是None在Python中是否足够特殊,以至于需要单独的运算符处理。不同的人持不同观点,这个很难有定论。
但是话又说回来,Python已经有Optional这个typing的先例了。某种意义上来讲,core dev承认None的特殊性。
- 这篇PEP,包括PEP532,设计过于复杂。Python的发展应该基于已有的语言规则,而不是重新定义一套规则。
PEP532是关于判断空值的魔术方法的提议,可以配合PEP505使用,但这种方式确实复杂。我只是利用了Python的If表达式简单的实现了这个功能,如果要完整实现PEP532和PEP505肯定不只是前端改造,还会涉及AST结构,字节码,PyTypeObject
结构的修改。这种修改可能会影响运行性能,引入BUG,造成易用性等问题。作为一向保守的core dev肯定不会接受那么大的改动的。