# 第八章 字符串
字符串和整形、浮点数以及布尔值很不一样。一个字符串是一个序列,意味着是对其他值的有序排列。在本章你将学到如何读取字符串中的字符,你还会学到一些字符串相关的方法。
## 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](http://7xnq2o.com1.z0.glb.clouddn.com/ThinkPythonFigure8.1.png)
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](http://7xnq2o.com1.z0.glb.clouddn.com/ThinkPythonFigure8.2.png)
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).