“Explicit is better than implicit” 是 The Zen of Python 中的一句格言。長久以來都覺得挺在理,直到有天有人用這句話為基礎,提出了一個我不甚贊同的觀點,才發現從來就沒有真正理解過它。
Explicit 是什麼含義?Explicit 這個單詞釋義為:
stated clearly and in detail, leaving no room for confusion or doubt
“清楚詳細地陳述,不容混淆或懷疑”。翻譯成中文有“顯式的”、“精密”、“不含糊”、“明確的”等多種翻譯。在程式碼的語境下,什麼樣的程式碼才能稱得上是 “explicit” 呢?網上看到了不同角度的觀點。
一些觀點把程式碼顯式寫出來顯式地寫出程式碼,也可以有多種理解方式,Making Games with Python and Pygame 中舉了一個示例:
def getButtonClicked(x, y): if YELLOWRECT.collidepoint( (x, y) ): return YELLOW elif BLUERECT.collidepoint( (x, y) ): return BLUE elif REDRECT.collidepoint( (x, y) ): return RED elif GREENRECT.collidepoint( (x, y) ): return GREEN return None # 這裡顯式地返回了 None
書中提到最後一行顯式寫 return None 能讓讀者更直接理解程式碼的用途。
思考:這個樣例容易理解,也很贊同,但是如何推而廣之呢?Explicit 在上面的例子中體現在哪呢?
我理解它的重點在於,當所有的 if 語句都不命中時,預設的行為是未知的,而顯示寫出的 return None 則清楚地描述了預設的情形,即使 Python 的預設行為發生變化,該方法的行為也不會發生變化。
要具體、要特化這篇文章 明確表達了自己對“Explicit is Better than Implicit”的理解:
Being explicit means being concrete and specific instead of abstract and general. It also means not to hide the behavior of a function.
Explicit 意味著要具體、特化,不要抽象、通用。同時不要隱藏函式的行為。
對於要具體、特化,文章中舉例如下:
# Explicit | # Implicit import requests | from requests import * r = requests.get("https://lotabout.me") | r = get("https://lotabout.me")
對這個例子也是比較認同的。但我認為重點不在於 requests.get 怎麼好,而在於 import * 不好。因為這樣的話 get 方法的來源就不明確了,容易混淆,需要靠猜。相對的,下面的程式碼我認為也是好的:
from requests import get r = get("https://lotabout.me")
這裡我的理解是,程式碼執行的邏輯,從溯源的角度上沒有二義。如果用了 import *,同時兩個模組中都有 get 方法,則容易混淆,不知道真正執行的是哪個方法。
不要隱藏函式行為同樣來自上面提到的文章 ,示例如下:
#Explicit def read_csv(filename): # code for reading a csv def read_json(filename): # code for reading a json #Implicit def read(filename): # code for reading a csv or json # depending on the file extension
這個示例我不認同。從觀念上,它與 OOP 中提到的封裝的思想;從使用上,檔案型別的判斷的要求並沒有消失,只是丟給使用者自己實現了;進而從結果上,只要有多種檔案型別存在,在某個層級上一定會有一個 read 方法的。
舉例說,如果說不要隱藏函式的行為,那麼我們在寫 Web 服務的時候,在我們訪問 DB 時,我們會希望直接處理 TCP 連線嗎?Spring 框架選擇隱藏這些行為,可以說是錯誤嗎?
No MagicDjango 的 Design philosophies 中有如下描述:
Magic shouldn’t happen unless there’s a really good reason for it. Magic is worth using only if it creates a huge convenience unattainable in other ways, and it isn’t implemented in a way that confuses developers who are trying to learn how to use the feature.
這裡指的是不要用複雜的語言特性(大家常把超程式設計稱作 Magic)。
這個觀點的持保留意見,我認為重點還是在於知識、背景是否匹配。例如當我熟悉 Decorator 時,就會覺得用 decorator 來指定一個 REST API 的路由很直觀,很容易理解。但對於不熟悉的人可能就完全不能理解資料的路徑(data path),不知道為什麼一個註解是怎麼真正完成 URL 到函式的繫結的。
a = [] # ① my understanding is that this is implicit if not a: print("list is empty") # ② my understanding is that this is explicit if len(a) == 0: print("list is empty")
這是一個“矛盾”的討論,題主認為 ② 是 explicit,下面的回答則指出寫成 ① 的方式能應對更多的情形,如 a 不是列表的情形。我能理解 ① 的作用,但同時也贊同題主的觀點,從閱讀的角度來說 ② 是更直接的。
if (person.sex === 1 and person.children.length > 0) { ...do something... } if (person.isFemale() and person.hasChildren()) { ...do something... } if (person.isFemaleParent()) { ...do something... }
從閱讀程式碼的角度,明顯上最後一種最容易閱讀,更符合語言習慣,不容易有歧義。
我所理解的 Explicit上面我們看到,“Explicit is Better than Implicit”這句話本身就是 implicit 的,有很多歧義的理解。
我自己的總結是:Minimal Knowledge, No Surprise。
對於閱讀者/使用者而言,需要最少的知識去理解它,在我們隱藏複雜度的過程中,要保證函式/API/…行為符合預期,沒有意外。
例如對於函式最後加上 return None 比不加要好,因為加上後,我們就不需要了解 Python 函式的預設返回值是什麼。類似的,顯式引用會更好:from requests import get,因為讀者不需要去找 get 方法的來源,以及有重名時是哪個函式生效。
對於“不要隱藏函式行為”的做法,就有一定的反對意見。例如 read_csv 和 read_json 是否優於 read 方法?我認為此時 No Surprise 很重要。對於 csv, json 等檔案格式我認為 read 更優,因為根據副檔名判斷型別是一個共識,並不會有 surprise 發生。而如果讀取的是 HDFS 上的檔案,由於很多檔案儲存時並不會按副檔名儲存,我認為此時 read 就容易有 Surprise,因此是不合適的。
對於 Magic,如果做法不是 common sense,則需要我們額外學習 Magic 的含義,就是屬於"Implicit" 的,此時用來是不用,就要看它能給我們帶來多大的好處了。同樣的還有語言中的語法糖,經常需要額外學習知識才能看懂/自己使用。
最後對於“含義更直接”,認為在 No Surprise 的前提下,越接近“共識”越好,因為需要更少的知識。
小結關於 “Explicit is Better than Implicit?” 的理解,文章羅列了網上搜索的一些觀點:
把程式碼顯式寫出來,如顯式加上 return None要具體、要特化,如顯式 import:from requests import get不要隱藏函式行為,如實現 read_csv 與 read_json 要好於只實現 readNo Magic,如非必要,不要使用超程式設計含義更直接,如用 len(a) == 0 判斷列表為空而不是 not a最後總結並說明了自己對 “explicit” 含義的理解:Minimal Knowledge, No Surprise
當然,我們會發現 Minimal Knowledge 或者說“共識”對於不同的群體,在不同上下文之下是不同的。這也是我們需要經驗去理解,需要花時間去溝通的內容了。