咱做Python开发的,谁没见过几行让人头疼的“烂代码”?接手老项目时,满屏的魔法数字、嵌套八层的循环、几百行功能混杂的巨型函数,改一个Bug可能引出一串新问题。再看看自己半年前写的代码,有时也会困惑:这玩意儿是我写的?怎么这么难懂?
更关键的是,糟糕的代码不仅自己维护起来费劲,团队协作效率低下,线上问题排查更是耗时耗力。Python本身是一门以简洁、优雅为设计哲学的语言,写出“烂代码”往往不是能力问题,而是缺乏良好的编码习惯和基础的重构意识。
代码重构并非炫技或无意义的重写,其核心是在不改变外部行为的前提下,优化代码的内部结构,使其更易读、易维护、更健壮。本文整理了10个立即可用的Python重构技巧,每个技巧都通过 “问题代码”与“优雅实现” 的对比来呈现。掌握它们,你的代码质量和工程效率将显著提升。
一、提取魔法数/字符串为常量
问题场景
代码中随处可见无意义的字面量数字、硬编码的字符串。其他人阅读时难以理解其含义,需要修改时则必须全局搜索,漏改一处就可能引发Bug,典型的“编写一时爽,维护火葬场”。
Before 问题代码
# 谁知道3.1415926是啥?60*60又是什么意思?
# 接口地址硬编码,改地址要改遍所有代码
def calculate_circle_area(radius):
return 3.1415926 * radius * radius
def get_user_info(user_id):
res = requests.get("https://api.xxx.com/v1/user/info", params={"id": user_id}, timeout=30)
if res.status_code == 200:
return res.json()
def cal_oneday_seconds():
return 60 * 60 * 24
After 优雅代码
import requests
# 提取所有魔法数/字符串为常量,大写命名+注释说明,语义清晰
PI = 3.1415926 # 圆周率
ONE_DAY_SECONDS = 86400 # 一天的秒数
USER_INFO_API = "https://api.xxx.com/v1/user/info" # 用户信息接口地址
REQUEST_TIMEOUT = 30 # 接口请求超时时间
HTTP_SUCCESS_CODE = 200 # HTTP请求成功状态码
def calculate_circle_area(radius):
return PI * radius * radius
def get_user_info(user_id):
res = requests.get(USER_INFO_API, params={"id": user_id}, timeout=REQUEST_TIMEOUT)
if res.status_code == HTTP_SUCCESS_CODE:
return res.json()
def cal_oneday_seconds():
return ONE_DAY_SECONDS
改进原理
- 语义化:常量命名配合注释,使代码自解释,降低对额外文档的依赖。
- 易维护:修改时只需调整常量定义一处,即可全局生效,避免遗漏。
- 规范统一:符合 Python PEP8 规范,常量使用全大写加下划线分隔,保持代码风格一致。
适用场景
代码中出现的、固定不变且可能被多次使用的数字、字符串,例如配置参数、状态码、数学常量、API地址等。
二、用列表推导式替换循环
问题场景
为了生成一个新列表,编写冗长的 for 循环配合 append 操作,代码行数多且逻辑不够直观,未能体现Python的简洁特性。
Before 问题代码
# 生成1-10的偶数列表,写了3行代码,存在冗余
even_nums = []
for num in range(1, 11):
if num % 2 == 0:
even_nums.append(num)
# 提取列表中所有字符串的长度,又是一堆循环
str_list = ["Python", "重构", "技巧", "优雅"]
len_list = []
for s in str_list:
len_list.append(len(s))
After 优雅代码
# 列表推导式,一行搞定,逻辑直观
even_nums = [num for num in range(1, 11) if num % 2 == 0]
# 映射操作也能轻松处理
str_list = ["Python", "重构", "技巧", "优雅"]
len_list = [len(s) for s in str_list]
# 甚至可以包含计算逻辑
num_list = [1,2,3,4]
square_list = [n**2 for n in num_list] # 生成平方数列表
改进原理
- 简洁高效:将多行循环浓缩为一行,减少模板代码,提升可读性。
- 执行更快:列表推导式在Python解释器层面进行了优化,通常比手动
for 循环加 append 效率更高。
- 逻辑连贯:将数据遍历、条件判断、结果处理集中在一处表达,无需在代码行间跳转阅读。
适用场景
简单的列表生成与转换场景,特别是涉及遍历、过滤和简单映射的情况。注意:推导式嵌套不宜超过两层,否则会影响可读性。
三、用上下文管理器 (with) 管理资源
问题场景
手动打开文件、建立数据库连接后,可能忘记调用 close() 方法导致资源泄漏;或者即使写了 close(),但在代码执行过程中抛出异常,close() 未能执行,资源同样无法释放。
Before 问题代码
# 手动打开文件,若忘记或异常跳过close(),将导致文件句柄泄漏
f = open("test.txt", "r", encoding="utf-8")
content = f.read()
# 此处如果抛出异常,下面的close()根本执行不到
print(content)
# f.close() # 容易被遗忘或跳过
# 数据库连接同理,手动关闭极易遗漏
import pymysql
conn = pymysql.connect(host="localhost", user="root", password="123456", db="test")
cursor = conn.cursor()
cursor.execute("select * from user")
conn.commit()
# 忘记关闭游标和连接,数据库连接池资源被占满
# cursor.close()
# conn.close()
After 优雅代码
# with上下文管理器,自动确保文件关闭,无论是否抛出异常
with open("test.txt", "r", encoding="utf-8") as f:
content = f.read()
print(content)
# 数据库连接也能用with管理(部分库需适配上下文协议)
import pymysql
with pymysql.connect(host="localhost", user="root", password="123456", db="test") as conn:
with conn.cursor() as cursor:
cursor.execute("select * from user")
conn.commit()
# 退出with块后,cursor和conn会自动关闭,无需手动操作
改进原理
上下文管理器实现了 __enter__ 和 __exit__ 魔法方法。进入 with 块时执行 __enter__,退出时(包括因异常退出)自动执行 __exit__,从而保证资源被正确释放。这是Python中管理资源的首选模式。
适用场景
所有需要显式申请和释放的资源,如文件操作、数据库连接、网络套接字、线程锁等。
四、用装饰器封装通用逻辑
问题场景
多个函数包含相同的前置或后置逻辑,例如日志记录、性能计时、参数校验、异常捕获等。如果每个函数都复制粘贴这些代码,会导致高度冗余,且修改逻辑时需要改动多处。
Before 问题代码
import time
import logging
logging.basicConfig(level=logging.INFO)
# 函数1:包含耗时统计和日志打印
def add(a, b):
start = time.time()
logging.info(f"开始执行add函数,参数:a={a}, b={b}")
res = a + b
end = time.time()
logging.info(f"add函数执行完成,结果:{res},耗时:{end-start:.2f}s")
return res
# 函数2:完全相同的日志和计时逻辑,代码重复
def multiply(a, b):
start = time.time()
logging.info(f"开始执行multiply函数,参数:a={a}, b={b}")
res = a * b
end = time.time()
logging.info(f"multiply函数执行完成,结果:{res},耗时:{end-start:.2f}s")
return res
After 优雅代码
import time
import logging
from functools import wraps
logging.basicConfig(level=logging.INFO)
# 定义通用装饰器,封装重复的日志+耗时统计逻辑
def log_and_calc_time(func):
@wraps(func) # 保留原函数的元信息(如函数名、文档字符串)
def wrapper(*args, **kwargs):
start = time.time()
logging.info(f"开始执行{func.__name__}函数,参数:args={args}, kwargs={kwargs}")
res = func(*args, **kwargs) # 执行原函数核心逻辑
end = time.time()
logging.info(f"{func.__name__}函数执行完成,结果:{res},耗时:{end-start:.2f}s")
return res
return wrapper
# 使用装饰器修饰函数,一行搞定,消除重复代码
@log_and_calc_time
def add(a, b):
return a + b
@log_and_calc_time
def multiply(a, b):
return a * b
# 新增函数只需添加装饰器,逻辑统一,修改装饰器即可全局生效
@log_and_calc_time
def subtract(a, b):
return a - b
改进原理
- 遵循 DRY 原则:将多个函数的共性横切关注点(如日志、计时)抽取到装饰器中,实现代码的高度复用。
- 解耦:业务核心逻辑与通用辅助逻辑分离,函数职责更单一、更清晰。
- 易扩展与维护:需要修改通用逻辑时,仅需改动装饰器定义一处。
适用场景
多个函数共享相同的非业务核心逻辑,如日志、性能监控、权限校验、缓存、重试机制等。这是实现 代码复用 和关注点分离的利器。
五、遵循单一职责原则(拆分巨型函数)
问题场景
编写了一个长达数百行、功能混杂的“巨型函数”,它可能同时负责数据读取、清洗、计算、存储和日志。调试时难以定位问题,修改一处逻辑可能无意间影响其他功能。
Before 问题代码
# 一个函数包揽所有事:读文件→处理数据→计算统计→保存结果,难以维护
def handle_data(file_path):
# 1. 读取文件
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
# 2. 处理数据:去除空行、分割
data = []
for line in lines:
line = line.strip()
if not line:
continue
data.append(line.split(","))
# 3. 计算统计:求第二列的平均值
total = 0
count = 0
for row in data[1:]: # 跳过表头
total += float(row[1])
count += 1
avg = total / count if count > 0 else 0
# 4. 保存结果
with open("result.txt", "w", encoding="utf-8") as f:
f.write(f"平均值:{avg:.2f}")
return avg
After 优雅代码
# 单一职责:每个函数只做一件明确的事,函数名即文档
def read_file(file_path):
"""读取文件,返回行列表"""
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
return lines
def process_data(lines):
"""处理数据,去除空行并分割,返回处理后的二维列表"""
data = []
for line in lines:
line = line.strip()
if not line:
continue
data.append(line.split(","))
return data
def calculate_average(data):
"""计算第二列的平均值,返回平均值"""
total = 0
count = 0
for row in data[1:]:
total += float(row[1])
count += 1
return total / count if count > 0 else 0
def save_result(avg):
"""将平均值保存到结果文件"""
with open("result.txt", "w", encoding="utf-8") as f:
f.write(f"平均值:{avg:.2f}")
# 主协调函数:逻辑清晰,像阅读流程图
def handle_data(file_path):
lines = read_file(file_path)
data = process_data(lines)
avg = calculate_average(data)
save_result(avg)
return avg
改进原理
- 单一职责:每个函数负责一个独立、完整的功能模块,符合高内聚的设计思想。
- 易于测试与调试:每个小函数都可以进行独立的单元测试,问题定位速度快。
- 高复用性:拆分后的功能模块(如
read_file, process_data)更容易在其他上下文中被复用。
- 易于维护与扩展:修改特定功能时,只需关注对应的单一函数,影响范围可控。
适用场景
任何代码行数过多(例如超过50行)、函数名无法清晰概括其所有行为、内部包含多个逻辑阶段的函数。
六、用枚举 (Enum) 替换魔法字符串
问题场景
代码中使用字符串字面量来表示状态、类型等有限集合的值(如 "success", "failed")。拼写错误无法在编码阶段发现,只能在运行时出错,且缺乏类型安全性和编辑器智能提示。
Before 问题代码
# 魔法字符串表示订单状态,拼写错误将导致Bug
def handle_order(order_id, status):
if status == "success":
print(f"订单{order_id}支付成功")
elif status == "failed":
print(f"订单{order_id}支付失败")
elif status == "pending":
print(f"订单{order_id}待支付")
else:
print(f"订单{order_id}状态未知")
# 调用时拼错,运行时才会抛出异常或逻辑错误
handle_order(1001, "succes") # 漏了一个's',静默失败或错误处理
After 优雅代码
from enum import Enum, unique
# 用枚举定义订单状态,@unique确保值唯一
@unique
class OrderStatus(Enum):
SUCCESS = "success" # 支付成功
FAILED = "failed" # 支付失败
PENDING = "pending" # 待支付
# 使用枚举类型作为参数,类型明确
def handle_order(order_id, status: OrderStatus):
if status == OrderStatus.SUCCESS:
print(f"订单{order_id}支付成功")
elif status == OrderStatus.FAILED:
print(f"订单{order_id}支付失败")
elif status == OrderStatus.PENDING:
print(f"订单{order_id}待支付")
else:
print(f"订单{order_id}状态未知")
# 正确调用:编辑器提供代码补全,避免拼写错误
handle_order(1001, OrderStatus.SUCCESS)
# 错误调用:OrderStatus.SUCCES 在编码或静态检查阶段就会报错
改进原理
- 类型安全:枚举成员是强类型的单例,拼写错误在编码或静态类型检查(如mypy)阶段即可发现。
- 增强的 IDE 支持:主流编辑器(PyCharm, VSCode)可对枚举成员进行自动补全和跳转。
- 集中化管理:所有可能的状态值在一个地方定义和管理,修改和扩展方便。
- 提升可读性:
OrderStatus.SUCCESS 比 "success" 语义更明确,自解释性更强。
适用场景
代码中需要表示固定的、有限集合的状态、类型、类别或返回码,例如订单状态、用户角色、错误类型、API动作等。
七、用生成器优化内存使用
问题场景
需要处理或生成一个非常大的数据集时,直接使用列表推导式或循环 append 会一次性在内存中创建完整的列表,可能导致内存占用过高,甚至引发 MemoryError。
Before 问题代码
# 生成1000万个数字的平方列表,直接占用数百MB内存
def generate_big_list(n):
return [i**2 for i in range(n)]
big_list = generate_big_list(10**7) # 瞬间消耗大量内存
# 遍历列表时,所有数据已加载在内存中
for num in big_list:
if num > 1000:
break # 实际上可能只需要前几个元素
After 优雅代码
# 生成器表达式:用圆括号替代方括号,惰性求值
def generate_big_generator(n):
return (i**2 for i in range(n))
# 生成器对象本身占用极小内存,与n大小无关
big_generator = generate_big_generator(10**7)
# 遍历时按需生成数据,用完即弃,内存友好
for num in big_generator:
if num > 1000:
break
# 函数式生成器:使用yield关键字,更灵活
def my_generator(n):
for i in range(n):
yield i**2 # 每次产生一个值后暂停
改进原理
- 惰性求值:生成器不会预先计算所有结果,而是在迭代时按需生成下一个值。
- 极致的内存效率:生成器对象只保存当前的迭代状态和生成规则,不保存结果集合,内存占用恒定且极小。
- 适用于流式处理:非常适合处理无法一次性装入内存的超大文件、数据库查询结果或网络数据流。
适用场景
需要处理超大数据集、数据只需被顺序遍历一次、或数据来源本身是流式/按需生成的情况。注意:生成器只能迭代一次,如需多次使用,需重新创建或转换为列表。
八、用字典映射替换多重 if-elif
问题场景
编写了冗长的 if-elif-else 链(通常超过3个分支)来进行条件分发。代码结构呈“阶梯状”,可读性差,添加新分支时需要修改函数内部结构,违反开闭原则。
Before 问题代码
# 多重if-elif,分支增多后代码冗长且不易读
def get_operation_result(op, a, b):
if op == "add":
return a + b
elif op == "subtract":
return a - b
elif op == "multiply":
return a * b
elif op == "divide":
if b == 0:
return "除数不能为0"
return a / b
else:
return "不支持的操作"
After 优雅代码
# 首先,定义每个分支对应的处理函数(职责单一)
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def multiply(a, b):
return a * b
def divide(a, b):
if b == 0:
return "除数不能为0"
return a / b
# 使用字典建立操作符到处理函数的映射
OPERATION_MAP = {
"add": add,
"subtract": subtract,
"multiply": multiply,
"divide": divide
}
def get_operation_result(op, a, b):
# 从字典中获取处理函数,未找到则返回None
func = OPERATION_MAP.get(op)
if func:
return func(a, b)
return "不支持的操作"
# 进阶:对于简单逻辑,可直接使用lambda表达式
OPERATION_MAP_SIMPLE = {
"add": lambda a,b: a+b,
"subtract": lambda a,b: a-b,
"multiply": lambda a,b: a*b
}
改进原理
- 扁平化结构:将线性的条件判断转换为查表操作,代码更加扁平、清晰。
- 符合开闭原则:新增操作类型时,只需向映射字典添加新的键值对,无需修改
get_operation_result 函数的内部逻辑。
- 逻辑集中:所有的业务分支映射关系在一个数据结构中一目了然,便于管理和审查。
适用场景
基于固定值(字符串、数字等)进行多路分发的场景,例如命令解析、状态机、策略模式、简单路由等。
九、使用数据类 (dataclass) 简化对象定义
问题场景
为了定义一个主要用来存储数据的简单类(如DTO、实体类),需要手动编写大量的模板代码:__init__、__repr__、__eq__ 等。这不仅繁琐,而且容易出错。
Before 问题代码
# 手动编写所有样板代码,冗长且易错
class User:
def __init__(self, id: int, name: str, age: int, email: str):
self.id = id
self.name = name
self.age = age
self.email = email
def __repr__(self):
return f"User(id={self.id}, name='{self.name}', age={self.age}, email='{self.email}')"
def __eq__(self, other):
if not isinstance(other, User):
return False
return (self.id == other.id and self.name == other.name
and self.age == other.age and self.email == other.email)
u1 = User(1, "张三", 20, "zhangsan@xxx.com")
u2 = User(1, "张三", 20, "zhangsan@xxx.com")
print(u1) # 需要自定义__repr__
print(u1 == u2) # 需要自定义__eq__
After 优雅代码
from dataclasses import dataclass
# @dataclass装饰器自动生成__init__, __repr__, __eq__等方法
@dataclass
class User:
id: int
name: str
age: int
email: str
# 高级用法:通过参数启用更多特性
@dataclass(frozen=True, order=True) # frozen表示不可变,order支持排序比较
class Order:
order_id: str
amount: float
create_time: str
# 使用方式完全一致,但定义代码量减少80%以上
u1 = User(1, "张三", 20, "zhangsan@xxx.com")
u2 = User(1, "张三", 20, "zhangsan@xxx.com")
print(u1) # 自动生成友好的表示
print(u1 == u2) # 自动基于字段值进行判等
o1 = Order("OD1001", 99.9, "2026-02-04")
o2 = Order("OD1002", 199.9, "2026-02-04")
print(o1 < o2) # 启用了order=True,支持比较(默认按字段声明顺序)
改进原理
dataclasses 是 Python 3.7+ 的标准库。@dataclass 装饰器会根据类中定义的类型注解,自动生成包括 __init__、__repr__、__eq__ 在内的常用魔法方法,彻底解放开发者,让其专注于定义数据本身。
适用场景
定义主要用于存储数据、而非包含复杂行为的类,例如配置对象、API请求/响应模型、数据库实体等。
十、为代码添加类型注解
问题场景
函数参数和返回值的类型模糊不清,调用者只能通过阅读函数体或文档来猜测。传递错误类型的参数只能在运行时抛出异常,静态检查工具无法提供帮助,降低了代码的可靠性和开发体验。
Before 问题代码
# 无类型提示,a和b的类型?返回值类型?全靠猜测或运行时报错
def add(a, b):
return a + b
# 类型错误直到运行时才暴露
result = add(1, "2") # TypeError: unsupported operand type(s) for +: 'int' and 'str'
def process_data(data, limit, offset):
return data[offset:offset+limit] # data的类型?是列表吗?
After 优雅代码
# 添加基础类型注解,意图一目了然
def add(a: int, b: int) -> int:
return a + b
# 使用typing模块处理复杂类型
from typing import List, Dict, Optional, Any
def process_data(
data: List[Any],
limit: int = 10, # 带默认值的参数
offset: Optional[int] = 0 # 可选参数,允许传入None
) -> List[Any]:
if offset is None:
offset = 0
return data[offset:offset+limit]
def get_user_dict() -> Dict[int, str]:
return {1: "张三", 2: "李四"}
# 调用时,支持类型检查的编辑器(如PyCharm, VSCode)或mypy会提前警告类型不匹配
add(1, "2") # 编辑器通常会标出类型错误
process_data([1,2,3], "10", 0) # 提示limit参数应为int
改进原理
- 代码即文档:类型注解清晰说明了接口契约,无需额外注释即可理解函数用法。
- 早期错误检测:配合静态类型检查工具(如
mypy),可以在代码运行前发现大量的类型错误。
- 提升 IDE 体验:编辑器能基于类型注解提供更准确的代码补全、参数提示和重构支持。
- 便于重构:修改函数签名后,类型检查器能快速定位所有不兼容的调用点。
适用场景
所有生产环境代码,尤其是团队合作项目。从Python 3.9开始,可以使用内置的 list, dict 等直接作为类型注解,更加简洁。
重构的核心原则
掌握具体技巧后,理解其背后的原则更能指导实践:
- 可读性优先:代码首先是写给人看的,其次才是让机器执行。重构的首要目标是提升代码的可读性和可理解性。
- 遵循 DRY 原则:消除重复是减少错误、提高可维护性的关键。发现重复逻辑,就应考虑抽象和封装。
- 小步迭代,避免过度设计:重构应是持续、渐进的过程,而非推翻重来的“大爆炸”。在满足当前需求的前提下保持代码整洁,无需为未知的未来需求做过度抽象。
日常重构自查清单
开发完成后,可以快速对照以下清单检查自己的代码:
- 是否有未提取的魔法数字或字符串?
- 是否有可用列表推导式简化的循环?
- 资源管理是否都使用了
with 语句?
- 多个函数的通用逻辑是否可用装饰器封装?
- 是否有超过50行、职责过多的函数需要拆分?
- 固定状态集是否用枚举而非字符串表示?
- 处理大数据集时是否考虑了生成器?
- 多重
if-elif 是否可用字典映射优化?
- 简单数据类是否使用了
@dataclass?
- 函数和方法的参数、返回值是否添加了类型注解?
希望这10个Python代码重构技巧能为你带来实质性的帮助。编程能力的提升是一个不断反思和改进的过程。如果你有自己独特的重构案例或心得,欢迎在技术社区如云栈社区与更多开发者交流分享,共同进步。