12 周课程 / 第 3 周 9 分钟阅读

第 3 周:字符串、列表与元组

字符串、列表和元组都属于“序列”:它们有顺序、可以按位置取元素,也可以用循环逐个访问。区别在于保存什么、能否修改,以及常用方法。

建议用时:8-10 小时。

本周目标#

学完本周,你应该能够:

  • 正确使用正索引和负索引。
  • 按“起点包含、终点不包含”的规则计算切片。
  • 使用字符串常用方法完成清理、查找、拆分和拼接。
  • 创建、修改、增加、删除和排序列表。
  • 区分原地修改方法与返回新对象的方法。
  • 理解列表赋值别名和真正复制之间的差异。
  • 创建、访问和解包元组。
  • 独立完成文本统计、购物清单和成绩排名。

3.1 序列的四个共同动作#

字符串、列表和元组都支持:

  1. 获取长度。
  2. 按下标访问元素。
  3. 切片。
  4. 使用 for 遍历。
text = "Python"
numbers = [10, 20, 30]
point = (4, 5)

print(len(text))    # 6
print(text[0])      # P
print(numbers[1])   # 20
print(point[-1])    # 5

3.2 索引与越界#

索引表示元素距离开头或末尾的位置。

 s =  P   y   t   h   o   n
正索引  0   1   2   3   4   5
负索引 -6  -5  -4  -3  -2  -1
s = "Python"
print(s[0])    # P
print(s[2])    # t
print(s[-1])   # n
print(s[-3])   # h

如果索引超出有效范围,会报 IndexError:

# print(s[6])
# print(s[-7])

列表和元组规则相同:

items = ["语文", "数学", "英语"]
print(items[1])   # 数学
print(items[-1])  # 英语

3.3 切片#

基本写法:

序列[start:stop:step]
  • 从 start 开始。
  • 到 stop 前停止,不包含 stop。
  • step 是每次前进的步长。
  • 省略 start 时默认从一端开始。
  • 省略 stop 时默认到另一端结束。
  • 省略 step 时默认是 1。
s = "Python"

print(s[1:4])    # yth
print(s[:3])     # Pyt
print(s[3:])     # hon
print(s[-3:])    # hon
print(s[:])      # Python
print(s[::2])    # Pto
print(s[::-1])   # nohtyP

s[1:4] 包含索引 1、2、3,不包含 4。可以记成“左闭右开”。

切片不会因为终点越界而报错#

print(s[2:100])  # thon
print(s[-100:2]) # Py

如果起点和终点让序列没有可取的区间,结果是空字符串 ""、空列表 [] 或空元组 ()。

步长为负数#

print(s[4:1:-1])  # oht
print(s[5:0:-2])  # nh

负步长表示从右向左。手推时先确定起点,再每次加上步长,直到越过终点前停止。

3.4 字符串#

字符串可以使用单引号、双引号或三引号:

name1 = '小林'
name2 = "李雨"
message = """第一行
第二行"""

单双引号作用相同。字符串内部出现同一种引号时,可以用另一种引号,或使用反斜杠转义:

print("他说:\"你好\"")
print('I\'m here')

常见转义:

写法 含义
\n 换行
\t 制表符
\\ 一个反斜杠
\" 双引号

字符串不可修改#

s = "Python"
# s[0] = "J"  # TypeError

需要修改时,创建一个新字符串:

s = "Python"
new_s = "J" + s[1:]
print(new_s)  # Jython

拼接、重复和成员判断#

print("Py" + "thon")    # Python
print("ab" * 3)         # ababab
print("th" in "Python") # True
print("py" in "Python") # False,区分大小写

遍历字符串#

for char in "Python":
    print(char)

如果需要同时得到索引和字符:

for index, char in enumerate("Python"):
    print(index, char)

enumerate() 每轮产生“索引、元素”两个值。

3.5 常用字符串方法#

字符串方法不会修改原字符串,而是返回一个新字符串或查询结果。

方法 作用 示例
len(s) 长度 len("abc") 是 3
upper() 全部转大写 "Ab".upper() 是 "AB"
lower() 全部转小写 "Ab".lower() 是 "ab"
strip() 去掉两端空白 " a ".strip() 是 "a"
lstrip() 去掉左边空白 " a ".lstrip() 是 "a "
rstrip() 去掉右边空白 " a ".rstrip() 是 " a"
find(sub) 查找位置,找不到返回 -1 "abc".find("b") 是 1
count(sub) 子串出现次数 "aba".count("a") 是 2
replace(old, new) 替换并返回新字符串 "a-b".replace("-", "/")
startswith(prefix) 是否以某内容开头 "PY-01".startswith("PY-")
endswith(suffix) 是否以某内容结尾 "a.py".endswith(".py")
split(sep) 按分隔符切成列表 "a,b".split(",")
join(items) 用当前字符串连接多个元素 "-".join(["a", "b"])

