跳转到内容

Think Python/字符串

来自维基教科书,开放的书籍,为开放的世界

字符串是一个序列

[编辑 | 编辑源代码]

字符串是序列字符。你可以使用括号运算符一次访问一个字符

>>> fruit = 'banana'
>>> letter = fruit[1]

第二个语句从fruit中选择第1个字符并将其赋值给letter

括号中的表达式称为索引。索引指示您想要序列中的哪个字符(因此得名)。

但是您可能不会得到您期望的结果

>>> print letter
a

对大多数人来说,'banana'的第一个字母是b,而不是a。但对于计算机科学家来说,索引是字符串开头的偏移量,第一个字母的偏移量为零。

>>> letter = fruit[0]
>>> print letter
b

所以b'banana'的第0个字母(“零次”),a是第1个字母(“一次”),n是第2个字母(“二次”)。

您可以使用任何表达式(包括变量和运算符)作为索引,但索引的值必须是整数。否则你会得到

>>> letter = fruit[1.5]
TypeError: string indices must be integers

len是一个内置函数,它返回字符串中的字符数

>>> fruit = 'banana'
>>> len(fruit)
6

要获得字符串的最后一个字母,您可能很想尝试以下方法

>>> length = len(fruit)
>>> last = fruit[length]
IndexError: string index out of range

IndexError的原因是'banana'中没有索引为6的字母。因为我们从零开始计数,所以六个字母编号为0到5。要获取最后一个字符,您必须从length中减去1

>>> last = fruit[length-1]
>>> print last
a

或者,您可以使用负索引,它们从字符串末尾反向计数。表达式fruit[-1]生成最后一个字母,fruit[-2]生成倒数第二个字母,依此类推。

使用 for 循环遍历

[编辑 | 编辑源代码]

许多计算涉及逐个字符处理字符串。它们通常从开头开始,依次选择每个字符,对其进行处理,并继续到结尾。这种处理模式称为遍历。使用while循环是编写遍历的一种方法

index = 0
while index < len(fruit):
    letter = fruit[index]
    print letter
    index = index + 1

此循环遍历字符串并在单独的行上显示每个字母。循环条件是index < len(fruit),所以当index等于字符串的长度时,条件为假,并且不执行循环体。访问的最后一个字符是索引为len(fruit)-1的字符,它是字符串中的最后一个字符。

练习 1    编写一个函数,该函数以字符串为参数,并将字母反向显示,每行一个字母。

使用for循环是编写遍历的另一种方法

for char in fruit:
    print char

每次循环时,字符串中的下一个字符都会被分配给变量char。循环继续,直到没有字符剩余。

以下示例展示了如何使用连接(字符串加法)和for循环生成一个按字母顺序排列的字母表系列(即按字母顺序排列)。在罗伯特·麦克洛斯基的书《为小鸭子让路》中,小鸭子的名字是杰克、凯克、莱克、麦克、纳克、奥克、帕克和夸克。此循环按顺序输出这些名称

prefixes = 'JKLMNOPQ'
suffix = 'ack'

for letter in prefixes:
    print letter + suffix

输出为

Jack
Kack
Lack
Mack
Nack
Oack
Pack
Qack

当然,这不太正确,因为“Ouack”和“Quack”拼写错误。

修改程序以修复此错误。

字符串切片

[编辑 | 编辑源代码]

字符串的一部分称为切片。选择切片类似于选择字符

>>> s = 'Monty Python'
>>> print s[0:5]
Monty
>>> print s[6:13]
Python

运算符[n:m]返回字符串从“第n个”字符到“第m个”字符的部分,包括第一个但不包括最后一个。这种行为违反直觉,但想象索引指向字符之间可能会有所帮助,如下面的图所示

<IMG SRC="book011.png">

如果您省略第一个索引(冒号之前),则切片从字符串开头开始。如果您省略第二个索引,则切片将一直到字符串结尾

>>> fruit = 'banana'
>>> fruit[:3]
'ban'
>>> fruit[3:]
'ana'

如果第一个索引大于或等于第二个索引,则结果为空字符串,用两个引号表示

>>> fruit = 'banana'
>>> fruit[3:3]

空字符串不包含任何字符,长度为0,但除此之外,它与任何其他字符串相同。

假设'fruit'是一个字符串,'fruit[:]'是什么意思?

字符串是不可变的

[编辑 | 编辑源代码]

您可能会很想在赋值的左侧使用[]运算符,目的是更改字符串中的字符。例如

>>> greeting = 'Hello, world!'
>>> greeting[0] = 'J'
TypeError: object does not support item assignment

