@
lolizeppelin #77
如果只是单纯辩论美丑,我原本没打算回复,但既然你提到了 python 用装饰器来减少代码,那我觉得还是可以讨论一下的。
生命周期管理这个事情的困难在于:资源的分配和释放可能时机不确定,又或者在代码中的位置不同,还可能是多线程的主体不确定。原地释放几乎没有人会写错,而 scope 变得很大的时候,错写或者漏写的几率就会变大。
考虑 with...as 加入到 python 中已经有二十年了,但是装饰器版本的 contextmanager 也就十年,异步安全的版本可能最近才实现。这里有个很现实的问题,作为使用者是更愿意手写 enter/exit 还是愿意手动管理?当 contextmanager 加入的时候,项目允许升级吗?当有异步、多线程需求的时候,开发者能写出正确的版本吗?
所以抛开美丑的主观看法不谈,defer 这种万金油几乎是个 Go 开发者就会写,而 with...as 使用率就要大打折扣。先让人用起来显然是重要地多的事情。这里不抬杠开发者水平高低的问题,defer 是一种兜底机制,当开发者不确定我应该在什么时候释放资源的时候就可以用,而 with...as 即便是装饰器的版本显得繁琐,写好几行肯定比不上写一个单词,这是人性。
之前的回复里我总是反复提,不要单独拿某个关键词来讨论特性,那就以 with...as 来彻底完整做个说明,证明一下为什么这样做不合适。
C/Python/Java 由于诞生较早,都是先有的实现,然后反过来总结出来的标准。比较新的语言,都是设计先于实现,终归会从之前语言中总结经验教训,所以不仅仅是 Go ,你会发现 C#/Rust/Kotlin 这些“现代”语言往往有些共性,共享的是新的设计理念。
这里 defer/with...as 反映的其实是对异常处理这件事的哲学思考不同。
传统语言中,一方面异常是不区分错误和 Panic 的,另一方面异常又与控制流强耦合,try...catch 实质是隐式的 goto 指令。
现代语言几乎都放弃了这样的思路,Panic 要立即终止,而错误是可以处理的。代码层面也尽量避免隐式跳转。这样的改变是大趋势,原因是大家都从 C 语言中汲取了足够的教训,隐式控制流是有害的( hidden control flow considered harmful ),特别是与 C 的宏机制一结合,人脑 debug 几乎变得不可能。
反过来,显式控制流的优点是显而易见的,一方面极大提高了代码的可读性以及可靠性,所见即所得,不存在那些写在其他地方的代码被意外调用执行的情况;另一方面对于并行编程的支持变得更加容易。
如果拿 Go 来举例可能没有说服力,我简单说下 Rust 和 Kotlin 。
- Rust 没有 exception ,只有 recoverable/unrecoverable 错误,类型分别为 Result<T, E> 和 panic! 只有后者是宏实现的跳转
- Kotlin 保留了 try 关键词,但 try 是表达式而非(控制流)语句,也就是说 try 可以返回(错误)值
在减轻开发者心智负担方面,新的设计哲学可谓是殊途同归。这个进步主要体现在工程化层面,当语言不区分错误和 Panic 的时候,开发者会主动选择最无脑的方式,也就是随意抛出集中处理。现代语言中砍掉了这样的机制,反过来倒逼开发者思考异常处理的本质,从而间接提高代码的可复用性,以及程序的可靠性。
回到 with...as 的问题上,Python 的错误处理还是传统的控制流模式。换句话说,就算语法糖写出花来,它的核心仍旧是基于 enter/exit 的对于 RAII 的模仿。由于 Python 没有 RAII ,这种模仿的结果就是一定要手动处理 scope 的问题。
先说一个比较接近最终方案的提案版本:
with EXPR:
____BLOCK
这个版本看上去相当美好,可是没多久就被枪毙了。原因有两个,一是 with 只能是 statement 而不能是表达式(需要同时支持 with VAR = EXPR: 这样的语法,VAR = EXPR 赋值本身也是合法的 EXPR );二是 EXPR 自身有可能再次抛出异常,或者包含 break/continue 等其他控制流指令,结果会导致 scope 失效。
第二条麻烦一下解释器也不是不可以,多维护一个基于栈的 trackback 机制。但第一条的影响是致命的:
with VAR = EXPR:
____BLOCK
想要对以上形式写装饰器,__exit__ 一定要在 VAR 上有定义,这就失去了装饰器省代码的意义。
为了不让 VAR 获得 EXPR 的赋值,最终实现的版本用 with EXPR [as VAR]: 这样的形式。相关实现细节 PEP 都能搜到,这里就不继续展开了。
即便是用上装饰器,代码大概是这样的:
@
contextmanagerdef open(file):
____f = open(file)
____try:
________yield f
____finally:
____f.close()
写这么多只是为了实现:可以在 with scope 里,不用单独写 f.close()。
装饰器的实现晚于 with...as 很久才进标准库,核心原因就是前面提到的隐式控制流。装饰器本身
客观地说,Python 本来就是试错型快速迭代偏好的语言,with...as 加上迭代器的语法糖的作用非常有限。它起到的主要作用是,当你知道什么时候该写 f.close() 的时候( scope 边界清晰),不用你再写了(自定义类型还是要写装饰器)。但是解决不了辅助开发者判断,什么时候该写的问题,你无法依赖解释器帮你兜底。
尽管在站队好或者不好的事情上,网友可能会支持你也可能会反对你,但不代表大家都是出于相同的推理逻辑。在语言设计这么关键的问题上,一半是妥协一半是取舍,单拿出来一个点做比较最终只会变成屁股之争。