例子:

text = "  Python is useful  "

clean_text = text.strip()
print(clean_text)                  # Python is useful
print(clean_text.lower())          # python is useful
print(clean_text.replace(" ", "-"))  # Python-is-useful
print(clean_text.find("is"))       # 7
print(clean_text.find("Java"))     # -1
print(clean_text.count("u"))       # 2

split() 和 join()#

sentence = "语文,数学,英语"
subjects = sentence.split(",")
print(subjects)  # ['语文', '数学', '英语']

joined = " / ".join(subjects)
print(joined)    # 语文 / 数学 / 英语

不写参数时,split() 会按连续空白拆分,并自动去掉多余空白:

print("  1   2   3  ".split())  # ['1', '2', '3']

join() 中的每个元素都必须是字符串,整数列表不能直接连接。

3.6 列表#

列表用方括号表示,元素可以修改:

scores = [88, 92, 75]
scores[0] = 90
print(scores)  # [90, 92, 75]

列表可以保存不同类型的数据:

student = ["S001", "张晨", 88, True]

不过同一个列表中混放太多不同含义的值,不利于阅读。结构化数据更适合字典或对象。

增加元素#

items = ["语文"]

items.append("数学")       # 在末尾增加一个元素
items.insert(1, "英语")    # 在索引 1 前插入
items.extend(["物理", "化学"])  # 把另一个可迭代对象逐个加入

print(items)
# ['语文', '英语', '数学', '物理', '化学']

差异:

  • append(["物理", "化学"]) 会把整个列表作为一个嵌套元素加入。
  • extend(["物理", "化学"]) 会分别加入“物理”和“化学”。

删除元素#

numbers = [10, 20, 30, 20]

numbers.remove(20)  # 删除第一个值为 20 的元素
last = numbers.pop()  # 删除并返回末尾元素
first = numbers.pop(0)  # 删除并返回索引 0 的元素
del numbers[0]
numbers.clear()

remove(99) 在值不存在时会报 ValueError。pop(100) 在索引越界时会报 IndexError。

查找、计数和成员判断#

students = ["张晨", "李雨", "王浩"]

print("李雨" in students)       # True
print(students.index("王浩"))   # 2
print(students.count("张晨"))   # 1

列表的 index() 找不到时抛出异常;字符串的 find() 找不到时返回 -1。两者不要混淆。

修改多个位置:切片赋值#

numbers = [1, 2, 3, 4]
numbers[1:3] = [20, 30, 40]
print(numbers)  # [1, 20, 30, 40, 4]

切片赋值会替换指定区间。基础题中先掌握单元素赋值即可。

3.7 排序:sort() 与 sorted()#

原地排序 sort()#

scores = [88, 76, 92]
result = scores.sort()

print(scores)  # [76, 88, 92]
print(result)  # None

sort() 修改原列表,并返回 None。不要写 scores = scores.sort(),否则会把 scores 变成 None。

降序:

scores = [88, 76, 92]
scores.sort(reverse=True)
print(scores)  # [92, 88, 76]

返回新列表 sorted()#

original = [3, 1, 2]
ascending = sorted(original)
descending = sorted(original, reverse=True)

print(original)    # [3, 1, 2]
print(ascending)   # [1, 2, 3]
print(descending)  # [3, 2, 1]

第一阶段常见推演:

a = [3, 1, 2]
a.sort(reverse=True)
b = sorted(a)
a.append(b[0])

print(a)  # [3, 2, 1, 1]
print(b)  # [1, 2, 3]

先让 a 变成 [3, 2, 1],b 是新列表 [1, 2, 3],最后把 1 加回 a。

反转#

numbers = [1, 2, 3]
numbers.reverse()
print(numbers)  # [3, 2, 1]

copy_reversed = list(reversed(numbers))
print(copy_reversed)  # [1, 2, 3]

reverse() 原地修改;reversed() 返回一个反向迭代器,常配合 list() 转成新列表。

3.8 列表赋值、别名与复制#

