編寫高質量Python程式(四)庫
本系列文章為《編寫高質量程式碼——改善Python程式的91個建議》的精華彙總。
按需選擇 sort()
或者 sorted()
Python 中常用的排序函式有 sort()
和 sorted()
兩者的函式形式分別如下:
sorted(iterable[, cmp[, key[, reverse]]])
s.sort([cmp[, key[, reverse]]])
sort()
和 sorted()
有3個共同的引數:
cmp
:使用者定義的任何比較函式,函式的引數為兩個可比較的元素(來自 iterable 或者 list ),函式根據第一個引數與第二個引數的關係依次返回 -1、0 或者 +1(第一個引數小於第二個引數則返回負數)。該引數預設值為None
key
是一個帶引數的函式,用來為每個元素提取比較值,預設為None
(即直接比較每個元素)reverse
表示排序結果是否反轉
兩者對比:
-
sorted()
作用於任何可迭代的物件;而sort()
一般作用於列表。 -
sorted()
函式會返回一個排序後的列表,原有列表保持不變;而sort()
函式會直接修改原有列表,函式返回為None
。實際應用過程中需要保留原有列表,使用sorted()
函式較為合適,否則可以選擇sort()
函式,因為sort()
函式不需要複製原有列表,消耗的記憶體較少,效率也較高。 -
無論是
sort()
還是sorted()
函式,傳入引數key
cmp
效率要高。cmp
傳入的函式在整個排序過程中會呼叫多次,函式開銷較大;而key
針對每個元素僅做一次處理,因此使用 key 比使用cmp
效率要高。 -
sorted()
功能非常強大,它可以對不同的資料結構進行排序,從而滿足不同需求。
例:
對字典進行排序:
>>> phone_book = {"Linda": "7750", "Bob": "9345", "Carol": "5834"} >>> from operator import itemgetter >>> sorted_pb = sorted(phone_book.items(), key=itemgetter(1)) >>> print(sorted_pb) [('Carol', '5834'), ('Linda', '7750'), ('Bob', '9345')]
多維 List 排序:實際情況下也會碰到需要對多個欄位進行排序的情況,這在 DB 裡面用 SQL 語句很容易做到,但使用多維列表聯合 sorted()
函式也可以輕易達到
>>> import operator
>>> game_result = [["Bob",95,"A"],["Alan",86,"C"],["Mandy",82.5,"A"],["Rob",86,"E"]]
>>> sorted(game_result, key=operator.itemgetter(2, 1))
[['Mandy', 82.5, 'A'], ['Bob', 95, 'A'], ['Alan', 86, 'C'], ['Rob', 86, 'E']]
字典中混合 List 排序:字典中的 key 或者值為列表,對列表中的某一個位置的元素排序
>>> my_dict = {"Li":["M",7],"Zhang":["E",2],"Wang":["P",3],"Du":["C",2],"Ma":["C",9],"Zhe":["H",7]}
>>> import operator
>>> sorted(my_dict.items(), key=lambda item:operator.itemgetter(1)(item[1]))
[('Du', ['C', 2]), ('Zhang', ['E', 2]), ('Wang', ['P', 3]), ('Zhe', ['H', 7]), ('Li', ['M', 7]), ('Ma', ['C', 9])]
List 中混合字典排序:列表中的每一個元素為字典形式,針對字典的多個 key 值進行排序
>>> import operator
>>> game_result = [{"name":"Bob","wins":10,"losses":3,"rating":75},{"name":"David","wins":3,"losses":5,"rating":57},{"name":"Carol","wins":4,"losses":5,"rating":57},{"name":"Patty","wins":9,"losses":3,"rating":71.48}]
>>> sorted(game_result, key=operator.itemgetter("rating","name"))
[{'losses': 5, 'name': 'Carol', 'rating': 57, 'wins': 4}, {'losses': 5, 'name': 'David', 'rating': 57, 'wins': 3}, {'losses': 3, 'name': 'Patty', 'rating': 71.48, 'wins': 9}, {'losses': 3, 'name': 'Bob', 'rating': 75, 'wins': 10}]
使用 copy 模組深拷貝物件
- 淺拷貝(shallow copy):構造一個新的複合物件,並將從原物件中發現的引用插入該物件中。淺拷貝的實現方式有多種,如工廠函式、切片操作、copy 模組中的
copy
操作等。 - 深拷貝(deep copy):也構造一個新的複合物件,但是遇到引用會繼續遞迴拷貝其所指向的具體內容,也就是說它會針對引用所指向的物件繼續執行拷貝,因此產生的物件不受其他引用物件操作的影響。深拷貝的實現需要依賴 copy 模組的
deepcopy()
操作。
淺拷貝並不能進行徹底的拷貝,當存在列表、字典等不可變物件的時候,它僅僅拷貝其引用地址。要解決上述問題需要用到深拷貝,深拷貝不僅拷貝引用也拷貝引用所指向的物件,因此深拷貝得到的物件和原物件是相互獨立的。
使用 Counter 進行計數統計
計數統計就是統計某一項出現的次數。可以使用不同資料結構來進行實現:
- 例如,使用
defaultdict
實現
from collections import defaultdict
some_data = ["a", "2", 2, 4, 5, "2", "b", 4, 7, "a", 5, "d", "a", "z"]
count_frq = defaultdict(int)
for item in some_data:
count_frq[item] += 1
print(count_frq)
# defaultdict(<class 'int'>, {'a': 3, '2': 2, 2: 1, 4: 2, 5: 2, 'b': 1, 7: 1, 'd': 1, 'z': 1})
但更優雅,更 Pythonic 的解決方法是使用 collections.Counter
:
from collections import Counter
some_data = ["a", "2", 2, 4, 5, "2", "b", 4, 7, "a", 5, "d", "z", "a"]
print(Counter(some_data))
# Counter({'a': 3, '2': 2, 4: 2, 5: 2, 2: 1, 'b': 1, 7: 1, 'd': 1, 'z': 1})
深入掌握 ConfigParser
常見的配置檔案格式有 XML 和 ini 等,其中在 MS Windows 系統上,ini 檔案格式用得尤其多,甚至作業系統的 API 也都提供了相關的介面函式來支援它。類似 ini 的檔案格式,在 Linux 等作業系統中也是極常用的,比如 pylint 的配置檔案就是這個格式。Python 有個標準庫來支援它,也就是 ConfigParser。
ConfigParser 的基本用法通過手冊可以掌握,但仍然有幾個知識點值得注意。首先就是 getboolean()
這個函式。getboolean()
根據一定的規則將配置項的值轉換為布林值,如以下的配置:
[section1]
option1=0
當呼叫 getboolean("section1", "option1")
時,將返回 False。
getboolean()
的真值規則: 除了 0 以外,no、false 和 off 都會被轉義為 False,而對應的 1、yes、true 和 on 則都被轉義為 True,其他值都會導致丟擲 ValueError
異常。
還需要注意的是配置項的查詢規則。首先,在 ConfigParser 支援的配置檔案格式裡,有一個 [DEFAULT]
節,當讀取的配置項不在指定的節裡時,ConfigParser 將會到 [DEFAULT]
節中查詢。
除此之外,還有一些機制導致專案對配置項的查詢更復雜,這就是 class ConfigParser 建構函式中的 defaults 形參以及其 get(section, option[, raw[, vars]])
中的全名引數 vars
。如果把這些機制全部用上,那麼配置項值的查詢規則:
- 如果找不到節名,就丟擲 NoSectionError
- 如果給定的配置項出現在
get()
方法的var
引數中,則返回var
引數中的值 - 如果在指定的節中含有給定的配置項,則返回其值
- 如果在 【DEFAULT】中有指定的配置項,則返回其值
- 如果在建構函式的 defaults 引數中有指定的配置項,則返回其值
- 丟擲 NoOptionError
使用 argparse 處理命令列引數
儘管應用程式通常能夠通過配置檔案在不修改程式碼的情況下改變行為,但提供靈活易用的命令列引數仍然非常有意義,比如:減輕使用者的學習成本,通常命令列引數的用法只需要在應用程式名後面加 --help 引數就能獲得,而配置檔案的配置方法通常需要通讀手冊才能掌握。
關於命令列處理,現階段最好用的引數處理標準庫是 argparse。
add_argument()
方法用以增加一個引數宣告。
import argparse
parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('integers', metavar='N', type=int, nargs='+',
help='an integer for the accumulator')
parser.add_argument('--sum', dest='accumulate', action='store_const',
const=sum, default=max,
help='sum the integers (default: find the max)')
args = parser.parse_args()
print(args.accumulate(args.integers))
- 除了支援常規的 int/float 等基本數值型別外,argparse 還支援檔案型別,只要引數合法,程式就能夠使用相應的檔案描述符。
parser = argparse.ArgumentParser()
parser.add_argument("bar", type=argparse.FileType("w"))
parser.parse_args(["out.txt"])
- 擴充套件型別也變得更加容易,任何可呼叫物件,比如函式,都可以作為 type 的實參。另外 choices 引數也支援更多的型別,比如:
parser.add_argument("door", type=int, choices=range(1, 4))
。 - 此外,add_argument() 提供了對必填引數的支援,只要把 required 引數設定為 True 傳遞進去,當缺失這一引數時,argparse 就會自動退出程式,並提示使用者。
- 還支援引數分組。add_argument_group() 可以在輸出幫助資訊時更加清晰,這在用法複雜的 CLI 應用程式中非常有幫助:
parser = argparse.ArgumentParser(prog="PROG", add_help=False)
group1 = parser.add_argument_group("group1", "group1 description")
group1.add_argument("foo", help="foo help")
group2 = parser.add_argument_group("group2", "group2 description")
group2.add_argument("--bar", help="bar help")
parser.print_help()
- 另外還有
add_mutually_exclusive_group(required=False)
非常實用:它確保組中的引數至少有一個或者只有一個(required=True)。 - argparse 也支援子命令,比如
pip
就有install/uninstall/freeze/list/show
等子命令,這些子命令又接受不同的引數,使用 ArgumentParser.add_subparsers() 就可以實現類似的功能。
import argparse
parser = argparse.ArgumentParser(prog="PROG")
subparsers = parser.add_subparsers(help="sub-command help")
parser_a = subparsers.add_parser("a", help="a help")
parser_a.add_argument("--bar", type=int, help="bar help")
parser.parse_args(["a", "--bar", "1"])
- 除了引數處理之外,當出現非法引數時,使用者還需要做一些處理,處理完成後,一般是輸出提示資訊並退出應用程式。ArgumentParser 提供了兩個方法函式,分別是
exit(status=0, message=None)
和error(message)
,可以省了import sys
再呼叫sys.exit()
的步驟。
理解模組 pickle 優劣
序列化,簡單地說就是把記憶體中的資料結構在不丟失其身份和型別資訊的情況下轉換成物件的文字或二進位制表示的過程。物件序列化後的形式經過反序列化過程應該能恢復原有物件。
Python 中有很多支援序列化的模組,如 pickle、json、marshal 和 shelve 等。
pickle 是最通用的序列化模組,它還有個 C 語言的實現 cPickle,相比 pickle 來說具有較好的效能,其速度大概是 pickle 的 1000 倍,因此在大多數應用程式中應該優先使用 cPickle(注:cPickle 除了不能被繼承之外,它們兩者的使用基本上區別不大)。pickle 中最主要的兩個函式對為 dump()
和 load()
,分別用來進行物件的序列化和反序列化。
pickle 良好的特性總結為以下幾點:
-
介面簡單,容易使用。使用
dump()
和load()
便可輕易實現序列化和反序列化。 -
pickle 的儲存格式具有通用性,能夠被不同平臺的 Python 解析器共享。比如 Linux 下序列化的格式檔案可以在 Windows 平臺的 Python 解析器上進行反序列化,相容性較好。
-
支援的資料型別廣泛。如數字、布林值、字串,只包含可序列化物件的元組、字典、列表等,非巢狀的函式、類以及通過類的
__dict__
或者__getstate__()
可以返回序列化物件的例項等。 -
pickle 模組是可以擴充套件的。對於例項物件,pickle 在還原物件的時候一般是不呼叫
__init__()
函式的,如果要呼叫__init__()
進行初始化,對於古典類可以在類定義中提供__getinitargs__()
函式,並返回一個元組,當進行 unpickle 的時候,Python 就會自動呼叫__init__()
,並把__getinitargs__()
中返回的元組作為引數傳遞給__init__()
,而對於新式類,可以提供__getnewargs__()
來提供物件生成時候的引數,在 unpickle 的時候以Class.__new__(Class, *arg)
的方式建立物件。對於不可序列化的物件,如 sockets、檔案控制代碼、資料庫連線等,也可以通過實現 pickle 協議來解決這些鉅獻,主要是通過特殊方法__getstate__()
和__setstate__()
來返回例項在被 pickle 時的狀態。示例:
import cPickle as pickle class TextReader: def __init__(self, filename): self.filename = filename # 檔名稱 self.file = open(filename) # 開啟檔案的控制代碼 self.postion = self.file.tell() # 檔案的位置 def readline(self): line = self.file.readline() self.postion = self.file.tell() if not line: return None if line.endswith("\n"): line = line[:-1] return "{}: {}".format(self.postion, line) def __getstate__(self): # 記錄檔案被 pickle 時候的狀態 state = self.__dict__.copy() # 獲取被 pickle 時的字典資訊 del state["file"] return state def __setstate__(self, state): # 設定反序列化後的狀態 self.__dict__.update(state) file = open(self.filename) self.file = file reader = TextReader("zen.text") print(reader.readline()) print(reader.readline()) s = pickle.dumps(reader) # 在 dumps 的時候會預設呼叫 __getstate__ new_reader = pickle.loads(s) # 在 loads 的時候會預設呼叫 __setstate__ print(new_reader.readline())
-
能夠自動維護物件間的引用,如果一個物件上存在多個引用,pickle 後不會改變物件間的引用,並且能夠自動處理迴圈和遞迴引用。
>>> a = ["a", "b"] >>> b = a # b 引用物件 a >>> b.append("c") >>> p = pickle.dumps((a, b)) >>> a1, b1 = pickle.loads(p) >>> a1 ["a", "b", "c"] >>> b1 ["a", "b", "c"] >>> a1.append("d") # 反序列化對 a1 物件的修改仍然會影響到 b1 >>> b1 ["a", "b", "c", "d"]
但 pickle 使用也存在以下一些限制:
- pickle 不能保證操作的原子性。pickle 並不是原子操作,也就是說在一個 pickle 呼叫中如果發生異常,可能部分資料已經被儲存,另外如果物件處於深遞迴狀態,那麼可能超出 Python 的最大遞迴深度。遞迴深度可以通過
sys.setrecursionlimit()
進行擴充套件。 - pickle 存在安全性問題。Python 的文件清晰地表明它不提供安全性保證,因此對於一個從不可信的資料來源接收到的資料不要輕易進行反序列化。由於 loads() 可以接收字串作為引數,精心設計的字串給入侵提供了一種可能。在 Python 直譯器中輸入程式碼
pickle.loads("cos\nsystem\n(S'dir\ntR.")
便可以檢視當前目錄下所有檔案。可以將 dir 替換為其他更具破壞性的命令。如果要進一步提高安全性,使用者可以通過繼承類 pickle.Unpickler 並重寫find_class()
方法來實現。 - pickle 協議是 Python 特定的,不同語言之間的相容性難以保障。用 Python 建立的 pickle 檔案可能其他語言不能使用。
序列化的另一個不錯的選擇——JSON
Python 的標準庫 JSON 提供的最常用的方法與 pickle 類似,dump/dumps 用來序列化,load/loads 用來反序列化。需要注意 json 預設不支援非 ASCII-based 的編碼,如 load 方法可能在處理中文字元時不能正常顯示,則需要通過 encoding 引數指定對應的字元編碼。在序列化方面,相比 pickle,JSON 具有以下優勢:
- 使用簡單,支援多種資料型別。JSON 文件的構成非常簡單,僅存在以下兩大資料結構:
- 名稱/值對的集合。在各種語言中,它被實現為一個物件、記錄、結構、字典、散列表、鍵列表或關聯陣列。
- 值的有序列表。在大多數語言中,它被實現為陣列、向量、列表或序列。在 Python 中對應支援的資料型別包括字典、列表、字串、整數、浮點數、True、False、None 等。JSON 中資料結構和 Python 中的轉換並不是完全一一對應,存在一定的差異。
- 儲存格式可讀性更為友好,容易修改。相比於 pickle 來說,json 格式更加接近程式設計師的思維,閱讀和修改上要容易得多。dumps() 函式提供了一個引數 indent 使生成的 json 檔案可讀性更好,0 意味著“每個值單獨一行”;大於 0 的數字意味著“每個值單獨一行並且使用這個數字的空格來縮排巢狀的資料結構”。但需要注意的是,這個引數是以檔案大小變大為代價的。
- json 支援跨平臺跨語言操作。如 Python 中生成的 json 檔案可以輕易使用 JavaScript 解析,互操作性更強,而 pickle 格式的檔案只能在 Python 語言中支援。此外 json 原生的 JavaScript 支援,客戶端瀏覽器不需要為此使用額外的直譯器,特別適用於 Web 應用提供快速、緊湊、方便地序列化操作。此外,相比於 pickle,json 的儲存格式更為緊湊,所佔空間更小。
- 具有較強的擴充套件性。json 模組還提供了編碼(JSONEncoder)和解碼類(JSONDecoder)以便使用者對其預設不支援的序列化型別進行擴充套件。
Python 中標準模組 json 的效能比 pickle 與 cPickle 稍遜。如果對序列化效能要求非常高的場景,可以使用 cPickle 模組。
使用 threading 模組編寫多執行緒程式
GIL 的存在使得 Python 多執行緒程式設計暫時無法充分利用多處理器的優勢,並不能提高執行速率,但在以下幾種情況,如等待外部資源返回,或者為了提高使用者體驗而建立反應靈活的使用者介面,或者多使用者應用程式中,多執行緒仍然是一個比較好的解決方案。
Python 為多執行緒程式設計提供了兩個非常簡單明瞭的模組:thread 和 threading。
thread 模組提供了多執行緒底層支援模組,以低階原始的方式來處理和控制執行緒,使用起來較為複雜;而 threading 模組基於 thread 進行包裝,將執行緒的操作物件化,在語言層面提供了豐富的特性。實際應用中,推薦優先使用 threading 模組而不是 thread 模組。
-
就執行緒的同步和互斥來說,threading 模組中不僅有 Lock 指令鎖,RLock 可重入指令鎖,還支援條件變數 Condition、訊號量 Semaphore、BoundedSemaphore 以及 Event 事件等。
-
threading 模組主執行緒和子執行緒互動友好,
join()
方法能夠阻塞當前上下文環境的執行緒,直到呼叫此方法的執行緒終止或到達指定的 timeout(可選引數)。利用該方法可以方便地控制主執行緒和子執行緒以及子執行緒之間的執行。
實際上很多情況下我們可能希望主執行緒能夠等待所有子執行緒都完成時才退出,這時使用 threading 模組守護執行緒,可以通過 setDaemon() 函式來設定執行緒的 daemon 屬性。當 daemon 屬性設定為 True 的時候表明主執行緒的退出可以不用等待子執行緒完成。預設情況下,daemon 標誌為 False,所有的非守護執行緒結束後主執行緒才會結束。
import threading
import time
def myfunc(a, delay):
print("I will calculate square of {} after delay for {}".format(a, delay))
time.sleep(delay)
print("calculate begins...")
result = a * a
print(result)
return result
t1 = threading.Thread(target=myfunc, args=(2, 5))
t2 = threading.Thread(target=myfunc, args=(6, 8))
print(t1.isDaemon())
print(t2.isDaemon())
t2.setDaemon(True)
t1.start()
t2.start()
使用 Queue 使多執行緒程式設計更安全
多執行緒程式設計不是件容易的事情。執行緒間的同步和互斥,執行緒間資料的共享等這些都是涉及執行緒安全要考慮的問題。
Python 中的 Queue 模組提供了 3 種佇列:
-
Queue.Queue(maxsize)
:先進先出,maxsize 為佇列大小,其值為非正數的時候為無限迴圈佇列 -
Queue.LifoQueue(maxsize)
:後進先出,相當於棧 -
Queue.PriorityQueue(maxsize)
:優先順序佇列
這 3 種佇列支援以下方法:
Queue.qsize()
:返回佇列大小。Queue.empty()
:佇列為空的時候返回 True,否則返回 FalseQueue.full()
:當設定了佇列大小的情況下,如果佇列滿則返回 True,否則返回 False。Queue.put(item[, block[, timeout]])
:往佇列中新增元素 item,block 設定為 False 的時候,如果佇列滿則丟擲 Full 異常。如果 block 設定為 True,timeout 為 None 的時候則會一直等待直到有空位置,否則會根據 timeout 的設定超時後丟擲 Full 異常。Queue.put_nowait(item)
:等於put(item, False).block
設定為 False 的時候,如果佇列空則丟擲 Empty 異常。如果 block 設定為 True、timeout 為 None 的時候則會一直等到有元素可用,否則會根據 timeout 的設定超時後丟擲 Empty 異常。Queue.get([block[, timeout]])
:從佇列中刪除元素並返回該元素的值Queue.get_nowait()
:等價於get(False)
Queue.task_done()
:傳送訊號表明入列任務已經完成,經常在消費者執行緒中用到Queue.join()
:阻塞直至佇列中所有的元素處理完畢
Queue 模組是執行緒安全的。需要注意的是, Queue 模組中的佇列和 collections.deque 所表示的佇列並不一樣,前者主要用於不同執行緒之間的通訊,它內部實現了執行緒的鎖機制;而後者主要是資料結構上的概念。
多執行緒下載的例子:
import os
import Queue
import threading
import urllib2
class DownloadThread(threading.Thread):
def __init__(self, queue):
threading.Thread.__init__(self)
self.queue = queue
def run(self):
while True:
url = self.queue.get() # 從佇列中取出一個 url 元素
print(self.name + "begin download" + url + "...")
self.download_file(url) # 進行檔案下載
self.queue.task_done() # 下載完畢傳送訊號
print(self.name + " download completed!!!")
def download_file(self, url): # 下載檔案
urlhandler = urllib2.urlopen(url)
fname = os.path.basename(url) + ".html" # 檔名稱
with open(fname, "wb") as f: # 開啟檔案
while True:
chunk = urlhandler.read(1024)
if not chunk:
break
f.write(chunk)
if __name__ == "__main__":
urls = ["https://www.createspace.com/3611970","http://wiki.python.org/moni.WebProgramming"]
queue = Queue.Queue()
# create a thread pool and give them a queue
for i in range(5):
t = DownloadThread(queue) # 啟動 5 個執行緒同時進行下載
t.setDaemon(True)
t.start()
# give the queue some data
for url in urls:
queue.put(url)
# wait for the queue to finish
queue.join()