跳至內容

Think Python/列表

來自華夏公益教科書,開放的書籍,為開放的世界

列表是一個序列

[編輯 | 編輯原始碼]

類似於字串,列表也是一個值的序列。在字串中,值是字元;在列表中,它們可以是任何型別。列表中的值被稱為元素,有時也稱為專案

有幾種方法可以建立新的列表;最簡單的方法是用方括號 ([]) 將元素括起來

[10, 20, 30, 40]
['crunchy frog', 'ram bladder', 'lark vomit']

第一個示例是包含四個整數的列表。第二個是包含三個字串的列表。列表的元素不必是相同的型別。以下列表包含一個字串、一個浮點數、一個整數以及(當然)另一個列表

['spam', 2.0, 5, [10, 20]]

另一個列表內的列表稱為巢狀

不包含任何元素的列表稱為空列表;您可以使用空括號 [] 建立一個。

正如您可能預期的那樣,您可以將列表值賦給變數

>>> cheeses = ['Cheddar', 'Edam', 'Gouda']
>>> numbers = [17, 123]
>>> empty = []
>>> print cheeses, numbers, empty
['Cheddar', 'Edam', 'Gouda'] [17, 123] []

列表是可變的

[編輯 | 編輯原始碼]

訪問列表元素的語法與訪問字串的字元相同——方括號運算子。方括號內的表示式指定索引。請記住,索引從 0 開始

>>> print cheeses[0]
Cheddar

與字串不同,列表是可變的。當方括號運算子出現在賦值的左側時,它標識將被賦值的列表元素。

>>> numbers = [17, 123]
>>> numbers[1] = 5
>>> print numbers
[17, 5]

numbers 的第一個元素(以前是 123)現在是 5。

您可以將列表視為索引和元素之間的關係。這種關係稱為對映;每個索引都“對映到”一個元素。以下是一個顯示 cheesesnumbersempty 的狀態圖

<IMG SRC="book013.png">

列表由帶有“列表”字樣的方框表示,方框外部是列表的元素,方框內部是列表的元素。cheeses 指向一個包含三個元素的列表,它們的索引分別是 0、1 和 2。numbers 包含兩個元素;該圖顯示了第二個元素的值已從 123 重新分配為 5。empty 指向一個不包含任何元素的列表。

列表索引的工作方式與字串索引相同

  • 任何整數表示式都可以用作索引。
  • 如果您嘗試讀取或寫入不存在的元素,您將得到一個 IndexError
  • 如果索引為負值,則從列表末尾倒數。

in 運算子也適用於列表。

>>> cheeses = ['Cheddar', 'Edam', 'Gouda']
>>> 'Edam' in cheeses
True
>>> 'Brie' in cheeses
False

遍歷列表

[編輯 | 編輯原始碼]

遍歷列表元素最常見的方法是使用 for 迴圈。語法與字串相同

for cheese in cheeses:
    print cheese

如果您只需要讀取列表的元素,這很有效。但是,如果您想寫入或更新元素,則需要索引。一種常見的做法是結合使用 rangelen 函式

for i in range(len(numbers)):
    numbers[i] = numbers[i] * 2

此迴圈遍歷列表並更新每個元素。len 返回列表中元素的數量。range 返回從 0 到 n−1 的索引列表,其中 n 是列表的長度。每次迴圈時,i 獲取下一個元素的索引。語句體中的賦值語句使用 i 來讀取元素的舊值並分配新值。

對空列表進行的 for 迴圈永遠不會執行語句體

for x in empty:
    print 'This never happens.'

雖然列表可以包含另一個列表,但巢狀列表仍然算作單個元素。此列表的長度為四

['spam', 1, ['Brie', 'Roquefort', 'Pol le Veq'], [1, 2, 3]]

列表操作

[編輯 | 編輯原始碼]

+ 運算子連線列表

>>> a = [1, 2, 3]
>>> b = [4, 5, 6]
>>> c = a + b
>>> print c
[1, 2, 3, 4, 5, 6]

類似地,* 運算子將列表重複指定次數

>>> [0] * 4
[0, 0, 0, 0]
>>> [1, 2, 3] * 3
[1, 2, 3, 1, 2, 3, 1, 2, 3]

