在日常开发中,我们常常会写出像 “金字塔” 一样的代码——多层 if 嵌套、循环套循环。这种代码不仅难以阅读,更容易在修改时引入 Bug。代码扁平化(Flattening) 就是一系列旨在减少嵌套层级、让主逻辑清晰浮现的技巧。代码扁平化通常还会引入一些副作用(Side effect),例如让代码的上下文长度变长。不过总的来说依旧是利大于弊的。
代码扁平化不是炫技,而是一种以可读性和可维护性为中心的工程实践。核心心法是:“让快乐路径(Happy Path)走直线,让异常情况早退出”。
本文将通过一个具体场景,一步步展示如何运用不同的扁平化策略,将一段深嵌套的代码重构为简洁、健壮、易维护的版本。
场景设置
假设我们有一组用户数据,每个用户包含姓名和订单列表,每个订单又包含 ID 和商品列表。我们的目标是:找到第一个购买了 “apple” 的有效订单,并打印出用户姓名和订单 ID。初始数据如下:
users = [
{"name": "Alice", "orders": [{"id": 101, "items": ["apple", "banana"]}]},
{"name": "Bob", "orders": [{"id": 102, "items": ["orange"]}, {"id": 103, "items": ["apple"]}]},
{"name": "Carro", "orders": []},
]
这是原始的深嵌套代码。形成了典型的 4 层嵌套,写法最直观但最不优雅。
def find_first_apple_order(users):
for user in users: # 第1层:遍历用户
if user.get("name"): # 第2层:检查用户名存在且非空
for order in user.get("orders", []): # 第3层:遍历该用户的订单
for item in order.get("items", []): # 第4层:遍历订单中的每个商品
if item == "apple":
print(f"{user['name']} bought an apple in order #{order['id']}")
问题分析:
- 可读性差:主逻辑(找 apple)被埋在第四层。
- 健壮性弱:如果
order中没有"id"字段,会直接抛出KeyError。 - 扩展困难:如果要增加新的校验条件(如订单必须已支付),嵌套会更深。
守卫语句(Guard Clauses) + 提前返回
守卫语句的核心思想是:在函数或循环开头快速排除非法情况,让主逻辑保持在外层。
def find_first_apple_order_exp2(users):
for user in users:
# 守卫1:用户数据必须包含有效的 name
if not user.get("name"):
print("Warning: Skipping user with missing/empty 'name'")
continue
# 守卫2:用户必须有 orders 列表
orders = user.get("orders")
if not isinstance(orders, list):
print("Warning: Skipping user with invalid 'orders'")
continue
for order in orders:
# 守卫3:订单必须包含 items 列表
items = order.get("items")
if not isinstance(items, list):
print("Warning: Skipping order with invalid 'items'")
continue
# 守卫4:订单必须有 id
if "id" not in order:
print("Warning: Skipping order with missing 'id'")
continue
# 主逻辑:检查是否包含 apple
if "apple" in items:
print(f"{user['name']} bought an apple in order #{order['id']}")
优化点:
- 使用
continue在循环中实现“提前退出”,避免了嵌套。 - 主逻辑(
if "apple" in items)清晰地出现在最外层。 - 增加了类型检查(
isinstance),提升了健壮性。
守卫模式 + 提前返回有一个显著优点,这种风格能用很规整的格式处理每一种错误场景,错误信息更具体,便于调试。
提取函数(Extract Function)
当某段逻辑足够独立时,将其提取成函数是降低复杂度的利器。
def is_valid_order_with_apple(order):
"""判断一个订单是否有效且包含 'apple'"""
if not isinstance(order.get("items"), list):
return False, None
if "id" not in order:
return False, None
if "apple" in order["items"]:
return True, order["id"]
return False, None
def find_first_apple_order_exp3(users):
for user in users:
# 用户级守卫
if not user.get("name") or not isinstance(user.get("orders"), list):
continue
for order in user["orders"]:
is_valid, order_id = is_valid_order_with_apple(order)
if is_valid:
print(f"{user['name']} bought an apple in order #{order_id}")
优化点:
- 将订单校验和业务逻辑封装在
is_valid_order_with_apple中,职责单一。 - 主函数
find_first_apple_order_exp3变得极其简洁,只关注“遍历用户 -> 遍历订单 -> 找到就返回”。 - 便于单元测试:可以单独测试
is_valid_order_with_apple。
函数式风格(使用 any + 生成器)
如果我们只关心 “是否存在”,而不需处理所有匹配项,可以用更声明式的方式。
def find_first_apple_order_exp4(users):
for user in users:
if not user.get("name") or not isinstance(user.get("orders"), list):
continue
# 使用生成器表达式查找第一个匹配的订单ID
apple_order_ids = (
order["id"]
for order in user["orders"]
if isinstance(order.get("items"), list)
and "id" in order
and "apple" in order["items"]
)
# 安全地获取第一个匹配项,无匹配则返回 `None`。
first_id = next(apple_order_ids, None)
if first_id is not None:
print(f"{user['name']} bought an apple in order #{first_id}")
用生成器表达式集中描述 “什么是有效的含 apple 订单”。
⚠注意:这种方式适合“找一个”的场景。如果要处理所有匹配项,还是用显式循环更清晰。
组合使用(守卫 + 提取函数 + 清晰主干)
综合以上优点,得到一个生产环境友好的版本:
def _has_valid_apple_order(orders):
"""内部工具函数:检查订单列表中是否有有效的含 apple 订单"""
for order in orders:
if (isinstance(order.get("items"), list)
and "id" in order
and "apple" in order["items"]):
return order["id"] # 返回第一个匹配的ID
return None
def find_first_apple_order(users):
"""
在用户列表中查找第一个购买了 'apple' 的有效订单。
成功找到返回 True,否则返回 False。
"""
for user in users:
# 快速守卫:跳过无效用户
if not user.get("name") or not isinstance(user.get("orders"), list):
continue
order_id = _has_valid_apple_order(user["orders"])
if order_id is not None:
print(f"{user['name']} bought an apple in order #{order_id}")
# 测试
if __name__ == "__main__":
users = [
{"name": "Alice", "orders": [{"id": 101, "items": ["apple", "banana"]}]},
{"name": "Bob", "orders": [{"id": 102, "items": ["orange"]}, {"id": 103, "items": ["apple"]}]},
{"name": "Carro", "orders": []},
]
find_first_apple_order(users)
经过努力,我们成功的将主函数压缩到了 8 行,逻辑一目了然。在代码编写的全过程都进行了错误前置,在函数前部快速判断函数错误退出条件,不干扰后面主干代码的逻辑。还对深层次函数逻辑进行了函数封装。整个代码易于测试和拓展。
其他小技巧
条件合并
如果对于 user["name"] 和 user["orders"] 存在性判断之后的处理逻辑相同,那么还能对这两个 if 判断进行合并。
if age < 15:
print("Too youny.")
return None
if ge > 18:
print("Too old.")
return None
go_to_high_school(user)
# 条件合并
if not age < 15 and age > 18:
go_to_high_school(user)
德·摩根定律
使用德·摩根定律简化逻辑。将复杂的 not (A and B) 转为 not A or not B,让否定更清晰。
if not (user.age >= 18 and user.has_id):
deny_access()
# 德·摩根定律
if user.age < 18 or not user.has_id:
deny_access()
return
其实这也是一种守卫风格。
字典映射
if action == "add":
result = a + b
elif action == "sub":
result = a - b
elif action == "mul":
result = a * b
# 字典映射
ops = {
"add": lambda a, b: a + b,
"sub": lambda a, b: a - b,
"mul": lambda a, b: a * b,
}
result = ops.get(action, lambda a, b: 0)(a, b)
仅适用于逻辑简单、副作用少的场景。
多态
当 if-elif-else 太多时,考虑用类和方法分发。
def get_sound(animal):
if animal == "dog":
return "Woof"
elif animal == "cat":
return "Meow"
elif animal == "cow":
return "Moo"
return "..."
下面是多态实现(震撼实现,长度翻倍)
from abc import ABC, abstractmethod
# 1. 抽象基类
class Animal(ABC):
@abstractmethod
def sound(self):
pass
# 2. 具体动物类
class Dog(Animal):
def sound(self): return "Woof"
class Cat(Animal):
def sound(self): return "Meow"
class Cow(Animal):
def sound(self): return "Moo"
# 3. 注册所有类型(代替 if-elif)
_ANIMALS = {
"dog": Dog,
"cat": Cat,
"cow": Cow,
}
# 4. 统一入口函数(接口和原来完全一样!)
def get_sound(animal: str) -> str:
cls = _ANIMALS.get(animal)
if cls is None:
return "..."
return cls().sound() # 创建实例并调用方法
# 使用(和原来一模一样!)
print(get_sound("dog")) # Woof
print(get_sound("cat")) # Meow
print(get_sound("bird")) # ...
⚠同时也可以看出,用 OOP 编程是多么的重。
总结
| 技巧 | 适用场景 | 关键词 |
|---|---|---|
| 守卫语句 | 函数/循环入口校验 | if ...: return/continue |
| 提前返回 | 多条件分支 | 消除 else 嵌套 |
| 提取函数 | 任何 >3 行的逻辑块 | 单一职责 |
| 合并条件 | 简单布尔判断 | and / or |
| 德·摩根定律 | 复杂否定逻辑 | not (A and B) → not A or not B |
| 字典映射 | 简单分发 | ops = {...} |
| 多态 | 类型驱动行为 | 面向对象 |
下次当你发现自己在写第三层 if 时,请停下来想一想:能不能用守卫语句把它提到外面?能不能抽成一个函数? 你的同事(以及未来的你)会感谢你的!
⚠附:所有扁平化技巧都应确保不改变原有逻辑和副作用。例如,日志打印、状态修改等操作的位置可能需要调整,以保证行为一致。
作业:上面的代码只会在每一个 user 的 orders 中返回第一个匹配上的 item-id。如何修改代码,来处理 orders 中存在多个 id 可供返回的场景?
示例
## 修改前
# 头重脚轻,整体代码复杂度分配不均匀
def error_handler(_error_counter, _error_info, silence_mode) -> bool:
"""错误处理函数"""
# 复杂逻辑排在前面了
if _error_counter >= cfg.maxCount:
logger.wr(f"连续错误次数达到上限 {cfg.maxCount},程序出现严重错误,需要立刻排查。{str(_error_info)}", logtype='error')
# 深层嵌套
if silence_mode:
return False
bot.send_msg(f"⚠️连续错误次数达到上限 {cfg.maxCount}\n⚠️程序出现严重错误,需要立刻排查\n⚠️{str(_error_info)}")
return False
# 简单逻辑排在后面了
else:
return True
## 修改后
def error_handler(_error_counter, _error_info, silence_mode) -> bool:
"""错误处理函数"""
# 未超过最大错误次数,返回 True
if _error_counter < cfg.maxCount:
return True
# 超过最大错误次数,触发告警
if _error_counter >= cfg.maxCount:
logger.wr(f"连续错误次数达到上限 {cfg.maxCount},程序出现严重错误,需要立刻排查。{str(_error_info)}", logtype='error')
if silence_mode:
bot.send_msg(f"⚠️连续错误次数达到上限 {cfg.maxCount}\n⚠️程序出现严重错误,需要立刻排查\n⚠️{str(_error_info)}")
return False