2012年11月16日 星期五

[Python] Pythonic到底是什麼意思?


作者:Martijn Faassen
譯者:賴勇浩(http://blog.csdn.net/lanphaday
繁中修訂:林世鵬
註:Martijn Zope 領域的專家,他為 Zope 系列產品做了許多開發工作,也開發了 lxml 等多個開源產品。你可以在這裡瞭解一下他的資訊http://www.zope.org/Members/faassen。這篇文章寫於 2005 年,雖然有少部分內容(主要是例子)看起來已經有些過時,但即便是在今天,它的中心思想依然有極高的指導意義。

這是幾個月前在 EuroPython mailing list(主要用來組織和計畫 EuroPython 會議的郵寄清單)出現的問題。這是一個非常有意思的問題,我看到Pythonic這個詞被無數次地使用,但很少有人嘗試解釋它的含義。在串討論中,許多人包括我自己,都給出了自己的答案。現在我把我的答案放到 blog 上,並且潤色了一下,希望它能對您有所幫助。

「Pythonic」其實是一個含糊的概念,儘管沒有「智慧」或「生命」那麼模糊,但當你嘗試定義它們的時候,就會發現那其實難以下手。雖然它們難以定義,但並不意味著它們沒用;人們總是能在混亂的定義上依舊做得很好。Pythonic 有點像「Python慣用法」的意味,但現在我們必須來描述一下它真正的含義。

隨著時間的推移,Python語言不斷演進,社群不斷成長,出現了許多關於如何正確使用 Python 的方法。一方面 Python 語言推薦使用大量的慣例 (idioms) 來完成任務 (「完成任務的唯一方法」亦即 “the one way to do it”);另一方面,社區不斷演變的新的慣用法又反過來影響了語言的進化,以便更好地支持所謂的慣例。比如 dictionary 中的 .get() 方法,它把 has_key() 和元素存取兩個操作組合為一個操作,便可看出這種進化。

慣例往往不能直接從其它程式設計語言移植過來使用。例如,下文是實現對一個序列裡的每個元素執行一個操作的 C 語言實作方法:
for (i=0; i < mylist_length; i++) {
   do_something(mylist[i]);
}
直接的等效 Python 程式碼是這樣的:
i = 0
while i < mylist_length:
   do_something(mylist[i])
   i += 1
這段程式碼能夠完成工作,但並不 Pythonic,它並不是 Python 語言推薦的慣例。讓我們來改進一下。典型的 Python 慣例是用內建的 range() 函數來生成索引值:
for i in range(mylist_length):
   do_something(mylist[i])
其實這種實作方法也並不 Pythonic,接下來大家看看語言本身推薦的方法,真正 Pythonic 的實作:
for element in mylist:
   do_something(element)

 「如何直接地傳遞或改變參照」(how to pass or modify references directly) 是 comp.lang.python 的月經文,但在只有賦值(importclassdef 等語句也可視為賦值)的 Python 中這是不可能的。這種需求通常是因為想讓函數回傳多個值出來,用 C 或者許多其它程式設計語言的方法是給這個函數傳入引用或指標:
void foo(int* a, float* b) {
    *a = 3;
    *b = 5.5;
}
...
int alpha;
int beta;
foo(&alpha, &beta);
Python 中可以用很奇怪的方法來實現:通過給函數傳遞序列參數來返回結果。寫出來的代碼可能像這樣:
def foo(a, b):
    a[0] = 3
    b[0] = 5.5

alpha = [0]
beta = [0]
foo(alpha, beta)
alpha = alpha[0]
beta = beta[0]
顯然這是毫無 Pythonic 可言的方法。在Python 中要讓函數返回多個值的慣例與此迥異,得益於 tuple 和 tuple unpacking,它看起來也舒服得多了:
def foo():
    return 3, 5.5

alpha, beta = foo()

在經驗老到的 Python 程式師看來,不夠 Pythonic 的代碼往往看起來古怪、累贅,而且也難以理解。因為它使用冗長的程式碼代替常見的、公認的、簡短的慣例來實現預期效果。更糟糕的是,因為語言本身傾向支援正確的慣例,因此非推薦的程式碼通常執行起來較慢。

Pythonic 就是以清晰、可讀的慣例使用Python語言內容與資料結構的意思。例如,Python在處理物件實體 (instances) 這個問題時選擇了使用動態資料型別 (dynamically typing),若當初採用的是靜態資料型別的話,那之後造成的語言繁瑣與冗長事實上就不 Pythonic 了。另外,也請只使用經驗豐富的 Python 程式師熟悉的方式來完成事情;老鳥不熟悉的方法,通常都是不 Pythonic 的(請遵循最小驚奇原則)。

Pythonic 一詞也能夠適用於底層的慣例。對於一個函式庫或框架來說,Pythonic意謂的是要讓programmer盡可能容易且自然的利用它來完成工作。如果一個用Python寫成的函式庫或框架必須迫使程式師編寫累贅或非慣例的程式碼時,那麼它就不是Pythonic的。也可能是為了讓函式庫更加方便使用或易懂,而沒有使用 Python 的一些理念,例如類別時,那也是不 Pythonic 的。函式庫中的類別定義應該盡可能地將資訊隱藏,雖然 Python 的許多操作都只作「寬鬆限制」(通常由程式師在屬性的前面加上一個底線來暗示這是私有成員),但也要做得像 Java 那樣嚴格。

當然,當函式庫或框架的規模很大的時候,它是否 Pythonic 就極具爭議性了。這裡給出一些 guidelines:首先,必須減少任何繁瑣冗餘的部分:Python函式庫的API都傾向於小型化和輕量化(相對於 java 程式庫而言)。重量級的、API過於細化的Python函式庫則並不非常 Pythonic。例如 W3C XML DOM API,儘管它的 Python 實現已經頗有時日,但大家並不認為它 Pythonic。有些人認為它是 Java 式的,雖然也有許多 Java 程式師認為並不如此。

一個 Pythonic 的框架不會對已經用慣例完成的東西重複發明輪子,而且它也遵循常用的 Python 慣例。

當然,問題是建構框架時肯定會不可能避免地使用一些看來不常見的的方法來完成事情。Zope2 是我極為熟悉的一個框架,它也是一個使用了許多的特殊的方法(如 Acquisition)來完成工作的例子,這些方法往往什麼地方都用不到,因此許多經驗豐富的 Python 程式師認為它並不 Pythonic

建立 Pythonic 的框架極其困難,什麼理念更酷、更符合語言慣例對此毫無説明,事實上這些年來優秀的 Python 代碼的特性也在不斷演化。比如現在認為像 generatorssetsunicode strings datetime 之類的特性尤為 PythonicZope2 的歷史悠久,它從1997年開始開發,你不能把不夠 Pythonic 歸咎於它,甚至考慮到這麼多年來它控制得如此之好,更應該感謝它。

關於 Pythonicness 新趨勢的一個例子是Python 對套件和模組結構的日益規範化。新的codebases TwistedZope3 PyPy 等或多或少都跟隨了這樣的模式:
  • 套件和模組的命名使用小寫,單數形式,並且簡潔。
  • 套件通常僅僅作為命名空間,也就是 __init__.py 中並沒有任何東西。
在我寫函式庫時(如 lxml),我也試著遵循了這樣的慣例。

因為以上的觀念,許多 Python programmer 會覺得一個功能不那麼強大,但易學的框架會比另一個功能強大,但需要較多時間來學習框架更為 pythonic。所以有時我覺得說一個軟體不Pythonic其實並不公平,甚至可能會因為這種想法而忽略了該軟體好的一面。

最後,作為什麼是 Pythonic設計的補充教材,你可以嘗試一下在 Python 直譯器中執行以下敘述:
import this

沒有留言:

張貼留言