Python 中常見的數據結構

Python 數據結構 Java 技術 算法 程序員 圖靈教育 2019-06-28
Python 中常見的數據結構

來自Unsplash

有沒有什麼是每個Python 開發者都應該進一步練習和學習的呢?

那就是數據結構。數據結構是構建程序的基礎。各個數據結構在組織方式上有自己的特點,以便在不同情況下高效訪問數據。我相信無論程序員的技術水平或經驗如何,掌握一些基本功總是有好處的。

我並不主張只專注於掌握更多的數據結構知識,這是一種“失效模式”(failure mode),只會讓人陷入假想理論上的幻境,而不會帶來任何實際的結果。不過花一些時間來補習數據結構(和算法)的知識總會有好處。

無論是花幾天時間“突擊”,還是利用零碎的時間持續學習,在數據結構上下點功夫都是值得的。那麼Python 中有哪些數據結構呢?列表、字典、集合,還有……棧?Python 有棧嗎?

看到沒?Python 在其標準庫中提供了大量的數據結構,但問題在於各自的命名有點詞不達意。

舉例來說,很多人甚至不清楚Python 是否具體實現了像棧這樣著名的“抽象數據類型”。相比之下,Java 等其他語言則更“計算機科學化”,其中的命名很明確。比如,Java 中的列表還細分成了LinkedList 和ArrayList。

這種細分的命名便於我們識別各個數據類型的預期行為和計算複雜度。Python 也傾向於使用簡單且“人性化”的命名方案。我喜歡Python 的方案,因為人性化也是Python 編程更有趣的原因之一。

這種方案的缺點在於,即使是經驗豐富的Python 開發人員,也不清楚內置的列表類型是以鏈表還是動態數組實現的。如果需要用到這些知識卻沒有掌握,則會讓人感到沮喪,也可能導致面試被拒。

本文將介紹Python 及其標準庫內置的基本數據結構和抽象數據類型的實現。

我們的目標是闡釋常見的抽象數據類型在Python 中對應的名稱及實現,並逐個進行簡單的介紹。這些內容也會幫助你在Python 面試中大放異彩。

如果你正在尋找一本能夠用來溫習通用數據結構知識的好書,我強烈推薦Steven S. Skiena的《算法設計手冊》。

這本書介紹了各種數據結構及其各自在不同算法中的實際應用,並在這兩個方面之間取得了很好的平衡。它對編寫本文提供了很大的幫助。

一、字典、映射和散列表

在Python 中,字典是核心數據結構。字典可以存儲任意數量的對象,每個對象都由唯一的字典鍵標識。

字典通常也被稱為映射、散列表、查找表或關聯數組。字典能夠高效查找、插入和刪除任何與給定鍵關聯的對象。

這在現實中意味著什麼呢?字典對象相當於現實世界中的電話簿。

電話簿有助於快速檢索與給定鍵(人名)相關聯的信息(電話號碼)。因此不必為了查找某人的號碼而瀏覽整本電話簿,根據人名基本上就能直接跳到需要查找的相關信息。

若想研究以何種方式組織信息才有利於快速檢索,上述類比就不那麼貼切了。但基本性能特徵相同,即字典能夠用來快速查找與給定鍵相關的信息。

總之,字典是計算機科學中最常用且最重要的數據結構之一。

那麼Python 如何處理字典呢?

我們來看看Python 及其標準庫中可用的字典實現。

1.dict——首選字典實現

由於字典非常重要,因此Python 直接在語言核心中實現了一個穩健的字典:dict 數據類型。

Python 還提供了一些有用的“語法糖”來處理程序中的字典。例如,用花括號字典表達式語法和字典解析式能夠方便地創建新的字典對象:

phonebook = {
'bob': 7387,
'alice': 3719,
'jack': 7052,
}
squares = {x: x * x for x in range(6)}
>>> phonebook['alice']
3719
>>> squares
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

關於哪些對象可以作為字典鍵,有一些限制。

Python 的字典由可散列類型的鍵來索引。可散列對象具有在其生命週期中永遠不會改變的散列值(參見__hash__),並且可以與其他對象進行比較(參見__eq__)。另外,相等的可散列對象,其散列值必然相同。

像字符串和數這樣的不可變類型是可散列的,它們可以很好地用作字典鍵。元組對象也可以用作字典鍵,但這些元組本身必須只包含可散列類型。

Python 的內置字典實現可以應對大多數情況。字典是高度優化的,並且是Python 語言的基石,例如棧幀中的類屬性和變量都存儲在字典中。

Python 字典基於經過充分測試和精心調整過的散列表實現,提供了符合期望的性能特徵。一般情況下,用於查找、插入、更新和刪除操作的時間複雜度都為O(1)。

大部分情況下,應該使用Python 自帶的標準字典實現。但是也存在專門的第三方字典實現,例如跳躍表或基於B 樹的字典。

除了通用的dict 對象外,Python 的標準庫還包含許多特殊的字典實現。它們都基於內置的字典類,基本性能特徵相同,但添加了其他一些便利特性。

下面來逐個瞭解一下。

2.collections.OrderedDict——能記住鍵的插入順序

collections.OrderedDict是特殊的dict 子類,該類型會記錄添加到其中的鍵的插入順序。

儘管在CPython 3.6 及更高版本中,標準的字典實現也能保留鍵的插入順序,但這只是CPython 實現的一個副作用,直到Python 3.7 才將這種特性固定下來了。因此,如果在自己的工作中很需要用到鍵順序,最好明確使用OrderedDict 類。

順便說一句,OrderedDict 不是內置的核心語言部分,因此必須從標準庫中的collections模塊導入。

>>> import collections
>>> d = collections.OrderedDict(one=1, two=2, three=3)
>>> d
OrderedDict([('one', 1), ('two', 2), ('three', 3)])
>>> d['four'] = 4
>>> d
OrderedDict([('one', 1), ('two', 2),
('three', 3), ('four', 4)])
>>> d.keys()
odict_keys(['one', 'two', 'three', 'four'])

3.collections.defaultdict——為缺失的鍵返回默認值

defaultdict 是另一個dict 子類,其構造函數接受一個可調用對象,查找時如果找不到給定的鍵,就返回這個可調用對象。

與使用get()方法或在普通字典中捕獲KeyError 異常相比,這種方式的代碼較少,並能清晰地表達出程序員的意圖。

>>> from collections import defaultdict
>>> dd = defaultdict(list)
# 訪問缺失的鍵就會用默認工廠方法創建它並將其初始化
# 在本例中工廠方法為list():
>>> dd['dogs'].append('Rufus')
>>> dd['dogs'].append('Kathrin')
>>> dd['dogs'].append('Mr Sniffles')
>>> dd['dogs']
['Rufus', 'Kathrin', 'Mr Sniffles']