第一個示例將 [0] 重複四次。第二個示例將列表 [1, 2, 3] 重複三次。

列表切片

[編輯 | 編輯原始碼]

切片運算子也適用於列表

>>> t = ['a', 'b', 'c', 'd', 'e', 'f']
>>> t[1:3]
['b', 'c']
>>> t[:4]
['a', 'b', 'c', 'd']
>>> t[3:]
['d', 'e', 'f']

如果您省略第一個索引,則切片從開頭開始。如果您省略第二個索引,則切片一直到結尾。因此,如果您省略了兩個索引,則切片就是整個列表的副本。

>>> t[:]
['a', 'b', 'c', 'd', 'e', 'f']

由於列表是可變的,因此在執行摺疊、旋轉或修改列表的操作之前,製作副本通常很有用。

賦值左側的切片運算子可以更新多個元素

>>> t = ['a', 'b', 'c', 'd', 'e', 'f']
>>> t[1:3] = ['x', 'y']
>>> print t
['a', 'x', 'y', 'd', 'e', 'f']

列表方法

[編輯 | 編輯原始碼]

Python 提供了對列表進行操作的方法。例如,append 將一個新元素新增到列表末尾

>>> t = ['a', 'b', 'c']
>>> t.append('d')
>>> print t
['a', 'b', 'c', 'd']

extend 接受一個列表作為引數,並將所有元素附加到該列表

>>> t1 = ['a', 'b', 'c']
>>> t2 = ['d', 'e']
>>> t1.extend(t2)
>>> print t1
['a', 'b', 'c', 'd', 'e']

此示例不會修改 t2

sort 將列表中的元素從低到高排列

>>> t = ['d', 'c', 'e', 'b', 'a']
>>> t.sort()
>>> print t
['a', 'b', 'c', 'd', 'e']

列表方法都是空方法;它們修改列表並返回 None。如果您不小心寫了 t = t.sort(),您會對結果感到失望。

對映、過濾和歸約

[編輯 | 編輯原始碼]

要將列表中的所有數字加起來,您可以使用以下迴圈

def add_all(t):
    total = 0
    for x in t:
        total += x
    return total

total 初始化為 0。每次迴圈時,x 獲取列表中的一個元素。+= 運算子提供了一種更新變數的簡短方法

    total += x

等效於

    total = total + x

隨著迴圈執行,total 累積元素的總和;以這種方式使用的變數有時稱為累加器

將列表中的元素加起來是一個非常常見的操作,Python 提供了它作為內建函式 sum

>>> t = [1, 2, 3]
>>> sum(t)
6

這種將一系列元素組合成單個值的運算有時稱為歸約

有時您想遍歷一個列表,同時構建另一個列表。例如,以下函式接受一個字串列表,並返回一個包含大寫字串的新列表

def capitalize_all(t):
    res = []
    for s in t:
        res.append(s.capitalize())
    return res

res 初始化為一個空列表;每次迴圈時,我們附加下一個元素。因此,res 是另一種累加器。

類似於 capitalize_all 的運算有時稱為對映,因為它將一個函式(在本例中是方法 capitalize)“對映”到序列中的每個元素上。

另一個常見的操作是從列表中選擇一些元素並返回一個子列表。例如,以下函式接受一個字串列表,並返回一個只包含大寫字串的列表

def only_upper(t):
    res = []
    for s in t:
        if s.isupper():
            res.append(s)
    return res

isupper 是一個字串方法,如果字串僅包含大寫字母,則返回 True

類似於 only_upper 的運算稱為過濾,因為它選擇了一些元素並過濾掉其他元素。

大多數常見的列表操作可以表示為對映、過濾和歸約的組合。由於這些操作非常常見,Python 提供了語言特性來支援它們,包括內建函式 map 和一個稱為“列表推導”的運算子。

編寫一個函式,該函式接受一個數字列表,並返回累積和;也就是說,一個新列表,其中第 'i' 個元素是原始列表中前 'i+1' 個元素的總和。例如,'[1, 2, 3]' 的累積和是 '[1, 3, 6]'

刪除元素

[編輯 | 編輯原始碼]

有幾種方法可以從列表中刪除元素。如果您知道要刪除元素的索引,可以使用 pop

