Flask 框架作者希望看到的 Python

艾凌風發表於2015-07-13

伯樂線上注:本文作者 Armin Ronacher 是知名 Python 框架 Flask 的開發者。文章寫於 2014 年 8 月。

我不是Python 3的粉,也不喜歡這門語言的發展方向,這都不是什麼祕密了。這也導致了最近幾個月,鋪天蓋地郵件詢問我,我到底想要Python怎樣。所以我覺得我應該公開分享一下我的想法,給將來的程式語言設計者提供一些靈感。:)

顯然Python作為一門程式設計程式,並不完美。然而,讓我感到沮喪的是,這門語言大多數問題都與直譯器的細節有關,很少是語言本身。然而這些直譯器的細節,正在變成語言的一部分,這也是為什麼這一點是很重要的。

我希望能帶你們一起遨遊,從直譯器中古怪的槽(slots)開始,到python語言設計中最大的錯誤結束。如果反響比較好的話,以後還會有更多類似的文章。

大體上講,這些文章將會探索直譯器中的設計決策以及這些決策是如何影響直譯器和語言的。我相信從程式語言設計的角度來看本文,會比將其作為對python將來如何發展的建議會更有趣。

 

語言vs實現

完成本文的初版後,我又新增了這段文字,因為我覺的我們在很大程度上忽視了,Python作為一種語言,CPython作為一種直譯器,他們之間並沒有開發者認為的那樣分得開。

雖說有語言規範,但是大多數情況下,它只是規定了直譯器要做什麼,甚至連這些都沒有規定。

在這種特定的情況下,直譯器的這種晦澀的實現細節,改變或是影響了語言的設計,並且迫使Python的其他實現方式要去適應它。比如我猜想PyPy並不知道什麼是槽(slots),但是仍然要操作它,就好像槽(slots)是直譯器的一部分一樣。

 

槽(Slots)

目前為止,我對專門語言最大的意見就是愚蠢的槽(slots)系統。我指的不是__slot__,而是特殊方法的內部型別槽。這些槽(slots)是語言的“特性”,這點在很大程度上是錯誤的,因為你很少需要去關心它。也就是說,我認為,槽(slots)存在的這一事實,是這門語言最大的問題。

那麼槽(slots)是什麼東西呢?slot是直譯器內部實現時產生的副作用。每個Python程式設計師都知道所謂的“魔術方法”,比如__add__。這些方法以兩個下劃線開頭,後面是這種特殊方法的名稱,然後又是兩個下劃線。正如每位開發者所知,a+b就相當於a.__add__(b)

不幸的是,這是一個謊言

Python實際上並不是這樣工作的。現如今的Python內部完全不是這樣工作的。這裡我們大致講解直譯器是如何工作的:

  1. 當一個型別被建立後,直譯器會尋找類裡面所有的描述符,同時還會尋找類似__add__這樣的特殊方法。
  2. 對於每一個特殊方法,直譯器找到它,把描述符的引用放在型別物件的一個預定義的槽(slots)中。

例如,特殊方法add對應兩個內部槽(slots),tp_as_number->nb_add 和 tp_as_sequence->sq_concat.

  1. 當直譯器想求a+b的值時,它會進行類似於TYPE_OF(a)->tp_as_number->nb_add(a, b)的呼叫(實際上比這複雜的多,因為__add__實際上有多個槽)

因此,表面上看a+b類似於type(a).__add__(a, b),但是即使這樣還是不準確的,這點你可以從槽(slots)的處理中看出來。你自己就可以輕易的證明這件事,通過在元類裡實現
__getattribute__並嘗試連線一個自定義的__add__。你會發現,它永遠不會被呼叫。

我認為槽(slots)系統簡直荒唐透頂。對於一些特定的型別(比如整型),它是一種優化,但是對於其他的型別則是毫無意義的。

為了證明這一點,請看這種完全無意義的型別(x.py):

 

因為我們有一個__add__方法,直譯器會在槽(slots)中建立它。速度有多快呢?我們做a+b的時候,會用到槽(slots),下面是執行所用時間:

 

然而如果我們使用a.__add__(b),則會繞過槽(slots)系統。直譯器會去檢視例項字典(在那裡它什麼也找不到),然後取檢視型別字典,它將找到這個方法。下面是執行所需的時間

你敢信嗎:不使用槽(slots)的版本實際上更快。這是什麼魔力?這一現象的原因我不是很明白,但是這種情況已經持續了很久很久了。實際上,對運算子來講舊式的類(舊式類不含有槽(slots))比新式的類速度要快很多,並且有更多的特性。

更多的特性?沒錯,因為舊式的類可以這樣做(Python 2.7):

是的。對於一個複雜的型別系統而言,如今我們擁有的特性比Python2還少。因為上面的那段程式碼,是不能新式類中使用的。

如果考慮到舊式的類是如此的輕量級,情況實際上更壞。

 

槽(slots)是哪來的?

