# 第八章 字符串
字符串和整形、浮點數以及布爾值很不一樣。一個字符串是一個序列,意味著是對其他值的有序排列。在本章你將學到如何讀取字符串中的字符,你還會學到一些字符串相關的方法。
## 8.1 字符串是序列
字符串就是一串有序的字符。你可以通過方括號操作符,每次去訪問字符串中的一個字符:
```py
>>> fruit = 'banana'
>>> letter = fruit[1]
```
第二個語句選擇了 fruit 這個字符串的序號為 1 的字符,并把這個字符賦值給了 letter 這個變量。
(譯者注:思考一下這里的 letter 是一個什么類型的變量。)
方括號內的內容叫做索引。索引指示了你所指定的字符串中字符的位置(就跟名字差不多)。
但你可能發現得到的結果和你預期的有點不一樣:
```py
>>> letter
'a'
```
大多數人都認為 banana 的第『1』個字符應該是 b,而不是 a。但對于計算機科學家來說,索引是字符串從頭的偏移量,所以真正的首字母偏移量應該是 0.
```py
>>> letter = fruit[0]>>> letter
'b'
```
所以 b 就是字符串 banana 的第『0』個字符,而 a 是第『1』個,n 就是第『2』個了。
你可以在方括號內的索引中使用變量和表達式:
```py
>>> i = 1
>>> fruit[i]
'a'
>>> fruit[i+1]
'n'
```
但要注意的事,索引的值必須是整形的。否則你就會遇到類型錯誤了:
```py
>>> letter = fruit[1.5]
TypeError: string indices must be integers
```
## 8.2 len 長度
len 是一個內置函數,會返回一個字符串中字符的長度:
```py
>>> fruit = 'banana'
>>> len(fruit) 6
```
要得到一個字符串的最后一個字符,你可能會想到去利用 len 函數:
```py
>>> length = len(fruit)
>>> last = fruit[length]
IndexError: string index out of range
```
出現索引錯誤的原因就是 banana 這個字符串在第『6』個位置是沒有字母的。因為我們從 0 開始數,所以這一共 6 個字母的順序是 0 到 5 號。因此要得到最后一次字符,你需要在字符串長度的基礎上減去 1 才行:
```py
>>> last = fruit[length-1]
>>> last
'a'
```
或者你也可以用負數索引,意思就是從字符串的末尾向前數幾位。fruit[-1]這個表達式給你最后一個字符,fruit[-2]給出倒數第二個,依此類推。
## 8.3 用 for 循環遍歷字符串
很多計算過程都需要每次從一個字符串中拿一個字符。一般都是從頭開始,依次得到每個字符,然后做點處理,然后一直到末尾。這種處理模式叫遍歷。寫一個遍歷可以使用 while 循環:
```py
index = 0
while index < len(fruit):
letter = fruit[index]
print(letter)
index = index + 1
```
這個循環遍歷了整個字符串,然后它再把買一個字符顯示在一行上面。循環條件是 index 這個變量小于字符串 fruit 的長度,所以當 index 與字符串長度相等的時候,條件就不成立了,循環體就不運行了。最后一個字符被獲取的時候,index 正好是 len(fruit)-1,這就已經是該字符串的最后一個字符了。
下面就練習一下了,寫一個函數,接收一個字符串做參數,然后倒序顯示每一個字符,每行顯示一個。
另外一種遍歷的方法就是 for 循環了:
```py
for letter in fruit:
print(letter)
```
每次循環之后,字符串中的下一個字符都會賦值給變量 letter。循環在進行到沒有字符剩余的時候就停止了。
下面的例子展示了如何使用級聯(字符串加法)以及一個 for 循環來生成一個簡單的序列(用字母表順序)。
在 Robert McCloskey 的一本名叫《Make Way for Ducklings》的書中,小鴨子的名字依次為:Jack, Kack, Lack, Mack, Nack, Ouack, Pack, 和 Quack。下面這個循環會依次輸出他們的名字:
```py
prefixes = 'JKLMNOPQ'
suffix = 'ack'
for letter in prefixes:
print(letter + suffix)
```
輸出結果如下:
```py
Jack Kack Lack Mack Nack Oack Pack Qack
```
當然了,有點不準確的地方,因為有“Ouack”和 “Quack”兩處拼寫錯了。做個練習,修改一下程序,改正這個錯誤。
## 8.4 字符串切片
字符串的一段叫做切片。從字符串中選擇一部分做切片,與選擇一個字符有些相似:
```py
>>> s = 'Monty Python'
>>> s[0:5]
'Monty'
>>> s[6:12]
'Python'
```
[n:m]這種操作符,會返回字符串中從第『n』個到第『m』個的字符,包含開頭的第『n』個,但不包含末尾的第『m』個。這個設計可能有點違背直覺,但可能有助于想象這個切片在字符串中的方向,如圖 8.1。
________________________________________