4.collections.ChainMap——搜索多個字典

collections.ChainMap 數據結構將多個字典分組到一個映射中,在查找時逐個搜索底層映射,直到找到一個符合條件的鍵。對ChainMap 進行插入、更新和刪除操作,只會作用於其中的第一個字典。

>>> from collections import ChainMap
>>> dict1 = {'one': 1, 'two': 2}
>>> dict2 = {'three': 3, 'four': 4}
>>> chain = ChainMap(dict1, dict2)
>>> chain
ChainMap({'one': 1, 'two': 2}, {'three': 3, 'four': 4})
# ChainMap 在內部從左到右逐個搜索,
# 直到找到對應的鍵或全部搜索完畢:
>>> chain['three']
3
>>> chain['one']
1
>>> chain['missing']
KeyError: 'missing'

5.types.MappingProxyType——用於創建只讀字典

MappingProxyType 封裝了標準的字典,為封裝的字典數據提供只讀視圖。該類添加自Python 3.3,用來創建字典不可變的代理版本。

舉例來說,如果希望返回一個字典來表示類或模塊的內部狀態,同時禁止向該對象寫入內容,此時MappingProxyType 就能派上用場。使用MappingProxyType 無須創建完整的字典副本。

>>> from types import MappingProxyType
>>> writable = {'one': 1, 'two': 2}
>>> read_only = MappingProxyType(writable)
# 代理是隻讀的:
>>> read_only['one']
1
>>> read_only['one'] = 23
TypeError:
"'mappingproxy' object does not support item assignment"
# 更新原字典也會影響到代理:
>>> writable['one'] = 42
>>> read_only
mappingproxy({'one': 42, 'two': 2})

6.ython 中的字典:總結

上面列出的所有Python 字典實現都是內置於Python 標準庫中的有效實現。一般情況下,建議在自己的程序中使用內置的dict 數據類型。這是優化過的散列表實現,功能多且已被直接內置到了核心語言中。

如果你有內置dict 無法滿足的特殊需求,那麼建議使用文章中列出的其他數據類型。

雖然前面列出的其他字典實現均可用,但大多數情況下都應該使用Python 內置的標準dict,這樣其他開發者在維護你的代碼時就會輕鬆一點。

7.關鍵要點

  • 字典是Python 中的核心數據結構。
  • 大部分情況下,內置的dict 類型就足夠了。
  • Python 標準庫提供了用於滿足特殊需求的實現,比如只讀字典或有序字典。

二、數組數據結構

大多數編程語言中都有數組這種基本數據結構,它在許多算法中都有廣泛的運用。

下面我們將介紹Python 中的一些數組實現,這些數組只用到了語言的核心特性或Python 標準庫包含的功能。

我們還會介紹每種實現的優缺點,這樣就能根據實際情況選擇合適的實現。不過在介紹之前,先來了解一些基礎知識。

首先要知道數組的原理及用途。

數組由大小固定的數據記錄組成,根據索引能快速找到其中的每個元素。

因為數組將信息存儲在依次連接的內存塊中,所以它是連續的數據結構(與鏈式列表等鏈式數據結構不同)。

現實世界中能用來類比數組數據結構的是停車場。

停車場可被視為一個整體,即單個對象,但停車場內的每個停車位都有唯一的編號索引。停車位是車輛的容器,每個停車位既可以為空,也可以停有汽車、摩托車或其他車輛。

各個停車場之間也會有區別。

有些停車場可能只能停一種類型的車輛。例如,汽車停車場不允許停放自行車。這種“有限制”的停車場相當於“類型數組”數據結構,只允許存儲相同數據類型的元素。

在性能方面,根據元素的索引能快速查找數組中對應的元素。合理的數組實現能夠確保索引訪問的耗時為常量時間O(1)。

Python 標準庫包含幾個與數組相似的數據結構,每個數據結構的特徵略有不同。下面來逐一介紹。

1.列表——可變動態數組

列表是Python 語言核心的一部分。雖然名字叫列表,但它實際上是以動態數組實現的。這意味著列表能夠添加或刪除元素,還能分配或釋放內存來自動調整存儲空間。

Python 列表可以包含任意元素,因為Python 中一切皆為對象,連函數也是對象。因此,不同的數據類型可以混合存儲在一個列表中。

這個功能很強大,但缺點是同時支持多種數據類型會導致數據存儲得不是很緊湊。因此整個結構佔據了更多的空間。

>>> arr = ['one', 'two', 'three']
>>> arr[0]
'one'
# 列表擁有不錯的__repr__方法:
>>> arr
['one', 'two', 'three']
# 列表是可變的:
>>> arr[1] = 'hello'
>>> arr
['one', 'hello', 'three']
>>> del arr[1]
>>> arr
['one', 'three']
# 列表可以含有任意類型的數據:
>>> arr.append(23)
>>> arr
['one', 'three', 23]

2.元組——不可變容器

與列表一樣,元組也是Python 語言核心的一部分。與列表不同的是,Python 的元組對象是不可變的。這意味著不能動態添加或刪除元素,元組中的所有元素都必須在創建時定義。

就像列表一樣,元組可以包含任意數據類型的元素。這具有很強的靈活性,但也意味著數據的打包密度要比固定類型的數組小。

>>> arr = 'one', 'two', 'three'
>>> arr[0]
'one'
# 元組擁有不錯的__repr__方法:
>>> arr
('one', 'two', 'three')
# 元組是可變的
>>> arr[1] = 'hello'
TypeError:
"'tuple' object does not support item assignment"
>>> del arr[1]
TypeError:
"'tuple' object doesn't support item deletion"
# 元組可以持有任意類型的數據:
#(添加元素會創建新元組)
>>> arr + (23,)
('one', 'two', 'three', 23)

3.array.array——基本類型數組

Python 的array 模塊佔用的空間較少,用於存儲C 語言風格的基本數據類型(如字節、32位整數,以及浮點數等)。

使用array.array 類創建的數組是可變的,行為與列表類似。但有一個重要的區別:這種數組是單一數據類型的“類型數組”。

由於這個限制,含有多個元素的array.array 對象比列表和元組節省空間。存儲在其中的元素緊密排列,因此適合存儲許多相同類型的元素。

此外,數組中有許多普通列表中也含有的方法,使用方式也相同,無須對應用程序代碼進行其他更改。

>>> import array
>>> arr = array.array('f', (1.0, 1.5, 2.0, 2.5))
>>> arr[1]
1.5
# 數組擁有不錯的__repr__方法:
>>> arr
array('f', [1.0, 1.5, 2.0, 2.5])
# 數組是可變的:
>>> arr[1] = 23.0
>>> arr
array('f', [1.0, 23.0, 2.0, 2.5])
>>> del arr[1]
>>> arr
array('f', [1.0, 2.0, 2.5])
>>> arr.append(42.0)
>>> arr
array('f', [1.0, 2.0, 2.5, 42.0])
# 數組中元素類型是固定的:
>>> arr[1] = 'hello'
TypeError: "must be real number, not str"

