代码评审里最常见的一类问题,不是架构设计,也不是算法复杂度,而是循环。一个简单的遍历,能写出五六种风格。有人执着于 for i in range(len(list)),有人钟情列表推导式,有人到处嵌套 zip,还有人把字典遍历写得像在拆炸弹。代码能跑,但味道不对。
循环是所有程序员的“基本功”。每天写,天天用,却很少有人认真对待它。真正的差距,不在于会不会写循环,而在于能不能写出清晰、优雅、意图明确的循环。
很多项目里依然能看到这样的代码:
for i in range(len(fruits)): print(fruits[i])
这类写法在C语言时代是常态,但在Python里,其实已经是“过时思维”。Python的for本质是迭代器协议,它不是围绕索引设计的,而是围绕对象本身设计的。直接遍历元素,才是顺着语言的设计走。
fruits = ['apple', 'banana', 'cherry'] for fruit in fruits: print(fruit)
这种写法看起来简单,但它背后的机制值得理解。for循环会调用对象的__iter__()方法,拿到一个迭代器,然后不断调用__next__(),直到抛出StopIteration。整个过程是惰性、流式的,不会一次性把所有数据压进内存。这也是为什么Python能优雅处理超大数据流。
再看字符串、字典、集合,本质都是可迭代对象。
遍历字符串
for char in "Python": print(char)
遍历字典的键
user = {'name': 'Alice', 'age': 25} for key in user: print(key, user[key])
遍历集合
tags = {'python', 'java', 'go'} for tag in tags: print(tag)
写循环的第一层境界,是“写对”。第二层境界,是“写顺”。顺着语言的哲学走,而不是对抗它。
很多时候需要的不只是值,还需要索引。有人会退回到range(len()),其实Python早就给了更优雅的工具——enumerate()。
languages = ['Python', 'Java', 'C++', 'Go'] for index, lang in enumerate(languages): print(f"{index}: {lang}")
enumerate()并不会生成额外列表,它返回的是一个迭代器对象,在遍历时动态生成索引和值的组合。内存开销几乎可以忽略。甚至还能指定起始值。
for index, lang in enumerate(languages, start=1): print(f"第{index}名: {lang}")
这种写法在日志分析中非常常见。比如逐行扫描文件时,需要记录行号。
with open('data.txt', 'r') as f: for line_num, line in enumerate(f, start=1): if 'ERROR' in line: print(f"第{line_num}行发现错误: {line.strip()}")
当代码表达的是“第几行出了问题”,而不是“第几个索引”,可读性会瞬间提升。很多Bug不是逻辑错误,而是意图表达不清。
再往复杂一点的场景走,多序列并行遍历。如果还在用索引同步两个列表,代码会迅速变得难看。
names = ['Alice', 'Bob', 'Charlie'] ages = [25, 30, 35] cities = ['Beijing', 'Shanghai', 'Shenzhen']
for name, age, city in zip(names, ages, cities): print(f"{name}, {age}岁, 来自{city}")
zip()像一条拉链,把多个序列按位置拼接。它遵循短板原则,长度由最短序列决定。
list1 = [1, 2, 3, 4, 5] list2 = ['a', 'b', 'c'] for num, char in zip(list1, list2): print(num, char)
只会输出3对
这种行为在大多数情况下是安全的,因为它避免了越界错误。但如果业务要求对齐最长序列,就需要zip_longest。
from itertools import zip_longest
for num, char in zip_longest(list1, list2, fillvalue='N/A'): print(num, char)
zip()不仅用于遍历,还常常用来构建字典。
keys = ['name', 'age', 'city'] values = ['Alice', 25, 'Beijing'] user_dict = dict(zip(keys, values))
{'name': 5, 'Bob': 3, 'Charlie': 7}
写到这里会发现,Python的循环设计更像“数据流组合”,而不是“索引驱动”。
字典遍历则是另一个常见误区。有些代码仍然在for key in dict后面手动取值。更推荐的是items()。
user = {'name': 'Alice', 'age': 25, 'city': 'Beijing'}
最推荐的方式
for key, value in user.items(): print(f"{key}: {value}")
items()返回的是视图对象,不是列表。不会额外占用内存。Python 3.7以后字典保持插入顺序,使遍历结果更加可预测。keys()、values()各有用途,但核心原则是——根据需求选择表达最清晰的方式。
嵌套字典场景更能体现思路清晰的重要性。
students = { 'class1': {'Alice': 90, 'Bob': 85}, 'class2': {'Charlie': 92, 'David': 88} }
for class_name, students_dict in students.items(): print(f"{class_name}:") for student, score in students_dict.items(): print(f" {student}: {score}分")
这种写法层次分明。阅读代码的人能立刻看到数据结构的形状。
再看列表推导式。它是Python里最具标志性的语法之一。很多数据转换场景,写成传统循环会显得冗长。
numbers = [1, 2, 3, 4, 5] squares = [x**2 for x in numbers]
[1, 4, 9, 16, 25]
带条件过滤时,表达力更强。
even_squares = [x**2 for x in numbers if x % 2 == 0]
[4, 16]
嵌套结构同样优雅。
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] flattened = [num for row in matrix for num in row]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
字典和集合也支持推导式。
users = ['Alice', 'Bob', 'Charlie'] user_dict = {name: len(name) for name in users}
{'Alice': 5, 'Bob': 3, 'Charlie': 7}
numbers = [1, 2, 2, 3, 3, 3, 4] unique_squares = {x**2 for x in numbers}
{1, 4, 9, 16}
推导式通常更快,因为在C层做了优化。但真正的优势在于表达力。代码更像数学公式,而不是步骤说明。
当数据量变大时,生成器表达式的价值就显现出来。
squares_list = [x**2 for x in range(1000000)]
squares_gen = (x**2 for x in range(1000000))
for square in squares_gen: if square > 100: break
生成器是惰性求值的。只有在需要时才计算下一个值。这种“按需生产”的机制,在处理日志流、大数据或无限序列时,几乎是唯一可行方案。
性能测试往往能打破一些直觉。
import timeit
data = list(range(100000))
方式1:直接遍历
def test1(): result = [] for item in data: result.append(item * 2) return result
方式2:使用enumerate
def test2(): result = [] for i, item in enumerate(data): result.append(item * 2) return result
方式3:列表推导式
def test3(): return [item * 2 for item in data]
print(f"直接遍历: {timeit.timeit(test1, number=100):.4f}秒") print(f"enumerate: {timeit.timeit(test2, number=100):.4f}秒") print(f"列表推导式: {timeit.timeit(test3, number=100):.4f}秒")
通常列表推导式会略快。但这种差距在大多数业务场景中并不致命。真正致命的是可读性差、逻辑混乱、变量命名随意。
写循环这件事,本质是在表达“数据如何流动”。表达清晰,比微小的性能差异重要得多。
选择方式其实很简单:只要值,用for-in;需要索引,用enumerate;多序列并行,用zip;字典优先items;数据转换,用推导式;数据量巨大,用生成器。
很多人觉得这些只是语法糖,但语法糖用多了,会改变思维方式。代码开始像自然语言一样流畅,而不是像拼装零件。
一个团队的代码风格,往往从这些细节里体现。循环写得清晰,函数通常也不会太糟糕。循环写得混乱,项目八成难维护。
写Python久了会发现,语言本身已经把最优雅的路径摆在那里。关键在于是否愿意放下旧习惯,顺着语言设计去写。
当循环不再是“机械重复”,而是“清晰表达”,代码的质感会完全不同。而这种质感,往往决定了一个项目能走多远,甚至决定了一个工程师能走多远……