这是我在 python-ideas 上发布的东西,但我认为它对更多人来说是有趣的。
最近有很多关于合并两个数据的操作符的讨论。
这促使我思考(一些)人喜欢操作符的原因,我想到了30多年前与我的导师Lambert Meertens的讨论。
对于数学家来说,运算符对他们的思维方式至关重要。以两个数字相加这样一个简单的操作为例,试着探索它的一些行为。
add(x, y) == add(y, x) (1)
方程(1)表达了加法是交换性的规律。它通常用运算符来写,这样就更简洁了:
x + y == y + x (1a)
这感觉是一个小小的收获。
现在考虑关联法:
add(x, add(y, z)) == add(add(x, y), z) (2)
公式(2)可以用运算符重写。
x + (y + z) == (x + y) + z (2a)
这比(2)要简单得多,并导致观察到括号是多余的,所以现在我们可以写
x + y + z (3)
,没有歧义(+运算符是向左还是向右结合得更紧并不重要)。
许多其他定律也可以用运算符更容易地写出来。这里还有一个例子,关于加法的同一元素。
add(x, 0) == add(0, x) == x (4)
比较
x + 0 == 0 + x == x(4a)
这里的总体想法是,一旦你学会了这种简单的符号,用它们写的方程就比用函数符号写的方程更容易*操作--就好像我们的大脑用不同的大脑机器来掌握运算符,而这是更有效率。
我认为,使用运算符写的公式更容易在视觉上进行处理,这与此有关:它们让大脑的视觉处理机器参与其中,该机器主要在潜意识中运作,并告诉意识部分它看到了什么(例如,"椅子 "而不是 "连接在一起的木片")。功能记号必须通过我们的大脑采取不同的路径,这是不那么下意识的(它与阅读和理解你所读的东西有关,这是在比视觉处理更晚的年龄学习/训练的)。
当你结合多个运算符时,视觉处理的力量才真正变得明显。例如,考虑分配律:
mul(n, add(x, y)) == add(mul(n, x), mul(n, y)) (5)
写起来很痛苦,我相信一开始你不会看到这个规律(或者至少如果我没有提到这是分配律的话,你不会马上看到)。
对比一下:
n * (x + y) == n * x + n * y (5a)
注意这也使用了相对运算符的优先级。通常数学家会把这个写得更紧凑:
n(x+y) == nx + ny (5b)
,但可惜的是,这目前已经超出了Python解析器的能力。
操作符符号的另一个非常强大的方面是,将它们应用于不同类型的对象是很方便的。例如,当x、y和z是相同大小的向量并且n是标量时,定律(1)到(5)也起作用(用一个零的向量代替字面的 "0"),如果它们是矩阵的话(同样,n必须是个标量)。
而且你可以对许多不同领域的对象进行这样的处理。例如,上述定律(1)到(5)也适用于函数(n还是标量)。
通过明智地选择运算符,数学家可以运用他们的视觉大脑来帮助他们更好地做数学:他们会更快地发现新的有趣的定律,因为有时黑板上的符号就会跳到你面前,暗示你通往一个难以捉摸的证明的路径。
现在,编程并不完全是与数学相同的活动,但我们都知道可读性很重要,这就是Python中运算符重载的作用。一旦你内化了运算符所具有的简单属性,使用 + 进行字符串或列表连接会比纯 OO 符号更易读,上面的 (2) 和 (3) 解释了 (部分) 为什么会这样。
当然,也有可能过度使用--那么你就会得到Perl。但我认为,那些指出 "已经有一种方法可以做到这一点 "的人忽略了一点,即与此相比,
d = d1 + d2
的意思确实更容易掌握:
d = d1.copy()
d.update(d2) # 更正了。这一行之前是错误的
,这不仅仅是减少代码行数的问题:第一种形式允许我们使用我们的视觉处理来帮助我们更快地看到意义--而且不会分散我们大脑的其他部分(例如,这些部分可能已经被跟踪d1和d2的意义所占据)。
当然,任何事情都是有代价的。你必须学习运算符,而且你必须学习它们应用于不同对象类型时的属性。(在数学中也是如此--对于数字来说,x*y == y*x,但这个属性不适用于函数或矩阵;相反,x+y == y+x适用于所有对象,正如关联法则一样。)
"但性能如何?"我听到你问。好问题。在我看来,可读性是第一位的,性能次之。在基本的例子中(d = d1 + d2),与使用更新的双行版本相比,没有任何性能损失,而且在可读性方面明显获胜。我可以想到很多情况,性能差异无关紧要,但可读性是最重要的,对我来说,这是默认的假设(即使在Dropbox -- 我们最关键的性能代码已经用丑陋的Python或Go重写了)。对于少数对性能要求很高的情况,很容易将运算符的版本转换为其他东西 -- 一旦你确认需要它*(可能是通过分析)。