咱做Python开发的,谁没见过/写过几行烂代码?接手别人的项目,满屏的硬编码数字、嵌套八层的循环、几百行的巨型函数,改一个bug能引出十个新问题;自己半年前写的代码,再看时一脸懵:这玩意谁写的?怎么这么丑?
更扎心的是,烂代码不仅自己维护起来费劲,同事看了直摇头,线上出问题排查起来更是头大。其实Python本就是一门以简洁、优雅为设计哲学的语言,写烂代码大多不是能力问题,而是没养成良好的编码习惯,不懂基础的重构技巧。
代码重构不是炫技,也不是无意义的重写,而是在不改变代码功能的前提下,让它更易读、易维护、更高效。今天就给大家整理了10个最实用的Python重构技巧,每个技巧都做了烂代码VS优雅代码的强烈对比,学会这些,你的代码能直接优雅10倍,再也不用被人吐槽写“屎山”了!
全文干货,建议收藏+点赞,重构时翻出来对照着改,亲测有效!
一、提取魔法数/字符串为常量
问题场景
代码里随处可见无意义的数字、硬编码的字符串,别人看代码时根本不知道这些值代表什么,要修改时得全局搜索,漏改一个就出bug,典型的“写时一时爽,维护火葬场”。
Before 烂代码
# 谁知道3.1415926是啥?60*60又是什么意思?
# 接口地址硬编码,改地址要改遍所有代码
defcalculate_circle_area(radius):
return3.1415926 * radius * radius
defget_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()
defcal_oneday_seconds():
return60 * 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请求成功状态码
defcalculate_circle_area(radius):
return PI * radius * radius
defget_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()
defcal_oneday_seconds():
return ONE_DAY_SECONDS
改进原理
- 1. 语义化:常量命名带注释,代码自解释,无需额外文档就能看懂;
- 2. 易维护:需要修改值时,只改常量定义处,一次修改全局生效,避免漏改;
- 3. 规范统一:符合Python PEP8规范,常量全大写+下划线分隔,代码风格一致。
适用场景
代码中出现固定不变的数字、字符串,尤其是多次重复出现的场景,比如接口地址、超时时间、状态码、数学常量等。
二、用列表推导式替换循环
问题场景
为了生成一个列表,写一堆冗余的for循环+append,代码行数多,逻辑还不直观,Python的简洁性完全没体现出来。
Before 烂代码
# 生成1-10的偶数列表,写了3行代码,纯纯的冗余
even_nums = []
for num inrange(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 inrange(1, 11) if num % 2 == 0]
# 嵌套逻辑也能轻松处理,比循环清晰10倍
str_list = ["Python", "重构", "技巧", "优雅"]
len_list = [len(s) for s in str_list]
# 甚至可以做简单的数值运算
num_list = [1,2,3,4]
square_list = [n**2for n in num_list] # 生成平方数列表
改进原理
- 1. 简洁高效:将多行动循环浓缩为一行,减少代码冗余,提升可读性;
- 2. 执行更快:列表推导式是Python底层优化的实现,比手动
for+append执行效率更高; - 3. 逻辑连贯:生成列表的遍历、判断、处理逻辑集中在一处,无需跳行看代码。
适用场景
简单的列表生成场景,尤其是需要遍历可迭代对象、加简单条件判断、做简单数据处理的情况,注意:不要嵌套超过3层,否则会影响可读性。
三、用上下文管理器(with)管理资源
问题场景
手动打开文件、数据库连接、网络套接字后,忘记写close(),导致资源泄漏;或者写了close(),但代码抛出异常时close()没执行,依旧泄漏资源。
Before 烂代码
# 手动打开文件,忘记close(),文件句柄泄漏
f = open("test.txt", "r", encoding="utf-8")
content = f.read()
# 此处如果抛出异常,下面的close()根本执行不到
print(content)
# 就算写了close(),也可能因为异常跳过
# 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上下文管理器,自动关闭文件,无论是否抛出异常都能保证资源释放
withopen("test.txt", "r", encoding="utf-8") as f:
content = f.read()
print(content)
# 数据库连接也能用with,部分库需要封装,原生pymysql可嵌套
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()
# 自动执行cursor.close()和conn.close(),无需手动操作
改进原理
上下文管理器实现了Python的__enter__和__exit__魔法方法,进入with块执行__enter__,退出时自动执行__exit__,即使代码抛出异常,也会保证__exit__执行,从而安全释放资源。
适用场景
所有需要手动申请和释放的资源,比如文件操作、数据库连接、网络套接字、锁操作等,是Python中资源管理的最佳实践。
四、用装饰器减少重复代码
问题场景
多个函数有相同的前置/后置逻辑,比如日志打印、耗时统计、参数校验、异常捕获,每次都要复制粘贴这些代码,代码冗余度极高,改一处要改所有函数。
Before 烂代码
import time
import logging
logging.basicConfig(level=logging.INFO)
# 函数1:统计耗时+打印日志
defadd(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:同样的统计耗时+打印日志,复制粘贴的代码
defmultiply(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)
# 定义通用装饰器,封装重复的日志+耗时统计逻辑
deflog_and_calc_time(func):
@wraps(func) # 保留原函数的元信息(名称、文档字符串等)
defwrapper(*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
defadd(a, b):
return a + b
@log_and_calc_time
defmultiply(a, b):
return a * b
# 新增函数只需加一个装饰器,逻辑统一,改装饰器即可全局生效
@log_and_calc_time
defsubtract(a, b):
return a - b
改进原理
- 1. DRY原则:Don't Repeat Yourself,将多个函数的重复逻辑封装到装饰器中,实现代码复用;
- 2. 解耦:业务逻辑和通用逻辑(日志、耗时)分离,函数只关注自己的核心功能;
- 3. 易扩展:需要修改通用逻辑时,只改装饰器代码,所有被修饰的函数都会同步更新。
适用场景
多个函数存在相同的通用逻辑,比如日志打印、耗时统计、参数校验、异常捕获、权限验证等,装饰器是解决这类问题的最优解。
五、单一职责原则(拆分函数)
问题场景
写了一个几百行的巨型函数,里面又有数据读取、又有数据处理、又有数据存储、又有日志打印,一个函数干了N件事,调试时根本找不到问题在哪,改一处逻辑还会影响其他功能。
Before 烂代码
# 一个函数干了所有事:读取文件→处理数据→计算统计→保存结果,堪称“屎山鼻祖”
defhandle_data(file_path):
# 1. 读取文件
withopen(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
# 2. 处理数据:去除空行、分割
data = []
for line in lines:
line = line.strip()
ifnot 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 > 0else0
# 4. 保存结果
withopen("result.txt", "w", encoding="utf-8") as f:
f.write(f"平均值:{avg:.2f}")
return avg
After 优雅代码
# 单一职责:每个函数只做一件事,函数名见名知意
defread_file(file_path):
"""读取文件,返回行列表"""
withopen(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
return lines
defprocess_data(lines):
"""处理数据,去除空行并分割,返回处理后的二维列表"""
data = []
for line in lines:
line = line.strip()
ifnot line:
continue
data.append(line.split(","))
return data
defcalculate_average(data):
"""计算第二列的平均值,返回平均值"""
total = 0
count = 0
for row in data[1:]:
total += float(row[1])
count += 1
return total / count if count > 0else0
defsave_result(avg):
"""将平均值保存到结果文件"""
withopen("result.txt", "w", encoding="utf-8") as f:
f.write(f"平均值:{avg:.2f}")
# 主函数:协调各个子函数,逻辑清晰
defhandle_data(file_path):
lines = read_file(file_path)
data = process_data(lines)
avg = calculate_average(data)
save_result(avg)
return avg
改进原理
- 1. 单一职责:一个函数只负责一个功能,符合面向对象的设计原则,代码逻辑更清晰;
- 2. 易调试/测试:每个小函数都可以单独调试、写单元测试,出问题能快速定位;
- 3. 可复用:子函数可以被其他函数调用,比如
read_file、process_data能在其他场景复用; - 4. 易扩展:需要修改某个功能时,只改对应的子函数,不会影响其他逻辑。
适用场景
函数代码超过50行、逻辑包含多个独立步骤、函数名无法用一句话描述功能的场景,必须拆分。
六、用枚举替换魔法字符串
问题场景
代码中用字符串表示状态、类型,比如"success"、"failed"、"pending",拼写错误时编译器不会报错,只能运行时发现,而且字符串无法做类型校验。
Before 烂代码
# 魔法字符串表示订单状态,拼错一个字母就是bug
defhandle_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}状态未知")
# 调用时拼错成"succes",运行时才会发现问题,编译器毫无提示
handle_order(1001, "succes")
After 优雅代码
from enum import Enum, unique
# 用枚举定义订单状态,装饰器保证枚举值唯一,避免重复
@unique
classOrderStatus(Enum):
SUCCESS = "success"# 支付成功
FAILED = "failed"# 支付失败
PENDING = "pending"# 待支付
# 用枚举作为参数,类型明确,拼写错误编译器直接报错
defhandle_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 会直接报错,提前发现问题
改进原理
- 1. 类型安全:枚举是强类型,拼写错误会在编码/编译阶段发现,而非运行时;
- 2. 代码提示:主流编辑器(PyCharm、VSCode)会对枚举提供代码提示,提升开发效率;
- 3. 可维护:状态集中管理,新增/修改状态只需在枚举中操作,无需全局搜索;
- 4. 语义化:枚举名和值语义清晰,比纯字符串更易读。
适用场景
代码中用字符串/数字表示固定的状态、类型、类别的场景,比如订单状态、支付类型、用户角色、接口返回码等。
七、用生成器替换列表(内存优化)
问题场景
生成超大列表时,直接用列表推导式或append,会将所有数据加载到内存中,导致内存占用过高,甚至出现MemoryError(内存溢出)。
Before 烂代码
# 生成1000万个数字的平方列表,直接占用几百MB内存
# 若数据量再大,直接内存溢出
defgenerate_big_list(n):
return [i**2for i inrange(n)]
big_list = generate_big_list(10**7)
# 遍历列表时,所有数据已在内存中
for num in big_list:
if num > 1000:
break
After 优雅代码
# 生成器:用()替代[],按需生成数据,内存占用几乎可以忽略
defgenerate_big_generator(n):
return (i**2for i inrange(n))
# 生成器对象,仅占几十字节内存,无论n多大
big_generator = generate_big_generator(10**7)
# 遍历生成器时,逐个生成数据,生成一个用一个,用完就销毁
for num in big_generator:
if num > 1000:
break
# 函数式生成器:用yield关键字,更灵活
defmy_generator(n):
for i inrange(n):
yield i**2# 每次yield返回一个值,暂停执行,下次继续
改进原理
- 1. 惰性求值:生成器不会一次性生成所有数据,而是按需生成,遍历到哪个值才生成哪个值;
- 2. 内存优化:生成器对象仅保存生成规则,不保存具体数据,内存占用恒定,不受数据量大小影响;
- 3. 高效遍历:适合一次性遍历的场景,避免不必要的内存开销。
适用场景
处理超大数据集、需要按需生成数据、只需一次性遍历的场景,比如大数据处理、文件逐行读取、无限序列生成等;注意:生成器只能遍历一次,需要多次遍历的场景仍用列表。
八、用字典替换多重if-elif
问题场景
写了一堆嵌套的if-elif-else,判断条件超过3个,代码层层嵌套,可读性极差,新增条件时还要继续加elif,越写越乱。
Before 烂代码
# 多重if-elif,判断条件多了之后,代码像“阶梯”,看着眼晕
defget_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"不支持的操作"
# 调用
print(get_operation_result("add", 1, 2))
After 优雅代码
# 字典映射:将条件作为key,处理逻辑作为value,一眼看懂所有映射关系
defadd(a, b):
return a + b
defsubtract(a, b):
return a - b
defmultiply(a, b):
return a * b
defdivide(a, b):
if b == 0:
return"除数不能为0"
return a / b
# 操作映射字典,集中管理所有操作和对应的函数
OPERATION_MAP = {
"add": add,
"subtract": subtract,
"multiply": multiply,
"divide": divide
}
defget_operation_result(op, a, b):
# 用get方法获取,默认返回提示语,无需写else
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
}
改进原理
- 1. 扁平化:将阶梯式的if-elif转换为字典的键值对,代码扁平化,可读性大幅提升;
- 2. 易扩展:新增条件时,只需在字典中添加键值对,无需修改函数内部的判断逻辑,符合开闭原则;
- 3. 易维护:所有的条件和处理逻辑集中管理,无需在一堆if-elif中找对应的逻辑。
适用场景
条件判断超过3个、条件是固定的字符串/数字、每个条件对应一个独立的处理逻辑的场景,比如操作判断、类型判断、状态判断等。
九、用数据类(dataclass)简化对象定义
问题场景
为了定义一个简单的实体类(比如用户、订单),手动写__init__、__repr__、__eq__等魔法方法,代码冗余,写起来还容易出错。
Before 烂代码
# 定义用户类,手动写__init__和__repr__,几十行代码全是模板
classUser:
def__init__(self, id: int, name: str, age: int, email: str):
self.id = id
self.name = name
self.age = age
self.email = email
# 自定义打印格式,否则打印对象是<__main__.User object at 0x7fxxxx>
def__repr__(self):
returnf"User(id={self.id}, name='{self.name}', age={self.age}, email='{self.email}')"
# 自定义相等判断,否则两个属性相同的对象判等为False
def__eq__(self, other):
ifnotisinstance(other, User):
returnFalse
return (self.id == other.idandself.name == other.name
andself.age == other.age andself.email == other.email)
# 使用
u1 = User(1, "张三", 20, "zhangsan@xxx.com")
u2 = User(1, "张三", 20, "zhangsan@xxx.com")
print(u1) # User(id=1, name='张三', age=20, email='zhangsan@xxx.com')
print(u1 == u2) # True
After 优雅代码
# 用dataclass装饰器,一行搞定所有魔法方法,代码简洁到飞起
from dataclasses import dataclass
# @dataclass自动生成__init__、__repr__、__eq__,还能加类型注解
@dataclass
classUser:
id: int
name: str
age: int
email: str
# 进阶:添加参数,实现更多功能
# frozen=True:不可变对象,赋值会报错;order=True:支持大小比较
@dataclass(frozen=True, order=True)
classOrder:
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) # User(id=1, name='张三', age=20, email='zhangsan@xxx.com')
print(u1 == u2) # True
o1 = Order("OD1001", 99.9, "2026-02-04")
o2 = Order("OD1002", 199.9, "2026-02-04")
print(o1 < o2) # True,因为order_id字典序OD1001 < OD1002
改进原理
dataclasses是Python3.7+引入的标准库,@dataclass装饰器会根据类的属性自动生成一系列魔法方法,包括__init__、__repr__、__eq__、__hash__等,无需手动编写,大幅减少模板代码。
适用场景
定义简单的实体类/数据容器,无需复杂的方法,只需要存储数据和基本的对象操作(初始化、打印、判等)的场景,比如用户、订单、商品、配置等数据类。
十、添加类型注解
问题场景
函数的参数和返回值类型不明确,调用时传错类型(比如传字符串给数字参数),运行时才报错;别人看代码时,根本不知道该传什么类型的参数,该接收什么类型的返回值。
Before 烂代码
# 毫无类型提示,a/b是什么类型?返回值是什么类型?全靠猜
defadd(a, b):
return a + b
# 传错类型,运行时才报错,编码阶段毫无提示
add(1, "2") # TypeError: unsupported operand type(s) for +: 'int' and 'str'
# 复杂函数更甚,参数多了之后,类型完全混乱
defprocess_data(data, limit, offset):
return data[offset:offset+limit]
After 优雅代码
# 添加类型注解,参数类型+返回值类型一目了然
defadd(a: int, b: int) -> int:
return a + b
# 复杂类型用typing模块,列表、字典、可选类型等都能标注
from typing importList, Dict, Optional, Any
# 标注列表、可选参数、字典返回值
defprocess_data(
data: List[Any],
limit: int = 10, # 默认参数
offset: Optional[int] = 0# 可选参数,可传None
) -> List[Any]:
if offset isNone:
offset = 0
return data[offset:offset+limit]
# 标注字典类型,键值对类型明确
defget_user_dict() -> Dict[int, str]:
return {1: "张三", 2: "李四"}
# 调用时,编辑器会提示类型错误,提前发现问题
add(1, "2") # PyCharm/VSCode直接标红,提示类型不匹配
process_data([1,2,3], "10", 0) # 同样标红,limit应为int
改进原理
- 1. 代码自解释:类型注解直接标明参数和返回值的类型,无需额外文档,别人一看就懂;
- 2. 提前报错:主流编辑器和静态检查工具(mypy)会根据类型注解在编码阶段发现类型错误,而非运行时;
- 3. 代码提示:编辑器会根据类型注解提供更精准的代码提示,提升开发效率;
- 4. 便于重构:重构代码时,修改类型后,所有调用处的类型错误会被直接提示,避免漏改。
适用场景
所有生产环境的代码,尤其是团队协作的项目,函数、类、变量都建议添加类型注解;Python3.9+对内置类型(list、dict)的注解做了优化,无需再用typing模块,更简洁。
代码重构的3个核心原则
学完10个技巧,更重要的是理解重构的底层逻辑,避免为了重构而重构,记住这3个原则,你的代码永远不会太烂:
1. 可读性优先
代码是写给人看的,机器只负责执行。重构的第一目标是提升代码的可读性,让自己和同事能快速看懂代码,而非追求炫技的语法或极致的性能。
2. 遵循DRY原则
Don't Repeat Yourself,重复的代码是烂代码的根源,任何重复的逻辑都要想办法封装、复用,这是减少代码冗余、提升可维护性的关键。
3. 适度优化,不过度设计
重构不是重写,也不是一次性做到完美,而是小步迭代;不要为了未来可能的需求做过度设计,满足当前需求的同时,保证代码易扩展即可,过度设计比烂代码更可怕。
重构检查清单
日常开发中,写完代码后对照这份清单检查一遍,轻松写出优雅代码,建议收藏:
- 3. 是否有手动管理资源未使用with上下文管理器的情况?
- 6. 是否有用字符串/数字表示固定状态未使用枚举的情况?
- 7. 是否有生成超大列表未使用生成器的内存浪费场景?
- 8. 是否有超过3个条件的多重if-elif未替换为字典?
- 9. 是否有手动写__init__的简单数据类未使用dataclass?
- 10. 函数/类的参数和返回值是否添加了类型注解?
- 11. 函数名、变量名是否见名知意,符合PEP8规范?
- 12. 一个函数是否只做一件事,符合单一职责原则?
互动时刻
看完这10个技巧,是不是发现自己之前写了不少烂代码?评论区晒出你的代码重构案例吧!比如用了哪个技巧、重构前有多烂、重构后有多优雅,一起交流进步~
如果这篇文章对你有帮助,记得收藏+点赞+在看,分享给身边的Python小伙伴,让更多人告别烂代码!
关注我,持续分享Python开发的实用技巧和最佳实践,让你的代码更优雅、更高效~
(全文约2400字)