01-字符串基础
本章代码基于 Python 3.11+ 编写
字符串是程序处理文本数据的基础,掌握字符串操作让程序能够处理各种文本信息。
本章讲解字符串的创建、索引、切片和常用方法。
概念铺垫
为什么需要字符串?一个真实的文本处理场景
问题场景: 你在开发一个日志分析程序,需要从日志中提取时间、错误类型等信息。
不使用字符串处理的困惑:
- 如何在程序中表示和操作文本?
- 如何从长文本中提取特定部分?
- 如何格式化输出信息?
使用字符串的解决方案:
# 日志分析示例
log: str = "[2024-01-15 10:30:45] ERROR: Connection failed"
# 提取时间(字符串切片)
time: str = log[1:20]
print(f"时间:{time}")
# 判断错误类型(字符串查找)
if "ERROR" in log:
print("发现错误日志")
# 格式化输出(f-string)
error_type: str = "Connection"
print(f"错误类型:{error_type}")这就是字符串的价值:让程序能够处理和分析文本数据。
字符串解决了什么问题?
字符串的本质是:用程序处理文本数据。
就像你用 Word 编辑文档,Python 用字符串处理文本。
字符串的常见用途:
- 存储文本:用户输入、文件内容、日志信息
- 提取信息:从长文本中找到特定内容
- 格式化输出:生成清晰易读的输出
- 文本分析:统计、搜索、替换
字符串的最简用法
字符串的核心操作:创建、访问、格式化。
# 创建字符串
name: str = "Python"
# 访问字符
print(name[0]) # 输出:P
# 格式化输出
version: float = 3.11
print(f"{name} {version}") # 输出:Python 3.11这就是字符串的基本用法。接下来我们详细学习字符串的操作。
L1 理解层:会用
创建字符串
# 单引号或双引号
text1 = 'Hello, World!'
text2 = "Hello, World!"
# 包含引号时的选择
quote1 = 'He said, "Hello!"' # 内部用双引号
quote2 = "It's a beautiful day" # 内部用单引号
# 多行字符串
multiline = """这是第一行
这是第二行
这是第三行"""
# raw 字符串(不转义)
path = r"C:\Users\Documents\file.txt"
# f-string(格式化字符串)
name = "Alice"
age = 25
greeting = f"Hello, {name}! You are {age} years old."字符串不可变性(核心)
字符串不可变: 创建后内容无法修改,只能创建新字符串。
不可变规则:
┌─────────────────────────────────────────────────────────────┐
│ 字符串不可变性 │
│ │
│ ❌ 不能修改单个字符 │
│ text[0] = "h" → TypeError │
│ │
│ ✅ 必须创建新字符串 │
│ text = "h" + text[1:] │
│ │
│ ⚠️ 为什么不可变? │
│ • 内存安全:多处引用不会互相影响 │
│ • 性能优化:Python 可以复用相同字符串 │
│ • 字典安全:可作为字典键(键必须不可变) │
└─────────────────────────────────────────────────────────────┘最简示例:
text = "Hello"
# ❌ 不能修改单个字符
# text[0] = "h" # TypeError
# ✅ 创建新字符串
new_text = "h" + text[1:]
print(new_text) # hello关键代码解释:
| 操作 | 结果 | 原因 |
|---|---|---|
text[0] = "h" | TypeError | 字符串不可修改 |
"h" + text[1:] | 创建新字符串 | 拼接产生新对象 |
索引访问
每个字符都有两个索引:正向索引(从 0 开始)和反向索引(从 -1 开始)。
┌─────────────────────────────────────────────────────────────┐
│ 字符串索引示意图 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 字符串: P y t h o n P r o g r a m m i n g │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ 正向索引: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 │
│ 反向索引:-18-17-16-15-14-13-12-11-10-9 -8 -7 -6 -5 -4 -3 -2 -1 │
│ │
│ • 正向索引从左往右,从 0 开始 │
│ • 反向索引从右往左,从 -1 开始(最后一个字符) │
│ │
└─────────────────────────────────────────────────────────────┘text = "Python Programming"
# 正向索引(从 0 开始)
print(text[0]) # P(第一个字符)
print(text[6]) # P(第7个字符,是空格后面的 P)
# 反向索引(从 -1 开始)
print(text[-1]) # g(最后一个字符)
print(text[-2]) # n(倒数第二个)
print(text[-18]) # P(等同于 text[0])索引越界会报错:
text = "Python"
# text[100] # IndexError: string index out of range
# text[-100] # IndexError: string index out of range字符串切片
切片用于提取字符串的一部分,语法:[start:end:step]
┌─────────────────────────────────────────────────────────────┐
│ 切片语法详解 │
├─────────────────────────────────────────────────────────────┤
│ │
│ text[start:end:step] │
│ │ │ │ │
│ │ │ │ │
│ 起点 终点 步长 │
│ (含) (不含) │
│ │
│ 关键规则: │
│ • start 包含在结果中 │
│ • end 不包含在结果中(左闭右开) │
│ • step 默认为 1 │
│ │
└─────────────────────────────────────────────────────────────┘切片原理图解:
字符串: "Python"
P y t h o n
索引: 0 1 2 3 4 5
text[0:3] → 取索引 0, 1, 2(不含 3)
┌─────────┐
P y t h o n
0 1 2 3 4 5
└────┘
结果: "Pyt"
text[2:5] → 取索引 2, 3, 4(不含 5)
P y t h o n
0 1 2 3 4 5
└────┘
结果: "tho"text = "Python Programming"
# 基本切片 [start:end]
print(text[0:6]) # Python(索引 0-5,不含 6)
print(text[7:]) # Programming(从索引 7 到末尾)
print(text[:6]) # Python(从开头到索引 5)
# 空切片
print(text[:]) # Python Programming(完整复制)
# 步长切片 [start:end:step]
numbers = "0123456789"
print(numbers[::2]) # 02468(每隔一个取一个)
print(numbers[1::2]) # 13579(从索引1开始,每隔一个)
print(numbers[::-1]) # 9876543210(反转,步长为负)
# 负索引切片
print(text[-11:]) # Programming(从倒数第11个到末尾)
print(text[:-12]) # Python(从开头到倒数第12个之前)步长为负数时,从右往左切片:
text = "Python"
# 反转字符串
print(text[::-1]) # nohtyP
# 从右往左取
print(text[4:1:-1]) # oh(从索引4往左取到索引2,不含1)常用方法
查找方法
text = "Hello, Python Programming!"
# find - 查找子串位置
print(text.find("Python")) # 7(找到,返回起始索引)
print(text.find("Java")) # -1(未找到,返回 -1)
# index - 查找子串位置(找不到会报错)
print(text.index("Python")) # 7
# print(text.index("Java")) # ValueError: substring not found
# rfind / rindex - 从右边开始查找
print(text.rfind("o")) # 14(最后一个 o 的位置)
print(text.rindex("o")) # 14find 和 index 的区别:
┌─────────────────────────────────────────────────────────────┐
│ find vs index │
├─────────────────────────────────────────────────────────────┤
│ │
│ 方法 找到时返回 未找到时 │
│ ───────────────────────────────────────────────────── │
│ find 索引位置 -1(安全,推荐) │
│ index 紧引位置 ValueError(需异常处理) │
│ │
│ 建议:优先使用 find,除非需要异常来处理未找到的情况 │
│ │
└─────────────────────────────────────────────────────────────┘# startswith / endswith - 检查开头和结尾
url = "https://www.example.com"
print(url.startswith("https")) # True
print(url.endswith(".com")) # True
# 可以传入元组检查多个前缀/后缀
filename = "document.pdf"
print(filename.endswith(".pdf", ".txt", ".doc")) # True
# in - 成员检查(最简单的方式)
print("Python" in text) # True
print("Java" in text) # False
# count - 统计出现次数
text = "Hello, Hello, Hello!"
print(text.count("Hello")) # 3大小写转换
text = "Hello, PyThon!"
print(text.lower()) # hello, python!
print(text.upper()) # HELLO, PYTHON!
print(text.title()) # Hello, Python!
print(text.capitalize()) # Hello, python!替换和去除
# replace - 替换子串
text = "Hello, World!"
print(text.replace("World", "Python")) # Hello, Python!
# replace 可以指定替换次数
text = "aaa-bbb-ccc"
print(text.replace("-", "|", 1)) # aaa|bbb-ccc(只替换第一个)
print(text.replace("-", "|", 2)) # aaa|bbb|ccc(只替换前两个)strip - 去除两端字符
# 去除空白字符(默认)
text = " Hello, World! "
print(text.strip()) # Hello, World!
print(text.lstrip()) # Hello, World! (只去除左边)
print(text.rstrip()) # Hello, World!(只去除右边)
# 去除指定字符
text = "xxHelloxx"
print(text.strip("x")) # Hello
print(text.strip("xH")) # ello(去除两端的 x 和 H)
# 去除多种字符
text = "***Hello!!!"
print(text.strip("*!")) # Hello
# 实际应用:清理用户输入
user_input = " yes "
if user_input.strip().lower() == "yes":
print("用户确认")⚠️ 注意:strip 只去除两端,不影响中间:
text = " Hello World "
print(text.strip()) # Hello World(中间空格保留)分割和连接
split 和 join 是一对互逆操作,常用于字符串与列表之间的转换。
┌─────────────────────────────────────────────────────────────┐
│ split 与 join 关系 │
├─────────────────────────────────────────────────────────────┤
│ │
│ split:字符串 → 列表(拆分) │
│ "a,b,c" ──split(",")──→ ['a', 'b', 'c'] │
│ │
│ join:列表 → 字符串(合并) │
│ ['a', 'b', 'c'] ──join(",")──→ "a,b,c" │
│ │
└─────────────────────────────────────────────────────────────┘split - 分割字符串
split() 将字符串按指定分隔符拆分成列表。
# 基本用法
text = "apple,banana,orange"
result = text.split(",")
print(result) # ['apple', 'banana', 'orange']
# 分隔符可以是任意字符串
path = "/usr/local/bin"
print(path.split("/")) # ['', 'usr', 'local', 'bin']
# 不指定分隔符:按空白字符分割(空格、制表符、换行)
sentence = "Hello World\tPython"
print(sentence.split()) # ['Hello', 'World', 'Python']
# 指定最大分割次数
text = "a-b-c-d-e"
print(text.split("-", 2)) # ['a', 'b', 'c-d-e'](只分割2次)join - 连接列表
join() 将列表中的元素用指定分隔符连接成字符串。
⚠️ 重要:join 是字符串的方法,不是列表的方法!
# 语法:分隔符.join(列表)
words = ["Hello", "World"]
# 用空格连接
print(" ".join(words)) # Hello World
# 用其他分隔符连接
print("-".join(words)) # Hello-World
print(",".join(words)) # Hello,World
# 无分隔符连接(空字符串)
print("".join(words)) # HelloWorld┌─────────────────────────────────────────────────────────────┐
│ join 语法详解 │
├─────────────────────────────────────────────────────────────┤
│ │
│ " ".join(words) │
│ ↑ ↑ │
│ │ │ │
│ 分隔符 列表 │
│ │
│ 分隔符决定元素之间用什么连接: │
│ • " " → 元素之间用空格连接 │
│ • "-" → 元素之间用减号连接 │
│ • "" → 元素之间无任何字符(直接拼接) │
│ • "\n" → 元素之间用换行连接 │
│ │
│ 结果:Hello[分隔符]World │
│ │
└─────────────────────────────────────────────────────────────┘# 不同分隔符的效果对比
words = ["apple", "banana", "orange"]
print(" ".join(words)) # apple banana orange
print("-".join(words)) # apple-banana-orange
print(",".join(words)) # apple,banana,orange
print("".join(words)) # applebananaorange
print("\n".join(words)) # 每个元素一行
# apple
# banana
# orangesplit 和 join 配合使用
# 实际应用:替换所有空格为下划线
filename = "my document file.txt"
new_name = "_".join(filename.split())
print(new_name) # my_document_file.txt
# 解析 CSV 数据
line = "Alice,25,Beijing"
name, age, city = line.split(",")
print(f"{name} is {age} years old, lives in {city}")
# 构建路径
parts = ["home", "user", "documents"]
path = "/".join(parts)
print(path) # home/user/documents常见错误:
# ❌ 错误:把 join 当作列表方法调用
words = ["Hello", "World"]
# words.join(" ") # AttributeError: 'list' has no attribute 'join'
# ✅ 正确:join 是字符串方法
" ".join(words) # Hello World
# ❌ 错误:join 的参数必须是字符串列表
numbers = [1, 2, 3]
# "".join(numbers) # TypeError: expected str
# ✅ 正确:先转换为字符串
"".join(str(n) for n in numbers) # "123"格式化字符串
f-string(推荐)
name = "Alice"
age = 25
pi = 3.14159
# 基本用法
print(f"Hello, {name}!")
# 表达式
print(f"Next year: {age + 1}")
# 调用方法
print(f"{name.lower()} is here")数字格式化:
pi = 3.14159
# 保留小数位数
print(f"PI: {pi:.2f}") # PI: 3.14
print(f"PI: {pi:.4f}") # PI: 3.1416
# 整数补零
print(f"{42:05d}") # 00042
# 千位分隔符
print(f"{1000000:,}") # 1,000,000
# 百分比
ratio = 0.856
print(f"{ratio:.2%}") # 85.60%
# 科学计数法
print(f"{1000000:.2e}") # 1.00e+06对齐和宽度:
┌─────────────────────────────────────────────────────────────┐
│ 格式化对齐语法 │
├─────────────────────────────────────────────────────────────┤
│ │
│ {value:width.align} │
│ │
│ 对齐符号: │
│ • < 左对齐(默认字符串) │
│ • > 右对齐(默认数字) │
│ • ^ 居中对齐 │
│ │
│ 示例:{text:10<} 左对齐,宽度10 │
│ │
└─────────────────────────────────────────────────────────────┘# 左对齐、右对齐、居中
text = "Hello"
print(f"{text:<10}") # Hello (左对齐,宽度10)
print(f"{text:>10}") # Hello(右对齐,宽度10)
print(f"{text:^10}") # Hello (居中,宽度10)
# 用指定字符填充
print(f"{text:*<10}") # Hello*****(用 * 填充)
print(f"{text:*>10}") # *****Hello
print(f"{text:*^10}") # **Hello***
# 数字对齐
num = 42
print(f"{num:>5}") # 42(右对齐)
print(f"{num:<5}") # 42 (左对齐)
# 表格输出示例
for name, score in [("Alice", 95), ("Bob", 87), ("Carol", 92)]:
print(f"{name:<10} {score:>5}")
# Alice 95
# Bob 87
# Carol 92format 方法
# 位置参数
print("{} {}".format("Hello", "World"))
# 命名参数
print("{greeting}, {name}!".format(greeting="Hello", name="World"))
# 格式化数字
print("PI: {:.2f}".format(3.14159))% 格式化(旧式,了解即可)
name = "Alice"
age = 25
print("Hello, %s!" % name) # Hello, Alice!
print("Age: %d" % age) # Age: 25
print("PI: %.2f" % 3.14159) # PI: 3.14从简单到复杂:字符串的渐进应用
层级1:基础操作
# 字符串创建和访问
text: str = "Hello, Python!"
print(text[0]) # H
print(text[7:13]) # Python层级2:常用方法
# 字符串方法
text: str = " hello world "
print(text.strip()) # "hello world"
print(text.upper()) # " HELLO WORLD "
print(text.replace("world", "Python"))层级3:格式化输出
# f-string 格式化
name: str = "张三"
age: int = 25
score: float = 85.5
print(f"姓名:{name}")
print(f"年龄:{age}岁")
print(f"成绩:{score:.1f}分")层级4:分割与连接
# 分割和连接
csv_line: str = "张三,25,85.5"
fields: list[str] = csv_line.split(",")
# 重新组合
new_line: str = " | ".join(fields)
print(new_line) # 张三 | 25 | 85.5层级5:文本处理
# 多行文本处理
log: str = """2024-01-15 10:30:45 ERROR Connection failed
2024-01-15 10:31:20 INFO Retry successful
2024-01-15 10:32:00 WARNING High memory usage"""
# 统计错误行数
lines: list[str] = log.split("\n")
error_count: int = sum(1 for line in lines if "ERROR" in line)
print(f"错误数量:{error_count}")综合应用:日志分析程序
这个示例综合运用字符串的多个知识点:
# 日志分析程序(Python 3.11+)
import re
from collections import Counter
from typing import Any
def analyze_logs(log_text: str) -> dict[str, Any]:
"""
分析日志文本
Args:
log_text: 日志文本
Returns:
分析结果字典
"""
lines: list[str] = log_text.strip().split("\n")
# 统计各级别日志数量
levels: list[str] = []
for line in lines:
if " ERROR " in line:
levels.append("ERROR")
elif " WARNING " in line:
levels.append("WARNING")
elif " INFO " in line:
levels.append("INFO")
level_counts: Counter = Counter(levels)
# 提取错误消息
errors: list[str] = []
for line in lines:
if " ERROR " in line:
# 提取时间戳
time_match = re.search(r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]', line)
if time_match:
errors.append(time_match.group(1))
return {
"total_lines": len(lines),
"level_counts": dict(level_counts),
"error_times": errors
}
# 使用示例
log_data: str = """
[2024-01-15 10:30:45] ERROR: Connection failed
[2024-01-15 10:31:20] INFO: Retry successful
[2024-01-15 10:32:00] WARNING: High memory usage
[2024-01-15 10:33:15] ERROR: Timeout
[2024-01-15 10:34:00] INFO: Process completed
"""
result: dict[str, Any] = analyze_logs(log_data)
print("=== 日志分析结果 ===")
print(f"总行数:{result['total_lines']}")
print(f"级别统计:{result['level_counts']}")
print(f"错误时间:{', '.join(result['error_times'])}")这个程序展示了:
- 字符串的分割操作
- 字符串的查找和判断
- 正则表达式匹配
- f-string 格式化
- 类型提示的现代语法
关键代码说明:
| 代码 | 含义 | 为什么这样写 |
|---|---|---|
log_text.strip().split("\n") | 先去除首尾空白,再按换行符拆分成行列表 | 链式调用:strip 防止空行干扰,split 逐行处理 |
" ERROR " in line | 检查行中是否包含 ERROR 级别标记 | 前后加空格避免误匹配(如 "SUPER_ERROR") |
Counter(levels) | 统计列表中每个值出现的次数 | Counter(["ERROR","INFO","ERROR"]) → {"ERROR":2,"INFO":1} |
re.search(r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]', line) | 正则匹配时间戳,() 括号为捕获组 | \d{4} 匹配 4 位数字,() 标记要提取的部分 |
time_match.group(1) | 提取正则第 1 个捕获组的内容 | group(0) 是整个匹配,group(1) 是第一个 () 内容 |
dict(level_counts) | 将 Counter 转为普通字典 | Counter 是 dict 的子类,转换后更通用 |
L2 实践层:用好
推荐做法
| 做法 | 原因 | 示例 |
|---|---|---|
用 join() 拼接大量字符串 | 只创建一次新字符串,O(n) 复杂度 | "".join(str(i) for i in range(1000)) |
| 用 f-string 格式化字符串 | 最简洁、编译期求值、性能最好 | f"{name} {age}" |
用 find() 而非 index() | 不抛异常,代码更安全 | pos = text.find("key") |
用 in 检查子串是否存在 | 最直接、最 Pythonic | if "key" in text: |
用 split() + join() 清洗空白 | 链式处理,一行完成 | " ".join(text.split()) |
用 startswith/endswith 配合元组 | 一次检查多种前缀/后缀 | filename.endswith((".png", ".jpg")) |
| 多步字符串操作用链式调用 | 避免中间变量,代码流畅 | text.strip().lower().replace(" ", "_") |
反模式:不要这样做
拼接字符串
# ❌ 错误做法:循环中使用 + 拼接
result = ""
for i in range(1000):
result += str(i) # 每次 += 都创建新字符串对象!
# 问题:
# 1. 每次 += 都要分配新内存、复制旧字符串,O(n²) 时间复杂度
# 2. 1000 次拼接产生 ~1000 个临时字符串对象,内存浪费巨大
# 3. 数据量大时性能急剧下降# ✅ 正确做法:用 "".join()
result = "".join(str(i) for i in range(1000))
# 或先收集到列表再 join(显式版本)
parts = []
for i in range(1000):
parts.append(str(i))
result = "".join(parts)格式化方式选择
# ❌ 错误做法:用 % 格式化(过时)
name = "Alice"
age = 25
msg = "Hello, %s! You are %d years old." % (name, age)
# 问题:
# 1. %s / %d 需要记住格式符,容易出错
# 2. 多参数时括号嵌套,可读性差
# 3. 不支持表达式和复杂格式化# ❌ 较差做法:不必要地用 .format()(当参数就在当前作用域时)
msg = "Hello, {}! You are {} years old.".format(name, age)
# 问题:
# 1. 比 f-string 多一次方法调用
# 2. 参数远离占位符,可读性不如 f-string# ✅ 正确做法:用 f-string
msg = f"Hello, {name}! You are {age} years old."
print(f"明年 {name} 就 {age + 1} 岁了") # 支持表达式字符串方法 vs 手动循环
# ❌ 错误做法:手动遍历实现替换
text = "Hello, World!"
result = ""
for ch in text:
if ch == "o":
result += "0"
elif ch == "l":
result += "1"
else:
result += ch
# 问题:
# 1. 重复造轮子,代码冗长
# 2. 手动拼接字符效率低
# 3. 复杂替换逻辑易出错# ✅ 正确做法:用内置方法和 translate
text = "Hello, World!"
# 简单替换链
result = text.replace("o", "0").replace("l", "1")
# 批量字符映射(比多次 replace 快 10 倍以上)
table = str.maketrans({"o": "0", "l": "1"})
result = text.translate(table)strip 的误区
# ❌ 错误做法:误以为 strip 删除所有指定字符
text = "***Hello***World***"
result = text.strip("*")
print(result) # "Hello***World" — 中间的 * 没有被删除!
# 问题:
# strip 的 "去除" 只作用于首尾,不在整个字符串中查找# ✅ 正确做法:明确 strip 的语义边界
text = "***Hello***World***"
# 只去除首尾
cleaned = text.strip("*") # "Hello***World"
# 如果要全局删除,用 replace
no_stars = text.replace("*", "") # "HelloWorld"
# 清洗用户输入的标准流程
user_input = " YES "
normalized = user_input.strip().lower() # "yes"适用场景
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 拼接 2-3 个变量 | f-string 或 + 均可 | 差异可忽略 |
| 拼接大量字符串(10+) | "".join() | 避免 O(n²) 性能陷阱 |
| 日志/报告格式化输出 | f-string | 最简洁,支持表达式和格式说明符 |
| 模板文本(用户或外部配置) | .format() 或 string.Template | 模板与数据分离,安全性更高 |
| 搜索子串位置 | find() | 返回 -1 而非抛异常,控制流更清晰 |
| 需要异常通知的搜索 | index() | 未找到时主动暴露问题 |
| 检查子串是否存在 | in 运算符 | 最 Pythonic,语义最清晰 |
| 清洗用户输入 | strip() + lower() 链式 | 标准化处理流程 |
| 列表/可迭代对象转字符串 | "分隔符".join(iterable) | 最高效的拼接方式 |
| 复杂批量字符替换 | str.translate() + str.maketrans() | 比多次 replace 快 10 倍以上 |
| 多种前缀/后缀条件判断 | str.startswith(tuple) / str.endswith(tuple) | 一次调用检查多个条件 |
L3 专家层:深入
Python 如何实现
字符串驻留(String Interning)
Python 自动缓存短字符串和标识符类字符串(类似 Python 变量名),这一机制称为字符串驻留:
字符串驻留机制:
┌─────────────────────────────────────────────────────────────┐
│ 字符串驻留(Interning) │
│ │
│ 触发条件: │
│ • 长度 ≤ 4096 且仅含 ASCII 字母、数字、下划线(标识符规则) │
│ • 编译期确定的字符串常量 │
│ │
│ s1 = "hello" │
│ s2 = "hello" │
│ s1 is s2 → True (指向同一个对象,驻留生效) │
│ │
│ s3 = "hello world" │
│ s4 = "hello world" │
│ s3 is s4 → False (含空格,不触发驻留) │
│ │
│ 显式驻留:sys.intern() 可强制驻留任意字符串 │
│ s5 = sys.intern("hello world") │
│ s6 = sys.intern("hello world") │
│ s5 is s6 → True │
└─────────────────────────────────────────────────────────────┘验证代码:
import sys
# 自动驻留:短字符串
s1 = "hello"
s2 = "hello"
print(f"s1 is s2: {s1 is s2}") # True
# 不触发驻留:含空格
s3 = "hello world"
s4 = "hello world"
print(f"s3 is s4: {s3 is s4}") # 可能 False(取决于运行环境)
# 显式驻留
s5 = sys.intern("hello world")
s6 = sys.intern("hello world")
print(f"s5 is s6: {s5 is s6}") # True
# 运行时生成的字符串不自动驻留
s7 = "".join(["he", "llo"])
s8 = "hello"
print(f"s7 is s8: {s7 is s8}") # False
print(f"s7 == s8: {s7 == s8}") # True(值相等)关键结论: 绝不要用
is比较字符串内容,始终用==。is比较的是对象标识(内存地址),==比较的是值。
字符串内存布局
字符串对象内存布局(Python 3.12+):
┌─────────────────────────────────────────────────────────────┐
│ PyUnicodeObject │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ object header (refcount + type pointer) │ │
│ ├──────────────────────────────────────────────────────┤ │
│ │ length: 字符数量(码点数) │ │
│ │ hash: 缓存的哈希值(-1 表示未计算) │ │
│ │ state: 内部标志位(紧凑/规范、ASCII 标记) │ │
│ ├──────────────────────────────────────────────────────┤ │
│ │ 实际字符数据: │ │
│ │ • ASCII 字符串:每个字符 1 字节(Latin-1 编码) │ │
│ │ • BMP 字符:每个字符 2 字节(UCS-2 编码) │ │
│ │ • 非 BMP 字符:每个字符 4 字节(UCS-4 编码) │ │
│ │ Python 根据字符串内容自动选择最紧凑的存储方式 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 关键特性: │
│ • 哈希值惰性计算,计算后缓存 → 字典键查找 O(1) │
│ • 紧凑存储:自动选择最小编码单元 → 内存高效 │
│ • 不可变:修改任何字符都必须创建新对象 → 线程安全 │
└─────────────────────────────────────────────────────────────┘验证代码:
import sys
# 不同字符集的内存占用量
ascii_str = "hello" # 纯 ASCII
bmp_str = "你好" # BMP 中文
emoji_str = "😂" # 非 BMP 字符(4 字节)
print(f"ascii_str 大小: {sys.getsizeof(ascii_str)} 字节") # 约 54
print(f"bmp_str 大小: {sys.getsizeof(bmp_str)} 字节") # 约 60
print(f"emoji_str 大小: {sys.getsizeof(emoji_str)} 字节") # emoji 更大
# 大规模字符串内存对比
big_ascii = "a" * 10000
big_chinese = "中" * 10000
print(f"\n10K ASCII: {sys.getsizeof(big_ascii)} 字节(~{sys.getsizeof(big_ascii)/1024:.0f} KB)")
print(f"10K 中文: {sys.getsizeof(big_chinese)} 字节(~{sys.getsizeof(big_chinese)/1024:.0f} KB)")
# 哈希值惰性计算
text = "hello"
print(f"hash('hello'): {hash(text)}") # 首次调用计算哈希
# 后续 dict 查询直接使用缓存值
d = {"hello": 1}
print(d[text]) # 1 — O(1) 查找性能考量
字节码验证:join vs +
import dis
def concat_plus():
result = ""
for i in range(5):
result += str(i)
return result
def concat_join():
return "".join(str(i) for i in range(5))
print("=== + 拼接字节码 ===")
dis.dis(concat_plus)
print("\n=== join 拼接字节码 ===")
dis.dis(concat_join)+ 拼接 的字节码中有循环体,每次迭代都执行 BINARY_OP(加法操作)并分配新字符串;join 在 C 层面一次性计算总长度并分配内存。
性能基准测试
import timeit
# 小规模拼接(10 个元素)
n_small = 10
plus_small = timeit.timeit(
f'''
result = ""
for i in range({n_small}):
result += str(i)
''',
number=10000
)
join_small = timeit.timeit(
f'"".join(str(i) for i in range({n_small}))',
number=10000
)
print(f"小型拼接 ({n_small} 个):")
print(f" + 拼接: {plus_small:.4f}s")
print(f" join: {join_small:.4f}s")
# 大规模拼接(1000 个元素)
n_large = 1000
plus_large = timeit.timeit(
f'''
result = ""
for i in range({n_large}):
result += str(i)
''',
number=1000
)
join_large = timeit.timeit(
f'"".join(str(i) for i in range({n_large}))',
number=1000
)
print(f"\n大规模拼接 ({n_large} 个):")
print(f" + 拼接: {plus_large:.4f}s")
print(f" join: {join_large:.4f}s")字符串操作时间复杂度
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
s[i] 索引访问 | O(1) | 直接定位到内存偏移,无需遍历 |
s[i:j] 切片 | O(k),k = j-i | 需复制数据到新字符串对象 |
s + t 拼接 | O(n + m) | 分配新内存,复制两个操作数 |
"sep".join(seq) | O(n),n = 总输出长度 | C 层一次分配,逐个填入 |
循环中 s += x | O(n²) | 每次 += 都重新分配和复制 |
s in t 子串检查 | O(nm) 最坏 | 朴素搜索,但 CPython 使用优化算法 |
s.find(sub) | O(nm) 最坏 | 使用 Two-way 算法优化平均情况 |
s.replace(old, new) | O(n) | 扫描一遍,构建新字符串 |
s.split(sep) | O(n) | 扫描分隔符,分配各片段 |
s.strip() | O(n) | 仅扫描首尾,不处理中间 |
s.lower()/upper() | O(n) | Unicode 大小写转换需查表 |
s == t | O(1) 平均 | 先比较长度和哈希,不匹配则立即返回 |
hash(s) | O(n) 首次 / O(1) 此后 | 哈希值惰性计算并缓存 |
设计动机
| 设计选择 | 原因 | 实际影响 |
|---|---|---|
| 字符串不可变 | 线程安全;可作为 dict 键;支持驻留优化 | 修改必须创建新对象,但带来安全性 |
| 紧凑 Latin-1/UCS-2/UCS-4 存储 | 自动选择最小编码单元,节省内存 | ASCII 文本比 Python 2 节省 ~50% |
| 哈希值惰性缓存 | 首次 hash() 计算后存储,后续 O(1) | 字典查询(用字符串作键)极快 |
join() 是 str 方法而非 list 方法 | 任何可迭代对象都能用,复用同一个分隔符 | ",".join(generator) 直接支持 |
| f-string 编译期求值 | 解析时转为高效字节码,运行时直接拼接 | 比 .format() 和 % 都快,且语法更简洁 |
re 模块自动缓存模式 | 减少用户手动 compile 的心智负担 | 最近 ~512 个模式 LRU 缓存,多数场景无需手动编译 |
知识关联
字符串知识关联:
┌─────────────────┐
│ Unicode 标准 │
└─────────────────┘
│
↓
┌───────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Python 字符串 │────→│ 编码 (encode) │────→│ bytes 类型 │
│ str 类型 │←────│ 解码 (decode) │ │ 字节序列 │
└───────────────┘ └─────────────────┘ └─────────────────┘
│ │
│ 哈希值缓存 │
↓ ↓
┌───────────────┐ ┌─────────────────┐
│ dict 键 │ 不可变 → 线程安全 │ 文件/网络 I/O │
└───────────────┘ └─────────────────┘
│
│ 序列协议(可索引、可切片、可迭代)
↓
┌───────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ list/tuple │ │ 生成器表达式 │ │ 正则表达式 │
│ 同属序列 │ │ (惰性处理) │ │ (re 模块) │
└───────────────┘ └─────────────────┘ └─────────────────┘前置依赖:基础语法(变量、运算符)
↓
当前概念:字符串基础(创建、索引、切片、方法、格式化)
↓
后续依赖:
├── 数据结构(列表、字典、集合)— 字符串作为元素和键
├── 文件 I/O(读写文本文件)
├── 字符串进阶(编码、bytes、正则表达式)
└── Web 开发(HTTP 请求/响应中的文本处理)本章小结
┌─────────────────────────────────────────────────────────────┐
│ 字符串 知识要点 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 创建方式: │
│ ✓ 单引号、双引号、三引号 │
│ ✓ raw 字符串 r""(不转义) │
│ ✓ f-string 格式化 │
│ │
│ 索引和切片: │
│ ✓ 正向索引从 0,反向索引从 -1 │
│ ✓ 切片 [start:end:step],左闭右开 │
│ ✓ [::-1] 反转字符串 │
│ │
│ 常用方法: │
│ ✓ find(返回-1)/ index(报错) │
│ ✓ replace(old, new, count) │
│ ✓ split() / join() │
│ ✓ strip() / lstrip() / rstrip() │
│ ✓ lower() / upper() / title() │
│ │
│ 格式化: │
│ ✓ f-string(推荐) │
│ ✓ 对齐:< 左 / > 右 / ^ 居中 │
│ ✓ 数字:.2f / :05d / :, / :.2% │
│ │
│ 实际应用: │
│ ✓ 日志分析、文本处理、数据清洗 │
│ │
└─────────────────────────────────────────────────────────────┘