下面的 b = a 不会创建新列表:

a = [1, 2]
b = a
b.append(3)

print(a)  # [1, 2, 3]
print(b)  # [1, 2, 3]
print(a is b)  # True

a 和 b 指向同一个列表,所以通过任何一个名称修改,另一边都能看见。

创建独立浅复制:

a = [1, 2]
b = a.copy()
b.append(3)

print(a)  # [1, 2]
print(b)  # [1, 2, 3]
print(a is b)  # False

切片 a[:] 和 list(a) 也能得到浅复制。若列表里还有嵌套列表,浅复制不会复制内层列表;本周先把它理解成“复制第一层”。

3.9 嵌套列表#

matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
]

print(matrix[0][1])  # 2
print(matrix[2][-1]) # 9

for row in matrix:
    for value in row:
        print(value, end=" ")
    print()

结果:

1 2 3
4 5 6
7 8 9

要先把 matrix[0] 取到第一行,再从这个列表中取索引 1。

3.10 元组#

元组用圆括号表示,创建后不能修改:

point = (4, 5)
print(point[0])  # 4

# point[0] = 10  # TypeError

只有一个元素时,必须写逗号:

not_tuple = (5)
real_tuple = (5,)

print(type(not_tuple).__name__)  # int
print(type(real_tuple).__name__) # tuple

解包#

student = ("S001", "张晨", 88)
student_id, name, score = student

print(student_id)  # S001
print(name)        # 张晨
print(score)       # 88

解包时左右数量要一致,否则会报 ValueError。

元组常用于表示“一起返回的一组结果”:

def findMinMax(values):
    minimum = min(values)
    maximum = max(values)
    return minimum, maximum

result = findMinMax([5, 2, 8])
print(result)             # (2, 8)
print(type(result).__name__)  # tuple

不带括号的 return minimum, maximum 也会构成元组。

3.11 本周作品一:文本统计器#

需求#

输入一句英文,输出:

  • 原句。
  • 去掉两端空白后的句子。
  • 总字符数(包含空格)。
  • 单词个数。
  • 大写形式。
  • 单词列表。
sentence = input("请输入一句英文:")
clean_sentence = sentence.strip()
words = clean_sentence.split()

print("原句:", sentence)
print("清理后:", clean_sentence)
print("字符数:", len(sentence))
print("单词数:", len(words))
print("大写:", clean_sentence.upper())
print("单词:", words)

输入:

  Python is fun and useful  

输出中的关键结果:

字符数:28
单词数:5
单词:['Python', 'is', 'fun', 'and', 'useful']

注意:输入内容两端的空格也属于原字符串,所以“字符数”是否包含它们取决于统计规则。题目说“总字符数”时,应明确统计原串还是清理后的字符串。

3.12 本周作品二:购物清单#

items = []

while True:
    command = input("add 名称 / remove 名称 / show / quit:")
    parts = command.split(maxsplit=1)
    action = parts[0]

    if action == "quit":
        break
    if action == "show":
        print("清单:", items)
    elif action == "add" and len(parts) == 2:
        items.append(parts[1])
        print("已添加:", parts[1])
    elif action == "remove" and len(parts) == 2:
        name = parts[1]
        if name in items:
            items.remove(name)
            print("已删除:", name)
        else:
            print("清单中没有", name)
    else:
        print("命令格式不正确")

print("最终清单:", items)

split(maxsplit=1) 最多拆成两部分,所以 add 牛奶 2 盒 中的名称能保留空格。

3.13 本周作品三:成绩排名#

scores = [88, 59, 92, 70, 100]

ascending = sorted(scores)
descending = sorted(scores, reverse=True)

print("原顺序:", scores)
print("升序:", ascending)
print("降序:", descending)
print("最高分:", descending[0])
print("最低分:", ascending[0])
print("前三名:", descending[:3])

如果需要保留姓名和分数的对应关系,两个平行列表容易错位。第 4 周会用字典或元组列表解决这个问题。

3.14 独立练习#

A. 快速判断题#

  1. "Python"[1:4] 是什么?
  2. "Python"[-2:] 是什么?
  3. "Python"[::-1] 是什么?
  4. " a b ".strip() 的结果是什么?
  5. "a,b,c".split(",") 的结果是什么?
  6. "-".join(["A", "B", "C"]) 的结果是什么?
  7. a = [1, 2]; b = a; b.append(3) 后,a 是什么?
  8. a = [1, 2]; b = a.copy(); b.append(3) 后,a 和 b 分别是什么?
  9. a.sort() 的返回值是什么?
  10. s[0] = "J" 能否修改字符串?

