8. 第八章 字符串

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个字母的下标是05.
要获得最后一个字符, 需要从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个字符(顾头不顾尾). 
这个行为有些违反直觉, 但如果想象下标是指向字符之间的位置, 可以帮助我们理解它, 如下图:

2023-03-17_00001

如果省略掉第一个下标(冒号之前的那个), 切片会从字符串开头开始.
如果省略掉第二个下标, 切片会继续到字符串的结尾.
>>> 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 搜索
下面这段函数是做什么的?
# 输入一个字符串和一个字符, 返回这个字符在字符串中的下标, 找不到就返回-1.
def find(word, letter):
    index = 0
    while index < len(word):
        if word[index] == letter:
            return index
        index = index + 1
    return -1


print(find('Hello', 'o'))  # 4
print(find('Hello', '1'))  # -1

从某种意义上说, find是[]操作符的方面. []操作符通过一个下标查找对应的字符不同,
它根据一个字符查找其出现在字符串中的下标. 如果没有找打字符, 函数返回-1.

这里我们第一次在循环内不看到return语句. 
如果word[index] == letter, 函数直接跳出循环并立刻返回.
如果字符没有出现在字符串中, 程序正常退出循环并返回-1.

这种计算的模型--遍历一个序列, 并当找到我们寻找的目标时返回--称为搜索.
作为练习, 修改find函数, 让它接收第3个参数, 表示从word的那个下标开始搜索.
# 输入一个字符串和一个字符, 返回这个字符在字符串中的下标, 找不到就返回-1.
def find(word, letter, index):
    
    while index < len(word):
        if word[index] == letter:
            return index
        index = index + 1
    return -1


print(find('Hello', 'H', 0))  # 0
print(find('Hello', 'H', 1))  # -1

8.7 循环和计数
下面的代码计算字母a在字符串中出现的次数.
word = 'banana'
count = 0

for letter in word:
    if letter == 'a':
        count = count + 1

print(count)  # 3

这个程序展示了另一种计算模式, 称为计算器,
变量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')  # 2
count('abcabcaa', 'a')  # 4

接着重写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)  # 2
count('hello', 'l', 3)  # 1

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并没有在字符串的下标12之间(不包括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_reverse
    if 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)  # 0 4
        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)  # 0 4
        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的帧显示在下图中:

2023-03-18_00001

我特意安排了帧中变量的位置, 并使用虚线来显示i和j执行word1和word2中的字符.
从这个图开始, 在纸上运行程序, 每个迭代修改i和j的值.
找到并修复这个函数的第二个错误.
def is_reverse(word1, word2):
    if len(word1) != len(word2):
        return False

    i = 0
    j = len(word2) - 1
	
	# 第二个错误在这里j > 0, 应该是j >= 0.  j的值应该从3到0, 且包含0.
    while j >= 0:
        # 在这里打印
        print(i, j)  # 0 4
        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'
# 删除开头和结尾删除出现的'c', 'm', 'o', 'w', 'z', '.' 没有顺序之分.
>>> 'www.example.com'.strip('cmowz.')
'example'

str.replace(old, new[, count)])
返回字符串的副本, 其中所有出现的子字符串old替换为new. 如果给定可选参数计数, 则仅替换第一个计数.
# 将字符串中所有的a换成i
>>> 'abcdefa'.replace('a', 'i')
'ibcdefi'

# 将字符串中的a换成i, 从左往右开始, 替换一次.
>>> '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. 练习3
字符串切片可以接受第三个下标用来指定'步长', 即相邻的字符之间的距离.
步长为2, 意思是切片每次取下来第二个字符, 步长3意思是每次取接下来第3个字符.
>>> fruit = 'banana'
>>> fruit[0:5:2]
'bnn'

步长为-1表示切片按照相反地方向访问字符串, 所有切片[::-1]会得到一个逆序的字符串.
使用这个特性来编写一个一行版本的is_palindrome函数(见练习6-3).
def is_palindrome(word):
    """如果单词是回文,则返回True."""
    # word的长度为0或1, 返回True. 0个单词和1个单词就是回文(正看, 反看都一样).
    if len(word) <= 1:
        return True
    
    # 第一个字符串不等于最后一个字符串, 返回False.
    if first(word) != last(word):
        return False
    
    # 递归调用, 排查第一个字符串和最后一个字符串, 
    # 如allen,则去掉首尾得到'lle', 将'lle'最为参数继续递归.
    return is_palindrome(middle(word))

# 改编后的程序.
def is_palindrome(word):
    return word == word[::-1]


print(is_palindrome('allen'))  # False
print(is_palindrome('bob'))  # True
print(is_palindrome('otto'))  # True
print(is_palindrome('redivider'))  # True

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'))  # True

def any_lowercase2(s):
    """
	错误的程序.
    :param s: 字符串
    :return: 字符串
    遍历字符串的长度次, 但判断一次字符'c' 是否为小写, 就放回字符串'True'.
    """
    for c in s:
        # 这个程序存在错误, 应该将'c', 'True', 'False'的引号去掉.
        if 'c'.islower():
            return 'True'
        else:
            return 'False'


print(any_lowercase2('A'))  # True

def any_lowercase3(s):
    """
	错误的程序.
    :param s: 字符串
    :return: 布尔值

    遍历字符串, 对每个字符进行判断是否为小写, 但是无论前面的字符串是否为小写,
    最后一个字符是大写, 则程序的逻辑完成错误.
    """
    for c in s:
        flag = c.islower()
    return flag


print(any_lowercase3('aaaaA'))  # False

def any_lowercase4(s):
    """
	正确的程序.
    :param s: 字符串
    :return: 布尔值
    遍历字符串, 对每个字符进行判断是否为小写, 只要有一个小写则flag为True.
    """
    flag = False
    for c in s:
        flag = flag or c.islower()
    return flag


print(any_lowercase4('aaD'))  # True

def any_lowercase5(s):
    """
	错误的程序.
    :param s: 字符串
    :return: 布尔值
    遍历字符串, 对每个字符进行判断是否为小写, 出现一个非小写字母则返回False.
    """
    for c in s:
        if not c.islower():
            return False
    return True


print(any_lowercase5('aAd'))  # False

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=' ')
    # A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

print()
for i in range(97, 123):
    print(chr(i), end=' ')
    # a b c d e f g h i j k l m n o p q r s t u v w x y z 

a从97开始, 那么所有的值就先减去97, 回到0-25. 然后加上步长, 再对26求余. 最后再加上97.
def rotate_word(word, rotation):
    # 遍历字母.
    for i in word:
        # 字母10进制加上步长, 转为字母, end参数设置print以''空字符结尾.
        print(chr((ord(i) - 97 + rotation) % 25 + 97), end='')


rotate_word('hello', 24)

# 区分大小写版本
if letter.isupper():
    start = ord('A')  # 65
elif letter.islower():
    start = ord('a')  # 97
else:
    return letter

c = ord(letter) - start
i = (c + n) % 26 + start


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值