谁喜欢 Python 啊,反正我不喜欢。
缩进噩梦 ¶
如果要说 Python 给一个普通人(不是语言律师或者数学家)带来的最大印象是什么,我想缩进决定代码块绝对是其中之一。
喜欢 Python 的人会觉得这种缩进非常的方便,少敲一个大括号,还不用在文件末尾看到 }}}}}
这样的经典 lisp 笑话(?)。但是稍微深入一点就会意识到这种缩进方式给 Python 带来了很大的先天缺陷。
Python 的缩进在发明之出或许是友好的,毕竟它强迫初学者写出“能看”的代码,便于老手查看,而无须跑 formatter。(并且据 Python 发明者所说,当年的格式化工具并没有现在好用)。但是现在的 formatter 技术已经非常成熟,为 JavaScript 设计的格式化工具随便就能说出五六种。缩进给 Python 带来的益处已经消失不见了,反而带来无数的问题。
最简单的,编辑是否方便的问题。别人写好的 Python 代码片段难以直接复制粘贴到编辑器中,你得先调好缩进。这也反应在现代编辑器中,想象你有这段代码:
|
|
在 VSCode 中你想要上下移动 a = 2
非常方便,只需要 Alt + ↑ / ↓ 就可以了。因为作用域是被括号明确标出的,编辑器很容易知道当 a = 2
移下两行的时候进入了另一个块中,可以自动调整这一行的缩进。但 Python 就不行,编辑器根本就看不出你是否打算让这一行进入另一个块中。这导致编码的时候你必须和缩进斗智斗勇,而不是你尽管写,由编辑器帮你跑 formatter,回车一下就能看到美观的代码。
其次,这直接导致 Python 先天性地难以应用函数式思想。众所周知,Python 的 lambda 函数几乎是个废物,你必须控制 lambda 函数在一行以内
lambda x : x + 1
很显然,抛开 Python 设计者看不起 lambda 函数不谈,python 缩进决定层次的思想就直接重创了 lambda 函数,因为 lambda 函数很难决定缩进位置。[1] 而 lambda 函数在函数式编程范式的美观性上极其重要,python 在这里直接缺一个
举个例子,工程上很容易遇到类似这样的例子:需要把一个数组 filter 几下,map 几下,得到新的数组,举个例子
const data = dataRaw
.filter((x) => x)
.map((x) => {
// balabala...
// something...
return y;
});
这在 python 中必须要费力定义一个映射函数来用,因为你根本就没法在 lambda 函数里写多行!
这在静态语言中都没这么致命,起码,要在外面定义一个函数,你得定义 arguments 的类型和 return 的类型,这样你起码知道一个函数传入了啥,返回啥
而 lambda 函数巧妙避免了这个问题,因为调用链上每一个函数的类型都是可以肉眼看出的,并且肉眼看不出还可以自动推导。
但是在 Python 里你必须得把这个函数拆出去,相当于分离了背景和逻辑。这个坑已经在 js 里犯过了,在 js 早期的“面向对象”中 this
完全就是梦魇,你根本就不知道 this 是个啥;在 Python 中这个问题以另一种方式体现,这些“本来没有被拆分的必要但是被拆分”的函数你难以肉眼瞪出参数和返回值。它不仅平添了代码的复杂性,让代码变得更丑陋,而且还更容易出错。
糟糕的设计 ¶
Digg’s v4 launch: an optimism born of necessity. | Irrational Exuberance 讲述了当年曾可以与 Reddit 匹敌的科技网站 Digg,因为 Python 默认参数的特性导致了一个内存泄露问题,让 Digg 的用户迅速流失,最终在一年以后被 Beatworks 以 50 万美金的价格收购的故事。
这个特性几乎可以说是 Python 独家。只要看下面的 REPL 过程,你就知道这是什么了:
>>> def push1(arr = []):
... arr.append(1)
... return arr
...
>>> push1()
[1]
>>> push1()
[1, 1]
>>> push1()
[1, 1, 1]
>>> push1()
[1, 1, 1, 1]
Python 选择了一种很蠢的默认传参方式,默认参数始终共用同一个对象。这在别的语言看来几乎是匪夷所思的,只有 Python 必须要写出这样的傻 x 代码来传入默认参数:
def push1(arr=None):
if arr is None:
arr = []
...
繁琐又愚蠢,完全不符合 DWIM 的思想。
这可能只是 Python 糟糕设计的冰山一角。在我看来 Python 当得上最死板最没有表现力的流行语言的称号。(也可能要和 Java 竞争,不过我不写 Java,谁知道呢)。实际上,Python 整体就体现出一种思维,程序员不值钱,不需要有自己的思想,只需要把思路翻译成 Python 代码让计算机跑就好了。可能这种特性让 Python 作为教学语言会很合适,但是真的不适合成为最流行的语言之一。
Python 的风格死板又诡异。既试图融入一些面向对象的元素,又摆脱不了过程式的影子,还想融一点函数式的元素。可以从简单的 len()
函数就能看出来,长度应该是某个对象的属性,但是 Python 选择用一个全局函数(What the fuck?)len
去处理。但是 Python 又确实提供了面向对象风格的类。Python 提供了函数式的 map
,却对 lambda 表达式嗤之以鼻。这些都让 Python 代码充满了一种诡异的不协调感。
有点极端的反面教材是 Ruby,以自由浪漫闻名的同样的动态语言。Rails 信条如是说:
是 Ruby 造就了 Rails,所以第一条信条便是从创造 Ruby 的核心理念所提炼出來。
早期 Ruby 的极端邪说就是把程序员的幸福度放到第一位。还把追求幸福置于驱动编程语言与生态圈前进的考量之上。
然而 Python 可能对于“用一种方法,最好只有一种方法来完成一件事”而感到自豪,而 Ruby 则喜欢自身表现力与巧妙。Java 是饱受软件工程师的强力推崇,Ruby 则在欢迎工具里就附上了自尽的绳子。Smalltalk 专注于消息传递的纯粹性,Ruby 则累积关键字和臃肿的语法构造。
举个例子,这是一段来自 discourse/discourse 的 Ruby 代码。它要做的就是定义一个 TrashMessage 的过程。
class TrashMessage
include Service::Base
params do
attribute :message_id, :integer
attribute :channel_id, :integer
validates :message_id, presence: true
validates :channel_id, presence: true
end
model :message
policy :invalid_access
transaction do
step :trash_message
step :destroy_notifications
step :update_last_message_ids
step :update_tracking_state
step :update_thread_reply_cache
end
step :publish_events
private
def fetch_message(params:)
...
end
Ruby 的放纵允许程序员定义出向上面这样离经叛道的写法,直接把步骤写成任务列表的格式,然后在按照列表里定义的名字编写每个小任务的函数。这使得 Ruby 程序非常富有表现力,可能看上去非常天马行空。
相比而言,Python 的语法就死板得完全不懂变通。
糟糕的……大儒 ¶
“入关之后,自有大儒为我辩经”
我还是很尊重真心的语言爱好者的,但是当谈论起一些流行语言——不只是 Python 的时候,总是有一种自有大儒为我辩经的既视感
Python 采用缩进?大儒们会说这是深思熟虑的选择,非常方便,大括号多么无用(哪怕 Python 之父后来自己承认这是一个败笔)Python 有意忽视 lambda 函数?大儒们会说又不是不能定义一个带名字的函数。反正大儒们终会找到理由。他们还有终极武器:Python 那么多库本身就说明搞底层的大佬接受这个语言的逻辑!
(那还有银行在用 1960s 的 COBOL 呢,怎么,是因为 COBOL 设计得让它们爱不释手吗?)
或者说大众语言本身是一种不好的话语权。如果大家都在用,那怕这语言设计得跟屎一样,你也得硬吃。典型的例子就是 Java 以后的语言花了 30 年的时间弥补 Java 的各种不足。典型的例子就是现代前端框架已经演变了好几代了还有人抱着 PHP 大喊 PHP 是世界上最好的语言,转手就写出一堆 SQL 注入的程序。
实际上这是可以解决的,经典函数式语言 Haskell 就是使用的空白字符缩进。但是 Haskell 在多行 lambda 函数上也同样有游标卡尺的问题,而且 Haskell 支持显式使用大括号缩进,也可以用更加函数式的方式规避这个问题。 ↩︎