Python的循环和迭代器看似简单,但若使用不当,代码可能变得冗长、难以维护,甚至隐藏bug。本文基于《Effective Python 3rd Edition》第三章,提炼出四个核心技巧,帮你写出更优雅、更高效的Python代码。无论你是刚入门的新手,还是经验丰富的开发者,这些技巧都能让你的代码质量更上一层楼。

技巧一:用enumerate替代range,让索引更清晰
内置的range函数在循环整数序列时非常有用,例如生成一个32位随机数:
from random import randint
random_bits = 0
for i in range(32):
if randint(0, 1):
random_bits |=1 << i
print(bin(random_bits))
>>>
0b11101000100100000111000010000001

当你需要遍历一个列表并同时获取索引时,很多新手会这样写:
flavor_list = ["vanilla", "chocolate", "pecan", "strawberry"]
for flavor in flavor_list:
print(f"{flavor} is delicious")
>>>
vanilla is delicious
chocolate is delicious
pecan is delicious
strawberry is delicious

但这样写既笨拙又容易出错——需要手动获取列表长度、通过索引访问元素。Python提供了更优雅的解决方案:enumerate。它会生成一个惰性迭代器,每次返回(索引, 值)的元组:
for i in range(len(flavor_list)):
flavor = flavor_list[i]
print(f"{i + 1}: {flavor}")
>>>
1: vanilla
2: chocolate
3: pecan
4: strawberry
用next()手动推进迭代器可以更清楚地看到它的工作原理:
it = enumerate(flavor_list)
print(next(it))
print(next(it))
>>>
(0, 'vanilla')
(1, 'chocolate')

在for循环中直接解包元组,代码瞬间清晰:
for i, flavor in enumerate(flavor_list):
print(f"{i + 1}: {flavor}")
>>>
1: vanilla
2: chocolate
3: pecan
4: strawberry

还可以通过第二个参数指定起始索引(比如从1开始):
for i, flavor in enumerate(flavor_list, 1):
print(f"{i}: {flavor}")

最佳实践:始终优先使用enumerate而非range(len(...))。它让代码更简洁、更易读,并且避免了索引越界的风险。
技巧二:用zip并行处理多个迭代器
处理多个相关列表时,zip是你的好帮手。例如,有一个姓名列表和对应的字符数列表:
names = ["Cecilia", "Lise", "Marie"]
counts = [len(n) for n in names]
print(counts)
>>>
[7, 4, 5]

如果使用range手动索引,代码会显得杂乱:
longest_name = None
max_count =0
for i in range(len(names)):
count = counts[i]
if count > max_count:
longest_name = names[i]
max_count = count
print(longest_name)
>>>
Cecilia

即使改用enumerate,也仍然需要显式索引counts[i]:
longest_name = None
max_count = 0
for i, name in enumerate(names): # Changed
count = counts[i]
if count > max_count:
longest_name = name # Changed
max_count = count
zip 的出现解决了这个问题。它将多个迭代器“拉链”在一起,生成一个惰性迭代器,每次返回一个包含所有迭代器当前元素的元组:
zipzipzip
longest_name = None
max_count =0
for name, count in zip(names, counts): # Changed
if count > max_count:
longest_name = name
max_count = count
⚠️ 注意:zip默认以最短的迭代器为准,会静默截断。如果输入长度不一致,这可能引入难以察觉的bug:
names.append("Rosalind")
for name, count in zip(names, counts):
print(name)
>>>
Cecilia
Lise
Marie

Python 3.10引入了strict参数,当长度不匹配时会抛出异常:
for name, count in zip(names, counts, strict=True): # Changed
print(name)
>>>
Cecilia
Lise
Marie
Traceback ...
ValueError: zip() argument 2 is shorter than argument 1

