本讲使用 Python 3,重点掌握字符串的内部行为,而不只是记住split()、replace()等方法。
一、字符串的本质
在 Python 中,字符串是str类型的对象,用于表示 Unicode 文本序列。
s = "Hello Python" print(type(s)) print(len(s)) print(id(s))输出的前两行:
<class 'str'> 12第三行是对象身份相关的整数,其具体值因运行环境而异。
字符串有三个核心特征:
有序:字符具有确定的位置。
不可变:创建后不能原地修改字符。
可迭代:可以使用
for逐个访问字符。
例如:
s = "robot" s[0] = "R"会产生:
TypeError: 'str' object does not support item assignment正确操作:
s = "robot" s = "R" + s[1:] print(s) # Robot这不是修改原来的字符串,而是构造新的字符串对象,再让变量s指向它。
注意,即使执行s += "!",从语义上仍然是重新绑定,而不是在原字符串上增加字符。
二、字符串索引与切片
2.1 正向索引和负向索引
字符串中的字符可以从两个方向定位。
s = "PYTHON" print(s[0]) # P print(s[2]) # T print(s[-1]) # N print(s[-3]) # H如果索引超过有效范围:
print(s[10])会抛出IndexError。
2.2 切片s[start:stop:step]
三个参数分别表示:
start:开始位置,包含。stop:结束位置,不包含。step:移动步长,可以为负数。
s = "PYTHON" print(s[1:4]) # YTH print(s[:3]) # PYT print(s[3:]) # HON print(s[::2]) # PTO print(s[::-1]) # NOHTYP print(s[-4:-1]) # THO为什么s[1:4]不包含索引 4?
因为 Python 使用左闭右开区间,这使切片长度在默认步长为 1 时可以直接计算:
\[ \text{长度}=\text{stop}-\text{start} \]
例如s[1:4]有 3 个字符。
几个重要边界条件:
s = "abc" print(s[0:100]) # abc print(s[3:3]) # 空字符串 print(s[-100:2]) # ab切片越界通常不会抛出异常,但单个索引越界会。
此外:
s[::0]会抛出ValueError,因为步长不能为零。
三、字符串常用方法及其返回结果
str的方法很多,但需要按用途分类,而不是孤立记忆。
类别 | 方法 | 作用 |
|---|---|---|
查找 |
| 查找子串位置 |
查找 |
| 查找位置,失败时报错 |
统计 |
| 统计非重叠子串出现次数 |
判断 |
| 前缀和后缀判断 |
大小写 |
| 大小写转换 |
大小写 |
| 更适合 Unicode 无大小写比较 |
清理 |
| 去掉首尾指定字符 |
替换 |
| 替换子串 |
分割 |
| 拆分字符串 |
合并 |
| 合并字符串序列 |
判断 |
| 字符类别判断 |
3.1find()和index()
s = "robot_control" print(s.find("control")) # 6 print(s.find("camera")) # -1而:
s.index("camera")会抛出ValueError。
二者的区别不在于能否查找,而在于没有找到时如何处理。
注意:
print("aaa".count("aa")) # 1count()默认统计非重叠匹配,不会统计两个相互重叠的"aa"。
3.2strip()的重要陷阱
s = " hello " print(s.strip()) # "hello" print(s.lstrip()) # "hello " print(s.rstrip()) # " hello"但:
s = "abba" print(repr(s.strip("ab")))结果:
''因为strip("ab")的含义不是删除子字符串"ab",而是不断去除首尾属于字符集合{'a', 'b'}的字符。
如果目的是删除确定的前缀、后缀,可使用:
s = "robot_control" print(s.removeprefix("robot_")) # control print(s.removesuffix("_control")) # robot3.3split()和join()
s = "J1,J2,J3" parts = s.split(",") print(parts) print("-".join(parts))结果:
['J1', 'J2', 'J3'] J1-J2-J3split()有一个经常被忽略的区别:
print(" a b ".split()) print(" a b ".split(" "))结果:
['a', 'b'] ['', 'a', '', 'b', '']原因是:
不传分隔符时,连续空白会作为一个分隔区域处理,并忽略首尾空白。
显式传入
" "时,严格按单个空格拆分,可能产生空字符串。
另一个边界:
print("".split()) # [] print("".split(",")) # ['']3.4replace()不会修改原字符串
s = "robot_arm" s.replace("_", "-") print(s) # robot_arm必须接收返回值:
s = s.replace("_", "-") print(s) # robot-arm这再次体现了字符串不可变的特点。
四、字符串比较与 Unicode 编码
4.1 字符串如何比较大小?
print("8" > "12") # True print("abc" < "abd") # True字符串按字符的 Unicode 码点顺序逐个比较,并不是先把整段文本转换成数字。
例如:
print(ord("A")) # 65 print(ord("a")) # 97 print(chr(65)) # Aord()返回单个字符的 Unicode 码点,chr()执行反向转换。
4.2 Unicode、UTF-8、bytes 的区别
最容易混淆的是字符串和字节序列。
str:表示 Unicode 文本。bytes:表示字节数据,元素为 0~255 的整数。UTF-8:将 Unicode 文本编码成字节的一种方式。
例如:
text = "机器人" data = text.encode("utf-8") print(len(text)) print(len(data)) print(type(data))结果:
3 9 <class 'bytes'>原因是这三个汉字在 UTF-8 中各占 3 个字节。
解码操作:
result = data.decode("utf-8") print(result) # 机器人需要特别注意:len(str)通常统计 Unicode 码点数量,而不是字节数,也不总是用户肉眼看到的字符数量。
例如:
s = "e\u0301" print(len(s)) # 2这里由字母e加上一个组合重音符号组成,视觉上可能显示为一个带重音的字母,但len()得到 2。
在文件通信、网络传输、串口协议和机器人控制系统中,必须区分文本与字节数据。
五、字符串格式化与特殊字符
5.1 三种常见格式化方式
name = "Joint1" angle = 90.12345 print("%s: %.2f" % (name, angle)) print("{}: {:.2f}".format(name, angle)) print(f"{name}: {angle:.2f}")输出均为:
Joint1: 90.12现代 Python 代码通常优先使用 f-string。
常用格式:
写法 | 意义 |
|---|---|
| 固定显示两位小数 |
| 整数宽度为 8,不足补零 |
| 右对齐,宽度为 10 |
| 左对齐 |
| 居中 |
| 按百分比显示 |
| 使用 |
例如:
x = 0.8567 print(f"{x:.1%}") # 85.7%5.2str()和repr()
s = "a\nb" print(str(s)) print(repr(s))输出:
a b 'a\nb'其中:
str()更侧重可读展示。repr()更侧重明确表达对象形式,便于调试。
5.3 转义字符与原始字符串
常见转义符:
\n # 换行 \t # 制表符 \\ # 反斜杠 \" # 双引号Windows 路径需要注意:
path = r"C:\new\robot"print(path)使用r前缀可以让反斜杠按原始方式处理,适合很多路径和正则表达式场景。但原始字符串不能以奇数个未转义的反斜杠结束。
六、字符串的时间复杂度
下面给出常见操作的数量级,用于判断代码在大量数据下的表现。
设字符串长度为 \(n\),待处理子串长度为 \(k\)。
操作 | 典型复杂度 | 说明 |
|---|---|---|
| O(1) | 单个字符索引 |
| O(1) | 长度信息已保存 |
| O(k) | 通常需复制切片内容 |
| O(n+m) | 通常需要构造结果 |
| O(n) | 创建逆序字符串 |
| 与搜索算法和输入有关 | 不宜一概视为 O(1) |
| 通常 O(n) | 扫描并创建分段 |
| O(结果总长度) | 一次构造连接结果 |
| 依赖匹配及替换规模 | 通常需扫描字符串 |
为什么不建议反复拼接大字符串?
考虑:
result = "" for i in range(10000): result += str(i)由于字符串不可变,反复连接可能导致频繁分配和复制,某些情况下累计成本可接近平方级。
更通用的方式:
parts = [] for i in range(10000): parts.append(str(i)) result = "".join(parts)join()可以一次性构建目标字符串,通常更适合大量片段的拼接。
不过需要理解:CPython 对部分+=场景具有优化,不能简单断言任何反复拼接一定都是 O(n²)。工程上优先使用join(),主要是因为性能更稳定、意图更明确。
七、对象身份、驻留与不可变性
字符串有时会被 Python 复用,这种现象称为字符串驻留(interning)。
a = "python" b = "python" print(a == b) # True这不代表所有内容相同的字符串都必须是同一个对象。
以下判断不可靠:
print(a is b)是否共享对象会受到解释器实现、编译优化和运行环境影响。
正确规则是:
比较文本内容用
==。判断是否为同一个对象才使用
is。不依赖字符串驻留结果编写业务逻辑。
在 CPython 中,字符串主要使用灵活的 Unicode 内部表示,根据字符范围选择合适的存储形式。它并不意味着每个字符都固定占用两个字节或四个字节。
本讲编程训练
按照字符串清洗、协议解析、算法实现三个方向递进。
第 1 题:机械臂指令标准化
输入:
" MOVE J1=90 J2=-45 "输出:
move j1=90 j2=-45要求去除首尾空白、将连续空白压缩为单个空格,并全部转为小写。
标准答案
command = " MOVE J1=90 J2=-45 " result = " ".join(command.split()).lower() print(result)解析:
split()默认处理连续空白。" ".join(...)用一个空格连接所有部分。lower()将大写字符转为小写。
这是一种适合普通空白分隔指令的文本规范化方式,但如果协议中的引号内空格具有特殊含义,就需要更严格的解析规则。
第 2 题:关节角度数据解析
输入:
" 90.5, -45.2, 0, 180, 30.75, -90 "要求解析成一个由浮点数组成的列表,结果如下:
[90.5, -45.2, 0.0, 180.0, 30.75, -90.0]并输出最大角度、最小角度与平均角度。
标准答案
data = " 90.5, -45.2, 0, 180, 30.75, -90 " angles = [] for item in data.split(","): angle = float(item.strip()) angles.append(angle) print("角度列表:", angles) print("最大角度:", max(angles)) print("最小角度:", min(angles)) average = sum(angles) / len(angles) print(f"平均角度:{average:.2f}")输出:
角度列表: [90.5, -45.2, 0.0, 180.0, 30.75, -90.0] 最大角度: 180.0 最小角度: -90.0 平均角度: 27.68这段代码假设输入非空,且各项都能转换成数字。对于实际接收的数据,还应逐步加入格式校验与异常处理。
第 3 题:字符串连续字符压缩
编写程序,实现简单的连续字符计数压缩。
示例:
输入:aaabbccccdd 输出:a3b2c4d2约束:
不能使用第三方库。
必须通过遍历实现。
要考虑空字符串。
分析时间复杂度。
标准答案
def compress(text): if not text: return "" result = [] count = 1 for i in range(1, len(text)): if text[i] == text[i - 1]: count += 1 else: result.append(text[i - 1] + str(count)) count = 1 result.append(text[-1] + str(count)) return "".join(result) print(compress("aaabbccccdd")) print(compress("abc")) print(compress(""))输出:
a3b2c4d2 a1b1c1注意最后的空行是因为第三次输出为空字符串。
算法原理:
从第二个字符开始,与前一个字符比较。相同则增加计数,不同则将前一组字符和数量写入结果列表,最后处理末尾的一组字符。
若输入长度为 \(n\):
时间复杂度:O(n),每个字符只需要常数次处理。
空间复杂度:O(n),最坏情况下结果存储空间与输入长度同阶。
这是一种游程编码(Run-Length Encoding)的简化形式。这里主要用于连续字符统计,并不是通用、无歧义的压缩格式,尤其当输入本身包含数字时需要额外设计解码规则。
本讲重点速查
关键问题 | 正确结论 |
|---|---|
字符串是否可变? | 不可变 |
索引越界和切片越界是否一样? | 不一样 |
| 不等价 |
| 不是,是去掉两端指定字符集合中的字符 |
| 否 |
| 否 |
| 3 |
| 9 |
| 不适合,应使用 |
大量拼接字符串优先使用什么? |
|