4.str——含有Unicode 字符的不可變數組

Python 3.x 使用str 對象將文本數據存儲為不可變的Unicode 字符序列。實際上,這意味著str 是不可變的字符數組。說來也怪,str 也是一種遞歸的數據結構,字符串中的每個字符都是長度為1 的str 對象。

由於字符串對象專注於單一數據類型,元組排列緊密,因此很節省空間,適合用來存儲Unicode 文本。因為字符串在Python 中是不可變的,所以修改字符串需要創建一個改動副本。最接近“可變字符串”概念的是存儲單個字符的列表。

>>> arr = 'abcd'
>>> arr[1]
'b'
>>> arr
'abcd'
# 字符串是可變的:
>>> arr[1] = 'e'
TypeError:
"'str' object does not support item assignment"
>>> del arr[1]
TypeError:
"'str' object doesn't support item deletion"
# 字符串可以解包到列表中,從而得到可變版本:
>>> list('abcd')
['a', 'b', 'c', 'd']
>>> ''.join(list('abcd'))
'abcd'
# 字符串是遞歸型數據類型:
>>> type('abc')
"<class 'str'>"
>>> type('abc'[0])
"<class 'str'>"

5.bytes——含有單字節的不可變數組

bytes 對象是單字節的不可變序列,單字節為0~255(含)範圍內的整數。從概念上講,bytes 與str 對象類似,可認為是不可變的字節數組。

與字符串一樣,也有專門用於創建bytes 對象的字面語法,bytes 也很節省空間。bytes對象是不可變的,但與字符串不同,還有一個名為bytearray 的專用“可變字節數組”數據類型,bytes 可以解包到bytearray 中。後面會介紹更多關於bytearray 的內容。

>>> arr = bytes((0, 1, 2, 3))
>>> arr[1]
1
# bytes 有自己的語法:
>>> arr
b'\\x00\\x01\\x02\\x03'
>>> arr = b'\\x00\\x01\\x02\\x03'
# bytes 必須位於0~255:
>>> bytes((0, 300))
ValueError: "bytes must be in range(0, 256)"
# bytes 是不可變的:
>>> arr[1] = 23
TypeError:
"'bytes' object does not support item assignment"
>>> del arr[1]
TypeError:
"'bytes' object doesn't support item deletion"

6.bytearray——含有單字節的可變數組

bytearray 類型是可變整數序列,包含的整數範圍在0~255(含)。bytearray 與bytes對象關係密切,主要區別在於bytearray 可以自由修改,如覆蓋、刪除現有元素和添加新元素,此時bytearray 對象將相應地增長和縮小。

bytearray 數可以轉換回不可變的bytes 對象,但是這需要複製所存儲的數據,是耗時為O(n)的慢操作。

>>> arr = bytearray((0, 1, 2, 3))
>>> arr[1]
1
# bytearray 的repr:
>>> arr
bytearray(b'\\x00\\x01\\x02\\x03')
# bytearray 是可變的:
>>> arr[1] = 23
>>> arr
bytearray(b'\\x00\\x17\\x02\\x03')
>>> arr[1]
23
# bytearray 可以增長或縮小:
>>> del arr[1]
>>> arr
bytearray(b'\\x00\\x02\\x03')
>>> arr.append(42)
>>> arr
bytearray(b'\\x00\\x02\\x03*')
# bytearray 只能持有byte,即位於0~255 範圍內的整數
>>> arr[1] = 'hello'
TypeError: "an integer is required"
>>> arr[1] = 300
ValueError: "byte must be in range(0, 256)"
# bytearray 可以轉換回byte 對象,此過程會複製數據:
>>> bytes(arr)
b'\\x00\\x02\\x03*'

7.關鍵要點

Python 中有多種內置數據結構可用來實現數組,上面只專注位於標準庫中和核心語言特性中的數據結構。

如果不想侷限於Python 標準庫,那麼從NumPy 這樣的第三方軟件包中可找到為科學計算和數據科學提供的許多快速數組實現。

對於Python 中包含的數組數據結構,選擇順序可歸結如下。

如果需要存儲任意對象,且其中可能含有混合數據類型,那麼可以選擇使用列表或元組,前者可變後者不可變。

如果存儲數值(整數或浮點數)數據並要求排列緊密且注重性能,那麼先嚐試array.array,看能否滿足要求。另外可嘗試準庫之外的軟件包,如NumPy 或Pandas。

如果有需要用Unicode 字符表示的文本數據,那麼可以使用Python 內置的str。如果需要用到“可變字符串”,則請使用字符列表。

如果想存儲一個連續的字節塊,不可變的請使用bytes,可變的請使用bytearray。

總之,在大多數情況下首先應嘗試列表。如果在性能或存儲空間上有問題,再選擇其他專門的數據類型。一般像列表這樣通用的數組型數據結構已經能同時兼顧開發速度和編程便利性的要求了。

強烈建議在初期使用通用數據格式,不要試圖在一開始就榨乾所有性能。

三、記錄、結構體和純數據對象

與數組相比,記錄數據結構中的字段數目固定,每個都有一個名稱,類型也可以不同。

下面我們將介紹Python 中的記錄、結構體,以及“純數據對象”,但只介紹標準庫中含有的內置數據類型和類。

順便說一句,這裡的“記錄”定義很寬泛。例如,這裡也會介紹像Python 的內置元組這樣的類型。由於元組中的字段沒有名稱,因此一般不認為它是嚴格意義上的記錄。

Python 提供了幾種可用於實現記錄、結構體和數據傳輸對象的數據類型。我們將快速介紹每個實現及各自特性,最後進行總結並給出一個決策指南,用來幫你做出自己的選擇。

好吧,讓我們開始吧!

1.字典——簡單數據對象

Python 字典能存儲任意數量的對象,每個對象都由唯一的鍵來標識。字典也常常稱為映射或關聯數組,能高效地根據給定的鍵查找、插入和刪除所關聯的對象。

Python 的字典還可以作為記錄數據類型(record data type)或數據對象來使用。在Python 中創建字典很容易,因為語言內置了創建字典的語法糖,簡潔又方便。

字典創建的數據對象是可變的,同時由於可以隨意添加和刪除字段,因此對字段名稱幾乎沒有保護措施。這些特性綜合起來可能會引入令人驚訝的bug,畢竟要在便利性和避免錯誤之間做出取捨。

