標籤:

怎樣才算精通Python?

在這篇文章中,我會1)先給出我對精通Python的理解;2)然後給出一些Python中有難度的知識點。如果大家在看完這篇文章之前,已經充分理解了我列出的各個知識點,那麼,我相信你已經算是精通Python了。如果不能,我希望這篇回答能讓你意識到自己Python知識還存在哪些不足,在之後的學習中,從哪些方面去改進。

精通是個偽命題

怎樣才算精通Python,這是一個非常有趣的問題。

很少有人會說自己精通Python,因為,這年頭敢說精通的人都會被人摁在地上摩擦。其次,我們真的不應該糾結於編程語言,而應該專註於領域知識。比如,你可以說你精通資料庫,精通分散式,精通機器學習,那都算你厲害。但是,你說你精通Python,這一點都不酷,在業界的認可度也不高。

再者,Python使用範圍如此廣泛,一個人精力有限,不可能精通所有的領域。就拿Python官網的Python應用領域來說,Python有以下幾個方面的應用:

  • Web Programming: Django, Pyramid, Bottle, Tornado, Flask, web2py

  • GUI Development: wxPython, tkInter, PyGtk, PyGObject, PyQt

  • Scientific and Numeric: SciPy, Pandas, IPython

  • Software Development: Buildbot, Trac, Roundup

  • System Administration: Ansible, Salt, OpenStack

如果有人真的精通上面所有領域,那麼,請收下我的膝蓋,並且,請收我為徒。

既然精通Python是不可能也是沒有意義的事情,那麼,為什麼各個招聘要求裡面,都要求精通Python呢?我覺得這都是被逼的。為什麼這麼說呢,且聽我慢慢說來。

為什麼招聘要求精通Python

絕大部分人對Python的認識都有偏差,認為Python比較簡單。相對於C、C++和Java來說,Python是比較容易學習一些,所以,才會有這麼多只是簡單地了解了一點語法,就聲稱自己會Python的工程師。

打個比方,如果一個工程師,要去面試一個C++的崗位,他至少會找一本C++的書認真學習,然後再去應聘。Python則不然,很多同學只花了一點點時間,了解了一下Python的語法,就說自己熟悉Python。這也導致Python的面試官相對於其他方向的面試官,更加容易遇到不合格的求職者,浪費了大家的時間。Python面試官為了不給自己找麻煩,只能提高要求,要求求職者精通Python。

怎樣才算精通Python

既然精通Python本身是一件不可能的事情,而面試官又要求精通Python,作為求職者,應該達到怎樣的水平,才敢去應聘呢?我的觀點是,要求精通Python的崗位都是全職的Python開發,Python是他們的主要使用語言,要想和他們成為同事,你至少需要:

1. 能夠寫出Pythonic的代碼(什麼是Pythonic的代碼,請看我在另一個問題下的回答:怎樣才能寫出pythonic的代碼? - 知乎用戶的回答)

2. 對Python的一些高級特性比較熟悉

3. 對Python的優缺點比較了解

這樣說可能比較抽象,不太好理解。我們來看幾個例子,如果能夠充分理解這裡的每一個例子,那麼,你完全能夠順利通過"精通Python"的崗位面試。

敢來挑戰嗎

1.上下文管理器

大家在編程的時候,經常會遇到這樣的場景:先執行一些準備操作,然後執行自己的業務邏輯,等業務邏輯完成以後,再執行一些清理操作。

比如,打開文件,處理文件內容,最後關閉文件。又如,當多線程程序需要訪問臨界資源的時候,線程首先需要獲取互斥鎖,當執行完成並準備退出臨界區的時候,需要釋放互斥鎖。對於這些情況,Python中提供了上下文管理器(Context Manager)的概念,可以通過上下文管理器來控制代碼塊執行前的準備動作以及執行後的收尾動作。

我們以處理文件為例來看一下在其他語言中,是如何處理這種情況的。 Java風格/C++風格的Python代碼:

myfile= open(rC:miscdata.txt)n try:n for line in myfile:n ...use line here...n finally:n myfile.close()n

Pythonic的代碼:

with open(rC:miscdata.txt) as myfile:n for line in myfile:n ...use line here...n

我們這個問題討論的是精通Python,顯然,僅僅是知道上下文管理器是不夠的,你還需要知道:

1. 上下文管理器的其他使用場景(如資料庫cursor,鎖)

    • 上下文管理器管理鎖

class FetchUrls(threading.Thread):n ...n def run(self):n ...n with self.lock: #使用"with"語句管理鎖的獲取和釋放n print lock acquired by %s % self.namen print lock released by %s % self.namen

    • 上下文管理器管理資料庫cursor