在这种情况下,“对象”是字符串,“项目”是您尝试分配的字符。目前,“对象”与值相同,但稍后我们将完善该定义。“项目”是序列中的一个值。

发生错误的原因是字符串是不可变的,这意味着您无法更改现有字符串。您能做的最好的事就是创建一个新的字符串,它是原始字符串的变化形式

>>> greeting = 'Hello, world!'
>>> new_greeting = 'J' + greeting[1:]
>>> print new_greeting
Jello, world!

此示例将一个新的第一个字母连接到greeting的切片上。它对原始字符串没有影响。

以下函数有什么作用?

def find(word, letter):
    index = 0
    while index &lt; len(word):
        if word[index] == letter:
            return index
        index = index + 1
    return -1

在某种意义上,find[]运算符相反。它不是接受一个索引并提取相应的字符,而是接受一个字符并查找该字符出现的索引。如果字符未找到,该函数返回-1

这是我们见到的第一个在循环内使用return语句的示例。如果word[index] == letter,则函数会退出循环并立即返回。

如果字符没有出现在字符串中,则程序会正常退出循环并返回-1

这种计算模式——遍历一个序列并在找到我们正在寻找的东西时返回——被称为搜索

修改 'find' 使其具有第三个参数,即 'word' 中应开始查找的索引。

循环和计数

[编辑 | 编辑源代码]

以下程序计算字母 a 在字符串中出现的次数。

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

该程序演示了另一种称为计数器的计算模式。变量 count 初始化为 0,然后每次找到一个 a 时就增加 1。当循环退出时,count 包含结果——a 的总数。

练习 5  将此代码封装到一个名为 'count' 的函数中,并将其泛化,使其接受字符串和字母作为参数。
练习 6  重写此函数,使其不遍历字符串,而是使用上一节中的 'find' 的三参数版本。

string 方法

[编辑 | 编辑源代码]

方法类似于函数——它接收参数并返回一个值——但语法不同。例如,方法 upper 接收一个字符串并返回一个所有字母都大写的新字符串。

它不使用函数语法 upper(word),而是使用方法语法 word.upper()

>>> word = 'banana'
>>> new_word = word.upper()
>>> print new_word
BANANA

这种点符号形式指定了方法的名称 upper 和要应用方法的字符串的名称 word。空括号表示此方法不接收任何参数。

方法调用称为调用;在这种情况下,我们可以说我们正在对 word 调用 upper

事实证明,存在一个名为 find 的字符串方法,它与我们编写的函数非常相似。

>>> word = 'banana'
>>> index = word.find('a')
>>> print index
1

在此示例中,我们对 word 调用 find 并将我们正在寻找的字母作为参数传递。

实际上,find 方法比我们的函数更通用;它可以查找子字符串,而不仅仅是字符。

>>> word.find('na')
2

它可以作为第二个参数接收它应该开始查找的索引。


>>> word.find('na', 3)
4

以及作为第三个参数接收它应该停止查找的索引。

>>> name = 'bob'
>>> name.find('b', 1, 2)
-1

此搜索失败,因为 b 未出现在从 12(不包括 2)的索引范围内。

练习 7  存在一个名为 'count' 的字符串方法,它类似于上一个练习中的函数。阅读此方法的文档并编写一个调用,该调用计算 banana 中 'a' 的数量。

in 运算符

[编辑 | 编辑源代码]

单词 in 是一个布尔运算符,它接收两个字符串,如果第一个字符串作为子字符串出现在第二个字符串中,则返回 True

>>> '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 有时读起来像英语。您可以阅读此循环,“对于 (每个) 字母在 (第一个) 单词中,如果 (该) 字母 (出现在) (第二个) 单词中,则打印 (该) 字母。”

以下是在比较苹果和橘子时得到的结果。

>>> in_both('apples', 'oranges')
a
e
s

字符串比较

[编辑 | 编辑源代码]

比较运算符适用于字符串。要查看两个字符串是否相等。

if word == 'banana':
    print  'All right, bananas.'

其他比较运算对于将单词按字母顺序排列很有用。

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, comes before banana.

解决此问题的常用方法是在执行比较之前将字符串转换为标准格式,例如全部小写。如果您必须为自己辩护以对抗拿着菠萝的男子,请牢记这一点。

当您使用索引遍历序列中的值时,很难正确地确定遍历的开始和结束。以下函数应该比较两个单词,如果其中一个单词是另一个单词的反转,则返回 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

第一个 if 语句检查单词的长度是否相同。如果不是,我们可以立即返回 False,然后,对于函数的其余部分,我们可以假设单词的长度相同。这是第 6.8 节中的守护模式的一个示例。

