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,除非你明确知道输入长度相同。

[AFFILIATE_SLOT_1]

技巧三:永远不要在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等其他语言中的类似模式。掌握它们,你的代码将更加简洁、健壮、易于维护。