car1 = {
'color': 'red',
'mileage': 3812.4,
'automatic': True,
}
car2 = {
'color': 'blue',
'mileage': 40231,
'automatic': False,
}
# 字典有不錯的__repr__方法:
>>> car2
{'color': 'blue', 'automatic': False, 'mileage': 40231}
# 獲取mileage:
>>> car2['mileage']
40231
# 字典是可變的:
>>> car2['mileage'] = 12
>>> car2['windshield'] = 'broken'
>>> car2
{'windshield': 'broken', 'color': 'blue',
'automatic': False, 'mileage': 12}
# 對於提供錯誤、缺失和額外的字段名稱並沒有保護措施:
car3 = {
'colr': 'green',
'automatic': False,
'windshield': 'broken',
}

2.元組——不可變對象集合

Python 元組是簡單的數據結構,用於對任意對象進行分組。元組是不可變的,創建後無法修改。

在性能方面,元組佔用的內存略少於CPython 中的列表,構建速度也更快。

從如下反彙編的字節碼中可以看到,構造元組常量只需要一個LOAD_CONST 操作碼,而構造具有相同內容的列表對象則需要多個操作:

>>> import dis
>>> dis.dis(compile("(23, 'a', 'b', 'c')", '', 'eval'))
0 LOAD_CONST 4 ((23, 'a', 'b', 'c'))
3 RETURN_VALUE
>>> dis.dis(compile("[23, 'a', 'b', 'c']", '', 'eval'))
0 LOAD_CONST 0 (23)
3 LOAD_CONST 1 ('a')
6 LOAD_CONST 2 ('b')
9 LOAD_CONST 3 ('c')
12 BUILD_LIST 4
15 RETURN_VALUE

不過你無須過分關注這些差異。在實踐中這些性能差異通常可以忽略不計,試圖通過用元組替換列表來獲得額外的性能提升一般都是入了歧途。

單純的元組有一個潛在缺點,即存儲在其中的數據只能通過整數索引來訪問,無法為元組中存儲的單個屬性制定一個名稱,從而影響了代碼的可讀性。

此外,元組總是一個單例模式的結構,很難確保兩個元組存儲了相同數量的字段和相同的屬性。

這樣很容易因疏忽而犯錯,比如弄錯字段順序。因此,建議儘可能減少元組中存儲的字段數量。

# 字段:color、mileage、automatic
>>> car1 = ('red', 3812.4, True)
>>> car2 = ('blue', 40231.0, False)
# 元組的實例有不錯的__repr__方法:
>>> car1
('red', 3812.4, True)
>>> car2
('blue', 40231.0, False)
# 獲取mileage:
>>> car2[1]
40231.0
# 元組是可變的:
>>> car2[1] = 12
TypeError:
"'tuple' object does not support item assignment"
# 對於錯誤或額外的字段,以及提供錯誤的字段順序,並沒有報錯措施:
>>> car3 = (3431.5, 'green', True, 'silver')

3.編寫自定義類——手動精細控制

類可用來為數據對象定義可重用的“藍圖”(blueprint),以確保每個對象都提供相同的字段。

普通的Python 類可作為記錄數據類型,但需要手動完成一些其他實現中已有的便利功能。

例如,向__init__構造函數添加新字段就很煩瑣且耗時。

此外,對於從自定義類實例化得到的對象,其默認的字符串表示形式沒什麼用。解決這個問題需要添加自己的__repr__方法。這個方法通常很冗長,每次添加新字段時都必須更新。

存儲在類上的字段是可變的,並且可以隨意添加新字段。使用@property 裝飾器能創建只讀字段,並獲得更多的訪問控制,但是這又需要編寫更多的膠水代碼。

編寫自定義類適合將業務邏輯和行為添加到記錄對象中,但這意味著這些對象在技術上不再是普通的純數據對象。

class Car:
def __init__(self, color, mileage, automatic):
self.color = color
self.mileage = mileage
self.automatic = automatic
>>> car1 = Car('red', 3812.4, True)
>>> car2 = Car('blue', 40231.0, False)
# 獲取mileage:
>>> car2.mileage
40231.0
# 類是可變的:
>>> car2.mileage = 12
>>> car2.windshield = 'broken'
# 類的默認字符串形式沒多大用處,必須手動編寫一個__repr__方法:
>>> car1
<Car object at 0x1081e69e8>

4.collections.namedtuple——方便的數據對象

自Python 2.6 以來添加的namedtuple 類擴展了內置元組數據類型。與自定義類相似,namedtuple 可以為記錄定義可重用的“藍圖”,以確保每次都使用正確的字段名稱。

與普通的元組一樣,namedtuple 是不可變的。這意味著在創建namedtuple 實例之後就不能再添加新字段或修改現有字段。

除此之外,namedtuple 就相當於具有名稱的元組。存儲在其中的每個對象都可以通過唯一標識符訪問。因此無須整數索引,也無須使用變通方法,比如將整數常量定義為索引的助記符。

namedtuple 對象在內部是作為普通的Python 類實現的,其內存佔用優於普通的類,和普通元組一樣高效:

>>> from collections import namedtuple
>>> from sys import getsizeof
>>> p1 = namedtuple('Point', 'x y z')(1, 2, 3)
>>> p2 = (1, 2, 3)
>>> getsizeof(p1)
72
>>> getsizeof(p2)
72

由於使用namedtuple 就必須更好地組織數據,因此無意中清理了代碼並讓其更加易讀。

我發現從專用的數據類型(例如固定格式的字典)切換到namedtuple 有助於更清楚地表達代碼的意圖。通常,每當我在用namedtuple 重構應用時,都神奇地為代碼中的問題想出了更好的解決辦法。

用namedtuple 替換普通(非結構化的)元組和字典還可以減輕同事的負擔,因為用namedtuple傳遞的數據在某種程度上能做到“自說明”。

>>> from collections import namedtuple
>>> Car = namedtuple('Car' , 'color mileage automatic')
>>> car1 = Car('red', 3812.4, True)
# 實例有不錯的__repr__方法:
>>> car1
Car(color='red', mileage=3812.4, automatic=True)
# 訪問字段:
>>> car1.mileage
3812.4
# 字段是不可變的:
>>> car1.mileage = 12
AttributeError: "can't set attribute"
>>> car1.windshield = 'broken'
AttributeError:
"'Car' object has no attribute 'windshield'"

5.typing.NamedTuple——改進版namedtuple

這個類添加自Python 3.6,是collections 模塊中namedtuple 類的姊妹。它與namedtuple非常相似,主要區別在於用新語法來定義記錄類型並支持類型註解(type hint)。

注意,只有像mypy 這樣獨立的類型檢查工具才會在意類型註解。不過即使沒有工具支持,類型註解也可幫助其他程序員更好地理解代碼(如果類型註解沒有隨代碼及時更新則會帶來混亂)。