Figure 8.1: Slice indices.
________________________________________
如果你忽略了第一個索引(就是冒號前面的那個),切片會默認從字符串頭部開始。如果你忽略了第二個索引,切片會一直包含到最后一位:
```py
>>> fruit = 'banana'
>>> fruit[:3]
'ban'
>>> fruit[3:]
'ana'
```
如果兩個索引相等,得到的就是空字符串了,用兩個單引號來表示:
```py
>>> fruit = 'banana'
>>> fruit[3:3]
''
```
空字符串不包含字符,長度為 0,除此之外,都與其他字符串是一樣的。
那么來練習一下,你覺得 fruit[:]這個是什么意思?在程序中試試吧。
## 8.5 字符串不可修改
大家總是有可能想試試把方括號在賦值表達式的等號左側,試圖去更改字符串中的某一個字符。比如:
```py
>>> greeting = 'Hello, world!'
>>> greeting[0] = 'J'
TypeError: 'str' object does not support item assignment
```
『object』是對象的意思,這里指的是字符串類 string,然后『item』是指你試圖賦值的字符串中的字符。目前來說,一個對象就跟一個值差不多,但后續在第十章第十節我們再對這個定義進行詳細討論。
產生上述錯誤的原因是字符串是不能被修改的,這意味著你不能對一個已經存在的字符串進行任何改動。你頂多也就能建立一個新字符串,新字符串可以基于舊字符串進行一些改動。
```py
>>> greeting = 'Hello, world!'
>>> new_greeting = 'J' + greeting[1:]
>>> new_greeting
'Jello, world!'
```
上面的例子中,對 greeting 這個字符串進行了切片,然后添加了一個新的首字母過去。這并不會對原始字符串有任何影響。(譯者注:也就是 greeting 這個字符串的值依然是原來的值,是不可改變的。)
## 8.6 搜索
下面這個函數是干啥的?
```py
def find(word, letter):
index = 0
while index < len(word):
if word[index] == letter:
return index
index = index + 1
return -1
```
簡單來說,find 函數,也就是查找,是方括號操作符[]的逆運算。方括號是之道索引然后提取對應的字符,而查找函數是選定一個字符去查找這個字符出現的索引位置。如果字符沒有被報道,函數就返回-1。
這是我們見過的第一個返回語句位于循環體內的例子。如果 word[index]等于 letter,函數就跳出循環立刻返回。如果字符在字符串里面沒出現,程序正常退出循環并且返回-1。
這種計算-遍歷一個序列然后返回我們要找的東西的模式就叫做搜索了。
做個練習,修改一下 find 函數,加入第三個參數,這個參數為查找開始的字符串位置。
## 8.7 循環和計數
下面這個程序計算了字母 a 在一個字符串中出現的次數:
```py
word = 'banana'
count = 0
for letter in word:
if letter == 'a':
count = count + 1
print(count)
```
這一程序展示了另外一種計算模式,叫做計數。變量 count 被初始化為 0,然后每次在字符串中找到一個 a,就讓 count 加 1.當循環退出的時候,count 就包含了 a 出現的總次數。
做個練習,把上面的代碼封裝進一個名叫 count 的函數中,泛化一下,一遍讓他接收任何字符串和字幕作為參數。
然后再重寫一下這個函數,這次不再讓它遍歷整個字符串,而使用上一節中練習的三參數版本的 find 函數。
## 8.8 字符串方法
字符串提供了一些方法,這些方法能夠進行很多有用的操作。方法和函數有些類似,也接收參數然后返回一個值,但語法稍微不同。比如,upper 這個方法就讀取一個字符串,返回一個全部為大寫字母的新字符串。
與函數的 upper(word)語法不同,方法的語法是 word.upper()。
```py
>>> word = 'banana'
>>> new_word = word.upper()
>>> new_word
'BANANA'
```
這種用點號分隔的方法表明了使用的方法名字為 upper,使用這個方法的字符串的名字為 word。后面括號里面是空白的,表示這個方法不接收參數。
A method call is called an invocation;方法的調用被叫做——調用(譯者注:中文都混淆成調用,英文里面 invocation 和 invoke 都有祈禱的意思,和 call 有顯著的意義差別,但中文都混淆成調用,這種例子不勝枚舉,所以大家盡量多讀原版作品。);在這里,我們就說調用了 word 的 upper 方法。
結果我們發現 string 有一個方法叫做 find,跟我們寫過的函數 find 有驚人的相似:
```py
>>> word = 'banana'
>>> index = word.find('a')
>>> index
1
```
在這里我們調用了 word 的 find 方法,然后給定了我們要找的字母 a 作為一個參數。
實際上,這個 find 方法比我們的 find 函數功能更通用;它不僅能查找字符,還能查找字符串:
```py
>>> word.find('na')
2
```
默認情況下 find 方法從字符串的開頭來查找,不過可以給它一個第二個參數,讓它從指定位置查找:
```py
>>> word.find('na', 3)
4
```
這是一個可選參數的例子;find 方法還能接收第三個參數,可以指定查找終止的位置:
```py
>>> name = 'bob'
>>> name.find('b', 1, 2)
-1
```
這個搜索失敗了,因為 b 并沒有在索引 1 到 2 且不包括 2 的字符中間出現。搜索到指定的第三個變量作為索引的位置,但不包括該位置,這就讓 find 方法與切片操作符相一致。
## 8.9 運算符 in
in 這個詞在字符串操作中是一個布爾操作符,它讀取兩個字符串,如果前者的字符串為后者所包含,就返回真,否則為假:
```py
>>> 'a' in 'banana'
True
>>> 'seed' in 'banana'
False
```
舉個例子,下面的函數顯示所有同時在 word1 和 word2 當中出現的字母:
```py
def in_both(word1, word2):
for letter in word1:
if letter in word2:
print(letter)
```
選好變量名的話,Python 有時候讀起來就跟英語差不多。你讀一下這個循環,就能發現,『對第一個 word 當中的每一個字母 letter,如果這個字母也在第二個 word 當中出現,就輸出這個字母 letter。』
```py
>>> in_both('apples', 'oranges')
a e s
```
## 8.10 字符串比較
關系運算符對于字符串來說也可用。比如可以看看兩個字符串是不是相等:
```py
if word == 'banana':
print('All right, bananas.')
```
其他的關系運算符可以來把字符串按照字母表順序排列:
```py
if word < 'banana':
print('Your word, ' + word + ', comes before banana.')
elif word > 'banana':
print('Your word, ' + word + ', comes after banana.')
else:
print('All right, bananas.')
```
Python 對大小寫字母的處理與人類常規思路不同。所有大寫字母都在小寫字母之前,所以順序上應該是:Your word,然后是 Pineapple,然后才是 banana。
一個解決這個問題的普遍方法是把字符串轉換為標準格式,比如都轉成小寫的,然后再進行比較。一定要記得哈,以免你遇到一個用 Pineapple 武裝著自己的家伙的時候手足無措。
## 8.11 調試
使用索引來遍歷一個序列中的值的時候,弄清楚遍歷的開頭和結尾很不容易。下面這個函數用來對比兩個單詞,如果一個是另一個的倒序就返回真,但這個函數代碼中有兩處錯誤:
```py
def is_reverse(word1, word2):
if len(word1) != len(word2):
return False
i = 0
j = len(word2)
while j > 0:
if word1[i] != word2[j]:
return False
i = i+1
j = j-1
return True
```
第一個 if 語句是檢查兩個詞的長度是否一樣。如果不一樣長,當場就返回假。對函數其余部分,我們假設兩個單詞一樣長。這里用到了守衛模式,在第 6 章第 8 節我們提到過。
i 和 j 都是索引:i 從頭到尾遍歷單詞 word1,而 j 逆向遍歷單詞 word2.如果我們發現兩個字母不匹配,就可以立即返回假。如果經過整個循環,所有字母都匹配,就返回真。
如果我們用這個函數來處理單詞『pots』和『stop』,我們希望函數返回真,但得到的卻是索引錯誤:
```py
>>> is_reverse('pots', 'stop')
... File "reverse.py", line 15, in is_reverse if word1[i] != word2[j]: IndexError: string index out of range
```
為了改正這個錯誤,第一步就是在出錯的那行之前先輸出索引的值。
```py
while j > 0:
print(i, j) # print here
if word1[i] != word2[j]:
return False
i = i+1
j = j-1
```
然后我再次運行函數,得到更多信息了:
```py
>>> is_reverse('pots', 'stop')
0 4
... IndexError: string index out of range
```
第一次循環完畢的時候,j 的值是 4,這超出了『pots』這個字符串的范圍了(譯者注:應該是 0-3)。最后一個索引應該是 3,所以 j 的初始值應該是 len(word2)-1。
```py
>>> is_reverse('pots', 'stop')
0 3 1 2 2 1
True
```
這次我們得到了正確的結果,但似乎循環只走了三次,這有點奇怪。為了弄明白帶到怎么回事,我們可以畫一個狀態圖。在第一次迭代的過程中,is_reverse 的框架如圖 8.2 所示。
________________________________________

