1. 程式人生 > >程式語言之問:何時該借用,何時該創造?

程式語言之問:何時該借用,何時該創造?

本文原創並首發於公眾號【Python貓】,未經授權,請勿轉載。

原文地址:https://mp.weixin.qq.com/s/OypPwnJ2vX2vJtZRkVa-Ug

6 月 22 日,Python 之父 Guido 發了一條推特,說了 Python 的一則歷史故事,他說 elif 是從 C 語言中偷過來的:

elif 是“else if”的簡寫,用於條件判斷。當只有兩個分支時,我們會寫成“if...else...”,當出現更多分支時,我們會寫成如下格式:

if 判斷條件1:
    做事情1
elif 判斷條件2:
    做事情2
else:
    做其它事

簡寫而成的 elif 不僅是減少了幾個字元,而且由於單一而清晰的用途,它還不會給我們帶來理解或使用上的困惑。

但是,簡寫法並不是主流,完整寫法才是主流,C 語言中就是採用完整的寫法:

if(判斷條件1)
{
   做事情1
}
else if(判斷條件2)
{
   做事情2
}
else 
{
   做其它事
}

沒錯,C 語言使用的是全拼寫法,但是在它的預處理/預編譯語句中,還有一個 elif 指令,Guido 所說的“偷”,就是從這來的:

#if 常量表達式1
// 編譯1
#elif 常量表達式2
// 編譯2
#else
// 編譯3
#endif

Python 沒有預編譯,所以所謂的偷,跟預編譯沒有關係,只是在對比兩種寫法後,借用了更簡潔的寫法而已。

為什麼 C 語言不把兩種寫法統一起來呢?這我不得而知了,而 Guido 在兩種寫法中,選擇了後一種非主流卻更好用的寫法。我想對他說,你“偷”得好啊!

實際上,留言區裡的人也有同感,紛紛表示:不介意、很 okay、非常喜歡,還有人說“不是偷,而是收穫(harvested)”、“不是偷,而是把它提升了一些高度”……

前不久,我寫了一篇《聊聊 print 的前世今生》,print 這個詞就是從 C 語言中借用來的。除此之外,如果有人仔細比較這兩種語言的關鍵字和習慣命名,肯定會發現不少相同的內容。

程式語言間有一些共享的元素,這很常見,創造一門語言並不意味著要原創每一個詞句,畢竟大部分思想是共通的,作為基礎設施的詞語更是如此。

那麼,我突然好奇了:創造一門程式語言時,什麼時候該借用,什麼時候該創造呢?

這個問題看起來可能沒啥意義,因為終其一生,我們多數人也不大可能會參與創造一門程式語言。

但我覺得它還是極有意義的,首先,提問精神值得肯定,其次,它還提供了一種溯源、甄別、遴選、創造的體系性視角,我認為這是求知的正確思維方式。

帶著這個疑惑,我特別想要考察的是 Python 的 for 迴圈。

如果你有其它語言基礎,就知道 “for 迴圈”通常指的是這樣的三段式結構:

for ( init; condition; increment ){
   statement(s);
}

// java
for(int x = 10; x < 20; x = x+1) {
    System.out.print("value of x : " + x );
    System.out.print("\n");
}

這種 C 風格的寫法是很初級的東西,不少語言都借用了。但是,它的寫法實在繁瑣,為了更方便地遍歷集合中的元素,人們在 for 迴圈之外又引入了升級版的 foreach 迴圈:

// java
int[] a = {1,2,3};
for(int i : a){
    System.out.print(i + ",");
}

// C#
int[] a = {1,2,3};
foreach(int i in a){
    System.Console.WriteLine(i);
}

Python 中也有 for 迴圈,但是,它借用有度,在設計上早早就有自己獨到的考慮,它直接摒棄了三段式的 for 迴圈,而是採用類似 foreach 的一種寫法:

for iterating_var in sequence:
   statements(s)

# 例子
for i in range(3):
    print(i)

for i in "hello":
    print(i)

從表面上看,Python 的 for 迴圈跟其它語言的 foreach 很相似,但實際上,它的工作原理卻很不相同。

為什麼會有不同呢?主要是因為 Python 的 for 語句用於可迭代物件上,而不僅僅是用於集合或者普通的容器(雖然它們也是可迭代物件),而可迭代物件還可再細分出迭代器與生成器,這會造成最終結果的極大差異。

先看看兩個例子:

# 例1,普通可迭代物件
x = [1, 2, 3]
for i in x:
    print(i)
for i in x:
    print(i)

# 例2,迭代器或生成器
y = iter([1, 2, 3])
# y = (i for i in [1,2,3])
for i in y:
    print(i)
for i in y:
    print(i)

例 1 中,“1 2 3”會被列印兩次,而在例 2 中,則只會列印一次。

普通可迭代物件只有 __iter__() 魔術方法,而不像迭代器一樣擁有 __next__() 魔術方法,這意味著它無法實現 自遍歷 過程,同時在經過 for 迴圈的 它遍歷 後,也不會破壞原有的結構。(這兩個是我創造的概念,詳見《Python進階:迭代器與迭代器切片》)。

但是,迭代器是一種匱乏的設計,具有單向損耗的特性,遍歷一次後就會被破壞掉,不能重複利用。(關於迭代器的設計問題,這篇文章值得一看《當談論迭代器時,我談些什麼?》)。

這表明了,Python 中 for 迴圈的使用場景很廣闊,而且它還可能帶來非純結果,即重複執行同樣的程式碼塊,會出現不同的結果。

這是不是跟別的語言很不同了呢?相同的關鍵字,相似的迴圈思想與寫法,但是,帶來的影響卻有差別。

