8. 字符串
字符串和整数 , 浮点数以及布尔类型都不同 , 字符串是一个序列 ( sequence ) ,
即它是一个由其它值组成的有序集合 .
本章中你将见到如何访问构成字符串的各个字符 , 并学到字符串类提供的一些方法 .
8.1 字符串是一个序列
字符串是一个字符的序列 ( sequence ) . 可以使用方括号操作符来访问字符串中单独的字符 :
>> > fruit = 'bananc'
>> > letter = fruit[ 1 ]
第二个语句选择fruit中的第一个字符 ( 这里故意写成 1 的 , 看后面 ) , 并将它赋值给letter变量 .
方括号中的表达式称为下标 ( index ) . 下标表示想要需要中哪一个字符 ( 所以用index '索引' 这个名称 ) .
但你可能会发现得到的和预料不一样 :
>> > letter
'a'
对大多数人来说 , 'banana' 的第一个字母是b , 而不是a .
但对计算机科学家来说 , 下标表示的离字符串开头的偏移量 , 而第一个字符串的偏移量是 0.
>> > letter = fruit[ 0 ]
>> > letter
'b'
所有b是 'banana' 的第 0 个字母 , a是第一个 , n是第二个 .
可以使用包括变量和操作符的表达式作为下标 .
>> > i = 1
>> > fruit[ i]
'a'
>> > fruit[ i + 1 ]
'n'
但下标的值必须是整数 , 否则你会得到 :
>> > letter = fruit[ 1.5 ]
Traceback ( most recent call last) : File "<stdin>" , line 1 , in < module>
TypeError: string indices must be integers.
类型错误: 字符串索引必须是整数.
8.2 len
len是一个内置函数 , 返回字符串中字符的个数 :
>> > fruit = 'banana'
>> > len ( fruit)
6
要获得字符串的最后一个字母 , 你可能会想这么写 :
>> > length = len ( fruit)
>> > last = fruit[ length]
Traceback ( most recent call last) : File "<stdin>" , line 1 , in < module>
IndexError: string index out of range .
索引错误: 字符串索引超出范围.
IndexError出现的原因是 'banana' 中没有下标为 6 的字母 .
因为我们是从 0 开始计算的 , 6 个字母的下标是 0 到 5.
要获得最后一个字符 , 需要从length里减 1 :
>> > last = fruit[ length - 1 ]
>> > last
'a'
或者 , 你可以使用负数下标 . 负数下标从字符结尾处倒着数 .
表达式fruit [ - 1 ] 返回最后一个字母 , 表达式fruit [ - 2 ] 返回倒数第二个字母 , 以此类推 .
8.3 使用for循环进行遍历
有很多计算机都设涉及对字符串每次处理一个字符的操作 .
它们常常从开头起 , 每一次选择一个字母 , 对它做一些处理 , 再继续 , 直到结束 .
这种处理的模式 , 我们称为遍历 ( traversal ) .
编写遍历逻辑的方法之一是使用while循环 :
fruit = 'banana'
index = 0 while index < len ( fruit) : letter = fruit[ index] print ( letter) index = index + 1
这个循环遍历字符串 , 并将每个字符显示在单独的一行上 .
循环的结束条件是 index < len ( fruit ) , 所以当index等于字符串的长度是 , 条件为假 , 循环体不被运行 .
最后访问的子过程下标为 len ( fruit ) - 1 , 正好是字符串最后一个字符 .
作为练习 , 写一个函数 , 接收一个字符串作为形参 , 并倒序显示它的字母 , 每个字母单独一行 .
def reverse_order ( s) : index = - 1 while index >= - len ( s) : print ( s[ index] ) index = index + - 1 reverse_order( 'hello' )
写遍历逻辑的另一个方式是使用for循环 :
for letter in fruit: print ( letter)
每次迭代之中 , 字符串中的下一个字符会被赋值给变量letter . 循环会继续直到没有剩余的字符为止 .
下面的实例展示了如果利用字符串拼接 ( 字符串加法 ) 和一个for循环来生成字母序列
( 也就是 , 安字母顺序排序的序列 ) .
在Rombert McClockey的书 < < 为小鸭让路 > > ( make way for ducklings ) 中 ,
小鸭们的名字是Jack , Kack , Lack , Mack , Nack , Ouack , Pack , 及Quack .
下面的循环按顺序输出这些名字 :
prefixes = 'JKLMNOPQ'
suffix = 'ack'
for letter in prefixes: print ( letter + suffix)
输出是 :
Jack
Kack
Lack
Mack
Nack
Oack
Pack
Qack
当然那并不完全正确 , 因为 'Ouack' 和 'Quack' 拼写错了 , 作为练习 , 修改程序解决这个问题 .
prefixes = 'JKLMNOPQ'
suffix = 'ack'
for letter in prefixes: if letter == 'O' or letter == 'Q' : print ( letter + 'u' + suffix) else : print ( letter + suffix)
8.4 字符串切片
字符串中的一段称为一个切片 ( slice ) . 选择一个切片和选择一个字符类似 :
>> > s = 'Monty Python'
>> > s[ 0 : 5 ]
'Monty'
>> > s[ 6 : 12 ]
'Python'
操作符 [ n:m ] 返回字符串中第n个字符到m个字符的部分 , 包含第n个字符 , 但不包含第m个字符 ( 顾头不顾尾 ) .
这个行为有些违反直觉 , 但如果想象下标是指向字符之间的位置 , 可以帮助我们理解它 , 如下图 :
如果省略掉第一个下标 ( 冒号之前的那个 ) , 切片会从字符串开头开始 .
如果省略掉第二个下标 , 切片会继续到字符串的结尾 .
>> > fruit = 'banana'
>> > fruit[ : 3 ]
'ban' >> > fruit[ 3 : ]
'ana'
如果第一个下标大于或等于第二个下标 , 结果是空字符串 , 用两个引号表示 :
>> > fruit[ 3 : 3 ]
''
字符串不包含任何字符 , 长度为 0 , 但除此之外 , 它和其他字符串一样 .
继续本例 , 你认为fruit [ : ] 表示什么? 尝试一下看看结果 .
>> > fruit[ : ]
'banana'
8.5 字符串是不可变的
想要修改字符串的某个字符 , 你可能会直接在赋值左侧使用 [ ] 操作符 . 例如 :
>> > greeting = 'Hello, World!'
>> > greeting[ 0 ] = 'J'
Traceback ( most recent call last) : File "<stdin>" , line 1 , in < module>
TypeError: 'str' object does not support item assignment
类型错误: 'str' 对象不支持项分配.
这里例子里的 '对象' ( object ) 是字符串 , 而 '项' ( item ) 是指你想要赋值的那个字符 .
就现在来说 , 一个对象和值是差不多的东西 , 但我们会在后面细谈它 ( 参见 10.10 节 ) . 这个错误产生的原因是因为字符串是不可变 ( immutable ) 的 , 也就是说 , 不能改变一个已经存在的字符串 .
你能做的最多是新建一个字符串 , 它和原来的字符串稍有不同 :
>> > greeting = 'Hello, World!'
>> > new_greeting = 'J' + greeting[ 1 : ]
>> > new_greeting
'Jello, World!'
这个例子使用新的首字符和greeting的一个切片拼接起来 . 它对原来的字符串没有影响 .
8.6 搜索
下面这段函数是做什么的?
def find ( word, letter) : index = 0 while index < len ( word) : if word[ index] == letter: return indexindex = index + 1 return - 1 print ( find( 'Hello' , 'o' ) )
print ( find( 'Hello' , '1' ) )
从某种意义上说 , find是 [ ] 操作符的方面 . 和 [ ] 操作符通过一个下标查找对应的字符不同 ,
它根据一个字符查找其出现在字符串中的下标 . 如果没有找打字符 , 函数返回- 1. 这里我们第一次在循环内不看到return语句 .
如果word [ index ] = = letter , 函数直接跳出循环并立刻返回 .
如果字符没有出现在字符串中 , 程序正常退出循环并返回- 1. 这种计算的模型--遍历一个序列 , 并当找到我们寻找的目标时返回--称为搜索 .
作为练习 , 修改find函数 , 让它接收第 3 个参数 , 表示从word的那个下标开始搜索 .
def find ( word, letter, index) : while index < len ( word) : if word[ index] == letter: return indexindex = index + 1 return - 1 print ( find( 'Hello' , 'H' , 0 ) )
print ( find( 'Hello' , 'H' , 1 ) )
8.7 循环和计数
下面的代码计算字母a在字符串中出现的次数 .
word = 'banana'
count = 0 for letter in word: if letter == 'a' : count = count + 1 print ( count)
这个程序展示了另一种计算模式 , 称为计算器 ,
变量count初始化为 0 , 接着每次找到一个a时计数器加 1.
当循环结束时 , count保存着结果--a出现的总次数 .
作为练习 , 将这段代码封装成函数count , 并泛化它接收的字符串和要计数的字母作为形参 .
def count ( word, find) : num = 0 for letter in word: if letter == find: num = num + 1 print ( num) count( 'hello' , 'l' )
count( 'abcabcaa' , 'a' )
接着重写count函数 , 不直接遍历字符串 , 而是使用前面一节中的 3 形参版本的find函数 .
def count ( word, find, index) : num = 0 while index < len ( word) : if find == word[ index] : num = num + 1 index = index + 1 print ( num) count( 'hello' , 'l' , 0 )
count( 'hello' , 'l' , 3 )
8.8 字符串方法
字符串提供了许多完成各种操作的有用方法 .
方法和函数很相似--它接收形参并返回值--但语法有所不同 .
例如 , 方法upper接收一个字符串 , 并返回一个全部字母都是大写的字符串 . 和函数的语法upper ( woed ) 不同 , 它使用方法的调用语法word . upper ( ) .
>> > word = 'banbana'
>> > new_word = word. upper( )
>> > new_word
'BANBANA'
这种句点表达法指定了方法的名称 , 以及方法应用到的字符串的名称word . 空括号表示这个方法没有任何参数 .
方法的调用称为invocation '调用' , ( 普通函数的调用称为call , invocation和call都被翻译成 '调用' ) .
在这个例子里 , 我们说我们在word字符串上调用方法upper . 实际上 , 字符串本开就有一个方法find , 和我们之前写的find函数非常相似 :
>> > word = 'banana'
>> > index = word. find( 'a' )
>> > index
1
在这个例子中 , 我们在word上调用find方法 , 并传入要查找的字母作为实参 .
实际上 , find方法比我们的函数更通用 , 它可以用来查找子字符串 , 而不仅仅是字符 :
>> > word. fin( 'na' )
2
默认情况下 , find在字符串的开始启动 , 但它还可以接受第二个实参 , 表示从哪一个下标开始查找 :
>> > word. find( 'na' , 3 )
4
这是可选参数的一个示例 . find还可以接收第三个实参 , 表示查找到哪个下标就结束 :
>>> name = 'bob'
>>> name = name.find('b', 1, 2)
-1
这个搜索失败 , 因为b并没有在字符串的下标 1 到 2 之间 ( 不包括 2 ) 出现 .
find在搜索时只搜索到第二个 ( 但不包括第二个 ) 下标为止 , 这使find和切片操作符的行为一致 .
8.9 操作符in
in 是一个布尔操作符 , 操作于两个字符串上 , 如果第一个 ( 字符 ) 是第二个 ( 字符串 ) 的子串 ,
则返回True , 否则返回False :
>> > 'a' in 'banana'
True
>> > 'seed' in 'banana'
False
例如 , 下面的函数打印出word1中出现且出现在word2中的所有字母 :
def in_both ( word1, word2) : for letter in word1: if letter in word2: print ( letter)
精心选择变量名称后 , Python有时候读起来很像英语 . 可以这样读这个变量 :
' for ( each ) letter in ( the first ) word ,
if ( the ) letter ( appears ) in ( the second ) word ,
print ( the ) letter '
对于 ( 第一 ) 单词中的 ( 每个 ) 字母 , 如果 ( 第二 ) 单词中出现了 ( 字母 ) , 打印字母 . 下面是用这个函数比较单词apples和oranger的结果 :
>> > in_both( 'apples' , 'oranger' )
a
e
s
8.10 字符串比较
关系操作符也可以用在字符串上 . 检查两个字符串是否相等 :
if word == 'banana' : print ( 'All right bananas.' )
其它相关操作符在单词按照字母顺序比较时有用 :
word = 'Pineapple'
if word < 'banana' : print ( 'Your word, ' + word + ', comes before banana.' )
elif word > 'banana' : print ( 'Your word, ' + word + ', comes after banana.' )
else : print ( 'All right, banana.' )
Python处理大小写字母和人处理时不一样 . 所有的大写字母都在小写字母之前 . 所以 :
Your word, Pineapple, comes before banana.
处理这个问题的常用办法是先将字符串转换为标准的形式 , 如都转换成全小写字母形式 , 再进行比较 .
如果你遇到一个武装着Pineapple的敌人需要保护自己时 , 请记住这个办法 .
8.11 调式
当使用下标来遍历序列中的值时 , 要正确实现遍历的开端和结尾并不容易 .
下面是一个函数 , 能够比较两个单词 , 如果它们互为倒序 , 则返回True , 但这个函数包含了两个错误 :
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 print ( is_reverse( 'pots' , 'stop' ) )
第一个if语句检查两个单词是否长度相同 . 如果不同 , 我们就立刻返回False ,
否则在后面整个函数中 , 都可以认为两个人单词数相同长度的 .
这是 6.8 节中讲到的守卫模式的一个实例 . i和j是下标 : i用于正向遍历word1 , 而j用于反向遍历word2 .
如果我们找到两个不匹配的字母 , 则可以立刻返回False .
如果完成整个循环后所有的字母任然都相等 , 则返回True . 如果使用单词 'pots' 和 'stop' 来测试这个函数 , 我们会预期返回值是True , 但实际上会得到一个IndexError :
Traceback ( most recent call last) : File "C:\Users\13600\PycharmProjects\test\tese.py" , line 15 , in < module> is_reverse( 'pots' , 'stop' ) File "C:\Users\13600\PycharmProjects\test\tese.py" , line 9 , in is_reverseif word1[ i] != word2[ j] :
IndexError: string index out of range
索引错误: 字符串索引超出范围
为了调试这类错误 , 第一步可以在发生错误的那行代码之前打印出索引的值 .
def is_reverse ( word1, word2) : if len ( word1) != len ( word2) : return False i = 0 j = len ( word2) while j > 0 : print ( i, j) if word1[ i] != word2[ j] : return False i = i + 1 j = j - 1 return True print ( is_reverse( 'pots' , 'stop' ) )
这样再一次运行程序时 , 能获得更多的信息 :
0 4
. . .
IndexError: string index out of range
第一个迭代时 , j的值是 4 , 超出了 'post' 的范围 .
最后一个字符的下标是 3 , 所有j的初始值应该是len ( word2 ) - 1.
def is_reverse ( word1, word2) : if len ( word1) != len ( word2) : return False i = 0 j = len ( word2) - 1 while j > 0 : print ( i, j) if word1[ i] != word2[ j] : return False i = i + 1 j = j - 1 return True print ( is_reverse( 'pots' , 'stop' ) )
修改错误后重新运行程序 , 会得到 :
0 3
1 2
2 1
True
这会我们得到了正确的结果 , 但看起来循环只运行了 3 次 , 有些可疑 .
未了对具体发生了什么有更清晰的印象 , 可以画一个状态图 .
第一个迭代中 , is_reverse的帧显示在下图中 :
我特意安排了帧中变量的位置 , 并使用虚线来显示i和j执行word1和word2中的字符 .
从这个图开始 , 在纸上运行程序 , 每个迭代修改i和j的值 .
找到并修复这个函数的第二个错误 .
def is_reverse ( word1, word2) : if len ( word1) != len ( word2) : return False i = 0 j = len ( word2) - 1 while j >= 0 : print ( i, j) if word1[ i] != word2[ j] : return False i = i + 1 j = j - 1 return True print ( is_reverse( 'pots' , 'stop' ) )
修改错误后重新运行程序 , 会得到 :
0 3
1 2
2 1
3 0
True
8.12 术语表
对象 ( object ) : 变量可以引用的一种事物 . 就现在来说 , 可以把 '对象' 当作 '值' 来使用 . 序列 ( sequence ) : 一个有序的值的集合 , 其中每个值都使用一个下标来定位 . 项 ( item ) : 序列中的一个值 . 下标 ( index ) : 用于在序列中选择元素的整数值 . 例如 , 可以用于在字符串选取字符 . 在Python中下标从 0 开始 . 切片 ( slice ) : 字符串的一部分 , 通过一个下标范围来定位 . 空字符串 ( empty string ) : 没有字符 , 长度为 0 的字符串 , 使用一对引号来表示 . 不可变 ( immutable ) : 序列的一种属性 , 表示它的元素是不可以改变的 . 遍历 ( traverse ) : 迭代访问序列中的每一个元素 , 并对每个元素进行相似的操作 . 搜索 ( search ) : 一种遍历的模式 , 当找到它想要的元素时停止 . 计数器 ( counter ) : 一种用来计算的变量 , 通常初数化为 0 , 后来会递增 . 方法调用 ( invocation ) : 调用一个方法的语句 . 可选参数 ( optional argument ) : 函数或方法中 , 并不必须有的参数 .
8.13 练习
1. 练习1
在 https : / / docs . python . org / 3 /library/stdtypes.html#string-methods 阅读字符串方法的文档 .
你可能会想实验一下其中的一些方法 , 以确保自己理解了它们的工作方式 . strip和replace特别有用 . 文档中使用了一种可能会引起困惑的语法 .
例如 , find ( sub [ , start [ , end ] ] ) 中的方括号表示可选参数 . 方括号外的参数是必需的 .
sub是必须提供的参数 , start是可选参数 , 并且如果使用start , 则end是可选的 .
str . strip ( [ 字符 ] ) : 返回删除前导字符和尾随字符的字符串副本 .
>> > ' spacious ' . strip( )
'spacious'
>> > 'www.example.com' . strip( 'cmowz.' )
'example'
str . replace ( old , new [ , count) ] )
返回字符串的副本 , 其中所有出现的子字符串old替换为new . 如果给定可选参数计数 , 则仅替换第一个计数 .
>> > 'abcdefa' . replace( 'a' , 'i' )
'ibcdefi'
>> > 'abcdefa' . replace( 'a' , 'i' , 1 )
'ibcdefa'
2. 练习2
有一个字符串方法叫作count , 阅读这个方法的文档 ,
并写一个程序调用它来计算 'banana' 中a出现的次数 .
str . count ( sub [ , start [ , end ] ] ) : 返回子字符串sub在 [ start, end ] 范围内不重叠的出现次数 .
可选参数start和end被解释为切片表示法 . 如果sub为空 , 则返回字符之间的空字符串数 , 即字符串长度加一 .
print ( 'banana' . count( 'a' ) )
3. 练习3
字符串切片可以接受第三个下标用来指定 '步长' , 即相邻的字符之间的距离 .
步长为 2 , 意思是切片每次取下来第二个字符 , 步长 3 意思是每次取接下来第 3 个字符 .
>> > fruit = 'banana'
>> > fruit[ 0 : 5 : 2 ]
'bnn'
步长为- 1 表示切片按照相反地方向访问字符串 , 所有切片 [ : : - 1 ] 会得到一个逆序的字符串 .
使用这个特性来编写一个一行版本的is_palindrome函数 ( 见练习 6 - 3 ) .
def is_palindrome ( word) : """如果单词是回文,则返回True.""" if len ( word) <= 1 : return True if first( word) != last( word) : return False return is_palindrome( middle( word) )
def is_palindrome ( word) : return word == word[ : : - 1 ] print ( is_palindrome( 'allen' ) )
print ( is_palindrome( 'bob' ) )
print ( is_palindrome( 'otto' ) )
print ( is_palindrome( 'redivider' ) )
4. 练习4
下面的几个函数目的都是检查一个字符串是否包含小写字母 , 但至少有一个错误的 .
对每个函数 , 描述一下这个函数到底做了什么 ( 假设形参是一个字符串 ) .
提示 : islower方法 , 判断字符是否为小写 , 是小写则返回True , 否则为Flase .
def any_lowercase1 ( s) : """正确的程序.:param s: 字符串:return: 布尔值遍历字符串中的每个字符, 判断字符是否为小写的, 如果是小写的则返回True, 否则返回False.""" for c in s: if c. islower( ) : return True else : return False print ( any_lowercase1( 'asd' ) )
def any_lowercase2 ( s) : """错误的程序.:param s: 字符串:return: 字符串遍历字符串的长度次, 但判断一次字符'c' 是否为小写, 就放回字符串'True'.""" for c in s: if 'c' . islower( ) : return 'True' else : return 'False' print ( any_lowercase2( 'A' ) )
def any_lowercase3 ( s) : """错误的程序.:param s: 字符串:return: 布尔值遍历字符串, 对每个字符进行判断是否为小写, 但是无论前面的字符串是否为小写,最后一个字符是大写, 则程序的逻辑完成错误.""" for c in s: flag = c. islower( ) return flagprint ( any_lowercase3( 'aaaaA' ) )
def any_lowercase4 ( s) : """正确的程序.:param s: 字符串:return: 布尔值遍历字符串, 对每个字符进行判断是否为小写, 只要有一个小写则flag为True.""" flag = False for c in s: flag = flag or c. islower( ) return flagprint ( any_lowercase4( 'aaD' ) )
def any_lowercase5 ( s) : """错误的程序.:param s: 字符串:return: 布尔值遍历字符串, 对每个字符进行判断是否为小写, 出现一个非小写字母则返回False.""" for c in s: if not c. islower( ) : return False return True print ( any_lowercase5( 'aAd' ) )
5. 练习5
凯撒密码 ( Caesar Cypher ) 是一个比较弱的加密形式 , 它涉及将单词每个字母 '轮转' 固定数量的位置 .
轮转一个字母意思是在字母表中移动它 , 如果需要 , 再新开头开始 .
所以 'A' 轮转 3 个位置是 'D' , 而 'Z' 轮转一个位置是 'A' .
要对一个单词镜像轮转操作 , 对其中每一个字母进行轮转即可 .
例如 , 'cheer' 轮转 7 位的结果是 'jolly' , 而 'melon' 轮转- 10 位的结果是 'cubed' .
在电影 < < 2001 太空漫游 > > 中 , 舰载机器人叫作HAL , 这个单词正是IBM轮转- 1 位的结果 . 编写一个函数rotate_word , 接收一个字符串以及一个整数作为参数 , 并返回一个新字符串 ,
其中的字母按照给定的整数值 '轮转' 位置 . 你可以使用内置函数ord , 它能够将一个字符转换为数值编码 , 以及函数chr , 它将数值编码转换为字符 .
字母表中的字母是按照字母顺序编码的 , 所有 , 例如 :
>> > ord ( 'c' ) - ord ( 'a' )
2
因为 'c' 在字母表中下标是 2. 但是请注意 : 大写字母的数字编码是不同的 .
因特网上有些可能冒犯人的笑话是用ROT13编码的 . ROT13是轮转 13 为的凯撒密码 .
如果你不容易被冒犯 , 可以寻找一些并解码 .
解答 : https : / / github . com / AllenDowney / ThinkPython2 / blob / master / code / rotate . py
大写写字母A-Z的十进制为 65 - 91.
小写写字母a-z的十进制为 97 - 122.
for i in range ( 65 , 91 ) : print ( chr ( i) , end= ' ' ) print ( )
for i in range ( 97 , 123 ) : print ( chr ( i) , end= ' ' )
a从 97 开始 , 那么所有的值就先减去 97 , 回到 0 - 25. 然后加上步长 , 再对 26 求余 . 最后再加上 97.
def rotate_word ( word, rotation) : for i in word: print ( chr ( ( ord ( i) - 97 + rotation) % 25 + 97 ) , end= '' ) rotate_word( 'hello' , 24 )
if letter. isupper( ) : start = ord ( 'A' )
elif letter. islower( ) : start = ord ( 'a' )
else : return letterc = ord ( letter) - start
i = ( c + n) % 26 + start