B. 编程题#

  1. 输入一个单词,输出它的第一个字符、最后一个字符和反转结果。
  2. 输入一个句子,统计其中字母 a 出现多少次,不区分大小写。
  3. 输入用逗号分隔的姓名,输出去掉两端空格后的姓名列表。
  4. 创建空列表,让用户连续输入 5 个整数,然后按从小到大输出。
  5. 对列表 [3, 1, 2] 先原地降序排序,再创建升序新列表,最后把新列表首元素追加到原列表,输出两个列表。
  6. 输入一个英文句子,找出最长的一个单词。若长度相同,取最先出现的单词。
  7. 编写函数 reverse_text(text),返回反转后的字符串,不修改原字符串。
  8. 编写函数 normalize_name(name),去掉姓名两端空格,并把英文姓名转换为每个单词首字母大写、其余小写。

3.15 练习答案与解释#

A. 快速判断题答案#

  1. "yth"。
  2. "on"。
  3. "nohtyP"。
  4. "a b"。只去掉两端空白,中间空格保留。
  5. ["a", "b", "c"]。
  6. "A-B-C"。
  7. [1, 2, 3]。a 和 b 指向同一个列表。
  8. a 是 [1, 2],b 是 [1, 2, 3]。
  9. None。排序效果体现在原列表中。
  10. 不能,字符串不可修改,会报 TypeError。

B. 编程题参考答案#

  1. 单词首尾和反转:

    word = input()
    print(word[0])
    print(word[-1])
    print(word[::-1])
    
  2. 统计字母 a,不区分大小写:

    sentence = input()
    count = sentence.lower().count("a")
    print(count)
    
  3. 清理姓名列表:

    text = input()
    names = text.split(",")
    clean_names = []
    
    for name in names:
        clean_names.append(name.strip())
    
    print(clean_names)
    

    也可以使用列表推导式 [name.strip() for name in names],但本周先掌握普通循环。

  4. 输入并排序 5 个整数:

    numbers = []
    for _ in range(5):
        numbers.append(int(input()))
    
    numbers.sort()
    print(numbers)
    
  5. 原地排序和新列表:

    a = [3, 1, 2]
    a.sort(reverse=True)
    b = sorted(a)
    a.append(b[0])
    
    print(a)  # [3, 2, 1, 1]
    print(b)  # [1, 2, 3]
    
  6. 最长单词:

    sentence = input()
    words = sentence.split()
    longest = words[0]
    
    for word in words:
        if len(word) > len(longest):
            longest = word
    
    print(longest)
    

    这里使用 > 而不是 >=,所以长度相同时保留先出现的单词。

  7. 反转函数:

    def reverse_text(text):
        return text[::-1]
    
    original = "Python"
    print(reverse_text(original))  # nohtyP
    print(original)                # Python
    
  8. 姓名标准化:

    def normalize_name(name):
        return name.strip().title()
    
    print(normalize_name("  zhang chen  "))
    # Zhang Chen
    

    title() 会把每个单词的首字母转为大写。中文不受影响。

3.16 常见错误清单#

  • 把终点索引算进切片。
  • 修改字符串中的单个字符。
  • 以为 split() 会自动把数字转成 int。
  • 以为 join() 可以直接连接整数列表。
  • 把 append() 和 extend() 混用。
  • remove() 删除所有相同值,实际只删第一个。
  • 把 sort() 的返回值赋给原变量,得到 None。
  • 用 b = a 误以为复制了列表。
  • 单元素元组漏写逗号。
  • 混用字符串 find() 的 -1 和列表 index() 的异常。

3.17 过关检查#

  • 能画出字符串正负索引表。
  • 能不运行代码写出 6 种切片结果。
  • 能说清字符串、列表、元组的可变性差异。
  • 能正确使用 split()、join()、strip()、replace()。
  • 能演示 append、insert、extend、remove、pop 的效果。
  • 能吃透 sort()、sorted()、reverse()、reversed() 的返回值差异。
  • 能通过例子解释列表别名和复制。
  • 能写出返回元组的函数并完成解包。
  • 独立完成文本统计器和成绩排名。
  • 第一阶段的列表与字符串推演题正确率达到 80%。

本周进度

完成讲义、练习和验收后再标记。