知乎热榜 ( ) • 2024-04-28 17:48
Snowflyt的回答

其实如今看来 Python 存在诸多不那么符合直觉,并且可能也不那么合理的设计(见仁见智)。

Python 在编程语言中其实处于一个中间位置:一方面它要比 C 和早期 Java 更强调一些相对现代的特性如元组、结构化绑定(序列解包)、基本的函数式编程支持等;但另一方面仍整体作为一门非常“命令式”而非函数式的编程语言,主流用户通常使用类 C 的控制流和可变变量与容器来实现各种操作,对函数式编程的支持实际比较有限、变量只拥有函数作用域而非块级作用域(除 try catch)、global 与 nonlocal 并不统一、从 Perl 等早期动态语言继承下来的对全局特殊变量的大量应用(如 __name____all__),略显怪异和原始的模块机制等。

你可以说 Python 这个位置有些尴尬,有些符合一门现代编程语言该有的样子,但又不完全是——但这也使 Python 更容易被有命令式编程语言背景的程序员接受,如 C 和 Java 程序员通常可以在学习一些简单语法后就把 Python 当成更方便的 Java 来用,同时 Python 自身提供的巨大标准库也在其推广上起了很大作用。

Python 并不是一门年轻的编程语言,其很多似乎怪异的特性甚至可以追溯到 Python 1,那个 Java 都尚未出现的时代,那时流行的还是 Perl 和 C,所以 Python 自然从它们中继承了一些思路。像是 Perl 就给获取数组长度做了个语法糖可以直接通过 @arr 获取数组长度,Python 可能也从中得到了一些启发所以设计了这个 len(). 在我能找到的最早的 Python 0.9.1 的档案(1991)中,这个语法就已经存在了:

我在这里没什么根据地猜测这个 len() 可能受到了 Perl 的一定影响。毕竟当时的 Python 虽然已经有了 class with inheritance,但似乎仍主要定位于一门用于取代 Shell 的做快速原型验证的编程语言:

Python can be used instead of shell, Awk or Perl scripts, to write prototypes of real applications, or as an extension language of large systems, you name it.

所以 Python 采用了当时主流动态语言 Perl 的一些设计大概并不奇怪,影响 Python 初版较大的几门编程语言如 ABC 也采用了类似的设计(#s),C 和 C++ 的 array 也是用 sizeof 获取数组长度的,可以说当年采用 .length.size 这种语法的编程语言反而不主流。而 Java 的发布在 1995 年,显然晚于 1991 年,而 C++ 的 Vector 则要到 1998 年有了 STL 才出来(即使 STL 有初步雏形的时间也不会早于 1991 年)。就算你硬要问那为什么当时 Python 不用 Ada 的 A'LENGTH 而用 len(lst),那大概也只能说是开发者的个人喜好问题了,或许只是 Guido C 用习惯了觉得这么写看着顺眼?谁知道呢。

如果抛开历史问题硬要找解释,Fluent Python 倒是给了个看上去挺令人信服的说法:

1.5 len 为什么不是方法
这个问题我在 2013 年问过核心开发人员 Raymond Hettinger,他在回答时引用了《Python 之禅》中的一句话,道出了玄机:“实用胜过纯粹。”1.3 节说过,当 x 是内置类型的实例时,len(x) 运行速度非常快。计算 CPython 内置对象的长度时不调用任何方法,而是直接读取 C 语言结构体中的字段。获取容器中的项数是一项常见操作,strlistmemoryview 等各种基本的容器类型必须高效完成工作。
换句话说,len 之所以不作为方法调用,是因为它经过了特殊处理,被当作 Python 数据模型的一部分,就像 abs 函数一样。但是,借助特殊方法 __len__,也可让 len 适用于自定义对象。这是一种相对公平的折中方案,既满足了内置对象对速度的要求,又保证了语言的一致性。这也体现了《Python 之禅》中的另一句话:“特殊情况不是打破规则的理由。”

这个说法有一定道理,但很难说是 Python 必须这么做的理由。Python 当然也完全可以给解释器开个洞,就像 Java 给 .class 开了个洞当作字面量用一样,给 .len 之类的属性做个特殊处理。反正归根结底,Python 已经形成了用魔术方法提升内置容器性能的惯例,说 len() 是为了保持此种惯例的一致性这个解释至少可以说得通。

另外 Python 本身受到一些数学背景的影响,也有些语法借鉴于 Haskell,如果把 len(arr) 看成一个一元运算符而不是个函数调用,或许更能够理解为什么 Guido 更喜好这个语法——毕竟伪代码里也经常使用 @arr 获取数组长度的,我倒不清楚到底是伪代码的这个惯例影响了 Perl 还是 Perl 影响了这种伪代码写法。

不过说实在话,len(arr) 这个语法着实给很长一段时间内的 Type hints 造成了一定困扰。像是 __len__ 这种魔术方法显然要归属于结构化类型(或者说鸭子类型)的特性,毕竟持有这些方法的类不需要实际继承某个类或实现某个接口才能工作。但在 Python 3.8 引入 Protocol 真正允许用户定义的结构化类型之前,要给众多魔术方法做上类型支持就是个苦差事了,引入了 typing.Sized 表示支持 __len__() 的类型、typing.Container 表示支持 __container__() 的类型等,并且当时的静态类型检查器还需要给这些类型做特殊支持——后来这些 typing 中的类型还在 Python 3.9 里废弃了并推荐去使用 collections.abc 里的同名类型。现在倒是不复杂了,用户可以直接用 Protocol 自定义结构化类型了,以后新增魔术方法也不一定需要添个对应类型了。

同理还有像是 __all__ 这样的特殊变量,静态类型检查器和 Linter 等都需要做特殊处理来解析它们的。说实话我个人不太喜欢 Python 的这类做法,引入这些有特殊行为的变量或方法在运行时上是很简单,但对于静态检查却增加了不少麻烦——不过这倒是当时不少动态类型语言的主流做法。

很多人都喜欢从 Python 之禅上给 Python 的很多行为找原因,其实背后没那么多原因,很多就是当初脑袋一抽想出来的做法结果后来想改发现晚了,或者就纯粹源于一些个人喜好。Python 也是个具有巨型标准库的编程语言,是社区许多人共同实现的成果,其中因为历史限制等种种原因不可避免地设计失败的 API 也可以说数不胜数,没什么必要都去给它们找个理由。

另外尽管如此我还是不支持用 arr.__len__() 代替 len(arr) 的,这些魔术方法按惯例还是适合元编程的时候用,至少还是遵守下 PEP 8 吧。