這就提出了一個問題:為什麼會存在槽(slots)。據我所知,槽(slots)的存在僅僅是一個歷史遺留問題。當Python的直譯器最初發明之時,像字串等內建型別,被實現為全域性變數,並且靜態分配了結構體,用來存放一個型別所需的全部的特殊方法

這都是在__add__出現之前。如果你看一下1990年的Python,你可以看到當時的物件是如何建立的。

比如說,整數看上去是這樣的:

如你所見,即使在曾經發布的第一版Python中,tp_as_number就存在了。不幸的是,好像某天程式碼倉庫因為一些修改而崩潰了,所以在一些非常老的Python版本中,很多重要的東西都丟失了(比如直譯器),所以我們需要稍微往將來看看,看看物件是如何被實現的。到1993年,這是直譯器的加法指令回撥的樣子:

那麼__add__等是何時實現的呢?就我看來,他們出現於1.1版。我想方設法搞到了Python 1.1,並耍了一些手段在OS X 10.9上對其進行了編譯:

當然,它經常會崩潰而且不是所有功能都能執行,但是它能讓你看到Python從前的樣子。比如說,C和Python在型別實現的時候有著巨大的不同:

如你所見,沒有對內建型別(比如整型)進行檢查。實際上,當add可以用於自定義類的時候,它是自定義類的完整特性:

所以,這一傳統一種保留到今天的Python中。Python中,型別的整體設計一直沒有改變,但是補丁卻打了很多很多年。

 

現代PyObject

如今,很多人會認為,C直譯器中Python物件的實現,和實際Python程式碼中的Python物件實現,它們的差別非常微小。在 Python 2.7 中,最大的不同應該是在 Python 中預設的 __repr__ 會報告 Python 中的型別實現為class,而 C 中的型別實現為type。事實上,repr中的不同表示了一個型別是靜態分配(型別)還是是在堆上動態分配。它並不會產生什麼實際差異,而且在Python3中這些都已經不存在了。特殊方法被複制到槽,反之亦然。在很大程度上,Python和C類的差別貌似已經消失了。

然而,不幸的是,它們仍然非常的不同。讓我們來看一看。

正如每一份Python程式設計師所知的那樣,Python的類是開放的。你可以檢視類裡存放的全部內容,分離(detach)或是重連(reattach)類中的方法,即使類的宣告已經結束。這一動態特性在直譯器類中是不能使用的。為什麼會這樣呢?

對於你為什麼不能連線一個方法到字典類(比方說)這件事而言,其本身並沒有什麼技術上的限制。直譯器不允許你這樣做的原因實際上和程式設計師沒什麼關係,內建型別不在堆(heap)中.為了理解它的廣泛影響,你需要明白Python是如何啟動直譯器的。

 

該死的直譯器

在Python中,直譯器的啟動是一個消耗很大的過程。每當你啟動一個可執行的Python檔案,你呼叫了一個龐大的工具,做了全部的事情。此外,它會引導內部型別,它會啟動匯入的工具,它會匯入需要的模組,和作業系統一起處理訊號,接受命令列引數,啟動內部狀態等。當這些工作終於都完成之後,它會執行你的程式碼然後停止。Python這麼做已經25年之久了。

用虛擬碼展示上面過程,是這樣的

這樣做的問題是,Python的直譯器有一大堆的全域性狀態。實際上,你只能有一個直譯器。一個更好的設計是啟動直譯器然後在上面執行一些東西:

實際上這也是其他一些動態語言如何工作的。例如,這是lua的工作方式,javascript引擎的工作方式,等等。這樣做明顯的好處是你可以有兩個直譯器。多麼新穎的概念啊。

誰會需要多個直譯器呢?你可能會感到驚訝。甚至是Python也需要,或是至少認為多個直譯器是有用的。比如說,如果存在多個直譯器,那麼一個嵌入在Python中的應用程式可以單獨的執行一些東西。(比如說,想象一些在mod_python中實現的web應用,它們想要獨立的執行)。所以,在Python中,有一些子直譯器。它們在直譯器中工作,但是因為有太多的全域性狀態。全域性狀態中最大也是最容易引起衝突的一個是:全域性直譯器鎖。Python已經選定了這種單一直譯器的理念,所以子直譯器之間有很多共享的資料。因為這些資料被共享,所以也就需要一個鎖,所以這個所在實際的直譯器中。哪些資料被共享了呢?

如果你看一下我上面貼過的程式碼,你可以看到那些無所事事的結構體。實際上這些結構體是作為全域性變數的。實際上,直譯器把這些型別結構體直接暴露給Python程式碼。通過OB_HEAD_INIT(&Typetype)巨集來啟動,這個巨集給予結構體必要的資料頭,以至於直譯器可以用它工作。比如說,這裡有一個該型別的引用計數(refcount)。

