# 第十二章 元組
本章我們要說的是另外一種內置類型,元組,以及列表、字典和元組如何協同工作。此外還有一個非常有用的功能:可變長度的列表,聚集和分散運算符。
一點提示:元組的英文單詞 tuple 怎么讀還有爭議。有人認為是發[t?p?l] 的音,就跟『supple』里面的一樣讀音。但編程語境下,大家普遍讀[tu:p?l],跟『quadruple』里一樣。
## 12.1 元組不可修改
元組是一系列的值。這些值可以是任意類型的,并且用整數序號作為索引,所以可以發現元組和列表非常相似。二者間重要的區別就是元組是不可修改的。
元組的語法是一系列用逗號分隔的值:
```py
>>> t = 'a', 'b', 'c', 'd', 'e'
```
通常都用一對圓括號把元組的元素包括起來,當然不這樣也沒事。
```py
>>> t = ('a', 'b', 'c', 'd', 'e')
```
要建立一個單個元素構成的元組,必須要在結尾加上逗號:
```py
>>> t1 = 'a',
>>> type(t1)
<class 'tuple'>
```
只用括號放一個值則并不是元組:
```py
>>> t2 = ('a')
>>> type(t2)
<class 'str'>
```
另一中建立元組的方法是使用內置函數 tuple。不提供參數的情況下,默認就建立一個空的元組。
```py
>>> t = tuple()
>>> t
()
```
如果參數為一個序列(比如字符串、列表或者元組),結果就會得到一個以該序列元素組成的元組。
```py
>>> t = tuple('lupins')
>>> t
('l', 'u', 'p', 'i', 'n', 's')
```
tuple 是內置函數命了,所以你就不能用來作為變量名了。
列表的各種運算符也基本適用于元組。方括號可以用來索引元素:
```py
>>> t = ('a', 'b', 'c', 'd', 'e')
>>> t[0]
'a'
```
切片運算符也可以用于選取某一區間的元素。
```py
>>> t[1:3]
('b', 'c')
```
但如果你想修改元組中的某個元素,就會得到錯誤了:
```py
>>> t[0] = 'A'
TypeError: object doesn't support item assignment
```
因為元組是不能修改的,你不能修改其中的元素。但是可以用另一個元組來替換已有的元組。
```py
>>> t = ('A',) + t[1:]
>>> t
('A', 'b', 'c', 'd', 'e')
```
上面這個語句建立了一個新的元組,然后讓 t 指向了這個新的元組。
關系運算符也適用于元組和其他序列;Python 從每個元素的首個元素開始對比。如果相等,就對比下一個元素,依此類推,之道找到不同元素為止。
有了不同元素之后,后面的其他元素就被忽略掉了(即便很大也沒用)。
```py
>>> (0, 1, 2) < (0, 3, 4)
True
>>> (0, 1, 2000000) < (0, 3, 4)
True
```
## 12.2 元組賦值
對兩個變量的值進行交換是一種常用操作。用常見語句來實現的話,就必須有一個臨時變量。比如下面這個例子中是交換 a 和 b:
```py
>>> temp = a
>>> a = b
>>> b = temp
```
這樣解決還是挺麻煩的;用元組賦值就更簡潔了:
```py
>>> a, b = b, a
```
等號左邊的是變量組成的一個元組;右邊的是表達式的元組。每個值都被賦給了對應的變量。等號右邊的表達式的值保留了賦值之前的初始值。
等號左右兩側的變量和值的數目都必須是一樣的。
```py
>>> a, b = 1, 2, 3
ValueError: too many values to unpack
```
更普適的情況下,等號右邊以是任意一種序列(字符串、列表或者元組)。比如,要把一個電子郵件地址轉換成一個用戶名和一個域名,可以用如下代碼實現:
```py
>>> addr = 'monty@python.org'
>>> uname, domain = addr.split('@')
```
split 的返回值是一個有兩個元素的列表;第一個元素賦值給了 uname 這個變量,第二個賦值給了 domain 這個變量。
```py
>>> uname
'monty'
>>> domain
'python.org'
```
## 12.3 用元組做返回值
嚴格來說,一個函數只能返回一個值,但如果這個值是一個元組,效果就和返回多個值一樣了。例如,如果你想要將兩個整數相除,計算商和余數,如果要分開計算 x/y 以及 x%y 就很麻煩了。更好的辦法是同時計算這兩個值。
內置函數 divmod 就會接收兩個參數,然后返回一個有兩個值的元組,這兩個值分別為商和余數。
可以把結果存儲為一個元組:
```py
>>> t = divmod(7, 3)
>>> t
(2, 1)
```
或者可以用元組賦值來分別存儲這兩個值:
```py
>>> quot, rem = divmod(7, 3)
>>> quot
2
>>> rem
1
```
下面的例子中,函數返回一個元組作為返回值:
```py
def min_max(t):
return min(t), max(t)
```
max 和 min 都是內置函數,會找到序列中的最大值或者最小值,min_max 這個函數會同時求得最大值和最小值,然后把這兩個值作為元組來返回。
## 12.4 參數長度可變的元組
函數的參數可以有任意多個。用星號*開頭來作為形式參數名,可以將所有實際參數收錄到一個元組中。例如 printall 就可以獲取任意多個數的參數,然后把它們都打印輸出:
```py
def printall(*args):
print(args)
```
你可以隨意命名收集來的這些參數,但 args 這個是約定俗成的慣例。下面展示一下這個函數如何使用:
```py
>>> printall(1, 2.0, '3')
(1, 2.0, '3')
```
與聚集相對的就是分散了。如果有一系列的值,然后想把它們作為多個參數傳遞給一個函數,就可以用星號*運算符。比如 divmod 要求必須是兩個參數;如果給它一個元組,是不能進行運算的:
```py
>>> t = (7, 3)
>>> divmod(t)
TypeError: divmod expected 2 arguments, got 1
```
但如果拆分這個元組,就可以了:
```py
>>> divmod(*t)
(2, 1)
```
很多內置函數都用到了參數長度可變的元組。比如 max 和 min 就可以接收任意數量的參數:
```py
>>> max(1, 2, 3)
3
```
但求和函數 sum 就不行了。
```py
>>> sum(1, 2, 3)
TypeError: sum expected at most 2 arguments, got 3
```
做個練習,寫一個名為 sumall 的函數,讓它可以接收任意數量的參數,返回總和。
## 12.5 列表和元組
zip 是一個內置函數,接收兩個或更多的序列作為參數,然后返回返回一個元組列表,該列表中每個元組都包含了從各個序列中的一個元素。這個函數名的意思就是拉鎖,就是把不相關的兩排拉鎖齒連接到一起。
下面這個例子中,一個字符串和一個列表通過 zip 這個函數連接到了一起:
```py
>>> s = 'abc'
>>> t = [0, 1, 2]
>>> zip(s, t)
<zip object at 0x7f7d0a9e7c48>
```
該函數的返回值是一個 zip 對象,該對象可以用來迭代所有的數值對。zip 函數經常被用到 for 循環中:
```py
>>> for pair in zip(s, t): ...
print(pair) ...
('a', 0) ('b', 1) ('c', 2)
```
zip 對象是一種迭代器,也就是某種可以迭代整個序列的對象。迭代器和列表有些相似,但不同于列表的是,你無法通過索引來選擇迭代器中的指定元素。
如果想用列表的運算符和方法,可以用 zip 對象來構成一個列表:
```py
>>> list(zip(s, t))
[('a', 0), ('b', 1), ('c', 2)]
```
返回值是一個由元組構成的列表;在這個例子中,每個元組都包含了字符串中的一個字母,以及列表中對應位置的元素。
在長度不同的序列中,返回的結果長度取決于最短的一個。
```py
>>> list(zip('Anne', 'Elk'))
[('A', 'E'), ('n', 'l'), ('n', 'k')]
```
用 for 循環來遍歷一個元組列表的時候,可以用元組賦值語句:
```py
t = [('a', 0), ('b', 1), ('c', 2)]
for letter, number in t:
print(number, letter)
```
每次經歷循環的時候,Python 都選中列表中的下一個元組,然后把元素賦值給字母和數字。該循環的輸出如下:
```py
0 a 1 b 2 c
```
如果結合使用 zip、for 循環以及元組賦值,就能得到一種能同時遍歷兩個以上序列的代碼組合。比如下面例子中的 has_match 這個函數,接收兩個序列 t1 和 t2 作為參數,然后如果存在一個索引位置 i 使得 t1[i] == t2[i]就返回真:
```py
def has_match(t1, t2):
for x, y in zip(t1, t2):
if x == y:
return True
return False
```
如果你要遍歷一個序列中的所有元素以及它們的索引,可以用內置的函數 enumerate:
```py
for index, element in enumerate('abc'):
print(index, element)
```
enumerate 函數的返回值是一個枚舉對象,它會遍歷整個成對序列;每一對都包括一個索引(從 0 開始)以及給定序列的一個元素。在本節的例子中,輸出依然如下:
```py
0 a 1 b 2 c
```
## 12.6 詞典與元組
字典有一個名為 items 的方法,會返回一個由元組組成的序列,每一個元組都是字典中的一個鍵值對。
```py
>>> d = {'a':0, 'b':1, 'c':2}
>>> t = d.items()
>>> t
dict_items([('c', 2), ('a', 0), ('b', 1)])
```
結果是一個 dict_items 對象,這是一個迭代器,迭代所有的鍵值對。可以在 for 循環里面用這個對象,如下所示:
```py
>>> for key, value in d.items():
... print(key, value)
... c 2 a 0 b 1
```
你也應該預料到了,字典里面的項是沒有固定順序的。
反過來使用的話,你就也可以用一個元組的列表來初始化一個新的字典:
```py
>>> t = [('a', 0), ('c', 2), ('b', 1)]
>>> d = dict(t)
>>> d
{'a': 0, 'c': 2, 'b': 1}
```
結合使用 dict 和 zip ,會得到一種建立字典的簡便方法:
```py
>>> d = dict(zip('abc', range(3)))
>>> d
{'a': 0, 'c': 2, 'b': 1}
```
字典的 update 方法也接收一個元組列表,然后把它們作為鍵值對添加到一個已存在的字典中。
把元組用作字典中的鍵是很常見的做法(主要也是因為這種情況不能用列表)。比如,一個電話字典可能就映射了姓氏、名字的數據對到不同的電話號碼。假如我們定義了 last,first 和 number 這三個變量,可以用如下方法來實現:
```py
directory[last, first] = number
```
方括號內的表達式是一個元組。我們可以用元組賦值語句來遍歷這個字典。
```py
for last, first in directory:
print(first, last, directory[last,first])
```
上面這個循環會遍歷字典中的鍵,這些鍵都是元組。程序會把每個元組的元素分別賦值給 last 和 first,然后輸出名字以及對應的電話號。
在狀態圖中表示元組的方法有兩種。更詳盡的版本會展示索引和元素,就如同在列表中一樣。例如圖 12.1 中展示了元組('Cleese', 'John') 。
________________________________________