>>> from typing import NamedTuple
class Car(NamedTuple):
color: str
mileage: float
automatic: bool
>>> car1 = Car('red', 3812.4, True)
# 實例有不錯的__repr__方法:
>>> car1
Car(color='red', mileage=3812.4, automatic=True)
# 訪問字段:
>>> car1.mileage
3812.4
# 字段是不可變的:
>>> car1.mileage = 12
AttributeError: "can't set attribute"
>>> car1.windshield = 'broken'
AttributeError:
"'Car' object has no attribute 'windshield'"
# 只有像mypy 這樣的類型檢查工具才會落實類型註解:
>>> Car('red', 'NOT_A_FLOAT', 99)
Car(color='red', mileage='NOT_A_FLOAT', automatic=99)

6.struct.Struct——序列化C 結構體

struct.Struct 類用於在Python 值和C 結構體之間轉換,並將其序列化為Python 字節對象。例如可以用來處理存儲在文件中或來自網絡連接的二進制數據。

結構體使用與格式化字符串類似的語法來定義,能夠定義並組織各種C 數據類型(如char、int、long,以及對應的無符號的變體)。

序列化結構體一般不用來表示只在Python 代碼中處理的數據對象,而是主要用作數據交換格式。

在某些情況下,與其他數據類型相比,將原始數據類型打包到結構體中佔用的內存較少。但大多數情況下這都屬於高級(且可能不必要的)優化。

>>> from struct import Struct
>>> MyStruct = Struct('i?f')
>>> data = MyStruct.pack(23, False, 42.0)
# 得到的是一團內存中的數據:
>>> data
b'\\x17\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00(B'
# 數據可以再次解包:
>>> MyStruct.unpack(data)
(23, False, 42.0)

7.types.SimpleNamespace——花哨的屬性訪問

這裡再介紹一種高深的方法來在Python 中創建數據對象:types.SimpleNamespace。該類添加自Python 3.3,可以用屬性訪問的方式訪問其名稱空間。

也就是說,SimpleNamespace 實例將其中的所有鍵都公開為類屬性。因此訪問屬性時可以使用obj.key 這樣的點式語法,不需要用普通字典的obj['key']方括號索引語法。所有實例默認都包含一個不錯的__repr__。

正如其名,SimpleNamespace 很簡單,基本上就是擴展版的字典,能夠很好地訪問屬性並以字符串打印出來,還能自由地添加、修改和刪除屬性。

>>> from types import SimpleNamespace
>>> car1 = SimpleNamespace(color='red',
... mileage=3812.4,
... automatic=True)
# 默認的__repr__效果:
>>> car1
namespace(automatic=True, color='red', mileage=3812.4)
# 實例支持屬性訪問並且是可變的:
>>> car1.mileage = 12
>>> car1.windshield = 'broken'
>>> del car1.automatic
>>> car1
namespace(color='red', mileage=12, windshield='broken')

8.關鍵要點

那麼在Python 中應該使用哪種類型的數據對象呢?從上面可以看到,Python 中有許多不同的方法實現記錄或數據對象,使用哪種方式通常取決於具體的情況。

如果只有兩三個字段,字段順序易於記憶或無須使用字段名稱,則使用簡單元組對象。例如三維空間中的(x, y, z)點。

如果需要實現含有不可變字段的數據對象,則使用collections.namedtuple 或typing.NamedTuple 這樣的簡單元組。

如果想鎖定字段名稱來避免輸入錯誤,同樣建議使用collections.namedtuple 和typing.NamedTuple。

如果希望保持簡單,建議使用簡單的字典對象,其語法方便,和JSON 也類似。

如果需要對數據結構完全掌控,可以用@property 加上設置方法和獲取方法來編寫自定義的類。

如果需要向對象添加行為(方法),則應該從頭開始編寫自定義類,或者通過擴展 collections. namedtuple 或 typing.NamedTuple 來編寫自定義類。

如果想嚴格打包數據以將其序列化到磁盤上或通過網絡發送,建議使用 struct.Struct。

一般情況下,如果想在 Python 中實現一個普通的記錄、結構體或數據對象,我的建議是在 Python 2.x中使用collections.namedtuple,在Python 3中使用其姊妹typing.NamedTuple。

四、集合和多重集合

下面我們將用標準庫中的內置數據類型和類在Python 中實現可變集合、不可變集合和多重集合(揹包)數據結構。首先來快速回顧一下集合數據結構。

集合含有一組不含重複元素的無序對象。集合可用來快速檢查元素的包含性,插入或刪除值,計算兩個集合的並集或交集。

在“合理”的集合實現中,成員檢查預計耗時為O(1)。並集、交集、差集和子集操作應平均耗時為O(n)。Python 標準庫中的集合實現都具有這些性能指標。

與字典一樣,集合在Python 中也得到了特殊對待,有語法糖能夠方便地創建集合。例如,花括號集合表達式語法和集合解析式能夠方便地定義新的集合實例:

vowels = {'a', 'e', 'i', 'o', 'u'}
squares = {x * x for x in range(10)}

但要小心,創建空集時需要調用set()構造函數。空花括號{}有歧義,會創建一個空字典。

Python 及其標準庫提供了幾個集合實現,讓我們看看。

1.set——首選集合實現

set 是Python 中的內置集合實現。set 類型是可變的,能夠動態插入和刪除元素。

Python 的集合由dict 數據類型支持,具有相同的性能特徵。所有可散列的對象都可以存儲在集合中。

>>> vowels = {'a', 'e', 'i', 'o', 'u'}
>>> 'e' in vowels
True
>>> letters = set('alice')
>>> letters.intersection(vowels)
{'a', 'e', 'i'}
>>> vowels.add('x')
>>> vowels
{'i', 'a', 'u', 'o', 'x', 'e'}
>>> len(vowels)
6

2.frozenset——不可變集合

frozenset 類實現了不可變版的集合,即在構造後無法更改。不可變集合是靜態的,只能查詢其中的元素(無法插入或刪除)。因為不可變集合是靜態的且可散列的,所以可以用作字典的鍵,也可以放置在另一個集合中,普通可變的set 對象做不到這一點。

>>> vowels = frozenset({'a', 'e', 'i', 'o', 'u'})
>>> vowels.add('p')
AttributeError:
"'frozenset' object has no attribute 'add'"
# 不可變集合是可散列的,可用作字典的鍵
>>> d = { frozenset({1, 2, 3}): 'hello' }
>>> d[frozenset({1, 2, 3})]
'hello'

3.collections.Counter——多重集合

Python 標準庫中的collections.Counter 類實現了多重集合(也稱揹包,bag)類型,該類型允許在集合中多次出現同一個元素。

如果既要檢查元素是否為集合的一部分,又要記錄元素在集合中出現的次數,那麼就需要用到這個類型。