關於 Python 的 for 迴圈,還有一個很獨特的設計,即 for-else 結構:

x = [1, 2, 3]
for i in x:
    print(i, end = " ")
else:
    print("ok")

# 輸出:1 2 3 ok

本文開頭提到了 if-else 結構,只有在不滿足 if 條件時,才會執行到 else 部分,也就是說,如果 if 語句為真,那執行完它的語句塊後,就會跳過 else 部分。

這是一種非此即彼的並行關係 ,直白地說是“如果...就...;否則就...” 。

但是,對於 for-else 結構,for 語句並不是在做真值判斷,它的程式體必然會執行(除非可迭代物件為空),執行後還會繼續執行 else 部分。

所以,它是一種先此後彼的序列關係 ,翻譯出來則是“對於...就...;然後...”。

這種結構肯定不是從 C 語言中借用來的,至於是否為 Python 所獨創,我不確定(大概率是,姑且認為是吧),如果有知情的同學,煩請告知。

那麼,為什麼 Python 要加上這種設計呢,它有什麼實際的用途麼?

x = [1,2,3]
for i in x:
    if i % 2 == 0:
        print(i)   # match
        break
else:
    print("mismatch")

上例的 for 部分增加了一個判斷以及 break,這個 break 不僅會跳出 for 迴圈本身,還會跳過 else 部分。

上例的作用是查詢偶數,如果找到則打印出來,如果 for 迴圈遍歷完都找不到,則進入到 else 分支,列印“mismatch”的結果。

所以,其實 else 是 for 迴圈有沒有正常遍歷結束的標記,如果在迴圈後沒有達到某種目標而跳出(break、return 或者 raise),就可以在 else 中做必要的補充(記錄日誌、丟擲異常等等)。

這種設計並不算一個好的設計,因為 else 會帶來誤解(if-else 那種非此即彼的關係),而且它的最大用途需要結合 break 等跳出迴圈的操作,但是這層資訊卻非顯而易見的。

在核心開發者的郵件列表裡,就有不少爭論點,2009 年的這封郵件梳理了大家的討論(https://mail.python.org/pipermail/python-ideas/2009-October/006155.html)。

其中,有開發者提議:

  • 移除這個寫法
  • 如果用了卻沒寫 break,就生成告警提示
  • 替換 else 關鍵字(如 then、finally、else no break)
  • 增加其它的功能

這封郵件一一列舉了這些觀點的提出原因及改進想法,然後又一一地反駁了它們,最後的結論是保持 for-else 寫法不變,也就是大家現在看到的實現方式。它的完整語義是:

execute the for-loop (or while-loop)
if you reach a `break`, jump to the end of the `for...else` block
else execute the `else` suite

也就是說,else 對標的是“是否執行 break”,如果沒有 break,則進入else。

但是,我並不認可這種做法,因為 break 是隱含條件,在直觀上我們只看到了 for-else,很容易產生 if-else 那樣的聯想。因此,我反而贊同把 else 改為 then,以消除誤會。

這封郵件的反駁意見是,改成 then 會引入新的關鍵字,因此不好。

我認為這個說法有些牽強(從使用者的角度),還記得本文開頭的內容麼,elif 就是新引入的關鍵字啊,看看它現在是多受歡迎。

elif 屬於那種初看不知何意,但知道後肯定會記住的詞,而且也不大可能拼寫錯誤。為了這點簡潔易拼寫的好處,它就被引入成新的關鍵字了。

for-else 中的 else 屬於那種初看以為知道含義的詞,但實際卻表達著不同意思(準確地說是,由於不知道隱含條件,而造成的誤解),為了清晰語義的好處,我認為可以引入新的關鍵詞 then 來替代 else。

不過,我轉念一想,現在討論這個已經沒有意義了,畢竟時間已經過去了,那都是 10 年前的討論了。

如果在 Python 創造之初,或者在 Python 3 大版本改動之初,這個討論就被提出,那很可能 for-else 會被設計成 for-then ,then 會像引入 elif 關鍵詞一樣被引入。

如果是那樣,說不定 Guido 某天心血來潮說起這則歷史小故事,留言區又會出現一大片的贊同之聲呢。

聊到這裡,意猶未盡,但主題似乎有點跑偏,我們來稍微總結幾個要點吧:

  • Python 從 C 中借用了 elif,受到讚許
  • Python 沒有借用 C 傳統的三段式 for 迴圈
  • Python 採用類似 foreach 的表達,但應用範圍更廣
  • Python 的 for 迴圈由於迭代器的設計原因,會造成一些陷阱
  • Python 創造了 for-else 結構,它的隱含語義是 for-(if break)-else,曾有討論是否要創造新的關鍵詞替換 for-else,但是被否決了

本文談到的內容很微小,好像沒有什麼實際的幫助,不知道 elif 來源、不知道 for 迴圈的細節、不知道 for-else 的用途與爭論,這些統統都不會造成語言使用上的障礙。

但我還是那個觀點:

閱讀 Python 的歷史,從中你可以看到設計者們對功能細節的打磨過程,最終你就明白了,Python 是如何一步一步地發展成今天的樣子。

這在我看來挺有趣的,更加增進了我對於 Python 的瞭解,以後在程式設計到某些用法的時候,腦海裡滿滿都是故事,它頓時也會變得立體生動起來。

如果你讀後有所收穫,或者產生了不同想法,歡迎來知識星球與我互動交流。

公眾號【Python貓】, 本號連載優質的系列文章,有喵星哲學貓系列、Python進階系列、好書推薦系列、技術寫作、優質英文推薦與翻譯等等,歡迎關注哦