如果你已经用 Python 编码有一段时间了,可能已经掌握了基础知识,也做过一些项目。现在你看着自己的代码,心里想着:“虽然能用,但……好像还不敢在代码评审时自信地展示。”我们都经历过这种阶段。
但随着你不断编码,写出整洁的代码就和写出能用的代码一样重要。本文为你汇总了一些实用技巧,帮助你的代码从“能跑,别动它”进阶到“这代码值得维护”。
1. 明确定义数据模型,别到处传递 dict
Python 的字典非常灵活,但这恰恰是问题所在。到处传递原始字典会引发拼写错误、键不存在等问题,让人搞不清到底应该有哪些数据字段。
不推荐这样写:
def process_user(user_dict):
if user_dict['status'] == 'active': # 如果 'status' 缺失会怎样?
send_email(user_dict['email']) # 有些地方写成 'mail' 怎么办?
# 是 'name'、'full_name' 还是 'username'?谁知道呢!
log_activity(f"Processed {user_dict['name']}")
这段代码假定字典里的键一定存在,没有任何校验,容易在运行时抛出 KeyError 异常,也没有文档说明期望有哪些字段。
推荐这样做:
from dataclasses import dataclass
from typing import Optional
from datetime import datetime
@dataclass
class User:
id: int
email: str
full_name: str
status: str
last_login: Optional[datetime] = None
def process_user(user: User):
if user.status == 'active':
send_email(user.email)
log_activity(f"Processed {user.full_name}")
Python 的@dataclass装饰器让你用很少的样板代码定义清晰、明确的数据结构。你的 IDE 现在可以自动补全属性,缺失字段会立刻报错。
需要复杂校验时,可以考虑 Pydantic:
from pydantic import BaseModel, EmailStr, validator
class User(BaseModel):
id: int
email: EmailStr # 校验邮箱格式
full_name: str
status: str
@validator('status')
def status_must_be_valid(cls, v):
if v not in {'active', 'inactive', 'pending'}:
raise ValueError('Must be active, inactive or pending')
return v
这样你的数据会自我校验,能早期发现错误,也清楚地记录了字段要求。
2. 用枚举类型管理固定选项
字符串字面量易拼错,也没自动补全,只有在运行时才能发现问题。
不推荐这样写:
def process_order(order, status):
if status == 'pending':
# 处理逻辑
elif status == 'shipped':
# 不同逻辑
elif status == 'delivered':
# 更多逻辑
else:
raise ValueError(f"Invalid status: {status}")
# 后面调用
process_order(order, 'shiped') # 拼写错了!IDE 没提示
推荐这样做:
from enum import Enum
class OrderStatus(Enum):
PENDING = 'pending'
SHIPPED = 'shipped'
DELIVERED = 'delivered'
def process_order(order, status: OrderStatus):
if status == OrderStatus.PENDING:
# 处理逻辑
elif status == OrderStatus.SHIPPED:
# 不同逻辑
elif status == OrderStatus.DELIVERED:
# 更多逻辑
# 调用
process_order(order, OrderStatus.SHIPPED) # IDE 自动补全,避免拼写错!
用枚举(Enum)类型:
IDE 能自动补全
拼写错误几乎不可能
可以遍历所有选项
类型提示更清晰,文档化参数类型
3. 用关键字参数提升调用可读性
Python 的参数系统很灵活,但当函数有多个可选布尔参数时,调用代码容易让人迷惑。
不推荐这样写:
def create_user(name, email, admin=False, notify=True, temporary=False):
# 实现
# 调用时
create_user("John Smith", "john@example.com", True, False)
看到True, False,你还记得哪个代表 admin,哪个代表 notify 吗?没有查函数定义根本看不出来。
推荐这样做:
def create_user(name, email, *, admin=False, notify=True, temporary=False):
# 实现
# 现在必须用关键字指定可选参数
create_user("John Smith", "john@example.com", admin=True, notify=False)
*语法强制之后的参数必须用关键字指定。这样调用函数时,自文档化,避免了“神秘布尔值”问题。
这个模式在 API 调用等场景尤其有用,能显著提升代码可读性。
4. 用 Pathlib 替代 os.path
Python 的os.path已经能用,但操作繁琐。pathlib更简洁、面向对象且更易移植。
不推荐这样写:
import os
data_dir = os.path.join('data', 'processed')
if not os.path.exists(data_dir):
os.makedirs(data_dir)
filepath = os.path.join(data_dir, 'output.csv')
with open(filepath, 'w') as f:
f.write('results\n')
json_path = os.path.splitext(filepath)[0] + '.json'
if os.path.exists(json_path):
with open(json_path) as f:
data = json.load(f)
字符串拼接、路径操作分散,语义不直观。
推荐这样做:
from pathlib import Path
import json
data_dir = Path('data') / 'processed'
data_dir.mkdir(parents=True, exist_ok=True)
filepath = data_dir / 'output.csv'
filepath.write_text('results\n')
json_path = filepath.with_suffix('.json')
if json_path.exists():
data = json.loads(json_path.read_text())
为什么 pathlib 更好:
路径拼接用/更直观
方法如mkdir()、exists()、read_text()都是对象方法
改扩展名用with_suffix()语义更清晰
跨平台更健壮
5. 用 Guard Clauses 尽早返回,减少嵌套
多层嵌套的 if 语句难以阅读和维护。用“守卫子句”提前返回,让主逻辑清晰可见。
不推荐这样写:
def process_payment(order, user):
if order.is_valid:
if user.has_payment_method:
payment_method = user.get_payment_method()
if payment_method.has_sufficient_funds(order.total):
try:
payment_method.charge(order.total)
order.mark_as_paid()
send_receipt(user, order)
return True
except PaymentError as e:
log_error(e)
return False
else:
log_error("Insufficient funds")
return False
else:
log_error("No payment method")
return False
else:
log_error("Invalid order")
return False
深层嵌套难以追踪逻辑分支。
推荐这样做:
def process_payment(order, user):
if not order.is_valid:
log_error("Invalid order")
return False
if not user.has_payment_method:
log_error("No payment method")
return False
payment_method = user.get_payment_method()
if not payment_method.has_sufficient_funds(order.total):
log_error("Insufficient funds")
return False
try:
payment_method.charge(order.total)
order.mark_as_paid()
send_receipt(user, order)
return True
except PaymentError as e:
log_error(e)
return False
守卫子句让错误情况一开始就被处理,主流程清晰明了,缩进层数大大减少。逻辑越复杂,这种风格越明显优于深嵌套。
6. 避免滥用列表推导式
列表推导(List Comprehension)是 Python 优雅的语法特性,但当逻辑变复杂时,过度使用会让代码难以阅读和维护。
不推荐这样写:
# 一行中塞入太多逻辑,难以一眼读懂
active_premium_emails = [user['email'] for user in users_list
if user['status'] == 'active' and
user['subscription'] == 'premium' and
user['email_verified'] and
not user['email'] in blacklisted_domains]
这种写法把多重条件和转换全部塞进一行,条件一多就很难理解和调试。
推荐这样做:
方案一:用具名函数提取复杂条件
将复杂的过滤条件提到一个具名函数中,使列表推导更专注于“做什么”而不是“怎么做”。
def is_valid_premium_user(user):
return (user['status'] == 'active' and
user['subscription'] == 'premium' and
user['email_verified'] and
not user['email'] in blacklisted_domains)
active_premium_emails = [user['email'] for user in users_list if is_valid_premium_user(user)]
方案二:当逻辑复杂时用传统循环
用 for 循环和提前 continue 语句,每个条件单独判断,便于调试和理解。
active_premium_emails = []
for user in users_list:
if user['status'] != 'active':
continue
if user['subscription'] != 'premium':
continue
if not user['email_verified']:
continue
if user['email'] in blacklisted_domains:
continue
# 可以在此做更多复杂的转换
email = user['email'].lower().strip()
active_premium_emails.append(email)
总之,列表推导式应提升可读性,不应让代码变难懂。复杂的逻辑建议:
拆成具名函数
用传统循环
拆分为多步操作
记住,代码的最终目的是让人易读易维护。
7. 写可复用的纯函数
纯函数(Pure Function)指:同样输入永远得到同样输出,且无副作用。
不推荐这样写:
total_price = 0 # 全局状态
def add_item_price(item_name, quantity):
global total_price
# 从全局库存查价
price = inventory.get_item_price(item_name)
# 应用折扣
if settings.discount_enabled:
price *= 0.9
# 修改全局状态
total_price += price * quantity
# 后面调用
add_item_price('widget', 5)
add_item_price('gadget', 3)
print(f"Total: ${total_price:.2f}")
这种写法依赖和修改全局状态,难以测试和复用。
推荐这样做:
def calculate_item_price(item, price, quantity, discount=0):
"""计算某商品数量的最终价格,可选折扣。"""
discounted_price = price * (1 - discount)
return discounted_price * quantity
def calculate_order_total(items, discount=0):
"""计算一组商品的总价。"""
return sum(
calculate_item_price(item, price, quantity, discount)
for item, price, quantity in items
)
# 调用
order_items = [
('widget', inventory.get_item_price('widget'), 5),
('gadget', inventory.get_item_price('gadget'), 3),
]
total = calculate_order_total(order_items,
discount=0.1 if settings.discount_enabled else 0)
print(f"Total: ${total:.2f}")
这种做法让所有依赖通过参数传递,不依赖外部状态,更易于测试和复用。
8. 为所有公共函数和类写文档字符串(Docstring)
文档不是事后补充,而是可维护代码的核心部分。好的 docstring 不仅说明做什么,还解释为什么要这样做,以及如何正确使用。
不推荐这样写:
def celsius_to_fahrenheit(celsius):
"""Convert Celsius to Fahrenheit."""
return celsius * 9/5 + 32
只有一句无实际信息的描述。
推荐这样做:
def celsius_to_fahrenheit(celsius):
"""
将摄氏温度转换为华氏温度。
使用公式:F = C × (9/5) + 32
参数:
celsius: 摄氏温度(float 或 int)
返回:
对应的华氏温度
示例:
>>> celsius_to_fahrenheit(0)
32.0
>>> celsius_to_fahrenheit(100)
212.0
>>> celsius_to_fahrenheit(-40)
-40.0
"""
return celsius * 9/5 + 32
好的 docstring 应该:
说明参数和返回值
说明可能抛出的异常
给出用法示例
你的 docstring 就是代码的实时文档。
9. 自动化代码风格检查与格式化
不要仅靠人工检查去发现代码风格问题和常见错误。自动化工具能帮你保证代码质量和风格一致性,大幅减轻维护负担。
你可以尝试以下代码检查和格式化工具:
Black
—— 代码格式化工具
Ruff
—— 高效的静态代码检查工具
mypy
—— 静态类型检查器
isort
—— 导入语句自动排序工具
可以通过 pre-commit 钩子集成这些工具,实现代码提交前自动检查和格式化:
安装 pre-commit:
pip install pre-commit
创建.pre-commit-config.yaml文件,配置好上述工具。
运行
pre-commit install
激活钩子。
这样可以在每次提交前自动检查并格式化代码,无需手动操作就能保证代码风格统一、早期发现潜在错误。
10. 避免“裸 except”捕获所有异常
通用异常捕获会隐藏 bug,给调试带来极大困难。它会捕获所有类型的异常,包括语法错误、内存错误和强制中断。
不推荐这样写:
try:
user_data = get_user_from_api(user_id)
process_user_data(user_data)
save_to_database(user_data)
except:
# 到底哪里出错了?完全不知道!
logger.error("Something went wrong")
这样会把所有错误(包括编程错误、系统错误、用户中断、网络超时等)都吞掉,调试极为困难。
推荐这样做:
try:
user_data = get_user_from_api(user_id)
process_user_data(user_data)
save_to_database(user_data)
except ConnectionError as e:
logger.error(f"API connection failed: {e}")
# 处理 API 连接失败
except ValueError as e:
logger.error(f"Invalid user data received: {e}")
# 处理数据校验问题
except DatabaseError as e:
logger.error(f"Database error: {e}")
# 处理数据库相关问题
except Exception as e:
# 最后一层兜底
logger.critical(f"Unexpected error processing user {user_id}: {e}", exc_info=True)
raise
只捕获你预期会处理的异常类型,并针对不同错误分别处理和记录。最外层的except Exception只作为最后的保护措施,同时用exc_info=True记录完整堆栈信息,然后考虑是否需要重新抛出。
如果确实需要“兜底”捕获所有异常,请用except Exception as e:,绝不要用裸except:,并务必记录完整异常信息。
总结
希望你能在自己的代码中实践上述技巧。将这些最佳实践融入项目后,你会发现代码变得:
更易维护
更易测试
更易理解
下次你想走捷径时,请记住:代码的阅读次数远远多于编写次数。
愿你写出越来越优雅、整洁的 Python 代码!
领取专属 10元无门槛券
私享最新 技术干货