Skip to content

01-字符串基础

本章代码基于 Python 3.11+ 编写

字符串是程序处理文本数据的基础,掌握字符串操作让程序能够处理各种文本信息。


本章讲解字符串的创建、索引、切片和常用方法。


概念铺垫

为什么需要字符串?一个真实的文本处理场景

问题场景: 你在开发一个日志分析程序,需要从日志中提取时间、错误类型等信息。

不使用字符串处理的困惑:

  • 如何在程序中表示和操作文本?
  • 如何从长文本中提取特定部分?
  • 如何格式化输出信息?

使用字符串的解决方案:

python
# 日志分析示例
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 用字符串处理文本。

字符串的常见用途:

  1. 存储文本:用户输入、文件内容、日志信息
  2. 提取信息:从长文本中找到特定内容
  3. 格式化输出:生成清晰易读的输出
  4. 文本分析:统计、搜索、替换

字符串的最简用法

字符串的核心操作:创建、访问、格式化。

python
# 创建字符串
name: str = "Python"

# 访问字符
print(name[0])  # 输出:P

# 格式化输出
version: float = 3.11
print(f"{name} {version}")  # 输出:Python 3.11

这就是字符串的基本用法。接下来我们详细学习字符串的操作。


L1 理解层:会用

创建字符串

python
# 单引号或双引号
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 可以复用相同字符串                        │
│  • 字典安全:可作为字典键(键必须不可变)                      │
└─────────────────────────────────────────────────────────────┘

最简示例:

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 开始(最后一个字符)             │
│                                                             │
└─────────────────────────────────────────────────────────────┘
python
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])

索引越界会报错:

python
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"
python
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个之前)

步长为负数时,从右往左切片:

python
text = "Python"

# 反转字符串
print(text[::-1])   # nohtyP

# 从右往左取
print(text[4:1:-1])  # oh(从索引4往左取到索引2,不含1)

常用方法

查找方法
python
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"))       # 14

find 和 index 的区别:

┌─────────────────────────────────────────────────────────────┐
│                  find vs index                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   方法      找到时返回      未找到时                         │
│   ─────────────────────────────────────────────────────     │
│   find      索引位置        -1(安全,推荐)                 │
│   index     紧引位置        ValueError(需异常处理)         │
│                                                             │
│   建议:优先使用 find,除非需要异常来处理未找到的情况        │
│                                                             │
└─────────────────────────────────────────────────────────────┘
python
# 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
大小写转换
python
text = "Hello, PyThon!"

print(text.lower())    # hello, python!
print(text.upper())    # HELLO, PYTHON!
print(text.title())    # Hello, Python!
print(text.capitalize())  # Hello, python!
替换和去除
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 - 去除两端字符

python
# 去除空白字符(默认)
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 只去除两端,不影响中间:

python
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() 将字符串按指定分隔符拆分成列表。

python
# 基本用法
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 是字符串的方法,不是列表的方法!

python
# 语法:分隔符.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                                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
python
# 不同分隔符的效果对比
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
# orange

split 和 join 配合使用

python
# 实际应用:替换所有空格为下划线
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

常见错误:

python
# ❌ 错误:把 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(推荐)
python
name = "Alice"
age = 25
pi = 3.14159

# 基本用法
print(f"Hello, {name}!")

# 表达式
print(f"Next year: {age + 1}")

# 调用方法
print(f"{name.lower()} is here")

数字格式化:

python
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                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘
python
# 左对齐、右对齐、居中
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        92
format 方法
python
# 位置参数
print("{} {}".format("Hello", "World"))

# 命名参数
print("{greeting}, {name}!".format(greeting="Hello", name="World"))

# 格式化数字
print("PI: {:.2f}".format(3.14159))
% 格式化(旧式,了解即可)
python
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:基础操作

python
# 字符串创建和访问
text: str = "Hello, Python!"
print(text[0])      # H
print(text[7:13])   # Python

层级2:常用方法