ij 是索引:i 正向遍历 word1,而 j 反向遍历 word2。如果我们找到两个不匹配的字母,我们可以立即返回 False。如果我们遍历整个循环并且所有字母都匹配,则返回 True

如果我们用单词“pots”和“stop”测试此函数,我们期望返回值 True,但我们得到一个 IndexError。


>>> is_reverse('pots', 'stop')
...
  File "reverse.py", line 15, in is_reverse
    if word1[i] != word2[j]:
IndexError: string index out of range

为了调试这种错误,我的第一步是在错误出现的行之前立即打印索引的值。

    while j > 0:
        print i, j        # print here
        
        if word1[i] != word2[j]:
            return False
        i = i+1
        j = j-1

现在,当我再次运行程序时,我获得了更多信息。

>>> is_reverse('pots', 'stop')
0 4
...
IndexError: string index out of range

第一次遍历循环时,j 的值为 4,这对于字符串 'pots' 来说超出了范围。最后一个字符的索引是 3,因此 j 的初始值应该是 len(word2)-1



如果我修复了该错误并再次运行程序,我得到。

>>> is_reverse('pots', 'stop')
0 3
1 2
2 1
True

这次我们得到了正确的答案,但看起来循环只运行了三次,这很可疑。为了更好地了解正在发生的事情,绘制状态图很有用。在第一次迭代期间,is_reverse 的帧如下所示。


<IMG SRC="book012.png">

我通过排列帧中的变量并添加虚线来稍微扩展了一点,以显示 ij 的值指示 word1word2 中的字符。

练习 8  从该图开始,在纸上执行程序,在每次迭代期间更改 'i' 和 'j' 的值。找到并修复此函数中的第二个错误。

词汇表

[编辑 | 编辑源代码]
对象
变量可以引用的东西。目前,您可以互换使用“对象”和“值”。
序列
一个有序集合;也就是说,一组值,其中每个值都由一个整型索引标识。
项目
序列中的一个值。
索引
用于选择序列中项目的整型值,例如字符串中的字符。
切片
由一组索引指定的部分字符串。
空字符串
没有字符且长度为 0 的字符串,用两个引号表示。
不可变
无法分配其项目的序列的属性。
遍历
迭代序列中的项目,对每个项目执行类似的操作。
搜索
一种遍历模式,它在找到正在寻找的东西时停止。
计数器
用于统计某物的变量,通常初始化为零,然后递增。
方法
与对象相关联的函数,使用点符号调用。
调用
调用方法的语句。

字符串切片可以接受第三个索引,该索引指定“步长”;也就是说,连续字符之间的空格数。

步长为 2 表示隔一个字符;3 表示隔两个字符,依此类推。

 >>> fruit = 'banana'
>>> fruit[0:5:2]
'bnn'

步长为 -1 表示向后遍历单词,因此切片 [::-1] 生成反转的字符串。

使用此习惯用法编写 is_palindrome 的单行版本,来自练习 '6.6'

练习 10

[编辑 | 编辑源代码]

阅读 'docs.python.org/lib/string-methods.html' 中字符串方法的文档。您可能需要尝试其中的一些方法,以确保您了解其工作原理。'strip' 和 'replace' 特别有用。

文档使用了一种可能令人困惑的语法。例如,在 find(sub[, start[, end]]) 中,方括号表示可选参数。所以 'sub' 是必需的,但 'start' 是可选的,如果您包含 'start',那么 'end' 是可选的。

练习 11

[编辑 | 编辑源代码]

以下函数都旨在检查字符串是否包含任何小写字母,但至少其中一些是错误的。对于每个函数,描述函数的实际功能。

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

练习 12

[编辑 | 编辑源代码]

ROT13 是一种弱加密形式,它涉及将单词中的每个字母“旋转”13 个位置[1]。旋转字母意味着在字母表中移动它,必要时环绕到开头,因此 'A' 向右移 3 个位置变为 'D','Z' 向右移 1 个位置变为 'A'。

编写一个名为rotate_word的函数,该函数以字符串和整数为参数,并返回一个新字符串,该字符串包含原始字符串中“旋转”指定数量的字母。

例如,“cheer” 旋转 7 个位置变为 “jolly”,而 “melon” 旋转 -10 个位置变为 “cubed”。

你可能想要使用内置函数 'ord'(将字符转换为数字代码)和 'chr'(将数字代码转换为字符)。

互联网上可能冒犯性的笑话有时会用 ROT13 编码。如果你不容易被冒犯,请找到并解码一些这样的笑话。

  1. 参见 wikipedia.org/wiki/ROT13
华夏公益教科书