標籤:

那些年,我們踩過的Python的坑

在總結完 那些年,我們踩過的PHP的坑 之後,輪到第2好和第3好的兩門語言了,Python。

0x01 2 or 3?

Python 3已經release 9年了,流行程度日益增長,然而時至今日,每一個初學者仍然要在2和3之間糾結一下。就算到了2018年,仍然會有一批不錯的庫只支持py2,與此同時,也會有一批不錯的庫只支持py3,那麼就尷尬了,社區陣營撕裂了。除了python,似乎沒有別的語言會把版本分的這麼清楚,學C++的人會在C++98和C++11之間選嗎?學Java的會在Java 6和Java 8之間選嗎?學JS的會在ES 6和ES 5之間選嗎?他們當然不需要,因為高版本向下兼容低版本。而Python 3卻是不兼容Python 2的,這種把財富和缺陷一起扔掉的做法,讓3在語言層面提升了不少,但壯士斷腕的做法,讓這種版本之爭持續了長達十年才徹底分出勝負。

2017年開始,再有人問學2 or 3,可以很負責任的說,學3,一旦學會了3,需要用到2的時候,稍微翻翻2和3的區別就馬上會了。

0x02 py2字元串編碼

在windows系統下,跑上來就遇到源文件編碼問題的人不在少數,掉坑裡了才學會頭部聲明文件編碼。

UnicodeDecodeError: ascii codec cant decode byte 0xe8 in position 1: ordinal not in range(128). -- Note: Markdown only accepts unicode input!n

然後還要學習一些被建議不要使用的黑魔法

import sys nreload(sys) nsys.setdefaultencoding(utf8)n

幸好我學Python的時候,已經不用windows了,少踩了這個坑。

搞定文件編碼之後,就碰到str編碼的坑了,python2有兩種字元串,str和unicode,unicode是真正意義上的字元串,不過是後來才加入的功能,早期只有str,它既是bytes,又是string,轉換規則是這樣的

  • 調用string的encode方法,它會變成二進位位元組序列bytes
  • 調用bytes的decode方法,它會變成字元串string

雖然別的語言也要面對編碼問題,但是除了Python2和PHP,很少有語言強行把string和bytes糅和在一種類型里了,這樣做只會讓更多的初學者一頭霧水。

0x03 函數中沒有static變數

很多語言都支持在函數中定義static變數,可以做一些function scope的cache,比如PHP

function fib($n) {n static $cache = []; n if (isset($cache[$n]))n return $cache[$n];nn if ($n >= 2) {n $cache[$n] = fib($n-1) + fib($n-2);n return $cache[$n];n } n n return 1;n}n

在計算第40個斐波那契數字的時候,用了這種cache,性能可以提升3個數量級。很遺憾,Python並不支持這個特性,只能通過其它特性去模擬,最常見的就是默認參數,比如:

def fib(n, cache={}):n if n in cache:n return cache[n]n if n >= 2:n v = fib(n-1) + fib(n-2)n cache[n] = vn return vn return 1n

或者更麻煩一些,用閉包

def fib():n cache = {}n def _fib(n):n if n in cache:n return cache[n]n if n >= 2:n v = _fib(n-1) + _fib(n-2)n cache[n] = vn return vn return 1n return _fibnnf = fib()nprint(f(40))n

或者用更複雜的方式去模擬,比如descriptor,總歸是有辦法解決的,但是跟static比,還是要麻煩那麼一些。針對這個具體的例子,其實也有一些庫可以緩解這種不便,比如這樣寫

from functools import lru_cachenn@lru_cache(maxsize=100)ndef fib(n):n if n >= 2:n return fib(n-1) + fib(n-2)n return 1nnprint(fib(40))n

0x04 private

一個支持OO的語言,卻沒有private/protected/public的成員隱私控制,難以想像吧,舉個栗子:

class Foo:n def _barx(self):n print("Calling BarX")nn def __bary(self):n print("Calling BarY")nnfoo = Foo()nfoo._barx()nfoo._bary() # 此處會報錯nfoo._Foo__bary()n

Python推薦使用_開頭表示私有變數,但這只是習慣,外面仍然可以通過foo._barx的方式訪問到私有變數。然後Python又想出來 name mangling的法子,兩個以上下劃線開頭的名字,會自動給你替換,在名字前加上下劃線和類名,比如Foo.__bary變成Foo._Foo_bary,知道這個規則的人,仍然可以強行在外面訪問你的私有成員。

那麼為何不再加強點限制呢?因為Python的反射功能太強和太簡潔了,dir(foo)可以拿到想要的一切,你再怎麼加強,它一反射就繞過了。

0x05 值 or 引用?

方法調用時,傳值還是傳引用,相信不管什麼語言的用戶都掌握的不錯,可是在python裡面,仍然有一些不經意間就會掉落的坑,比如:

arr = [[1, 2, 3]] * 3narr[0][0] += 100n

錯誤的期望是

[[101, 2, 3], [1, 2, 3], [1, 2, 3]]n

實際上是