python
# 字符串方法
text: str = "  hello world  "
print(text.strip())       # "hello world"
print(text.upper())       # "  HELLO WORLD  "
print(text.replace("world", "Python"))

层级3:格式化输出

python
# f-string 格式化
name: str = "张三"
age: int = 25
score: float = 85.5

print(f"姓名:{name}")
print(f"年龄:{age}岁")
print(f"成绩:{score:.1f}分")

层级4:分割与连接

python
# 分割和连接
csv_line: str = "张三,25,85.5"
fields: list[str] = csv_line.split(",")

# 重新组合
new_line: str = " | ".join(fields)
print(new_line)  # 张三 | 25 | 85.5

层级5:文本处理

python
# 多行文本处理
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
# 日志分析程序(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 检查子串是否存在最直接、最 Pythonicif "key" in text:
split() + join() 清洗空白链式处理,一行完成" ".join(text.split())
startswith/endswith 配合元组一次检查多种前缀/后缀filename.endswith((".png", ".jpg"))
多步字符串操作用链式调用避免中间变量,代码流畅text.strip().lower().replace(" ", "_")

反模式:不要这样做

拼接字符串
python
# ❌ 错误做法:循环中使用 + 拼接
result = ""
for i in range(1000):
    result += str(i)  # 每次 += 都创建新字符串对象!

# 问题:
# 1. 每次 += 都要分配新内存、复制旧字符串,O(n²) 时间复杂度
# 2. 1000 次拼接产生 ~1000 个临时字符串对象,内存浪费巨大
# 3. 数据量大时性能急剧下降
python
# ✅ 正确做法:用 "".join()
result = "".join(str(i) for i in range(1000))

# 或先收集到列表再 join(显式版本)
parts = []
for i in range(1000):
    parts.append(str(i))
result = "".join(parts)
格式化方式选择
python
# ❌ 错误做法:用 % 格式化(过时)
name = "Alice"
age = 25
msg = "Hello, %s! You are %d years old." % (name, age)

# 问题:
# 1. %s / %d 需要记住格式符,容易出错
# 2. 多参数时括号嵌套,可读性差
# 3. 不支持表达式和复杂格式化
python
# ❌ 较差做法:不必要地用 .format()(当参数就在当前作用域时)
msg = "Hello, {}! You are {} years old.".format(name, age)

# 问题:
# 1. 比 f-string 多一次方法调用
# 2. 参数远离占位符,可读性不如 f-string
python
# ✅ 正确做法:用 f-string
msg = f"Hello, {name}! You are {age} years old."
print(f"明年 {name}{age + 1} 岁了")  # 支持表达式
字符串方法 vs 手动循环
python
# ❌ 错误做法:手动遍历实现替换
text = "Hello, World!"
result = ""
for ch in text:
    if ch == "o":
        result += "0"
    elif ch == "l":
        result += "1"
    else:
        result += ch

# 问题:
# 1. 重复造轮子,代码冗长
# 2. 手动拼接字符效率低
# 3. 复杂替换逻辑易出错
python
# ✅ 正确做法:用内置方法和 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 的误区
python
# ❌ 错误做法:误以为 strip 删除所有指定字符
text = "***Hello***World***"
result = text.strip("*")
print(result)  # "Hello***World" — 中间的 * 没有被删除!

# 问题:
# strip 的 "去除" 只作用于首尾,不在整个字符串中查找
python
# ✅ 正确做法:明确 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                                            │
└─────────────────────────────────────────────────────────────┘

验证代码:

python
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)             │
│  • 紧凑存储:自动选择最小编码单元 → 内存高效                │
│  • 不可变:修改任何字符都必须创建新对象 → 线程安全           │
└─────────────────────────────────────────────────────────────┘

验证代码:

python
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 +
python
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 层面一次性计算总长度并分配内存。

性能基准测试
python
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 += xO(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 == tO(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%                            │
│                                                             │
│   实际应用:                                                 │
│   ✓ 日志分析、文本处理、数据清洗                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