import pymysqlnn def get_conn(**kwargs):n return pymysql.connect(host=kwargs.get(host, localhost),n port=kwargs.get(port, 3306),n user=kwargs.get(user),n passwd=kwargs.get(passwd))nn def main():n conn = get_conn(user=laimingxing, passwd=laimingxing)n with conn as cur:n cur.execute(show databases)n print cur.fetchall()nn if __name__ == __main__:n main()n

    • 上下文管理器控制運算精度

with decimal.localcontext() as ctx:n ctx.prec = 22n print(decimal.getcontext().prec)n

2. 上下文管理器可以同時管理多個資源

假設你需要讀取一個文件的內容,經過處理以後,寫入到另外一個文件中。你能寫出Pythonic的代碼,所以你使用了上下文管理器,滿意地寫出了下面這樣的代碼:

with open(data.txt) as source:n with open(target.txt, w) as target:n target.write(source.read())n

你已經做得很好了,但是,你時刻要記住,你是精通Python的人啊!精通Python的人應該知道,上面這段代碼還可以這麼寫:

with open(data.txt) as source, open(target.txt, w) as target:n target.write(source.read())n

3. 在自己的代碼中,實現上下文管理協議

你知道上下文管理器的語法簡潔優美,寫出來的代碼不但短小,而且可讀性強。所以,作為精通Python的人,你應該能夠輕易地實現上下文管理協議。在Python中,我們就是要自己實現下面兩個協議:

你知道上下文管理器的語法簡潔優美,寫出來的代碼不但短小,而且可讀性強。所以,作為精通Python的人,你應該能夠輕易地實現上下文管理協議。在Python中,我們就是要自己實現下面兩個協議:

  • __enter__(self)
  • __exit__(self, exception_type, exception_value, traceback)

當然,更優美的方法是使用contextmanager裝飾器。

2. 裝飾器

由於我們這個問題的題目是精通Python,所以,我假設大家已經知道裝飾器是什麼,並且能夠寫簡單的裝飾器。那麼,你是否知道,寫裝飾器也有一些注意事項呢。

我們來看一個例子:

def is_admin(f):n def wrapper(*args, **kwargs):n if kwargs.get("username") != admin:n raise Exception("This user is not allowed to get food")n return f(*args, **kwargs)n return wrappernnn @is_adminn def barfoo(username=someone):n """Do crazy stuff"""n passnn print barfoo.func_docn print barfoo.__name__nn Nonen wrappern

我們用裝飾器裝飾完函數以後,無法正確地獲取到原函數的函數名稱和幫助信息,為了獲取這些信息,我們需要使用@functool.wraps。 如下所示:

import functoolsn n def is_admin(f):n @functools.wraps(f)n def wrapper(*args, **kwargs):n if kwargs.get("username") != admin:n raise Exception("This user is not allowed to get food")n return f(*arg, **kwargs)n return wrappern

再比如,我們要獲取被裝飾的函數的參數,以進行判斷,如下所示:

import functoolsn def check_is_admin(f):n @functools.wraps(f)n def wrapper(*args, **kwargs):n if kwargs.get(username) != admin:n raise Exception("This user is not allowed to get food")n return f(*args, **kwargs)n return wrappern n @check_is_adminn def get_food(username, food=chocolate):n return "{0} get food: {1}".format(username, food)n n print get_food(admin)n

這段代碼看起來沒有任何問題,但是,執行將會出錯,因為,username是一個位置參數,而不是一個關鍵字參數,我們在裝飾器裡面,通過kwargs.get(username)是獲取不到username這個變數的。為了保證靈活性,我們可以通過inspect來修改裝飾器的代碼,如下所示:

import inspectn def check_is_admin(f):n @functools.wraps(f)n def wrapper(*args, **kwargs):n func_args = inspect.getcallargs(f, *args, **kwargs)n print func_argsn if func_args.get(username) != admin:n raise Exception("This user is not allowed to get food")n return f(*args, **kwargs)n return wrappern

裝飾器還有很多知識,比如裝飾器怎麼裝飾一個類,裝飾器的使用場景,裝飾器有哪些缺點,這些,你們都知道嗎?

3. 全局變數

關於Python的全局變數,我們先從一個問題開始:Python有沒有全局變數?可能你看到這個問題的時候就蒙圈了,沒關係,我來解釋一下。

從Python自己的角度來說,Python是有全局變數的,所以,Python為我們提供了global關鍵字,我們能夠在函數裡面修改全局變數。但是,從C/C++/Java程序員的角度來說,Python是沒有全局變數的。因為,Python的全局變數並不是程序級別的(即全局唯一),而是模塊級別的。模塊就是一個Python文件,是一個獨立的、頂層的命名空間。模塊內定義的變數,都屬於該命名空間下,Python並沒有真正的全局變數,變數必然屬於某一個模塊。

我們來看一個例子,就能夠充分理解上面的概念。三種不同的修改全局變數的方法:

import sysn n import testn n a = 1n n def func1():n global an a += 1n n def func2():n test.a += 1n n def func3():n module = sys.modules[test]n module.a += 1n n func1()n func2()n func3()n