>>> from collections import Counter
>>> inventory = Counter()
>>> loot = {'sword': 1, 'bread': 3}
>>> inventory.update(loot)
>>> inventory
Counter({'bread': 3, 'sword': 1})
>>> more_loot = {'sword': 1, 'apple': 1}
>>> inventory.update(more_loot)
>>> inventory
Counter({'bread': 3, 'sword': 2, 'apple': 1})

Counter 類有一點要注意,在計算Counter 對象中元素的數量時需要小心。調用len()返回的是多重集合中唯一元素的數量,而想獲取元素的總數需要使用sum 函數:

>>> len(inventory)
3 # 唯一元素的個數
>>> sum(inventory.values())
6 # 元素總數

4.關鍵要點

  • 集合是Python 及其標準庫中含有的另一種有用且常用的數據結構。
  • 查找可變集合時可使用內置的set 類型。
  • frozenset 對象可散列且可用作字典和集合的鍵。
  • collections.Counter 實現了多重集合或“揹包”類型的數據。

五、棧(後進先出)

棧是含有一組對象的容器,支持快速後進先出(LIFO)的插入和刪除操作。與列表或數組不同,棧通常不允許隨機訪問所包含的對象。插入和刪除操作通常稱為入棧(push)和出棧(pop)。

現實世界中與棧數據結構相似的是一疊盤子。

新盤子會添加到棧的頂部。由於這些盤子非常寶貴且很重,所以只能移動最上面的盤子(後進先出)。要到達棧中位置較低的盤子,必須逐一移除最頂端的盤子。

棧和隊列相似,都是線性的元素集合,但元素的訪問順序不同。

從隊列刪除元素時,移除的是最先添加的項(先進先出,FIFO);而棧是移除最近添加的項(後進先出,LIFO)。

在性能方面,合理的棧實現在插入和刪除操作的預期耗時是O(1)。

棧在算法中有廣泛的應用,比如用於語言解析和運行時的內存管理(“調用棧”)。樹或圖數據結構上的深度優先搜索(DFS)是簡短而美麗的算法,其中就用到了棧。

Python 中有幾種棧實現,每個實現的特性略有不同。下面來分別介紹並比較各自的特性。

1.列表——簡單的內置棧

Python 的內置列表類型能在正常的O(1)時間內完成入棧和出棧操作,因此適合作為棧數據結構。

Python 的列表在內部以動態數組實現,這意味著在添加或刪除時,列表偶爾需要調整元素的存儲空間大小。列表會預先分配一些後備存儲空間,因此並非每個入棧或出棧操作都需要調整大小,所以這些操作的均攤時間複雜度為O(1)。

這麼做的缺點是列表的性能不如基於鏈表的實現(如collections.deque,下面會介紹),後者能為插入和刪除操作提供穩定的O(1)時間複雜度。另一方面,列表能在O(1)時間快速隨機訪問堆棧上的元素,這能帶來額外的好處。

使用列表作為堆棧應注意下面幾個重要的性能問題。

為了獲得O(1)的插入和刪除性能,必須使用append()方法將新項添加到列表的末尾,刪除時也要使用pop()從末尾刪除。為了獲得最佳性能,基於Python 列表的棧應該向高索引增長並向低索引縮小。

從列表前部添加和刪除元素很慢,耗時為O(n),因為這種情況下必須移動現有元素來為新元素騰出空間。這是一個性能反模式,應儘可能避免。

>>> s = []
>>> s.append('eat')
>>> s.append('sleep')
>>> s.append('code')
>>> s
['eat', 'sleep', 'code']
>>> s.pop()
'code'
>>> s.pop()
'sleep'
>>> s.pop()
'eat'
>>> s.pop()
IndexError: "pop from empty list"

2.collections.deque——快速且穩健的棧

deque 類實現了一個雙端隊列,支持在O(1)時間(非均攤)從兩端添加和移除元素。因為雙端隊列支持從兩端添加和刪除元素,所以既可以作為隊列也可以作為棧。

Python 的deque 對象以雙向鏈表實現,這為插入和刪除元素提供了出色且一致的性能,但是隨機訪問位於棧中間元素的性能很差,耗時為O(n)。

總之,如果想在Python 的標準庫中尋找一個具有鏈表性能特徵的棧數據結構實現,那麼collections.deque 是不錯的選擇。

>>> from collections import deque
>>> s = deque()
>>> s.append('eat')
>>> s.append('sleep')
>>> s.append('code')
>>> s
deque(['eat', 'sleep', 'code'])
>>> s.pop()
'code'
>>> s.pop()
'sleep'
>>> s.pop()
'eat'
>>> s.pop()
IndexError: "pop from an empty deque"

3.queue.LifoQueue——為並行計算提供鎖語義

queue.LifoQueue 這個位於Python 標準庫中的棧實現是同步的,提供了鎖語義來支持多個併發的生產者和消費者。

除了LifoQueue 之外,queue 模塊還包含其他幾個類,都實現了用於並行計算的多生產者/多用戶隊列。

在不同情況下,鎖語義即可能會帶來幫助,也可能會導致不必要的開銷。在後面這種情況下,最好使用list 或deque 作為通用棧。

>>> from queue import LifoQueue
>>> s = LifoQueue()
>>> s.put('eat')
>>> s.put('sleep')
>>> s.put('code')
>>> s
<queue.LifoQueue object at 0x108298dd8>
>>> s.get()
'code'
>>> s.get()
'sleep'
>>> s.get()
'eat'
>>> s.get_nowait()
queue.Empty
>>> s.get()
# 阻塞,永遠停在這裡……

4.比較Python 中各個棧的實現

從上面可以看出,Python 中有多種棧數據結構的實現,各自的特性稍有區別,在性能和用途上也各有優劣。

如果不尋求並行處理支持(或者不想手動處理上鎖和解鎖),可選擇內置列表類型或collections.deque。兩者背後使用的數據結構和總體易用性有所不同。

  • 列表底層是動態數組,因此適用於快速隨機訪問,但在添加或刪除元素時偶爾需要調整大小。列表會預先分配一些備用存儲空間,因此不是每個入棧或出棧操作都需要調整大小,這些操作的均攤時間複雜度為O(1)。但需要小心,只能用append()和pop()從“右側”插入和刪除元素,否則性能會下降為O(n)。
  • collections.deque 底層是雙向鏈表,為從兩端的添加和刪除操作進行了優化,為這些操作提供了一致的O(1)性能。collections.deque 不僅性能穩定,而且便於使用,不必擔心在“錯誤的一端”添加或刪除項。

總之,我認為collections.deque 是在Python 中實現棧(LIFO 隊列)的絕佳選擇。

