Python編程技巧筆記
轉載自:http://www.elias.cn/Python/PythonHint?from=Develop.PythonHint#toc9
(重定向自 Develop.PythonHint)
On this page... (hide)
- 1. 易犯錯誤
- 1.1 避免混用空格和Tab
- 1.2 避免and or語法出錯
- 1.3 避免用列表給函數置默認值
- 1.4 小心list的+=操作
- 2. 好用語法糖
- 2.1 在列表中使用for if子句
- 3. 常見問題
- 3.1 import特定目錄下的腳本
- 3.2 Unicode與字符串編碼
- 4. 經驗技巧
1. 易犯錯誤
1.1 避免混用空格和Tab
這是最初級的原則了。Python解釋器會在執行代碼時將Tab當作數個空格來處理,但是到底當成幾個,其實沒有非常嚴格的保證。因此如果混用空格和Tab,就有可能使縮進產生問題,造成程序的邏輯混亂。爲了代碼穩定並且一致,還是建議統一使用空格。並且絕大多數現代的編輯器是能夠按照使用者的設置自動將Tab轉換爲空格存儲的,並不會因爲使用空格而使編程更麻煩。
1.2 避免and or語法出錯
“and or”語法在一些情況下可以替代if語句,使語法更簡潔清晰。其含義是這樣的:“判斷條件 and 條件爲真時的結果 or 條件爲假時的結果”,例如“x = len(l) > 0 and l[0] or None”。但這個語法本質是利用了Python的邏輯算符的運算特徵來實現,因而存在一定的使用限制,處理不好就有可能出錯。
這裏的主要問題是“條件爲真時的結果”不能取值爲假,否則即使條件爲真也不會返回這個值。在Python裏除了False符號之外,如None、整數0、空的list等等都會被當作邏輯假值來處理。比如“x = len(l) <= 0 and None or l[0]”這種寫法,由於None被當作邏輯假值來處理,所以無論len(l)的取值爲何,最後總是會執行l[0]的,這顯然與該語句原先的預期效果不同。
“and or”語法的另一個可能的問題是and後面的兩個子句會被執行還是被跳過其實不是很明確,需要仔細查閱Python文檔看是否提供了嚴格的保證。
如果希望避免這種出錯可能,那麼也可以乾脆換用另外一種表達方法:“if_true if condition else if_false”,這個表達式在condition爲邏輯真值時執行並返回if_true的值,condition爲邏輯假值時返回if_false的值。
1.3 避免用列表給函數置默認值
如果把列表當作某個函數的默認參數值,那麼每次調用這個函數的時候這一默認參數都會指向同一個列表實例,如果這個列表實例又作爲函數返回值的一部分,有時候會出問題。比如下面這個例子:
def __init__(self, data = []):
self.data = data
foo1 = Foo()
foo2 = Foo()
那麼所有不指定data參數得到的Foo實例,其self.data屬性都會指向同一個列表實例。也就是說foo1.data和foo2.data會指向同一個列表,一個變的時候,另一個也會跟着變。爲解決這個問題,正確的寫法是這樣的:
def __init__(self, data = None):
self.data = data or []
1.4 小心list的+=操作
list的+=操作是存在一定歧義的。一般我們會認爲a += b和a = a + b是等價的,但實際上不總是這麼回事,至少對list來說不完全是一回事。我們會發現a += b會修改到a所原來指向的列表實例,也就是說,其實這和a.extend(b)是等價的。而a = a + b則不修改a所原來指向的列表實例,而只是利用a和b的內容組合出一個新的列表實例以名字a記錄下來罷了。如果這個描述不夠清楚,試着運行下面代碼就清楚了:
b = range(5)
a_orig = a
a += b
print a
print a_orig # a_orig在以上處理過程中取值被改變了。
a = range(10)
b = range(5)
a_orig = a
a = a + b
print a
print a_orig # a_orig還是原來的值,沒有變。
2. 好用語法糖
2.1 在列表中使用for if子句
Python允許在創建列表的過程中套用for if子句來構建更方便的表達方式,稱爲list comprehension,比如:
[x**2 for x in range(10)]
# 把列表中,大於3的元素,乘以2
vec = [2, 4, 6]
[2*x for x in vec if x > 3] # 得到[8, 12]
一種很重要的用法是初始化多維列表,正確的做法是這樣:
p1 = [1] * 100
# 二維
p2 = [[1] * 100 for i in xrange(100)]
# 三維
p3 = [[[1] * 100 for i in xrange(100)] for j in xrange(100)]
僅僅使用
生成器表達式
有時我們會看到下面這樣看起來很像的表達方式:
3. 常見問題
3.1 import特定目錄下的腳本
Python裏的import指令有自己的目錄查詢順序,那麼如果我們想引入特定目錄下的一個腳本文件,比如上一層目錄中的一段代碼時,該怎麼實現呢?可以像下面這樣:
>>> sys.path.append('..')
>>> import test
3.2 Unicode與字符串編碼
Python 2.x中字符編碼的處理也是經常會碰到的,據說Python 3.0將會對這一點進行改進。在2.x中,其實str字符串是對C語言char的簡單封裝,因此通常就是ascii,如果其中出現多字節字符,那麼Python會使用當前設置的默認編碼來進行解釋,中文Windows下一般是GBK或者GB18030,中文Mac下則通常是UTF-8,如果猜錯了呢,Python就會拋出UnicodeDecodeError等異常。在大約2.4以前的版本中,是可以使用sys.setdefaultencode方法來指定Python運行環境的默認編碼,但後來的版本將這個方法設置爲默認不可見,除非顯式重載sys模塊,並且這個方法也不推薦使用。
在涉及非ascii編碼數據時,比如抓取網頁或者從文件系統讀取數據就很可能出現這種情況,通常建議使用str字符串的decode方法將數據顯式轉換爲unicode,在處理完成準備進行存儲時,則根據當前操作系統等的需要調用unicode類型的encode方法轉換爲需要的目標編碼進行存儲。如果無法確定應該使用什麼樣的編碼來進行處理,也可以考慮捕獲UnicodeDecodeError異常來逐個猜測可用的編碼。
Python默認的文件IO界面的open函數其實是把數據當作ascii來進行處理的,如果要指定編碼完成文件IO操作,則可以使用codecs模塊的open方法來替代。
4. 經驗技巧
4.1 利用Queue模塊完成多線程編程
使用 Python 進行線程編程這篇文章介紹了一種比較簡單地實現Python多線程編程的方法,並且這種用法被認爲是Python線程編程的最佳實踐之一。其本質是利用了線程安全的Queue模塊來作爲線程之間數據溝通的界面,從而簡化了很多用於處理死鎖的代碼。在實際使用時,既可以在代碼開頭將所有待處理數據一股腦扔進隊列裏供線程池逐個處理,也可以對線程進行分工,有的爲隊列生產數據,有的負責消耗隊列中的數據。參考Queue模塊的源代碼,可以仿照該模塊的接口進行擴展。
類似地,可以利用Queue模塊配合Python processing模塊實現比較簡單的Python多進程模型開發。用進程代替線程,雖然增加了資源佔用,但是能夠繞過Python Gil機制的限制,更有效地利用多CPU的運算能力。
但是這裏有一個問題沒有搞明白,除了利用多CPU計算能力的考慮外,在什麼情況下應該使用進程,又是什麼情況下適合使用線程呢。所以我做了一個實驗來比較Python不同的併發實現方案的性能,見Python幾種併發實現方案的性能比較。
4.2 給耗時操作增加統一的TimeOut超時處理機制
無論是否啓用了Python的多線程機制,只要利用signal模塊就可以爲耗時操作增加統一的超時處理機制(當然在使用了多線程的情況下還是有一些不一樣的地方,只有在主線程裏面纔可以調用signal.signal函數,而子線程可以調用signal.alarm函數對信號的狀態進行設置,具體需參照signal模塊自身文檔)。單線程情況下,可直接參考如下示例:
def handler(signum, frame):
print 'Signal handler called with signal', signum
raise TimeOutError, "TimeOut!"
try:
# Set the signal handler and a 1-second alarm
signal.signal(signal.SIGALRM, handler)
signal.alarm(1)
# This while loop hang indefinitely
while True:
print 'a',
signal.alarm(0) # Disable the alarm
except:
print 'Time out caught!'