>>> t = ['a', 'b', 'c']
>>> x = t.pop(1)
>>> print t
['a', 'c']
>>> print x
b

pop 會修改列表並返回被移除的元素。如果未提供索引,則會刪除並返回最後一個元素。

如果不需要移除的值,可以使用del 運算子

>>> t = ['a', 'b', 'c']
>>> del t[1]
>>> print t
['a', 'c']

如果知道要移除的元素(但不知道索引),可以使用remove

>>> t = ['a', 'b', 'c']
>>> t.remove('b')
>>> print t
['a', 'c']

remove 的返回值為None

要移除多個元素,可以使用del 以及切片索引

>>> t = ['a', 'b', 'c', 'd', 'e', 'f']
>>> del t[1:5]
>>> print t
['a', 'f']

與往常一樣,切片選擇所有元素,直到但不包括第二個索引。

列表和字串

[edit | edit source]

字串是字元序列,列表是值的序列,但字元列表與字串不同。要將字串轉換為字元列表,可以使用list

>>> s = 'spam'
>>> t = list(s)
>>> print t
['s', 'p', 'a', 'm']

因為list 是內建函式的名稱,所以應避免將其用作變數名。我還避免使用l,因為它看起來太像1。所以這就是我使用t 的原因。

list 函式將字串分解為單個字母。如果要將字串分解為單詞,可以使用split 方法

>>> s = 'pining for the fjords'
>>> t = s.split()
>>> print t
['pining', 'for', 'the', 'fjords']

一個可選引數稱為 **分隔符**,它指定哪些字元用作單詞邊界。以下示例使用連字元作為分隔符

>>> s = 'spam-spam-spam'
>>> delimiter = '-'
>>> s.split(delimiter)
['spam', 'spam', 'spam']

joinsplit 的反向操作。它接受字串列表並將元素連線起來。join 是字串方法,因此您必須在分隔符上呼叫它並將列表作為引數傳遞

>>> t = ['pining', 'for', 'the', 'fjords']
>>> delimiter = ' '
>>> delimiter.join(t)
'pining for the fjords'

在這種情況下,分隔符是一個空格字元,因此join 在單詞之間放置一個空格。要將字串連線起來而不留空格,可以使用空字串'' 作為分隔符。

物件和值

[edit | edit source]

如果執行這些賦值語句

a = 'banana'
b = 'banana'

我們知道CODE>aCODE> 和 b 都引用一個字串,但我們不知道它們是否引用 同一個 字串。有兩種可能的狀態

<IMG SRC="book014.png">

在一種情況下,ab 引用兩個具有相同值的不同的物件。在第二種情況下,它們引用同一個物件。

要檢查兩個變數是否引用同一個物件,可以使用is 運算子。

>>> a = 'banana'
>>> b = 'banana'
>>> a is b
True

在本例中,Python 只建立了一個字串物件,ab 都引用它。

但是當您建立兩個列表時,您將獲得兩個物件

>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a is b
False

所以狀態圖看起來像這樣

<IMG SRC="book015.png">

在這種情況下,我們會說這兩個列表是 **等效** 的,因為它們具有相同的元素,但不是 **相同** 的,因為它們不是同一個物件。如果兩個物件相同,那麼它們也是等效的,但如果它們是等效的,則它們不一定相同。

到目前為止,我們一直在交替使用“物件”和“值”,但更準確地說,物件具有值。如果您執行a = [1,2,3]a 引用一個列表物件,其值為特定元素序列。如果另一個列表具有相同的元素,我們會說它具有相同的值。

別名

[edit | edit source]

如果a 引用一個物件,並且您分配b = a,那麼這兩個變數都引用同一個物件

>>> a = [1, 2, 3]
>>> b = a
>>> b is a
True

狀態圖看起來像這樣

<IMG SRC="book016.png">

變數與物件之間的關聯稱為 **引用**。在本例中,存在對同一個物件的兩個引用。

具有多個引用的物件具有多個名稱,因此我們說該物件是 **別名** 的。

如果別名物件是可變的,則使用一個別名進行的更改會影響另一個別名

>>> b[0] = 17
>>> print a
[17, 2, 3]

雖然這種行為可能很有用,但它容易出錯。一般來說,在處理可變物件時,最好避免使用別名。