5.關鍵要點

  • Python 中有幾個棧實現,每種實現的性能和使用特性略有不同。
  • collections.deque 提供安全且快速的通用棧實現。
  • 內置列表類型可以作為棧使用,但要小心只能使用append()和pop()來添加和刪除項,以避免性能下降。

六、隊列(先進先出)

下面我們將介紹僅使用 Python 標準庫中的內置數據類型和類來實現 FIFO 隊列數據結構,首先來 回顧一下什麼是隊列。

隊列是含有一組對象的容器,支持快速插入和刪除的先進先出語義。插入和刪除操作有時稱為入隊(enqueue)和出隊(dequeue)。與列表或數組不同,隊列通常不允許隨機訪問所包含的對象。

來看一個先進先出隊列在現實中的類比。

想象在 PyCon 註冊的第一天,一些 Python 高手等著領取會議徽章。新到的人依次 進入會場並排隊領取徽章,隊列後面會有其他人繼續排隊。移除動作發生在隊列前端, 因為開發者領取徽章和會議禮品袋後就離開了。

另一種記住隊列數據結構特徵的方法是將其視為管道。

新元素(水分子、乒乓球等)從管道一端移向另一端並在那裡被移除。當元素在隊列中(想象成位於一根堅固的金屬管中)時是無法接觸的。唯一能夠與隊列中元素交互的方法是在管道後端添加新元素(入隊)或在管道前端刪除元素(出隊)。

隊列與棧類似,但刪除元素的方式不同。

隊列刪除的是最先添加的項(先進先出),而棧刪除的是最近添加的項(後進先出)。

在性能方面,實現合理的隊列在插入和刪除方面的操作預計耗時為 O(1)。插入和刪除是隊列 上的兩個主要操作,在正確的實現中應該很快。

隊列在算法中有廣泛的應用,經常用於解決調度和並行編程問題。在樹或圖數據結構上進行 寬度優先搜索(BFS)是一種簡短而美麗的算法,其中就用到了隊列。

調度算法通常在內部使用優先級隊列。這些是特化的隊列,其中元素的順序不是基於插入時 間,而是基於優先級。隊列根據元素的鍵計算到每個元素的優先級。後面會詳細介紹優先級隊列以及它們在 Python 中的實現方式。

不過普通隊列無法重新排列所包含的元素。就像在管道示例中一樣,元素輸入和輸出的順序 完全一致。 Python 中實現了幾個隊列,每種實現的特徵略有不同,下面就來看看。

1.列表——非常慢的隊列

普通列表可以作為隊列,但從性能角度來看並不理想。由於在起始位置插入或刪除元素需要將所有其他元素都移動一個位置,因此需要的時間為O(n)。

因此不推薦在Python 中湊合用列表作為隊列使用(除非只處理少量元素):

>>> q = []
>>> q.append('eat')
>>> q.append('sleep')
>>> q.append('code')
>>> q
['eat', 'sleep', 'code']
# 小心,這種操作很慢!
>>> q.pop(0)
'eat'

2.collections.deque——快速和穩健的隊列

deque 類實現了一個雙端隊列,支持在O(1)時間(非均攤)中從任一端添加和刪除元素。由於deque 支持從兩端添加和移除元素,因此既可用作隊列也可用作棧。

Python 的deque 對象以雙向鏈表實現。這為插入和刪除元素提供了出色且一致的性能,但是隨機訪問位於棧中間元素的性能很差,耗時為O(n)。

因此,默認情況下collections.deque 是Python 標準庫中不錯的隊列型數據結構:

>>> from collections import deque
>>> q = deque()
>>> q.append('eat')
>>> q.append('sleep')
>>> q.append('code')
>>> q
deque(['eat', 'sleep', 'code'])
>>> q.popleft()
'eat'
>>> q.popleft()
'sleep'
>>> q.popleft()
'code'
>>> q.popleft()
IndexError: "pop from an empty deque"

3.queue.Queue——為並行計算提供的鎖語義

queue.Queue 在Python 標準庫中以同步的方式實現,提供了鎖語義來支持多個併發的生產者和消費者。

queue 模塊包含其他多個實現多生產者/多用戶隊列的類,這些隊列對並行計算很有用。

在不同情況下,鎖語義可能會帶來幫助,也可能會導致不必要的開銷。在後面這種情況下,最好使用collections.deque 作為通用隊列:

>>> from queue import Queue
>>> q = Queue()
>>> q.put('eat')
>>> q.put('sleep')
>>> q.put('code')
>>> q
<queue.Queue object at 0x1070f5b38>
>>> q.get()
'eat'
>>> q.get()
'sleep'
>>> q.get()
'code'
>>> q.get_nowait()
queue.Empty
>>> q.get()
# 阻塞,永遠停在這裡……

4.multiprocessing.Queue——共享作業隊列

multiprocessing.Queue 作為共享作業隊列來實現,允許多個併發worker 並行處理隊列中的元素。由於CPython 中存在全局解釋器鎖(GIL),因此無法在單個解釋器進程上執行某些並行化過程,使得大家都轉向基於進程的並行化。

作為專門用於在進程間共享數據的隊列實現,使用multiprocessing.Queue 能夠方便地在多個進程中分派工作,以此來繞過GIL 的限制。這種類型的隊列可以跨進程存儲和傳輸任何可pickle 的對象:

>>> from multiprocessing import Queue
>>> q = Queue()
>>> q.put('eat')
>>> q.put('sleep')
>>> q.put('code')
>>> q
<multiprocessing.queues.Queue object at 0x1081c12b0>
>>> q.get()
'eat'
>>> q.get()
'sleep'
>>> q.get()
'code'
>>> q.get()
# 阻塞,永遠停在這裡……

5.關鍵要點

  • Python 核心語言及其標準庫中含有幾種隊列實現。
  • 列表對象可以用作隊列,但由於性能較差,通常不建議這麼做。
  • 如果不需要支持並行處理,那麼collections.deque 是Python 中實現FIFO 隊列數據結構的最佳選擇。collections.deque 是非常優秀的隊列實現,具備期望的性能特徵,並且可以用作棧(LIFO 隊列)。

七、優先隊列

優先隊列是一個容器數據結構,使用具有全序關係的鍵(例如用數值表示的權重)來管理元素,以便快速訪問容器中鍵值最小或最大的元素。

優先隊列可被視為隊列的改進版,其中元素的順序不是基於插入時間,而是基於優先級的。對鍵進行處理能得到每個元素的優先級。

優先級隊列通常用於處理調度問題,例如優先考慮更加緊急的任務。

來看看操作系統任務調度器的工作。

理想情況下,系統上的高優先級任務(如玩實時遊戲)級別應高於低優先級的任務(如在後臺下載更新)。優先級隊列將待執行的任務根據緊急程度排列,任務調度程序能夠快速選取並優先執行優先級最高的任務。