Figure 12.1: State diagram.
________________________________________
但隨著圖解規模變大,你也許需要省略掉一些細節。比如電話字典的圖解可能會像圖 12.2 所示。
________________________________________

Figure 12.2: State diagram.
________________________________________
圖中的元組用 Python 的語法來簡單表示。其中的電話號碼是 BBC 的投訴熱線,所以不要給人家打電話哈。
## 12.7 由序列組成的序列
之前我一直在講由元組組成的列表,但本章幾乎所有的例子也適用于由列表組成的列表、元組組成的元組以及列表組成的元組。為了避免枚舉所有的組合,咱們直接討論序列組成的序列就更方便一些。
很多情況下,不同種類的序列(字符串、列表和元組)是可以交換使用的。那么該如何選擇用哪種序列呢?
先從最簡單的開始,字符串比起其他序列,功能更加有限,因為字符串中的元素必須是字符。而且還不能修改。如果你要修改字符串里面的字符(而不是要建立一個新字符串),你最好還是用字符列表吧。
列表用的要比元組更廣泛,主要因為列表可以修改。但以下這些情況下,你還是用元組更好:
在某些情況下,比如返回語句中,用元組來實現語法上要比列表簡單很多。
如果你要用一個序列作為字典的鍵,必須用元組或者字符串這樣不可修改的類型才行。
如果你要把一個序列作為參數傳給一個函數,用元組能夠降低由于別名使用導致未知情況而帶來的風險。
由于元組是不可修改的,所以不提供 sort 和 reverse 這樣的方法,這些方法都只能修改已經存在的列表。但 Python 提供了內置函數 sorted,該函數接收任意序列,然后返回一個把該序列中元素重新排序過的列表,另外還有個內置函數 reversed,接收一個序列然后返回一個以逆序迭代整個列表的迭代器。
## 12.8 調試
列表、字典以及元組,都是數據結構的一些樣例;在本章我們開始見識這些復合的數據結構,比如由元組組成的列表,或者包含元組作為鍵而列表作為鍵值的字典等等。符合數據結構非常有用,但容易導致一些錯誤,我把這種錯誤叫做結構錯誤;這種錯誤往往是由于一個數據結構中出現了錯誤的類型、大小或者結構而引起的。比如,如果你想要一個由一個整形構成的列表,而我給你一個單純的整形變量(不是放進列表的),就會出錯了。
要想有助于解決這類錯誤,我寫了一個叫做 structshape 的模塊,該模塊提供了一個同名函數,接收任何一種數據結構作為參數,然后返回一個字符串來總結該數據結構的形態。可以從 [這里](http://thinkpython2.com/code/structshape.py)下載。
下面是一個簡單列表的示范:
```py
>>> from structshape import structshape
>>> t = [1, 2, 3]
>>> structshape(t)
'list of 3 int'
```
更帶勁點的程序可能還應該寫“list of 3 ints”,但不理會單復數變化有利于簡化問題。下面是一個列表的列表:
```py
>>> t2 = [[1,2], [3,4], [5,6]]
>>> structshape(t2)
'list of 3 list of 2 int'
```
如果列表元素是不同類型,structshape 會按照順序,把每種類型都列出:
```py
>>> t3 = [1, 2, 3, 4.0, '5', '6', [7], [8], 9]
>>> structshape(t3)
'list of (3 int, float, 2 str, 2 list of int, int)'
```
下面是一個元組的列表:
```py
>>> s = 'abc'
>>> lt = list(zip(t, s))
>>> structshape(lt)
'list of 3 tuple of (int, str)'
```
下面是一個有三個項的字典,該字典映射了從整形數到字符串。
```py
>>> d = dict(lt)
>>> structshape(d)
'dict of 3 int->str'
```
如果你追蹤自己的數據結構有困難,structshape 這個模塊能有所幫助。
## 12.9 Glossary 術語列表
tuple:
An immutable sequence of elements.
>元組:一列元素組成的不可修改的序列。
tuple assignment:
An assignment with a sequence on the right side and a tuple of variables on the left. The right side is evaluated and then its elements are assigned to the variables on the left.
>元組賦值:一種賦值語句,等號右側用一個序列,左側為一個變量構成的元組。右側的內容先進行運算,然后這些元素會賦值給左側的變量。
gather:
The operation of assembling a variable-length argument tuple.
>收集:變量長度可變元組添加元素的運算。
scatter:
The operation of treating a sequence as a list of arguments.
>分散:將一個序列拆分成一系列參數組成的列表的運算。
zip object:
The result of calling a built-in function zip; an object that iterates through a sequence of tuples.
>拉鏈對象:調用內置函數 zip 得到的返回結果;一個遍歷元組序列的對象。
iterator:
An object that can iterate through a sequence, but which does not provide list operators and methods.
>迭代器:迭代一個序列的對象,這種序列不能提供列表的運算和方法。
data structure:
A collection of related values, often organized in lists, dictionaries, tuples, etc.
>數據結構:一些有關系數據的集合體,通常是列表、字典或者元組等形式。
shape error:
An error caused because a value has the wrong shape; that is, the wrong type or size.
>結構錯誤:由于一個值有錯誤的結構而導致的錯誤;比如錯誤的類型或者大小。
## 12.10 練習
### 練習 1
寫一個名為 most_frequent 的函數,接收一個字符串,然后用出現頻率降序來打印輸出字母。找一些不同語言的文本素材,然后看看不同語言情況下字母的頻率變化多大。然后用你的結果與[這里](http://en.wikipedia.org/wiki/Letter_frequencies)的數據進行對比。[樣例代碼](http://thinkpython2.com/code/most_frequent.py)。
### 練習 2
更多變位詞了!
1. 寫一個函數,讀取一個文件中的一個單詞列表(參考 9.1),然后輸出所有的變位詞。
下面是可能的輸出樣式的示范:
['deltas', 'desalt', 'lasted', 'salted', 'slated', 'staled']
['retainers', 'ternaries']
['generating', 'greatening']
['resmelts', 'smelters', 'termless']
提示:你也許可以建立一個字典,映射一個特定的字母組合到一個單詞列表,單詞列表中的單詞可以用這些字母來拼寫出來。那么問題來了,如何去表示這個字母的集合,才能讓這個集合能用作字典的一個鍵?
2.修改一下之前的程序,讓它先輸出變位詞列表中最長的,然后是其次長的,依此類推。
3. 在拼字游戲中,當你已經有七個字母的時候,再添加一個字母就能組成一個八個字母的單詞,這就 TMD『bingo』 了(什么鬼東西?老外拼字游戲就跟狗一樣,翻著惡心死了)。然后哪八個字母組合起來最可能得到 bingo?提示:有七個。(簡直就是狗一樣的題目,麻煩死了,這里數據結構大家學會了就好了。)[樣例代碼](http://thinkpython2.com/code/anagram_sets.py).
### 練習 3
兩個單詞,如果其中一個通過調換兩個字母位置就能成為另外一個,就成了一個『交換對』。協議額函數來找到詞典中所有的這樣的交換對。提示:不用測試所有的詞對,不用測試所有可能的替換方案。[樣例代碼](http://thinkpython2.com/code/metathesis.py)。 鳴謝:本練習受啟發于[這里](http://puzzlers.org)的一個例子。
### 練習 4
接下來又是一個[汽車廣播字謎](http://www.cartalk.com/content/puzzlers):
一個英文單詞,每次去掉一個字母,又還是一個正確的英文單詞,這是什么詞?
然后接下來字母可以從頭去掉,也可以從末尾去掉,或者從中間,但不能重新排列其他字母。每次去掉一個字母,都會的到一個新的英文單詞。然后最終會得到一個字母,也還是一個英文單詞,這個單詞也能在詞典中找到。符合這樣要求的單詞有多少?最長的是哪個?
給你一個合適的小例子:Sprite。這個詞就滿足上面的條件。把 r 去掉了是 spite,去掉結尾的 e 是 spit,去掉 s 得到的是 pit,it,然后是 I。
寫一個函數找到所有的這樣的詞,然后找到其中最長的一個。
這個練習比一般的練習難以些,所以下面是一些提示:
1. 你也許需要寫一個函數,接收一個單詞然后計算一下這個單詞去掉一個字母能得到單詞組成的列表。列表中這些單詞就如同原始單詞的孩子一樣。
2. 只要一個 單詞的孩子還可以縮減,那這個單詞本身就虧縮減。你可以認為空字符串是可以縮減的,這樣來作為一個基準條件。
3. 我上面提供的 words.txt 這個詞表,不包含單個字母的單詞。所以你需要自行添加 I、a 以及空字符串上去。
4. 要提高程序性能的話,你最好存儲住已經算出來能被繼續縮減的單詞。[樣例代碼](http://thinkpython2.com/code/reducible.py)。