對於像字串這樣的不可變物件,別名並不是什麼大問題。在本例中

a = 'banana'
b = 'banana'

ab 是否引用同一個字串幾乎沒有區別。

列表引數

[edit | edit source]

將列表傳遞給函式時,函式會獲得對該列表的引用。如果函式修改列表引數,則呼叫者會看到更改。例如,delete_head 從列表中刪除第一個元素

def delete_head(t):
    del t[0]

以下是它的使用方法

>>> letters = ['a', 'b', 'c']
>>> delete_head(letters)
>>> print letters
['b', 'c']

引數 t 和變數 letters 是同一個物件的別名。堆疊圖看起來像這樣

<IMG SRC="book017.png">

由於列表由兩個幀共享,因此我在它們之間繪製了它。

重要的是要區分修改列表的操作和建立新列表的操作。例如,append 方法會修改列表,但+ 運算子會建立一個新列表

>>> t1 = [1, 2]
>>> t2 = t1.append(3)
>>> print t1
[1, 2, 3]
>>> print t2
None

>>> t3 = t1 + [3]
>>> print t3
[1, 2, 3]
>>> t2 is t3
False

這種區別在編寫旨在修改列表的函式時很重要。例如,此函式不會 刪除列表的頭部

def bad_delete_head(t):
    t = t[1:]              # WRONG!

切片運算子建立一個新列表,賦值使t 引用它,但所有這些都不會影響作為引數傳遞的列表。

另一種選擇是編寫一個建立並返回新列表的函式。例如,tail 返回除第一個元素之外的所有元素

def tail(t):
    return t[1:]

此函式不會修改原始列表。以下是它的使用方法

>>> letters = ['a', 'b', 'c']
>>> rest = tail(letters)
>>> print rest
['b', 'c']

練習 2

[edit | edit source]

編寫一個名為 'chop' 的函式,它接受一個列表並對其進行修改,刪除第一個和最後一個元素,並返回 'None'

然後編寫一個名為 'middle' 的函式,它接受一個列表並返回一個包含除第一個和最後一個元素之外所有元素的新列表。

除錯

[edit | edit source]

不小心使用列表(和其他可變物件)會導致長時間的除錯。以下是一些常見的陷阱以及避免它們的方法

  • 不要忘記大多數列表方法會修改引數並

返回None。這與字串方法相反,字串方法會返回一個新字串並保留原始字串。如果您習慣於編寫這樣的字串程式碼

word = word.strip()

您可能會忍不住編寫這樣的列表程式碼

t = t.sort()           # WRONG!


因為sort 返回None,所以您對t 執行的下一個操作很可能會失敗。

在使用列表方法和運算子之前,應仔細閱讀文件,然後在互動模式下對其進行測試。列表與其他序列(如字串)共享的方法和運算子在docs.python.org/lib/typesseq.html 中有記錄。僅適用於可變序列的方法和運算子在docs.python.org/lib/typesseq-mutable.html 中有記錄。

  • 選擇一種習慣用法並堅持使用它。

列表問題的一部分是,有太多種方法可以做事情。例如,要從列表中刪除一個元素,可以使用popremovedel,甚至可以使用切片賦值。

要新增一個元素,可以使用append 方法或+ 運算子。但不要忘記這些是正確的

t.append(x)
t = t + [x]

而這些是錯誤的

t.append([x])          # WRONG!
t = t.append(x)        # WRONG!
t + [x]                # WRONG!
t = t + x              # WRONG!

嘗試在互動模式下執行每個示例,以確保您瞭解它們的作用。請注意,只有最後一個會導致執行時錯誤;其他三個是合法的,但它們執行了錯誤的操作。

  • 建立副本以避免使用別名。


如果您想使用sort 之類的會修改引數的方法,但您也需要保留原始列表,則可以建立副本。

orig = t[:]
t.sort()

在本例中,您也可以使用內建函式sorted,它返回一個新的排序列表並保留原始列表。但在這種情況下,您應該避免使用sorted 作為變數名!

術語表

