循环是Python里写得最多的结构,但大多数人写来写去就是for x in list加range(len())。那些能省掉嵌套、避免flag变量、让循环自解释的技巧,往往用到的时候才后悔没早点知道。
1. for-else:没被break才执行的else
Python的for...else可能是最被误解的语法——else不是在循环结束后执行,而是在循环没有被break打断时才执行。
经典场景:搜索元素,找到了处理,找不到做默认操作。不需要额外flag变量:
# 传统写法:需要found标志
deffind_admin(users):
found = False
for user in users:
if user["role"] == "admin":
print(f"管理员: {user['name']}")
found = True
break
ifnot found:
print("未找到管理员")
# for-else写法
deffind_admin(users):
for user in users:
if user["role"] == "admin":
print(f"管理员: {user['name']}")
break
else:
print("未找到管理员")
验证质数也很自然:
defis_prime(n):
if n < 2:
returnFalse
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
returnFalse
else:
returnTrue
print(is_prime(97)) # True
print(is_prime(100)) # False
for-else的语义可以这么记:else属于for,不属于if——"如果循环跑完了全程没被打断"。
2. 干净地跳出多层嵌套循环
Python没有goto,也没有带标签的break。嵌套循环里内层要跳出外层,最常见的做法是设一个flag——代码瞬间膨胀:
# ❌ flag变量污染
found = False
for i in range(100):
for j in range(100):
if matrix[i][j] == target:
result = (i, j)
found = True
break
if found:
break
三种更干净的方式:
方式一:封装成函数,用return直接跳出
deffind_target(matrix, target):
for i, row in enumerate(matrix):
for j, val in enumerate(row):
if val == target:
return i, j
returnNone
pos = find_target(matrix, target)
这是最Pythonic的方案——return天然能跳出所有嵌套层级,同时把逻辑封装成了可测试的函数。
方式二:for-else配合continue接力
for i in range(100):
for j in range(100):
if matrix[i][j] == target:
break
else:
continue# 内层没break,继续外层
break# 内层break了,跳出外层
技巧在于else+continue+break三重配合:内层正常结束→else触发→continue跳到外层下一轮。内层break→跳过else→执行break跳出外层。
方式三:itertools.product扁平化嵌套
from itertools import product
for i, j in product(range(100), range(100)):
if matrix[i][j] == target:
result = (i, j)
break
product把两层循环压成一层,一个break就够了。代价是去掉了行列之间的逻辑分层——适合纯遍历场景。
3. 迭代时删除元素:倒着来
在遍历列表的同时删除元素,正向遍历会跳过元素(因为索引前移了):
# ❌ 错误:会跳过元素
numbers = [1, 2, 3, 4, 5, 6]
for i in range(len(numbers)):
if numbers[i] % 2 == 0:
del numbers[i] # 索引混乱
# ✅ 反向遍历删除
numbers = [1, 2, 3, 4, 5, 6]
for i in range(len(numbers) - 1, -1, -1):
if numbers[i] % 2 == 0:
del numbers[i]
print(numbers) # [1, 3, 5]
反向删除不会影响前面元素的索引。当然更推荐的方式是新建列表(数据量不大时):
numbers = [n for n in numbers if n % 2 != 0]
但如果必须原地修改(比如列表被多个引用持有),反向删除是最安全的。
4. 用else子句消除一次性布尔变量
很多循环需要回答"有没有发生过某种情况"——比如"这些订单里有没有超时的"。传统写法设一个has_timeout = False然后循环里改它。其实一步到位:
# ❌ 需要一个has_timeout变量
has_timeout = False
for order in orders:
if order["elapsed"] > 30:
has_timeout = True
break
if has_timeout:
send_alert()
# ✅ any()短路求值
if any(order["elapsed"] > 30for order in orders):
send_alert()
# 反之,"全部满足"用all()
if all(order["status"] == "completed"for order in orders):
close_batch()
any()和all()用的是生成器表达式,遇第一个结果就短路返回,不遍历完整列表。
5. walrus在while循环中的应用
Python 3.8的海象运算符让while循环的"读取-判断-处理"模式少写一行:
# 传统写法:读取和判断分离
line = file.readline()
while line:
process(line)
line = file.readline()
# walrus写法:读取和判断合一
while (line := file.readline()):
process(line)
更实用的场景——分批从API拉数据:
import requests
deffetch_all_pages(url_template):
results = []
page = 1
while (resp := requests.get(url_template.format(page), timeout=10)).ok and resp.json():
results.extend(resp.json())
page += 1
return results
分页拉取的经典四步——发请求、判成功、取数据、页码递增——全部压缩在循环条件里,循环体只负责积累结果。逻辑分离得非常干净。
6. 批量迭代:一次处理N个元素
需要按固定大小分组处理时,手动写索引计数很啰嗦:
# Python 3.12+ 直接用itertools.batched
from itertools import batched
data = list(range(100))
for batch in batched(data, 16):
process_batch(list(batch))
Python 3.12之前可以用zip+iter的技巧一行实现:
defgrouper(iterable, n):
"""把迭代器按每组n个元素分组,不足的用None填充"""
args = [iter(iterable)] * n
return zip_longest(*args)
for batch in grouper(range(100), 16):
valid = [x for x in batch if x isnotNone]
process_batch(valid)
原理:[iter(iterable)] * n创建n个指向同一个迭代器的引用。zip每次从每个引用取一个元素——但由于它们都指向同一个迭代器,实际效果就是每次取n个。
7. 同时遍历两个序列的对齐方式
两个列表长度可能不一致时,zip和zip_longest的选择影响结果的对错:
from itertools import zip_longest
names = ["张三", "李四", "王五", "赵六"]
scores = [85, 92]
# zip:静默截断到最短的那个(数据丢失!)
for name, score in zip(names, scores):
print(f"{name}: {score}")
# 张三: 85
# 李四: 92
# ← 王五和赵六的数据丢了,不报错
# Python 3.10+ strict模式:不等长立即抛异常
for name, score in zip(names, scores, strict=True):
print(f"{name}: {score}")
# ValueError: zip() argument 2 is shorter than argument 1
# zip_longest:短的用默认值填充
for name, score in zip_longest(names, scores, fillvalue=0):
print(f"{name}: {score}")
# 张三: 85
# 李四: 92
# 王五: 0
# 赵六: 0
数据处理流水线里,strict=True应该默认开启——静默截断导致数据丢失比程序报错危险得多。
8. 循环中避开属性查找开销
循环体内反复通过点号访问对象属性,每次都要走属性查找。把属性提到循环外绑定到局部变量,能省掉重复的查找开销:
# ❌ 每次迭代都做 list.append 属性查找
result = []
for item in huge_list:
result.append(process(item))
# ✅ 把方法绑定到局部变量
result = []
append = result.append
for item in huge_list:
append(process(item))
实测1000万次循环,局部变量绑定的版本能快15-20%。类似的,len、range等内置函数也可以提出来:
# 循环内反复调len
for i in range(len(data)):
...
# 提出来
n = len(data)
for i in range(n):
...
这个技巧在简单循环里看起来过度优化,但如果你写的循环要跑几百万次——比如数据处理pipeline、图像像素遍历——能省出明显的时间。
9. 反向迭代:reversed比你写的反向range更好
要反向遍历列表,很多人会写range(len(seq)-1, -1, -1)。reversed()更清晰:
items = ["a", "b", "c", "d"]
# ❌ 绕
for i in range(len(items) - 1, -1, -1):
print(items[i])
# ✅ 直观
for item in reversed(items):
print(item)
reversed()返回的是反向迭代器,不创建新列表。如果要同时拿索引,用enumerate配合reversed:
for i, item in enumerate(reversed(items)):
print(f"倒数第{i+1}个: {item}")
10. 循环结束后的清理:用上下文管理器替代try/finally
如果循环里打开了多个资源(文件、网络连接),退出时需要确保全部关闭。与其在循环外套一层try/finally,不如每个资源迭代时用上下文管理器:
# ❌ 手动管理一堆文件
files = []
try:
for name in filenames:
f = open(name)
files.append(f)
for f in files:
process(f)
finally:
for f in files:
f.close()
# ✅ ExitStack统一管理
from contextlib import ExitStack
with ExitStack() as stack:
files = []
for name in filenames:
try:
f = stack.enter_context(open(name))
files.append(f)
except FileNotFoundError:
print(f"跳过: {name}")
for f in files:
process(f.read())
# 退出with块时,所有打开的文件自动关闭
ExitStack还能动态注册清理回调:
with ExitStack() as stack:
temp_dir = Path("temp_work")
temp_dir.mkdir()
stack.callback(shutil.rmtree, temp_dir) # 退出时自动删除临时目录
for item in data:
result_file = temp_dir / f"{item['id']}.txt"
result_file.write_text(process(item))
# ... 中间异常了也不要紧,shutil.rmtree一定会执行
这10个技巧覆盖了循环从写法到性能的常见痛点。关键不在于记住每一个——在于下次写循环时多问一句:这个flag变量能删吗?这层嵌套真的需要吗?这个属性查找在循环里重复了几百万次?多问一句,代码就干净一层。