現在你可以看到將要發生什麼了。這些物件被子直譯器所共享。所以,想象你可以在你的Python程式碼中改變這一物件。兩個完全獨立的Python程式碼,彼此間沒有任何的關係,他們的狀態都會改變。想象如果這是在JavaScript中,Faceboox選項卡將可以改變內建陣列型別而且Google選項卡可以馬上看到這一事件所帶來的影響。

這一1990年左右的設計方案,它帶來的影響至今我們仍然可以感受的到。

從好的方面來看,這種不可變的內建型別,已經越來越被社群當做一個好的特性所接受。可變的內建型別所帶來的問題,已經被其他語言所證明,這不是我們所想要的。

但仍然有很多是我們想要的。

 

什麼是虛擬函式表(VTable)?

那些來自C語言的Python變數型別,大多是不可變的。那還有其他的什麼東西是不同的呢?

另外一個巨大的不同點仍然和Python類的開放特點有關。Python中的類有它們的“虛”方法。雖然這裡並沒有“真”的C++式的虛擬函式表,所有的方法都儲存在類字典裡而且有一種查詢演算法,但是歸結起來其實幾乎是一樣的。這樣做的結果很明顯。當你建立一個子類並過載一個方法的時候,在這個過程中很有可能另外一個方法會間接的被改變,因為它進行了呼叫。

另外一個好的例子是集合。很多集合都有很方便的方法。例如Python中的字典有兩個方法來檢索字典中的物件:__getitem__()get()。當你在Python實現一個類的時候,你通常會通過諸如返回self.__getitem__(key)另一個類來實現它

直譯器對於型別的實現是不同的。其原因仍然是因為slots和字典間的不同。比如說你想要在直譯器裡實現一個字典。你的目標是重用程式碼,所以你要從get呼叫__getitem__。你要如何著手去做呢?

C裡面的Python方法,僅僅是一個具有特殊標識的C函式。這就是第一個問題了。這個函式首要目的是處理Python層的引數並把他們轉換為你可以在C層使用的東西。至少,你需要從一個Python元組或字典(args and kwargs)中把單獨的引數取出,放入區域性變數。因此一個
普通的模式是,dict__getitem__在內部僅僅解析引數,然後使用實際引數呼叫dict_do_getitem。你應該能料到接下來會怎樣,dict__getitem__dict_get 都會呼叫dict_get,它是一個內部靜態函式。你不能夠過載它。

There really is no good way around this. The reason for this is related to the slot system. There is no good way from the interpreter internally issue a call through the vtable without going crazy. The reason for this is related to the global interpreter lock.when you are a dictionary your API contract to the outside world is that your operations are atomic. That contract completely goes out of the window when your internal call goes through a vtable. Why? Because that call might now go through Python code which needs to manage the global interpreter lock itself or you will run into massive problems.
這裡真的沒有什麼好辦法了。這和槽(slots)系統有關。沒有什麼好辦法讓直譯器正常的通過虛擬函式表來進行內部呼叫。這都是因為這個全域性直譯器鎖。當作為一個字典的時候,字典通過API與外界聯絡。當內部呼叫遍歷虛擬函式表的時候,這一聯絡就完全消失了。
為什麼?因為這一呼叫現在也需要通過Python程式碼,這需要它去處理全域性直譯器鎖,不然就會產生大量的錯誤。

Imagine the pain of a dictionary subclass overriding an internal dict_get which would kick off a lazy import. You throw all your guarantees out of the window. Then again, maybe we should have done that a long time ago.
想象一個字典的子類過載一個內部dict_get的痛苦。你無法做出任何的保證。再說一次,也許我們很早以前就應該這樣做了。

 

給將來的參考

近些年來,有一個明顯的趨勢就是要把Python變的更加複雜。我希望能夠看到與之截然相反的趨勢。

我希望看到一個內部直譯器的設計可以基於一些相互獨立工作的直譯器,擁有區域性基型別,更像JavaScript的工作方式。這將馬上開啟基於訊息傳遞的嵌入和併發的大門。CPU不會變的更快了:)

與其把槽和字典看做是虛擬函式表,我們不如用字典做實驗。Objective-C 作為一門語言,完全建立在訊息機制上,這使得它的呼叫非常的快。它的呼叫,就我所知,比Python在最好的情況下,表現的還要快。字串在Python裡是被限制的,使得它的比較非常的快。我跟你賭,它一點都不慢,即使稍微慢那麼一點點,它也會是一個更簡單的系統,也就更容易優化。

你應該看一下Python的程式碼庫,處理槽(slots)統需要多少額外的邏輯呢?多到不可思議。

我深信槽(slot)系統是個壞點子,在很久之前就該丟棄了。移除它甚至可能會對PyPy有所助益,因為我敢肯定他們需要故意限制直譯器來使其像CPython一樣,來獲得相容性。

打賞支援我翻譯更多好文章,謝謝!

打賞譯者

打賞支援我翻譯更多好文章,謝謝!

任選一種支付方式

Flask 框架作者希望看到的 Python Flask 框架作者希望看到的 Python

相關文章