[edit | edit source]
列表
值的序列。
元素
列表(或其他序列)中的一個值,也稱為項。
索引
表示列表中一個元素的整數值。
巢狀列表
作為另一個列表的元素的列表。
列表遍歷
按順序訪問列表中的每個元素。
對映
一種關係,其中一個集合中的每個元素都對應於另一個集合中的元素。例如,列表是索引到元素的對映。
累加器
在迴圈中用於累加或累積結果的變數。
減少
一種處理模式,它遍歷序列並將元素累積到單個結果中。
對映
一種處理模式,它遍歷序列並對每個元素執行操作。
過濾
一種處理模式,它遍歷列表並選擇滿足某些條件的元素。
物件
變數可以引用的東西。物件具有型別和值。
等效
具有相同的值。
相同
是同一個物件(這意味著等效)。
引用
變數與其值之間的關聯。
別名
兩個變數引用同一個物件的情況。
分隔符
用於指示應在何處拆分字串的字元或字串。

練習

[edit | edit source]

編寫一個名為 is_sorted 的函式,該函式以列表作為引數,如果列表按升序排序,則返回 'True',否則返回 'False'。您可以假設(作為先決條件)列表的元素可以使用比較運算子 '<'、'>' 等進行比較。

例如,is_sorted([1,2,2]) 應該返回 'True',而 is_sorted(['b','a']) 應該返回 'False'

如果您可以重新排列一個單詞的字母以拼出另一個單詞,那麼這兩個單詞就是迴文。編寫一個名為 is_anagram 的函式,該函式接受兩個字串並返回 'True',如果它們是迴文。

(所謂的)生日悖論

  • 編寫一個名為 has_duplicates 的函式,該函式接受一個列表,如果列表中存在任何元素出現超過一次,則返回 'True'。它不應該修改原始列表。
  • 如果你的班級有 23 名學生,你們中有兩人生日相同的機率是多少?您可以透過生成 23 個生日的隨機樣本並檢查匹配項來估計此機率。提示:您可以使用 'random' 模組中的 'randint' 函式生成隨機生日。

您可以在 'wikipedia.org/wiki/Birthday_paradox' 上閱讀有關此問題的資訊,您也可以在 'thinkpython.com/code/birthday.py' 中檢視我的解決方案。


編寫一個名為 remove_duplicates 的函式,該函式接受一個列表並返回一個新列表,其中只包含原始列表中的唯一元素。提示:它們不必按相同的順序排列。


編寫一個函式,該函式讀取 'words.txt' 檔案並構建一個列表,每個單詞一個元素。編寫此函式的兩個版本,一個使用 'append' 方法,另一個使用習慣用法 't = t + [x]'。哪個執行時間更長?為什麼?

您可以在 'thinkpython.com/code/wordlist.py' 中檢視我的解決方案。

要檢查單詞是否在單詞列表中,您可以使用 'in' 運算子,但這會很慢,因為它會按順序搜尋單詞。

由於單詞按字母順序排列,我們可以透過二分搜尋來加快速度,這與您在字典中查詢單詞時所做的類似。您從中間開始,檢查您要查詢的單詞是否位於列表中間的單詞之前。如果是,則以相同的方式搜尋列表的前半部分。否則,您將搜尋後半部分。

無論哪種方式,您都會將剩餘的搜尋空間減半。如果單詞列表有 113,809 個單詞,則大約需要 17 步才能找到該單詞或得出它不存在的結論。

編寫一個名為 'bisect' 的函式,該函式接受一個排序的列表和一個目標值,並返回列表中該值的索引(如果存在),如果不存在,則返回 'None'

或者您可以閱讀 'bisect' 模組的文件並使用它!

如果一個單詞是另一個單詞的反轉,則這兩個單詞被稱為“反轉對”。編寫一個程式,查詢單詞列表中的所有反轉對。

練習 10

[編輯 | 編輯原始碼]

如果從每個單詞中交替取字母形成一個新詞1,則兩個單詞“互鎖”。例如,“shoe” 和 “cold” 互鎖形成 “schooled”。

  • 編寫一個程式,查詢所有互鎖的單詞對。提示:不要列舉所有對!
  • 你能找到任何三個方向互鎖的單詞嗎?也就是說,從第一個、第二個或第三個字母開始,每隔三個字母形成一個單詞?

1
本練習的靈感來自 puzzlers.org 上的一個示例。
華夏公益教科書