如何像Python高手(Pythonista)一樣程式設計
最近在網上看到一篇介紹Pythonic程式設計的文章:Code Like a Pythonista: Idiomatic Python,其實作者在2006的PyCon會議後就寫了這篇文章,寫這篇文章的主要原因是作者發現很多有經驗的Pythoner寫出的程式碼不夠Pythonic。我覺得這篇文章很不錯,所以將它用中文寫了下來(不是逐字的翻譯,中間加了一些自己的理解),分享給大家。另:由於本人平時時間有限,這篇文章翻譯了比較長的時間,如果你發現了什麼不對的地方,歡迎指出。。
一、Python之禪(The Zen of Python)
The Zen of Python是Python語言的指導原則,遵循這些基本原則,你就可以像個Pythonista一樣
The Zen of Python, by Tim Peters Beautiful is better than ugly. # 優美勝於醜陋(Python以編寫優美的程式碼為目標) Explicit is better than implicit. # 明瞭勝於晦澀(優美的程式碼應當是明瞭的,命名規範,風格相似) Simple is better than complex. # 簡潔勝於複雜(優美的程式碼應當是簡潔的,不要有複雜的內部實現) Complex is better than complicated. # 複雜勝於凌亂(如果複雜不可避免,那程式碼間也不能有難懂的關係,要保持介面簡潔) Flat is better than nested. # 扁平勝於巢狀(優美的程式碼應當是扁平的,不能有太多的巢狀) Sparse is better than dense. # 間隔勝於緊湊(優美的程式碼有適當的間隔,不要奢望一行程式碼解決問題) Readability counts. # 可讀性很重要(優美的程式碼是可讀的) Special cases aren't special enough to break the rules. Although practicality beats purity. # 即便假借特例的實用性之名,也不可違背這些規則(這些規則至高無上) Errors should never pass silently. Unless explicitly silenced. # 不要包容所有錯誤,除非你確定需要這樣做(精準地捕獲異常,不寫except:pass風格的程式碼) In the face of ambiguity, refuse the temptation to guess. # 當存在多種可能,不要嘗試去猜測 There should be one-- and preferably only one --obvious way to do it. # 而是儘量找一種,最好是唯一一種明顯的解決方案(如果不確定,就用窮舉法) Although that way may not be obvious at first unless you're Dutch. # 雖然這並不容易,因為你不是 Python 之父(這裡的Dutch是指Guido) Now is better than never. Although never is often better than *right* now. # 做也許好過不做,但不假思索就動手還不如不做(動手之前要細思量) If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. # 如果你無法向人描述你的方案,那肯定不是一個好方案;反之亦然(方案測評標準) Namespaces are one honking great idea -- let's do more of those! # 名稱空間是一種絕妙的理念,我們應當多加利用(倡導與號召)
這首特別的“詩”開始作為一個笑話,但它確實包含了很多關於Python背後的哲學真理。Python之禪已經正式成文PEP 20,具體內容見:PEP 20
二、PEP8: Python編碼規範(PEP8: Style Guide for Python Code)
Abelson & Sussman在《計算機程式的構造和解釋》一書中說道:程式是寫來給人讀的,只是順帶讓機器執行。所以,我們在編碼時應該儘量讓它更易讀懂。PEP8是Python的編碼規範,官方文件見:PEP 8,PEP是Python Enhancement Proposal的縮寫。PEP8包括很多編碼的規範,下面主要介紹一下縮排和命名等內容。
空格和縮排(WhiteSpace and Indentation)
空格和縮排在Python語言中非常重要,它替代了其他語言中{}的作用,用來區分程式碼塊和作用域。在這方面PEP8有以下的建議:
1、每次縮排使用4個空格 2、不要使用Tab,更不要Tab和空格混用 3、兩個方法之間使用一個空行,兩個Class之間使用兩個空行 4、新增一個空格在字典、列表、序列、引數列表中的“,“後,以及在字典中的”:“之後,而不是之前 5、在賦值和比較兩邊放置一個空格(引數列表中除外) 6、緊隨括號後面或者引數列表前一個字元不要存在空格
Python命名
命名規範是程式設計語言的基礎,而且大部分的規範對於高階語言來說都是一樣的,Python的基本規範如下:
1、方法 & 屬性:joined_lower 2、常量:joined_lower or ALL_CAPS 3、類:StudlyCaps 4、類屬性:interface, _internal, __private 5、camelCase only to conform to pre-existing conventions
以上內容只是對PEP8做了非常簡單的介紹,由於今天的主題不在於此,所以就不在這裡多講。想要更加深入的瞭解Python編碼規範,可以閱讀PEP8官方文件和Google Python編碼規範等內容。
三、交換變數值(Swap Values)
在其他語言中,交換兩個變數值的時候,可以這樣寫:
temp = a a = b b = temp
在Python中,我們可以簡單的這樣寫:
b, a = a, b
可能你已經在其他地方見過這種寫法,但是你知道Python是如何實現這種語法的嗎?首先,逗號(,)是Python中tuple資料結構的語法;上面的語法會執行一下的操作:
1、Python會先將右邊的a, b生成一個tuple(元組),存放在記憶體中;
2、之後會執行賦值操作,這時候會將tuple拆開;
3、然後將tuple的第一個元素賦值給左邊的第一個變數,第二個元素賦值給左邊第二個變數。
再舉個tuple拆分的例子:
In [1]: people = ['David', 'Pythonista', '15145551234'] In [2]: name, title, phone = people In [3]: name Out[3]: 'David' In [4]: title Out[4]: 'Pythonista' In [5]: phone Out[5]: '15145551234'
這種語法在For迴圈中非常實用:
In [6]: people = [['David', 'Pythonista', '15145551234'], ['Wu', 'Student', '15101365547']] In [7]: for name, title, phone in people: ...: print name, phone ...: David 15145551234 Wu 15101365547
PS:在使用這種語法時,需要確保左邊的變數個數和右邊tuple的個數一致,否則,Python會丟擲ValueError異常。
更多tuple的例子:
>>> 1, (1,) >>> (1,) (1,) >>> (1) 1 >>> value = 1, >>> value (1,)
我們知道:逗號(,)在Python中是建立tuple的構造器,所以我們可以按照上面的方式很方便的建立一個tuple;需要注意的是:如果宣告只有一個元素的tuple,末尾必須要帶上逗號,兩個以上的元素則不需要。宣告tuple的語法很簡單,但同時它也比較坑:如果你發現Python中的變數不可思議的變成了tuple,那很可能是因為你多寫了一個逗號。。
四、Python控制檯的"_"(Interactive "_")
這是Python中比較有用的一個功能,不過有很多人不知道(我也是接觸Python很久之後才知道的)。。在Python的互動式控制檯中,當你計算一個表示式或者呼叫一個方法的時候,運算的結果都會放在一個臨時的變數 _ 裡面。_(下劃線)用來儲存上一次的列印結果,比如:
>>> import math >>> math.pi / 3 1.0471975511965976 >>> angle = _ >>> math.cos(angle) 0.50000000000000011 >>> _ 0.50000000000000011
PS:當返回結果為None的時候,控制檯不會列印,_ 裡面儲存的值也就不會改變。
五、合併字串(Building Strings from Sub strings)
假如現在有一個list,裡面是一些字串,你現在需要將它們合併成一個字串,最簡單的方法,你可以按照下面的方式去處理:
colors = ['red', 'blue', 'green', 'yellow'] result = '' for s in colors: result += s
但是,很快你會發現:這種方法非常低效,尤其當list非常大的時候。Python中的字串物件是不可改變的,因此對任何字串的操作如拼接,修改等都將產生一個新的字串物件,而不是基於原字串。所以,上面的方法會消耗很大的記憶體:它需要計算,儲存,同時扔掉中間的計算結果。正確的方法是使用Python中的join方法:
result = ','.join(colors)
當合並元素比較少的時候,使用join方法看不出太大的效果;但是當元素多的時候,你會發現join的效率還是非常明顯的。不過,在使用的時候請注意:join只能用於元素是字串的list,它不會進行任何的強制型別轉換。連線一個存在一個或多個非字串元素的list時將丟擲異常。
六、使用關鍵字in(Use in where possible)
當你需要判斷一個KEY是否在dict中或者要遍歷dict的KEY時,最好的方法是使用關鍵字in:
d = {'a': 1, 'b': 2} if 'c' in d: print True # DO NOT USE if d.has_key('c'): print True for key in d: print key # DO NOT USE for key in d.keys(): print key
Python的dict物件是對KEY做過hash的,而keys()方法會將dict中所有的KEY作為一個list物件;所以,直接使用in的時候執行效率會比較快,程式碼也更簡潔。
七、字典(Dictionary)
dict是Python內建的資料結構,在寫Python程式時會經常用到。這裡介紹一下它的get方法和defaultdict方法。
1、get
在獲取dict中的資料時,我們一般使用index的方式,但是如果KEY不存在的時候會丟擲KeyError。這時候你可以使用get方法,使用方法:dict.get(key, default=None),可以避免異常。例如:
d = {'a': 1, 'b': 2} print d.get('c') # None print d.get('c', 14) # 14
2、fromkeys
dict本身有個fromkeys方法,可以通過一個list生成一個dict,不過得提供預設的value,例如:
# ⽤序列做 key,並提供預設value >>> dict.fromkeys(['a', 'b', 'c'], 1) # {'a': 1, 'c': 1, 'b': 1}
3、setdefault
有些情況下,我們需要給dict的KEY一個預設值,你可以這樣寫:
equities = {} for (portfolio, equity) in data: if portfolio in equities: equities[portfolio].append(equity) else: equities[portfolio] = [equity]
上面的實現方式很麻煩,使用dict的setdefault(key, default)方法會更簡潔,更效率。
equities = {}
for (portfolio, equity) in data:
equities.setdefault(portfolio, []).append(equity)
setdefault方法相當於"get, or set & get",或者相當於"set if necessary, then get"
八、defaultdict
defaultdict是Python2.5之後引入的功能,具體的用法我已經在另外一篇文章中詳細介紹:Python的defaultdict模組和namedtuple模組
九、字典的組裝和拆分(Building & Splitting Dictionaries)
在Python中,你可以使用zip方法將兩個list組裝成一個dict,其中一個list的值作為KEY,另外一個list的值作為VALUE:
>>> given = ['John', 'Eric', 'Terry', 'Michael'] >>> family = ['Cleese', 'Idle', 'Gilliam', 'Palin'] >>> pythons = dict(zip(given, family)) >>> print pythons {'John': 'Cleese', 'Michael': 'Palin', 'Eric': 'Idle', 'Terry': 'Gilliam'}
相反的,你可以使用dict的keys()和values()方法來獲取KEY和VALUE的列表:
>>> s.keys() ['John', 'Michael', 'Eric', 'Terry'] >>> pythons.values() ['Cleese', 'Palin', 'Idle', 'Gilliam']
需要注意的是:由於dict本身是無序的,所以通過keys()和values()方法獲得的list的順序已經和原始的list不一樣了。。
十、Python的True值(Truth Values)
在Python中,判斷一個變數是否為True的時候,你可以這樣做:
# 這樣寫 if x: pass # !不要這樣寫 if x == True: pass # 對於list,要這樣寫 if items: pass # !不要這樣寫 if len(items) == 0: pass
Python中的真值物件有以下幾個:
False | True |
---|---|
False (== 0) | True (== 1) |
"" (空字串) | 除 "" 之外的字串(" ", "anything") |
0, 0.0 | 除 0 之外的數字(1, 0.1, -1, 3.14) |
[], (), {}, set() | 非空的list,tuple,set和dict ([0], (None,), ['']) |
None | 大部分的物件,除了明確指定為False的物件 |
對於自己宣告的class,如果你想明確地指定它的例項是True或False,你可以自己實現class的__nonzero__或__len__方法。當你的class是一個container時,你可以實現__len__方法,如下:
class MyContainer(object):
def __init__(self, data):
self.data = data
def __len__(self):
""" Return my length. """
return len(self.data)
如果你的class不是container,你可以實現__nonzero__方法,如下:
class MyClass(object): def __init__(self, value): self.value = value def __nonzero__(self): """ Return my truth value (True or False). """ # This could be arbitrarily complex: return bool(self.value)
在Python 3.x中,__nonzero__方法被__bool__方法替代。考慮到相容性,你可以在class定義中加上以下的程式碼:
__bool__ = __nonzero__
十一、enumerate:索引和元素(Index & Item: enumerate)
在Python中,我們在遍歷列表的時候,可以通過enumerate方法來獲取遍歷時的index,比如:
>>> items = 'zero one two three'.split() >>> print list(enumerate(items)) [(0, 'zero'), (1, 'one'), (2, 'two'), (3, 'three')] >>> for (index, item) in enumerate(items): print index, item
enumerate方法是惰性方法,所以它只會在需要的時候生成一項,也因此在上述程式碼print的時候需要包裝一個list。enumerate其實是一個生成器(generator),這個下面會講到。使用enumerate之後,for迴圈變得很簡單:
for (index, item) in enumerate(items): print index, item # compare: index = 0 for item in items: print index, item index += 1 # compare: for i in range(len(items)): print i, items[i]
使用enumerate的程式碼比其他兩個都短,而且更簡單,更容易讀懂。下面的例子可以說明一下enumerate實際返回的資料:一個迭代器,
>>> enumerate(items)
<enumerate object at 0x011EA1C0>
>>> e = enumerate(items)
>>> e.next()
(0, 'zero')
>>> e.next()
(1, 'one')
>>> e.next()
(2, 'two')
>>> e.next()
(3, 'three')
>>> e.next()
Traceback (most recent call last):
File "<stdin>", line 1, in ?
StopIteration
十二、Python中的變數 & 引用(variables & names)
在很多其他高階語言中,給一個變數賦值時會將"value"放在一個"盒子"裡:
int a = 1;
如圖:
現在,盒子"a"中包含了一個整數 1;將另外一個"value"賦值給同一個變數時,會將"盒子"中的內容替換掉:
a = 2;
如圖:
現在,盒子"a"中包含了一個整數 2;將變數賦值給其他一個變數時,會將"value"拷貝一份放在一個新的"盒子"中:
int b = a;
如圖:
盒子"b"是第二個"盒子",裡面是整數 2的一個拷貝,盒子"a"中是另外一個拷貝。
在Python中,變數沒有資料型別,是附屬於物件的標示符名稱,如下圖:實際,這段表明了像,PHP這類動態指令碼語言中“變數”包含了兩個內容:1 識別符號名稱 2 識別符號所對應(引用)的值(物件),也就是說“變數”不在是一個容器。
a = 1
這裡,整數 1 物件有一個名字為 "a" 的變數(tag)。如果我們給變數 "a" 重新賦值,對Python來說,只是將變數(tag) "a" 指向另外一個物件:
a = 2
現在,變數 "a" 是附屬在整數物件 2 上面。最初的整數物件 1 已經沒有指向它的變數 "a",它可能還存在,但是我們已經不能通過變數 "a"獲得。當一個物件沒有了指向它的引用的時候,它將會被從記憶體中刪除(垃圾回收)。如果我們將存在的變數賦值給一個新的變數,Python會在已經存在的物件上加上一個指向自己的變數(tag)。
b = a
變數 "a"和"b" 是指向同一個整數物件的。
PS:Python中的變數,引用等設計和其他語言不同,這裡只是將原文翻譯說明了一下,更多的介紹可以參看:Python中的變數、引用、拷貝和作用域
十三、Python方法中引數的預設值(Default Parameter Values)
對於Python初學者來說,Python的方法預設引數有一個很容易犯錯的地方:在預設引數中使用可變物件,甚至有不少Python老鳥也可能會在這個問題上掉坑裡,如果他們不能理解Python的物件引用。。問題如下:
def bad_append(new_item, a_list=[]): a_list.append(new_item) return a_list >>> print bad_append('one') ['one'] >>> print bad_append('two') ['one', 'two']
這個問題的主要原因是:a_list引數的預設值是一個空的list,它在函式定義的時候已經被建立。所以,之後每次呼叫該函式的時候,a_list的預設值都是這個list物件。List,dict和set是可變物件,如果想在函式中獲取一個預設的list(dict or set)物件,正確的做法是在函式中建立:
def good_append(new_item, a_list=None): if a_list is None: a_list = [] a_list.append(new_item) return a_list
十四、字串格式化(String Formatting)
在許多程式設計語言中都包含有格式化字串的功能,比如C語言中的格式化輸入輸出。Python中內建有對字串進行格式化的操作符 "%" 以及str.format()方法。
1、操作符 "%"
Python中的 "%" 操作符和C語言中的sprintf類似。簡單來說,使用 "%" 來格式化字串的時候,你需要提供一個字串模板和用來插入的值。模板中有格式符,這些格式符為真實值預留位置,並說明真實數值應該呈現的格式。Python用一個tuple將多個值傳遞給模板,每個值對應一個格式符。注意:給定的值一定要和模板中的格式符一一對應!
name = 'xianglong' messages = 3 text = ('Hello %s, you have %i messages' % (name, messages)) print text # Output: Hello xianglong, you have 3 messages
在上面的例子中,"Hello %s, you have %i messages" 是字串模板。%s為第一個格式符,表示一個字串。%i為第二個格式符,表示一個十進位制整數。(name, messages)的兩個元素為替換%s和%i的真實值。在模板和tuple之間,有一個%號分隔,它代表了格式化操作。
常用的格式符如下:
格式 | 描述 |
%% | 百分號 % 標記 |
%s | 字串 (採用str()的顯示) |
%r | 字串 (採用repr()的顯示) |
%c | 字元及其ASCII碼 |
%b | 二進位制整數 |
%d | 十進位制整數 (有符號整數) |
%u | 十進位制整數 (無符號整數) |
%i | 十進位制整數 (有符號整數) |
%o | 八進位制整數 (無符號整數) |
%x | 十六進位制整數 (無符號整數) |
%X | 十六進位制整數 (無符號整數) |
%e | 指數 (基底寫為e) |
%E | 指數 (基底寫為E) |
%f | 浮點數 |
%F | 浮點數,與上相同 |
%g | 指數(e)或浮點數 (根據顯示長度) |
%G | 指數(E)或浮點數 (根據顯示長度) |
%p | 指標(用十六進位制列印值的記憶體地址) |
%n | 儲存輸出字元的數量放進引數列表的下一個變數中 |
使用操作符 "%" 也可以通過字典格式化字串:
values = {'name': name, 'messages': messages} print ('Hello %(name)s, you have %(messages)i messages' % values) # Output: Hello xianglong, you have 3 messages
上面的程式碼中,我們指定了用來格式化的值的名字,然後可以根據name在字典中查詢相應的value。其實,上面的"name"和"messages"已經在local名稱空間中定義,所以,我們可以利用這一點:
print ('Hello %(name)s, you have %(messages)i messages' % locals())
locals()方法返回一個包含所有本地變數的字典。這個功能非常強大,你可以不必擔心提供的values是否和模板匹配;但是同時這個也是非常危險的:你將會暴露整個本地名稱空間給呼叫者,這一點需要你注意。
在Python中,物件有一個__dict__屬性,你可以在格式化字串的時候使用;
print ("We found %(error_count)d errors" % self.__dict__) # 等同於 print ("We found %d errors" % self.error_count)
另外,我們還可以用如下的方式,對字串格式化進一步的控制:%[(name)][flags][width].[precision]typecode,其中:
(name)為命名
flags可以有+,-,' '或0。+表示右對齊。-表示左對齊。' '為一個空格,表示在正數的左側填充一個空格,從而與負數對齊。0表示使用0填充。
width表示顯示寬度
precision表示小數點後精度
比如:
print("%+10x" % 10) # +a print("%04d" % 5) # 0005 print("%6.3f" % 2.3) # 2.300
上面的width, precision為兩個整數。我們可以利用*,來動態代入這兩個量。比如:
print("%.*f" % (4, 1.2)) # 1.2000
Python實際上用4來替換*。所以實際的模板為"%.4f"。
2、str.format()方法
str.format()方法是在Python 2.6中引入的,它通過 {} 和 : 來代替 % ,功能非常強大。具體的用法見下面的例子:
In [1]: name = 'xianglong' In [2]: messages = 4 # 通過位置 In [3]: 'Hello {0}, you have {1} messages'.format(name, messages) Out[3]: 'Hello xianglong, you have 4 messages' # 通過關鍵字引數 In [4]: 'Hello {name}, you have {messages} messages'.format(name=name, messages=messages) Out[4]: 'Hello xianglong, you have 4 messages' # 通過下標 In [5]: 'Hello {0[0]}, you have {0[1]} messages'.format([name, messages]) Out[5]: 'Hello xianglong, you have 4 messages' # 格式限定符:填充與對齊 # ^、<、>分別是居中、左對齊、右對齊,後面頻寬度 # :號後面帶填充的字元,只能是一個字元,不指定的話預設是用空格填充 In [6]: 'Hello {0:>14}, you have {1:>14} messages'.format(name, messages) Out[6]: 'Hello xianglong, you have 4 messages' # 格式限定符:精度與型別f In [7]: '{:.2f}'.format(321.33345) Out[7]: '321.33' # 格式限定符:b、d、o、x分別是二進位制、十進位制、八進位制、十六進位制 In [8]: '{:b}'.format(14) Out[8]: '1110' In [9]: '{:d}'.format(14) Out[9]: '14' In [10]: '{:o}'.format(14) Out[10]: '16' In [11]: '{:x}'.format(14) Out[11]: 'e' # 格式限定符:千位分隔符 In [12]: '{:,}'.format(1234567890) Out[12]: '1,234,567,890'
十五、迭代器(List comprehensions)
List Comprehensions即迭代器(列表生成式),是Python內建的非常簡單卻強大的可以用來建立list的生成式。在不使用迭代器的時候,建立一個新列表可以使用for和if來實現:
new_list = [] for item in a_list: if condition(item): new_list.append(fn(item))
使用迭代器的話:
new_list = [fn(item) for item in a_list if condition(item)]
列表生成式非常簡潔的,不過是在某種程度上。你可以在列表生成式中使用多個for迴圈和多個if語句,但是兩個以上的for和if語句會讓列表生成式非常複雜,這時候建議直接用for迴圈。根據Zen of Python,選擇更容易讀的方式。下面是一些例子:
>>> [n ** 2 for n in range(10)] [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] >>> [n ** 2 for n in range(10) if n % 2] [1, 9, 25, 49, 81]
十六、生成器(Generator & Generator expressions)
先出一個題:計算1 ~ 100的平方和。最簡單的方法就是使用一個for迴圈:
total = 0 for num in range(1, 101): total += num * num
其實,我們可以使用Python內建的sum方法計算:
# 迭代器(列表生成式) total = sum([num * num for num in range(1, 101)]) # 生成器 total = sum(num * num for num in xrange(1, 101))
生成器和上面提到的迭代器差不多,可以說:生成器是一種特殊的迭代器;但是它們之間有一個很大