[[101, 2, 3], [101, 2, 3], [101, 2, 3]]n

這樣寫,就理解了

v = [1, 2, 3]narr = [v] * 3 #arr實際上是[v, v, v],三個元素是對同一個list的引用n

0x06 詭異的tuple操作

arr = ([foo], bar)narr[0] += [haha]n

按照一般理解,arr[0]是[foo],[foo] += [haha]就是[foo, haha],執行後的arr就是

([foo, haha], bar)n

完全正確,執行後就是這個結果,但是你猜到了結局,沒猜到過程,因為執行過程中會拋異常。

Traceback (most recent call last):n File "t.py", line 3, in <module>n arr[0] += [haha]nTypeError: tuple object does not support item assignmentn

因為

arr[0] += [haha] #不是簡單的一個動作,它跟下面的兩句等價nv = arr[0].__iadd__([haha]) #這行執行符合預期narr[0] = v #這行執行會報錯,因為tuple是immutable數據類型,沒有賦值改變其值的方法n# arr[0].__iadd__是個原地操作,arr[0]指向的list還是同一個,但是list自己變了n

0x07 弱化的lambda

python的lambda,只能是單個的expression,等價於

def <lambda>(arguments):n return expressionn# lambda里不能有 statements or annotationsn

用人話說,就是lambda只能用在一行代碼就能寫明白的邏輯里,不能把它當作普通的匿名函數,嚴重削弱了lambda的實用性,很多時候是需要多個expression才能表達的。

0x08 1/2

在任何主流語言里,1/2結果就是0,可是python3創新了,認為那樣不符合沒學過編程的人的思維,應該默認接近數學習慣,於是

>>> 1/2n0.5n

想整除怎麼辦?那就多寫一個除號

>>> 1//2n0n

0x09 GIL

原本不打算拿GIL說事,因為同類語言PHP還不支持多線程呢,Python好歹有個可以等待IO的多線程,做不了CPU密集型,做做IO密集型還是可以的。比較複雜的東西,比如腳本解釋器和UI線程,都是單線程執行的,做成多線程的,會非常複雜和容易出錯。

GIL的存在,使得wsgi部署的時候,不得不用多進程模型,而不是JVM那樣的多線程模型,大大增加了共享數據的難度,雖然我們主張share nothing的架構,有時候還是需要share一點東西的,通過redis等外部服務去share,簡單的數據結構問題不大,複雜的東西,redis這類簡單的緩存,無能為力啊。

在JVM上的python是沒有GIL的,可惜jython始終沒有流行開來,pypy也嘗試過消除GIL,這塊硬骨頭目前還是沒有啃下來。

0x0A 性能

獨裁的Python支父一直覺得性能沒那麼重要,畢竟是膠水語言,畢竟可以方便的寫C擴展,畢竟可以方便的用cffi調用dll/so,畢竟還有cython,畢竟還有pypy,畢竟還可以用google的Grumpy編譯成go語言代碼。然而,作為唯一的主流,CPython的的性能卻始終徘徊在高性能大門之外。

特別是Python 3,直到今年年初才趕上Python 2的性能,早就被PHP 7吊打在千里之外。多虧FPM模型不能常駐內存,導致大框架如Laravel只能做到2000rps,而同等身材的Django卻能做到8000rps,在業務代碼不是CPU密集型時,Python才不顯得比PHP慢,甚至還有了一些優勢。

近幾年,PHP除了7的3倍性能,還有一個更重要的擴展,就是swoole,它的出現,讓PHP一口氣往前跨越了10年,它客服了FPM不能常駐內存的缺點,也增加了協程和非同步IO等很多實用功能,雖然文檔差了點,真心要用可以自己翻代碼嘛。如果swoole的中英文文檔都能達到及格水平,用不了幾年,swoole定會被納入PHP官方倉庫,甚至將來取代FPM作為默認的部署方式。

在Python的世界裡,也不是乏善可陳,Gevent和Tornado是最有名的兩個,隨著Python 3支持coroutine,還會湧現出更多的高性能框架來,比如用法接近Flask的sanic。而Python本身VM也在改進中,反正2的開發力量轉移到3去了,3的性能每次改進一點點,長期看,預計還能提升不少,追到PHP的1/2應該是有戲的。

0x0B 動態類型

拿到一個方法的返回值的時候,比如foo,輸入foo.的時候,沒有跳出需要的自動補全,甚至你都不知道foo是個什麼類型,你需要想辦法跳轉到方法的定義,或者找文檔里的說明。是的,所有的動態類型語言都有這個毛病,對IDE不友好,對重構不友好,對補全不友好,甚至有時候會埋下bug。動態類型對於JIT優化,也是相當不友好的。好在Python不是弱類型,Python 3也有了typing聲明,能一定程度緩解這種動態隨意性。


推薦閱讀:

Kivy中文編程指南:打包為 Android 系統可執行文件
OSX 10.9.4 如何安裝 Python 2.7?
使用Python爬取網頁圖片
2017,再來聊一聊Python,未來發展怎樣?

TAG:Python | PHP |