Figure 8.2: State diagram.
________________________________________
我通過設置變量框架中添加虛線表明,i 和 j 的值顯示在人物 word1and word2 拿許可證。
從這個圖上運行的程序,文件,更改這些值 I 和 J 在每一次迭代過程。發現并解決此函數中的二次錯誤。
## 8.12 Glossary 術語列表
object:
Something a variable can refer to. For now, you can use “object” and “value” interchangeably.
>對象:一個值能夠指代的東西。目前為止,你可以把對象和值暫且作為一碼事來理解。
sequence:
An ordered collection of values where each value is identified by an integer index.
>序列:一系列值的有序排列,每一個值都有一個唯一的整數序號。
item:
One of the values in a sequence.
>元素:一列數值序列當中的一個值。
index:
An integer value used to select an item in a sequence, such as a character in a string. In Python indices start from 0.
>索引:一個整數值,用來指代一個序列中的特定一個元素,比如在字符串里面就指代一個字符。在 Python 里面索引從 0 開始計數。
slice:
A part of a string specified by a range of indices.
>切片:字符串的一部分,通過一個索引區間來取得。
empty string:
A string with no characters and length 0, represented by two quotation marks.
>空字符串:沒有字符的字符串,長度為 0,用兩個單引號表示。
immutable:
The property of a sequence whose items cannot be changed.
>不可更改:一個序列中所有元素不能被改變的性質。
traverse:
To iterate through the items in a sequence, performing a similar operation on each.
>遍歷:在一個序列中依次對每一個元素進行某種相似運算的過程。
search:
A pattern of traversal that stops when it finds what it is looking for.
>搜索:一種遍歷的模式,找到要找的內容的時候就停止。
counter:
A variable used to count something, usually initialized to zero and then incremented.
>計數:一種用來統計某種東西數量的變量,一般初始化為 0,然后逐次遞增。
invocation:
A statement that calls a method.
>方法調用:調用方法的語句。
optional argument:
A function or method argument that is not required.
>可選參數:一個函數或者方法中有一些參數是可選的,非必需的。
## 8.13 練習
### 練習 1
閱讀 [這里](http://docs.python.org/2/library/stdtypes.html#string-methods)關于字符串的文檔。你也許會想要試試其中一些方法,來確保你理解它們的意義。比如 strip 和 replace 都特別有用。
文檔的語法有可能不太好理解。比如在 find 這個方法中,方括號表示了可選參數。所以 sub 是必須的參數,但 start 是可選的,如果你包含了 start,end 就是可選的了。
### 練習 2
字符串有個方法叫 count,與咱們在 8.7 中寫的 count 函數很相似。 閱讀一下這個方法的文檔,然后寫一個調用這個方法的代碼,統計一下 banana 這個單詞中 a 出現的次數 。
### 練習 3
字符串切片可以使用第三個索引,作為步長來使用;步長的意思就是取字符的間距。一個步長為 2 的意思就是每隔一個取一個字符;3 的意思就是每次取第三個,以此類推。
```py
>>> fruit = 'banana'
>>> fruit[0:5:2]
'bnn'
```
步長如果為-1,意思就是倒序讀取字符串,所以[::-1]這個切片就會生成一個逆序的字符串了。
使用這個方法把練習三當中的 is_palindrome 寫成一個一行代碼的版本。
### 練習 4
下面這些函數都試圖檢查一個字符串是不是包含小寫字母,但他們當中肯定有些是錯的。描述一下每個函數真正的行為(假設參數是一個字符串)。
```py
def any_lowercase1(s):
for c in s:
if c.islower():
return True
else:
return False
def any_lowercase2(s):
for c in s:
if 'c'.islower():
return 'True'
else:
return 'False'
def any_lowercase3(s):
for c in s:
flag = c.islower()
return flag
def any_lowercase4(s):
flag = False
for c in s:
flag = flag or c.islower()
return flag
def any_lowercase5(s):
for c in s:
if not c.islower():
return False
return True
```
### 練習 5
凱撒密碼是一種簡單的加密方法,用的方法是把每個字母進行特定數量的移位。對一個字母移位就是把它根據字母表的順序來增減對應,如果到末尾位數不夠就從開頭算剩余的位數,『A』移位 3 就是『D』,而『Z』移位 1 就是『A』了。
要對一個詞進行移位,要把每個字母都移動同樣的數量。比如『cheer』這個單詞移位 7 就是『jolly』,而『melon』移位-10 就是『cubed』。在電影《2001 太空漫游》中,飛船的電腦叫 HAL,就是 IBM 移位-1。
寫一個名叫 rotate_word 的函數,接收一個字符串和一個整形為參數,返回將源字符串移位該整數位得到的新字符串。
你也許會用得上內置函數 ord,它把字符轉換成數值代碼,然后還有個 chr 是用來把數值代碼轉換成字符。字母表中的字母都被編譯成跟字母表中同樣的順序了,所以如下所示:
```py
>>> ord('c') - ord('a')
2
```
c 是字母表中的第『2』個(譯者注:從 0 開始數哈)的位置,所以上述結果是 2。但注意:大寫字母的數值代碼是和小寫的不一樣的。
網上很多有冒犯意義的玩笑都是用 ROT13 加密的,也就是移位 13 的凱撒密碼。如果你不太介意,找一下這些密碼解密一下吧。[樣例代碼](http://thinkpython2.com/code/rotate.py).