如果需要填充缺失值,可以使用itertools.zip_longest(参见Item 24)。推荐始终使用strict=True,除非你明确知道输入长度相同。
技巧三:永远不要在for/while循环后使用else块
Python的循环有一个独特的特性:可以在循环体后直接跟else块:
for i in range(3):
print("Loop", i)
else:
print("Else block!")
>>>
Loop 0
Loop 1
Loop 2
Else block!

很多新手会误以为else表示“如果循环没有执行”,但事实恰恰相反——只有当循环正常结束(未遇到break)时,else块才会执行:
for i in range(3):
print("Loop", i)
if i == 1:
break
else:
print("Else block!")
>>>
Loop 0
Loop 1
更令人困惑的是,如果循环从未执行(比如迭代空序列),else块也会运行:
for x in []:
print("Never runs")
else:
print("For else block!")
>>>
For else block!
while False:
print("Never runs")
else:
print("While else block!")
>>>
While else block!

虽然这种设计在特定场景下(如判断互质数)看似有用:
a = 4
b = 9
for i in range(2, min(a, b) + 1):
print("Testing", i)
if a % i == 0 and b % i == 0:
print("Not coprime")
break
else:
print("Coprime")
>>>
Testing 2
Testing 3
Testing 4
Coprime

但实际上,更好的做法是使用辅助函数:
- 方法一:尽早返回——找到结果就return,循环结束后返回默认值:
def coprime(a, b): for i in range(2, min(a, b) + 1): if a % i == 0 and b % i == 0: return False return True assert coprime(4, 9) assert not coprime(3, 6)
- 方法二:使用结果变量——找到后跳出循环,最后返回变量:
def coprime_alternate(a, b): is_coprime = True for i in range(2, min(a, b) + 1): if a % i == 0 and b % i == 0: is_coprime = False break return is_coprime assert coprime_alternate(4, 9) assert not coprime_alternate(3, 6)
核心建议:完全避免在循环后使用else块。它违反了直觉,给代码维护者(包括未来的自己)带来不必要的认知负担。在Python、JavaScript、Java、C++等主流语言中,这种模式都不存在,说明它并非必要。
⚠️ 技巧四:循环结束后切勿使用循环变量
Python中,for循环的迭代变量在循环结束后仍然存在:
for i in range(3):
print(f"Inside {i=}")
print(f"After {i=}")
>>>
Inside i=0
Inside i=1
Inside i=2
After i=2

有些人会利用这个特性来简化代码,比如分组算法中:
categories = ["Hydrogen", "Uranium", "Iron", "Other"]
for i, name in enumerate(categories):
if name == "Iron":
break
print(i)
>>>
2

当找不到元素时,循环变量会保留最后一个值:
for i, name in enumerate(categories):
if name == "Lithium":
break
print(i)
>>>
3

但这种方法存在严重隐患——如果循环从未执行(空迭代器),变量根本不存在:
categories = []
for i, name in enumerate(categories):
if name == "Lithium":
break
print(i)
>>>
Traceback ...
NameError: name 'i' is not defined
相比之下,列表推导式和生成器表达式不会泄漏循环变量:
my_numbers = [37, 13, 128, 21]
found =[i for i in my_numbers if i % 2 == 0]
print(i) # Always raises
>>>
Traceback ...
NameError: name 'i' is not defined
最佳实践:永远不要在循环结束后依赖循环变量。如果需要保留某个值,在循环内部显式赋值给一个新变量。这样代码更清晰,也避免了空迭代器导致的运行时错误。
[AFFILIATE_SLOT_2]总结
本文提炼了Python循环与迭代器的四个核心技巧:
- ✅ 用
enumerate替代range获取索引 - ✅ 用
zip并行处理多个迭代器,并考虑使用strict=True - ✅ 绝对不要在循环后使用
else块 - ✅ 循环结束后不要依赖循环变量
这些原则不仅适用于Python,也适用于TypeScript、JavaScript、Java等其他语言中的类似模式。掌握它们,你的代码将更加简洁、健壮、易于维护。
浙公网安备 33010602011771号