數據結構
這一章對你已經學習過的內容進行深入探討,並且會加入一些新的內容。
5.1 More on Lists
list
數據類型還有一些方法。下列是 list
對象的所有方法:
list.append(x)
在列表末尾添加一個項。等價於 a[len(a):] = [x]
。
list.extend(iterable)
將 iterable
中所有的項都加到列表中。等價於 a[len(a):] = iterable
。
list.insert(i, x)
在指定位置插入一個項。第一個參數是要插入的位置的索引,因此 a.insert(0, x)
在列表頭部進行插入,a.insert(len(a), x)
等價於 a.append(x)
。
list.remove(x)
移除列表中第一個值爲 x 的項。如果不存在值爲 x 的項,會拋出 ValueError
異常。
list.pop([i])
移除並返回指定位置的項。如果沒有指定索引,a.pop
移除並返回列表的最後一個項。(在方法簽名中 i
周圍的方括號表示這個參數是可選的,不是讓你把方括號也一起敲入。在Python Library Reference 中你會經常見到這些方括號。)
list.clear()
移除列表中所有的項。等價於 del a[:]
。
list.index(x[, start[, end]])
返回第一個值爲 x
的項的索引(以零爲基準)。如果列表中不存在這樣的 x
,拋出 ValueError
異常。
可選參數 start
和 end
被解釋爲切片表示法,並且用於將搜索範圍縮小爲一個列表的特定子序列。index()
方法返回的索引是從整個序列的開頭開始計數的,而不是以 start
爲頭開始計數的。
list.count(x)
返回 x 在列表中出現的次數。
list.sort(key=None, reverse=False)
對列表中的元素進行排序(函數參數可以用於定製排序規則,詳細見 sorted())
list.reverse()
反轉列表。
list.copy()
返回列表的一份拷貝。等價於 a[:]
。
列表方法使用示例:
>>> fruits = ['orange', 'apple', 'pear', 'banana', 'kiwi', 'apple', 'banana']
>>> fruits.count('apple')
2
>>> fruits.count('tangerine')
0
>>> fruits.index('banana')
3
>>> fruits.index('banana', 4) # Find next banana starting a position 4
6
>>> fruits.reverse()
>>> fruits
['banana', 'apple', 'kiwi', 'banana', 'pear', 'apple', 'orange']
>>> fruits.append('grape')
>>> fruits
['banana', 'apple', 'kiwi', 'banana', 'pear', 'apple', 'orange', 'grape']
>>> fruits.sort()
>>> fruits
['apple', 'apple', 'banana', 'banana', 'grape', 'kiwi', 'orange', 'pear']
>>> fruits.pop()
'pear'
你可能注意到了像 insert
、remove
或者 sort
這樣的方法只修改列表,卻沒有顯示返回值 —— 那是因爲它們返回了默認的 None
。這是 Python 中所有可變數據結構的一個設計原則。
可能你還注意到了不是所有數據都可以被排序或者比較的。比如,[None, 'hello', 10]
不能進行排序,因爲 integer 不能和 string 進行比較,None 不能和其他類型進行比較。還有,有些類型沒有被定義順序關係。比如,3+4j < 5+7j
不屬於合法的比較。
5.1.1. 把列表當作棧來使用
列表的方法使得列表作爲棧(stack)來使用變得非常簡單,最後添加的元素就是最先取出的元素(後進先出)。想要在棧頂添加元素,就用 append()
。想要從棧頂取出一個元素,就用沒有指定索引的 pop()
。例如:
>>> stack = [3, 4, 5]
>>> stack.append(6)
>>> stack.append(7)
>>> stack
[3, 4, 5, 6, 7]
>>> stack.pop()
7
>>> stack
[3, 4, 5, 6]
>>> stack.pop()
6
>>> stack.pop()
5
>>> stack
[3, 4]
5.1.2. 將列表作爲隊列來使用
還可以將列表作爲隊列來使用,最先添加的元素會被最先取出(先進先出)。但是,用列表實現的隊列並不高效。列表尾部的插入和彈出操作可以很快的進行,但在列表頭部進行的插入和彈出操作卻很慢(因爲所有插入位置後的元素都必須移動一個單位)。
要想實現隊列,就使用 collection.deque
,它被設計成可以快速地對兩端進行 append
和 pop
操作。例如:
>>> from collections import deque
>>> queue = deque(["Eric", "John", "Michael"])
>>> queue.append("Terry") # Terry arrives
>>> queue.append("Graham") # Graham arrives
>>> queue.popleft() # The first to arrive now leaves
'Eric'
>>> queue.popleft() # The second to arrive now leaves
'John'
>>> queue # Remaining queue in order of arrival
deque(['Michael', 'Terry', 'Graham'])
5.1.3. 列表推導式
列表推導式(list comprehension
)提供了創建列表的簡潔方法。一般用於創建新列表,新列表中每個元素都是將一些運算作用於另一個 sequence
或者 iterable
中的元素後產生的結果;或者用於創建這些元素的滿足特定條件的子序列。
比如,假設我們想要創建一個平方數的列表:
>>> squares = []
>>> for x in range(10):
... squares.append(x**2)
...
>>> squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
注意變量 x
在這個過程中被創建(或覆寫),並且它在循環結束後仍然可以使用。我們可以毫無副作用地創建一個平方數列表:
squares = list(map(lambda x: x**2, range(10)))
或者,等價地:
squares = [x**2 for x in range(10)]
這更加簡潔,更具可讀性。
一個列表推導式方括號組成,其中包含一個表達式,表達式後面跟着 for
子句,for
子句後面還可以有 0
個或多個 for
或 if
子句。
列表推導的結果是一個新的列表,這個列表根據其後的 for
和 if
子句組成的上下文推導得出。例如,下面的列表推導式將兩個列表中不同的元素進行組合:
>>> [(x, y) for x in [1,2,3] for y in [3,1,4] if x != y]
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]
這等同於:
>>> combs = []
>>> for x in [1,2,3]:
... for y in [3,1,4]:
... if x != y:
... combs.append((x, y))
...
>>> combs
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]
注意,在這兩個代碼片段中,for
和 if
語句的順序是相同的。
如果表達式是一個 tuple
(如,前面例子中的 (x, y)
),就必須用圓括號括起來:
>>> vec = [-4, -2, 0, 2, 4]
>>> # create a new list with the values doubled
>>> [x*2 for x in vec]
[-8, -4, 0, 4, 8]
>>> # filter the list to exclude negative numbers
>>> [x for x in vec if x >= 0]
[0, 2, 4]
>>> # apply a function to all the elements
>>> [abs(x) for x in vec]
[4, 2, 0, 2, 4]
>>> # call a method on each element
>>> freshfruit = [' banana', ' loganberry ', 'passion fruit ']
>>> [weapon.strip() for weapon in freshfruit]
['banana', 'loganberry', 'passion fruit']
>>> # create a list of 2-tuples like (number, square)
>>> [(x, x**2) for x in range(6)]
[(0, 0), (1, 1), (2, 4), (3, 9), (4, 16), (5, 25)]
>>> # the tuple must be parenthesized, otherwise an error is raised
>>> [x, x**2 for x in range(6)]
File "<stdin>", line 1, in <module>
[x, x**2 for x in range(6)]
^
SyntaxError: invalid syntax
>>> # flatten a list using a listcomp with two 'for'
>>> vec = [[1,2,3], [4,5,6], [7,8,9]]
>>> [num for elem in vec for num in elem]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
列表推導式可以包含複雜的表達式和嵌套的函數:
>>> from math import pi
>>> [str(round(pi, i)) for i in range(1, 6)]
['3.1', '3.14', '3.142', '3.1416', '3.14159']
5.1.4. 嵌套的列表推導式
列表推導式中的初始化表達式可以是任何表達式,包括列表推導式。
考慮下面這個 3x4 的矩陣,它被一個列表所實現,這個列表中包含 3 個長度爲 4 的列表:
>>> matrix = [
... [1, 2, 3, 4],
... [5, 6, 7, 8],
... [9, 10, 11, 12],
... ]
下面的列表推導式將對行和列進行轉換:
>>> [[row[i] for row in matrix] for i in range(4)]
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]
正如我們在上一節看到的,the nested listcomp is evaluated in the context of the for
that follows it#TODO
所以上面的例子等同於:
>>> transposed = []
>>> for i in range(4):
... transposed.append([row[i] for row in matrix])
...
>>> transposed
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]
依次的,上面的代碼等同於:
>>> transposed = []
>>> for i in range(4):
... # the following 3 lines implement the nested listcomp
... transposed_row = []
... for row in matrix:
... transposed_row.append(row[i])
... transposed.append(transposed_row)
...
>>> transposed
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]
實際應用中,與複雜的流語句相比,你可能對內置函數更感興趣。zip()
函數將能很好的處理這類問題:
>>> list(zip(*matrix))
[(1, 5, 9), (2, 6, 10), (3, 7, 11), (4, 8, 12)]
有關這一行中星號的更多內容,請看Unpacking Argument Lists。
5.2. del 語句 ✔
有一種方式可以使用列表的索引,而不是元素值來刪除列表的元素:del
語句。del 語句與有返回值的 pop()
方法不同。del 語句還可用於移除列表的切片或者清空整個列表(之前我們將一個空列表賦值給切片來清空列表):
>>> a = [-1, 1, 66.25, 333, 333, 1234.5]
>>> del a[0]
>>> a
[1, 66.25, 333, 333, 1234.5]
>>> del a[2:4]
>>> a
[1, 66.25, 1234.5]
>>> del a[:]
>>> a
[]
del
還可以用於刪除一個變量:
>>> del a
此後引用變量 a
將會產生錯誤(至少在賦新的值給它前是這樣)。後面我們會研究 def
的更多用途。
5.3. Tuples 和 Sequences ✔
我們可以看到 list
和 string
有許多共通的地方,比如索引操作和切片操作。它們是 sequance
類型的兩個例子(見Sequence Types — list, tuple, range)。因爲 Python 還在不斷髮展,所以其他的數據類型可能會被加入到 Python 中。還有一種標準的 sequence
數據類型:tuple
。
一個 tuple
由用逗號分隔的一些值組成,如:
>>> t = 12345, 54321, 'hello!'
>>> t[0]
12345
>>> t
(12345, 54321, 'hello!')
>>> # Tuples 可以嵌套:
... u = t, (1, 2, 3, 4, 5)
>>> u
((12345, 54321, 'hello!'), (1, 2, 3, 4, 5))
>>> # Tuples 不可修改:
... t[0] = 88888
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> # 但可以包含可修改的對象:
... v = ([1, 2, 3], [3, 2, 1])
>>> v
([1, 2, 3], [3, 2, 1])
如你所見,在輸出時,元組總是被圓括號括起來,這樣就可以更準確地解釋嵌套的元組。元組可能在輸入時帶有括號,也可能不帶括號,儘管通常來說圓括號是必要的(如果元組是一個更大的表達式中的一部分)。不能將一個值賦給元組中的獨立元素,但是可以創建帶有可修改對象(如列表)的元組。
儘管 tuple
可能看上去和 list
很像,但是它們經常用於不同的場景和不同的目的。元組是不可修改的,通常包含一個序列的異構(heterogeneous)的元素,這些元素通過解包(見本節後面的內容)或索引來訪問(在 namedtuples
中,甚至可以通過屬性來訪問)。list
是可修改的,list
的元素通常是同構的(homogeneous),並通過遍歷 list
來訪問。
構建包含0個或1個元素的元組是一個特別的問題:Python 語法對此有額外的兼容來適應這個問題。空元組通過一對圓括號來構建;只有一個元素的元組通過跟隨着一個逗號的值來構建(僅用括號括起一個值是不夠的)。有點醜,但是很高效。如:
>>> empty = ()
>>> singleton = 'hello', # <-- 注意最後的逗號
>>> len(empty)
0
>>> len(singleton)
1
>>> singleton
('hello',)
語句 t = 12345, 54321, 'hello!'
是 tuple packing
的一個例子:12345
,54321
和 'hello!'
被 pack
在了一個元組裏。相反的操作同樣可行:
>>> x, y, z = t
這被稱爲 sequence unpacking
,這對右邊的任何 sequence
都起作用。sequence unpacking
要求等號左邊的變量個數與 sequence
中的元素個數相同。注意,多重賦值(multiple assignment)其實就是 tuple packing
和 sequence unpacking
的組合。
5.4. 集合 ✔
Python 還支持 set
。set
是一種沒有重複元素的無序集合。基本用法有 membership testing 和消除重複項。set
對象支持一些數學運算,如並、交、差和對稱差。
可以用花括號或 set()
函數來創建 set
。注意,如果要創建一個空的 set
,必須使用set()
,而不是{}
,後者會創建一個空的字典(一種我們下一節會討論的數據結構)。
這裏有些簡短的示例:
>>> basket = {'apple', 'orange', 'apple', 'pear', 'orange', 'banana'}
>>> print(basket) # show that duplicates have been removed
{'orange', 'banana', 'pear', 'apple'}
>>> 'orange' in basket # fast membership testing
True
>>> 'crabgrass' in basket
False
>>> # Demonstrate set operations on unique letters from two words
...
>>> a = set('abracadabra')
>>> b = set('alacazam')
>>> a # unique letters in a
{'a', 'r', 'b', 'c', 'd'}
>>> a - b # letters in a but not in b
{'r', 'd', 'b'}
>>> a | b # letters in a or b or both
{'a', 'c', 'r', 'd', 'b', 'm', 'z', 'l'}
>>> a & b # letters in both a and b
{'a', 'c'}
>>> a ^ b # letters in a or b but not both
{'r', 'd', 'b', 'm', 'z', 'l'}
與列表推導式類似,set
推導式也被 Python 支持:
>>> a = {x for x in 'abracadabra' if x not in 'abc'}
>>> a
{'r', 'd'}
5.5. 字典 ✔
另一種 Python 中有用的內置數據類型是字典(見Mapping Types — dict)。字典在其他語言中有時被稱爲聯合存儲器(associative memories)或者關聯數組(“associative arrays”)。字典不像 sequance 那樣用一個範圍的數字作爲索引,而是使用 key 作爲索引,key 可以是不可修改的類型;字符串和數字都可以作爲 key。元組如果只包含字符串、數字或者元組,就可以作爲 key 來使用;元組中如果直接或間接包含可修改的對象,就不能作爲 key 來使用。不能用 list 作爲字典,因爲 list 可以通過索引賦值、切片賦值或者 append() 和 extend() 這樣的方法來修改。
最好將字典看作一個鍵值對的集合,並要求鍵都是唯一的(在一個字典內)。空字典通過一對花括號({}
)來創建。在花括號中輸入用逗號分隔鍵值對的列表,可以爲字典添加初始鍵值對;這也是字典在輸出中打印的方式。
字典中的主要操作是通過 key 來存儲 value 和提取 value。可以用 del 刪除鍵值對。如果你用一個已經存在的 key 來存儲值,那麼原來的 value 會被覆蓋。用一個不存在 key 來提取 value 會產生錯誤。
對字典使用 list(d)
會返回包含字典中所有 key 的列表,列表中元素順序爲插入時的順序(如果你希望它是被排序過的,就替換成 sorted(d)
)。使用 in 關鍵字來檢查一個 key 是否存在於字典中。
字典的簡單示例:
>>> tel = {'jack': 4098, 'sape': 4139}
>>> tel['guido'] = 4127
>>> tel
{'jack': 4098, 'sape': 4139, 'guido': 4127}
>>> tel['jack']
4098
>>> del tel['sape']
>>> tel['irv'] = 4127
>>> tel
{'jack': 4098, 'guido': 4127, 'irv': 4127}
>>> list(tel)
['jack', 'guido', 'irv']
>>> sorted(tel)
['guido', 'irv', 'jack']
>>> 'guido' in tel
True
>>> 'jack' not in tel
False
dict()
通過鍵值對序列來構建字典:
>>> dict([('sape', 4139), ('guido', 4127), ('jack', 4098)])
{'sape': 4139, 'guido': 4127, 'jack': 4098}
另外,字典推導式可以通過任意鍵和值的表達式來創建字典
>>> {x: x**2 for x in (2, 4, 6)}
{2: 4, 4: 16, 6: 36}
當 key 是簡單的字符串時,用關鍵字參數來指定鍵值對會更加簡單:
>>> dict(sape=4139, guido=4127, jack=4098)
{'sape': 4139, 'guido': 4127, 'jack': 4098}
5.6. 循環技巧 ✔
在遍歷字典時,使用字典的 items()
方法可以同時將字典中的 key
以及對應的 value
提取出來。
>>> knights = {'gallahad': 'the pure', 'robin': 'the brave'}
>>> for k, v in knights.items():
... print(k, v)
...
gallahad the pure
robin the brave
當遍歷一個 sequence
時,使用 enumerate()
函數可以同時將索引和對應的值提取出來。
>>> for i, v in enumerate(['tic', 'tac', 'toe']):
... print(i, v)
...
0 tic
1 tac
2 toe
想要同時循環兩個或多個序列,可以將序列與 zip() 函數一起使用。
>>> questions = ['name', 'quest', 'favorite color']
>>> answers = ['lancelot', 'the holy grail', 'blue']
>>> for q, a in zip(questions, answers):
... print('What is your {0}? It is {1}.'.format(q, a))
...
What is your name? It is lancelot.
What is your quest? It is the holy grail.
What is your favorite color? It is blue.
想要倒序遍歷一個 sequence
,可以先指定正向的 sequence
,然後調用 reversed()
函數。
>>> for i in reversed(range(1, 10, 2)):
... print(i)
...
9
7
5
3
1
想要以排好序的順序遍歷一個 sequence
,用 sorted()
函數。該函數返回一個排好序的 list
,保持原始對象不變。
>>> basket = ['apple', 'orange', 'apple', 'pear', 'orange', 'banana']
>>> for f in sorted(set(basket)):
... print(f)
...
apple
banana
orange
pear
有時你在遍歷一個 list
的過程中,你會很想要改變這個 list
;然而,通常情況下更簡單和安全的方式是創建一個新的 list
。
>>> import math
>>> raw_data = [56.2, float('NaN'), 51.7, 55.3, 52.5, float('NaN'), 47.8]
>>> filtered_data = []
>>> for value in raw_data:
... if not math.isnan(value):
... filtered_data.append(value)
...
>>> filtered_data
[56.2, 51.7, 55.3, 52.5, 47.8]
5.7. More on Conditions ✔
while
語句和 if
語句中使用的條件可以包含任何運算符,而不僅僅是比較運算符。
比較運算符 in
和 not in
檢查一個值是否存在於一個 sequence
中。is
和 is not
操作符比較兩個對象是否是相同的對象;這隻適用於像 list
這樣的可修改對象。所有的比較運算符有相同的優先級,比較運算符的優先級低於數值運算符
比較運算符可以被鏈接(chained)起來。比如,a < b == c
檢查 a
是否小於 b
,且 b
是否等於 c
。
比較運算可以通過布爾運算符 and
和 or
來合併在一起,比較運算(或者其他布爾表達式)的結果可以使用 not
運算符進行否定。and
、or
和 not
的優先級低於比較運算符;在 and
、or
和 not
中,not
的優先級最高,or
最低,所以 A and not B or C
等價於 (A and (not B) or C)
。通常,圓括號可以用於明確所需的組合。
布爾運算符 and 和 or 屬於短路(short-circuit)運算符:短路運算符的計算從左往右進行,並且,一旦結果確定,就停止計算。比如,如果 A
和 C
的值爲 True
,但 B
爲 False
,表達式 A and B and C
就不會再計算 C
。當作爲普通值而不是布爾值時,短路運算符的返回值爲最後一個計算的操作數。
可以將比較運算或者其他布爾表達式的結果賦值給一個變量。比如:
>>> string1, string2, string3 = '', 'Trondheim', 'Hammer Dance'
>>> non_null = string1 or string2 or string3
>>> non_null
'Trondheim'
Python 與 C 不同,Python 表達式內的賦值必須使用 walrus operator
:=
進行。這能避免在 C 中經常遇到的一類問題:在表達式中本該使用 ==
的地方,卻使用了 =
。
5.8. 比較 Sequence 和其他類型
sequence 對象可以與其他具有相同 sequence 類型的對象進行比較。比較通過字典順序(lexicographical order)進行:首先比較兩個序列的第一項,如果不同,那麼就可以確定比較結果;如果相同,就比較兩個序列的下一項,如此類推,直到其中一個序列的元素被比較完爲止。如果進行比較的兩個項,本身就是同一類型的 sequence
,則遞歸地進行字典順序的比較。如果比較下來,兩個序列中所有的項都相等,那麼就認爲這兩個序列相等。如果一個序列是另一個序列的初始子序列(initial sub-sequence),較短的序列是較小的那個。字符串的字典順序比較使用 Unicode code point number 來排序單個字符。一些同類型序列之間進行比較的例子:
(1, 2, 3) < (1, 2, 4)
[1, 2, 3] < [1, 2, 4]
'ABC' < 'C' < 'Pascal' < 'Python'
(1, 2, 3, 4) < (1, 2, 4)
(1, 2) < (1, 2, -1)
(1, 2, 3) == (1.0, 2.0, 3.0)
(1, 2, ('aa', 'ab')) < (1, 2, ('abc', 'a'), 4)
注意,將不同類型的對象進行 <
或 >
比較是合法的,前提是這些對象具有恰當的比較方法。比如,mixed numeric type 通過它們的 numeric value 進行比較,因此 0 等同於 0.0。否則,解釋器會拋出一個 TypeError
異常,而不是提供一個任意排序的結果。
腳註
Other languages may return the mutated object, which allows method chaining, such as d->insert("a")->remove("b")->sort();