這段代碼雖然看起來都是在對全局變數操作,其實,還涉及到命名空間和模塊的工作原理,如果不能很清楚的知道發生了什麼,可能需要補充一下自己的知識了。

4. 時間複雜度

我們都知道,在Python裡面list是異構元素的集合,並且能夠動態增長或收縮,可以通過索引和切片訪問。那麼,又有多少人知道,list是一個數組而不是一個鏈表

關於數組和鏈表的知識,我想大家都知道了,這裡就不再贅述。如果我們在寫代碼的過程中,對於自己最常用的數據結構,連它的時間複雜度都不知道,我們又怎麼能夠寫出高效的代碼呢。寫不出高效的代碼,那我們又怎麼能夠聲稱自己精通這門編程語言呢。

既然list是一個數組,那麼,我們要使用鏈表的時候,應該使用什麼數據結構呢?在寫Python代碼的時候,如果你需要一個鏈表,你應該使用標準庫collections中的deque, deque是雙向鏈表。標準庫裡面有一個queue,看起來和deque有點像,它們是什麼關係?這個問題留著讀者自己回答。

我們再來看一個很實際的例子:有兩個目錄,每個目錄都有大量文件,求兩個目錄中都有的文件,此時,用Set比List快很多。因為,Set的底層實現是一個hash表,判斷一個元素是否存在於某個集合中,List的時間複雜度為O(n),Set的時間複雜度為O(1),所以這裡應該使用Set。我們應該非常清楚Python中各個常用數據結構的時間複雜度,並在實際寫代碼的過程中,充分利用不同數據結構的優勢。

5. Python中的else

最後我們來看一個對Python語言優缺點理解的例子,即Python中增加的兩個else。相對於C++語言或者Java語言,Python語法中多了兩個else。

一個在while循環或for循環中:

while True:n ....n else:n ....n

另一個在try...except語句中:

try:n ....n except:n ....n else:n ....n finally:n ....n

那麼,哪一個是好的設計,哪一個是不好的設計呢?要回答這個問題,我們先來看一下在大家固有的觀念中,else語句起到什麼作用。在所有語言中,else都是和if語句一起出現的:

if <condition>n statement1n elsen statement2n

翻譯成自然語言就是,如果條件滿足,則執行語句1,否則,執行語句2。注意我們前面的用語,是否則,也就是說,else語句在我們固有的觀念中,起到的作用是「否則」,是不滿足條件的情況下才執行的。

我們來看Python中,while循環後面的else語句。這個else語句是在while語句正常結束的時候執行的。所以,按照語意來說,while循環的else起到的作用是and。也就是說,在Python中,while循環末尾的else換做and才是更加合適的。

你可能覺得我有點鑽牛角尖,那好,我再強調一遍,while循環中的else語句是在循環正常結束的時候執行的,那麼請問:

1. 如果while循環裡面遇到了break語句,else語句會執行嗎

2. 如果while循環最後,遇到了continue語句,else語句還會執行嗎

3. 如果while循環內部出現異常,else語句還會執行嗎

這裡的幾個問題,大多數人都不能夠很快的正確回答出來。而我們的代碼是寫給人看的,不應該將大多數人排除在能夠讀懂這段代碼之外。所以我認為,Python語言中循環語句末尾的else語句是一個糟糕的設計

現在,我們再來看try...except語句中的else,這個else設計得特別好,其他語言也應該吸取這個設計。這個設計的語義是,執行try裡面的語句,這裡面的語句可能會出現異常,如果出現了異常,就執行except裡面的語句,如果沒有出現異常,就執行else裡面的語句,最後,無論是否出現異常,都要執行finally語句。這個設計好就好在,else的語句完全和我們的直觀感受是一樣的,是在沒有出現異常的情況下執行。並且,有else比沒有else好,有了else以後,正確地將程序員認為可能出現異常的代碼和不可能出現異常的代碼分開,這樣,更加清楚的表明了是哪一條語句可能會出現異常,更多的暴露了程序員的意圖,使得代碼維護和修改更加容易。

結論:我這篇回答很長,但是,我相信對很多人都會有幫助。這裡想說的是,Python是一門編程語言,使用範圍非常廣泛,大家不要去追求精通Python程序語言自身,而應該將精力放在自己需要解決的實際問題上。其次,絕大多數人對Python的認識都存在誤區,認為Python很簡單,只是簡單地了解一下就開始寫Python代碼,寫出了一堆很不好維護的代碼,我希望這一部分人看到我的回答以後,能夠回去重新學習Python。最後,對於一些同學的疑慮——招聘職位要求精通Python,我的回答是,他們並不奢望招到一個精通Python的人,他們只是想招到一個合格的工程師,而大部分的Python工程師,都,不,合,格!
推薦閱讀:

TAG:Python |