前言
for是 Python 中出现频率最高的关键字之一,但很多刚上手的人会默认「for就是按下标走一遍」,于是写出for i in range(len(data))这种从 C/Java 迁移过来的写法。Python 的for实际是迭代器协议(iterator protocol)驱动的:它调用iter(对象)拿到一个迭代器,然后反复调用next(),直到抛出StopIteration。理解这一点,才能解释为什么遍历字符串得到的是单个字符、遍历字典默认得到的是键而不是值。
另一个常见误解是「字符串也能像列表一样边遍历边删」。实际上str是不可变(immutable)类型,根本不支持原地删除;而dict、set在遍历过程中改变大小会直接抛RuntimeError。本文就把字符串、列表、字典这三类最常被for踩坑的对象的遍历流程讲清楚,并给出可复制的示例。
需要提前说明的是:本文全部使用 Python 3 的写法。Python 2.7 已于 2020 年 1 月 1 日停止维护,Python 2 里的dict.iteritems()、iterkeys()、itervalues()在 Python 3 中已被移除,请统一改用.items()、.keys()、.values()。
一、for 循环背后到底发生了什么
Python 的for x in obj:大致等价于下面这段手动展开的代码:
# 适用于 Python 3.8+
obj = [1, 2, 3]
it = iter(obj) # 取得迭代器
while True:
try:
x = next(it) # 取下一个元素
except StopIteration:
break # 迭代结束,安静地退出
print(x)这里有三点必须记住:
for只要求对象实现__iter__()(返回迭代器)或者__getitem__()(支持从 0 开始的整数下标),并不要求它是list。- 结束信号是异常
StopIteration,for会自己吞掉它,所以正常遍历不会看到这个异常。 - 迭代器是一次性的。同一个迭代器对象遍历完一遍后就是空的,想再遍历必须重新
iter()。
二、遍历字符串:得到的是字符
在 Python 3 里,str是 Unicode 码点序列,因此逐个遍历拿到的是长度为 1 的字符串:
# 适用于 Python 3.8+
s = "Python"
for ch in s:
print(ch)
# P
# y
# t
# h
# o
# n如果需要「字符 + 下标」,用内置函数enumerate(),不要自己维护计数器:
# 适用于 Python 3.8+
s = "abc"
for i, ch in enumerate(s):
print(i, ch)
# 0 a
# 1 b
# 2 cenumerate(iterable, start=0)返回的是(下标, 元素)这样的二元组,start可以指定起始编号。
需要注意:Python 3 里len("中文")是 2,遍历也确实是 2 次,因为每个汉字是一个码点。但如果字符串里含组合字符或 emoji(比如带肤色修饰符的表情),它们可能由多个码点组成,逐个遍历会得到「半个字」。这属于 Unicode 本身的特性,不是 bug。
三、遍历列表:不要在循环里改长度
列表是最直观的:for依次给出元素本身。
# 适用于 Python 3.8+
nums = [10, 20, 30]
for n in nums:
print(n)如果确实需要下标,有两条路:
# 适用于 Python 3.8+
nums = [10, 20, 30]
# 方式 A:enumerate,推荐
for i, n in enumerate(nums):
print(i, n)
# 方式 B:range(len(...)),能用但不 Pythonic
for i in range(len(nums)):
print(i, nums[i])在遍历列表时增删元素是典型的坑。下面的代码不会报错,但结果和直觉不符:
# 适用于 Python 3.8+
nums = [1, 2, 3, 4, 5]
for n in nums:
if n % 2 == 0:
nums.remove(n)
print(nums) # [1, 3, 5]这里2被删掉了,4却留了下来——因为删除2之后,后面的元素整体前移,迭代器的内部下标已经越过4。正确做法是遍历副本或者用列表推导式生成新列表:
# 适用于 Python 3.8+
nums = [1, 2, 3, 4, 5]
odds = [n for n in nums if n % 2 != 0]
print(odds) # [1, 3, 5]四、遍历字典:默认给键,不是给键值对
这是初学者最容易踩的一个点。直接for k in d拿到的是键:
# 适用于 Python 3.8+
user = {"name": "Alice", "age": 30}
for k in user: # 等价于 for k in user.keys()
print(k)
# name
# age要同时拿到键和值,用.items():
# 适用于 Python 3.8+
user = {"name": "Alice", "age": 30}
for k, v in user.items():
print(k, "=", v)
# name = Alice
# age = 30.keys()、.values()、.items()在 Python 3 里返回的是视图(view)对象,不是列表。视图是动态的:字典变了,视图跟着变;但它不支持下标访问,需要列表时得显式list(user.keys())。
从Python 3.7 起,字典保证按插入顺序迭代(3.6 是 CPython 的实现细节,不应依赖)。所以下面的输出顺序是确定的:
# 适用于 Python 3.7+
d = {}
d["b"] = 1
d["a"] = 2
print(list(d)) # ['b', 'a'],按插入顺序三种可迭代对象的对比:
| 对象类型 | for x in obj得到 | 是否可变 | 遍历中改大小会怎样 |
|---|
str | 单个字符(长度 1 的str) | 不可变 | 无法原地修改,不涉及 |
list | 元素本身 | 可变 | 不报错,但结果可能不符合预期 |
dict | 键(key) | 可变 | 抛RuntimeError |
set | 元素本身,顺序不保证 | 可变 | 抛RuntimeError |
五、实战:一次遍历三种结构
下面这段程序演示了对字符串、列表、字典的统一处理流程,可直接存为demo.py运行:
# 适用于 Python 3.8+
def iter_text(text):
for i, ch in enumerate(text):
print("字符", i, "->", ch)
def iter_nums(nums):
total = 0
for n in nums:
total += n
return total
def iter_mapping(mapping):
result = []
for key, value in mapping.items():
result.append(f"{key}:{value}")
return ", ".join(result)
if __name__ == "__main__":
iter_text("abc")
print("求和 =", iter_nums([1, 2, 3, 4]))
print(iter_mapping({"name": "Alice", "age": 30}))输出:
字符 0 -> a
字符 1 -> b
字符 2 -> c
求和 = 10
name:Alice, age:30常见坑点
- 遍历字典时以为拿到的是键值对
❌for x in user: print(x, user[x])里x是键,写法啰嗦又容易和值搞混 ✅for k, v in user.items(): print(k, v)直接用.items()
- 遍历列表时删除元素
❌for n in nums: if cond(n): nums.remove(n)会漏删、错删 ✅nums = [n for n in nums if not cond(n)]或for n in nums[:]:
- 遍历字典时新增或删除键
❌for k in d: d["new"] = 1抛RuntimeError: dictionary changed size during iteration✅ 先收集要改的键keys = list(d),遍历keys再去改d
- 用
range(len(...))硬凑下标
❌for i in range(len(s)): print(s[i])✅for ch in s: print(ch);需要下标时用enumerate(s)
- 以为
.keys()返回列表可以下标访问
❌d.keys()[0]在 Python 3 里抛TypeError(视图不支持下标) ✅list(d.keys())[0],或者直接用next(iter(d))
- 把迭代器当成可重复遍历的对象
❌it = iter(nums)之后sum(it)再用sum(it),第二次得到0✅ 需要多次遍历就每次都从原容器iter(),或先把迭代器转成list
- 沿用 Python 2 的
iteritems()
❌for k, v in d.iteritems():在 Python 3 中是AttributeError(该方法已移除) ✅for k, v in d.items():(Python 3 的.items()本身就是惰性视图,不再需要iter*变体)
- 遍历字符串时按「可见字符」数下标
❌for i, ch in enumerate("👍"):期望只循环一次,实际可能循环多次(组合码点) ✅ 明确需求:按码点循环就用for ch in s;按字节循环就s.encode("utf-8")后遍历
总结
| 遍历目标 | 推荐写法 | 关键点 |
|---|
| 字符串 | for ch in s | 得到单个字符,Python 3 按码点 |
| 需要下标 | for i, x in enumerate(obj) | 默认从 0 开始,可传start |
| 列表元素 | for x in lst | 循环内别改长度 |
| 字典键 | for k in d | 3.7+ 保证插入顺序 |
| 字典键值 | for k, v in d.items() | 视图对象,实时反映字典 |
| 两个序列并行 | for a, b in zip(x, y) | 以短的那个为准 |
for循环的语义由迭代器协议决定,而不是下标。记住「字符串给字符、列表给元素、字典给键」这条主线,再配合enumerate()和.items(),绝大多数遍历写法就不会出错。涉及增删的场合,一律先复制或改为生成新对象,避免掉进「改了却看不到效果」的陷阱。