下面我們將介紹如何使用Python 語言內置或位於標準庫中的數據結構來實現優先隊列。每種實現都有各自的優缺點,但其中有一種實現能應對大多數常見情況,下面一起來看看。

1.列表——手動維護有序隊列

使用有序列表能夠快速識別並刪除最小或最大的元素,缺點是向列表插入元素表是很慢的O(n)操作。

雖然用標準庫中的bisect.insort能在O(logn)時間內找到插入位置,但緩慢的插入操作才是瓶頸。

向列表添加並重新排序來維持順序也至少需要O(nlogn)的時間。另一個缺點是在插入新元素時,必須手動重新排列列表。缺少這一步就很容易引入bug,因此擔子總是壓在開發人員身上。

因此,有序列表只適合在插入次數很少的情況下充當優先隊列。

q = []
q.append((2, 'code'))
q.append((1, 'eat'))
q.append((3, 'sleep'))
# 注意:每當添加新元素或調用bisect.insort()時,都要重新排序。
q.sort(reverse=True)
while q:
next_item = q.pop()
print(next_item)
# 結果:
# (1, 'eat')
# (2, 'code')
# (3, 'sleep')

2.heapq——基於列表的二叉堆

heapq 是二叉堆,通常用普通列表實現,能在O(logn)時間內插入和獲取最小的元素。

heapq 模塊是在Python 中不錯的優先級隊列實現。由於heapq 在技術上只提供最小堆實現,因此必須添加額外步驟來確保排序穩定性,以此來獲得“實際”的優先級隊列中所含有的預期特性。

import heapq
q = []
heapq.heappush(q, (2, 'code'))
heapq.heappush(q, (1, 'eat'))
heapq.heappush(q, (3, 'sleep'))
while q:
next_item = heapq.heappop(q)
print(next_item)
# 結果:
# (1, 'eat')
# (2, 'code')
# (3, 'sleep')

3.queue.PriorityQueue——美麗的優先級隊列

queue.PriorityQueue 這個優先級隊列的實現在內部使用了heapq,時間和空間複雜度與heapq 相同。

區別在於PriorityQueue 是同步的,提供了鎖語義來支持多個併發的生產者和消費者。

在不同情況下,鎖語義可能會帶來幫助,也可能會導致不必要的開銷。不管哪種情況,你都可能更喜歡PriorityQueue 提供的基於類的接口,而不是使用heapq 提供的基於函數的接口。

from queue import PriorityQueue
q = PriorityQueue()
q.put((2, 'code'))
q.put((1, 'eat'))
q.put((3, 'sleep'))
while not q.empty():
next_item = q.get()
print(next_item)
# 結果:
# (1, 'eat')
# (2, 'code')
# (3, 'sleep')

4.關鍵要點

  • Python 提供了幾種優先隊列實現可以使用。
  • queue.PriorityQueue 是其中的首選,具有良好的面向對象的接口,從名稱就能明白其用途。
  • 如果想避免queue.PriorityQueue 的鎖開銷,那麼建議直接使用heapq 模塊。

——本文節選自《深入理解Python特性》,影響全球1 000 000以上程序員的PythonistaCafe社區創始人Dan Bader著作!手把手帶你提升Python實踐技能,快速寫出更高效、更專業的Python代碼。

Python 中常見的數據結構

本書致力於幫助Python開發人員挖掘這門語言及相關程序庫的優秀特性,避免重複勞動,同時寫出簡潔、流暢、易讀、易維護的代碼。用好Python需要了解的最重要的特性、Python 2過渡到Python 3需要掌握的現代模式、有其他編程語言背景想快速上手Python的程序員需要特別注意的問題,等等,本書都可以解決。

掌握的Python技巧多一點兒,寫出來的Python代碼性能就高一截兒。 :)

目錄

版權聲明

Python高手對本書的評論

第 1 章 簡介

第 2 章 Python整潔之道

第 3 章 高效的函數

第 4 章 類與面向對象

第 5 章 Python中常見的數據結構

第 6 章 循環和迭代

第 7 章 字典技巧

第 8 章 Python式高效技巧

第 9 章 結語

從我第一次接觸Python這門編程語言到現在已經有將近10年了。多年前第一次學習Python時,我還有點不情願。在此之前,我使用另一門語言編程,但在工作中突然被分配到了另一個團隊,其中每個人都在使用Python。我的Python之旅就從那裡開始了。

第一次接觸Python時,我被告知Python很容易,能快速上手。當我向同事詢問學習Python的資源時,他們只會給我Python官方文檔的鏈接。剛上手就閱讀文檔會讓人一頭霧水,我就這樣掙扎了一段時間,之後才慢慢適應。遇到問題時,我通常需要在Stack Overflow上尋找答案。

由於之前使用過另一門編程語言,我沒有尋找介紹如何編程或者什麼是類和對象這樣的入門資源,而是一直在尋找能夠介紹Python專有特性的資料,並嘗試瞭解用Python編程與使用其他語言有何區別。

我花了好幾年才充分理解了這門語言。當我閱讀這本書時就在想,要是在當初開始學習Python時能有這樣一本書該有多好。

舉例來說,在眾多獨特的Python特性中,首先讓我感到驚訝的是列表解析式。正如達恩在書中提到的那樣,從編寫for循環的方式就能看出一個人是否剛從其他語言轉到Python。我記得在剛用Python編程時,最初得到的代碼審查評論中有一條就是:“為什麼不在這裡使用列表解析式?”達恩在第6章中清楚地解釋了這個概念。他首先介紹瞭如何用具有Python特色的方式編寫循環,之後介紹了迭代器和生成器。

在2.5節中,達恩介紹了幾種在Python中格式化字符串的方法。字符串格式化無視了“Python之禪”,即做一件事應該只有一種明確的方式。達恩介紹了幾種不同的處理方式,其中包括我最喜歡的Python新增功能f-string。除此之外,他還介紹了每種方法的優缺點。

第8章是本書的另一個亮點,其中介紹了Python編程語言之外的內容,包括如何調試程序和管理依賴關係,並且一窺了Python字節碼的究竟。

我很榮幸也很樂意推薦這本書。

通過以CPython核心開發人員的身份向Python做貢獻,我與許多社區成員建立了聯繫。在我的Python之旅中,我遇到了不少導師、志同道合者,並結交了許多新朋友。這些經歷提醒我,Python不僅僅是一門編程語言,更是一個社區。

掌握Python編程不僅要掌握該語言的理論方面,理解和採用社區使用的慣例和最佳實踐同樣重要。

達恩的書會幫助你完成這個旅程。我相信讀完本書後,你在編寫Python程序時會更有信心。

Mariatta Wijaya

Python核心開發人員(mariatta.ca)

【圖靈教育】

技術改變世界,閱讀塑造人生

相關推薦

推薦中...