
上周排查bug,一个简单的数据处理脚本,居然有5个不同的错误。看着满屏的报错信息,我突然意识到:很多Python错误,其实不是技术问题,而是习惯问题。
今天就来盘点那些导致Python频繁报错的坏习惯,以及如何改正它们。相信我,改掉这些习惯,你的代码报错率至少能降低80%。
错误示范:
# 这是什么意思?三个月后你还记得吗?
a = 10
b = 20
c = a+b # c是什么?总和?平均值?
x = get_data() # x是什么数据?
y = process(x) # process做了什么?
z = save(y) # 保存到哪里?
# 更糟糕的:使用内置函数名
list = [1, 2, 3] # 覆盖了list()函数
str = "hello" # 覆盖了str()函数导致的问题:
TypeError: 'list' object is not callable(覆盖了内置函数)NameError: name 'total' is not defined(变量名记错)正确做法:
# 使用有意义的变量名
student_count = 10
average_score = 85.5
total_amount = calculate_total(price, quantity)
# 集合类型加后缀
user_list = ["张三", "李四"]
config_dict = {"host": "localhost", "port": 8080}
name_set = {"Alice", "Bob"}
# 布尔值用is_、has_开头
is_valid = True
has_permission = False
is_connected = check_connection()
# 函数名用动词开头
defcalculate_average(scores):
returnsum(scores) /len(scores)
defvalidate_email(email):
return"@"inemail
# 常量用大写
MAX_RETRIES = 3
DEFAULT_TIMEOUT = 30
API_BASE_URL = "https://api.example.com"命名规范检查:
# 安装pylint检查命名
# pip install pylint
# pylint your_script.py
# 或者使用flake8插件
# pip install flake8-naming
# flake8 your_script.py错误示范:
# API返回可能是None
user_data = fetch_user_data(user_id)
# 直接访问属性,可能报错
email = user_data["email"] # 如果user_data是None,TypeError
name = user_data.get("name") # 如果user_data是None,AttributeError
# 函数可能返回None
result = find_user_by_email(email)
result.send_notification() # 如果result是None,AttributeError导致的问题:
AttributeError: 'NoneType' object has no attribute 'xxx'TypeError: 'NoneType' object is not subscriptable正确做法:
# 方法1:明确检查None
user_data = fetch_user_data(user_id)
ifuser_dataisnotNone:
email = user_data.get("email", "default@example.com")
name = user_data.get("name", "未知用户")
else:
# 处理None情况
email = "default@example.com"
name = "未知用户"
# 方法2:使用or提供默认值
user_data = fetch_user_data(user_id) or {}
email = user_data.get("email", "default@example.com")
# 方法3:使用Walrus运算符(Python 3.8+)
if (data := fetch_user_data(user_id)) isnotNone:
process_data(data)
# 方法4:使用try-except
try:
user_data = fetch_user_data(user_id)
email = user_data["email"]
except (TypeError, KeyError):
email = "default@example.com"
# 方法5:使用Optional类型提示
fromtypingimportOptional
deffind_user(user_id: str) ->Optional[dict]:
"""明确表示可能返回None"""
# 实现...
pass
# 使用时IDE会提示可能为None
user = find_user("123")
ifuser: # IDE会提示检查None
email = user["email"]None安全工具:
# 使用pydantic确保数据完整性
frompydanticimportBaseModel, Field
fromtypingimportOptional
classUser(BaseModel):
name: str
email: str
age: Optional[int] = None # 明确标注可选字段
# 使用后不会出现意外的None
user = User(name="张三", email="zhang@example.com")
print(user.age) # 输出: None,但不会报错经典坑爹错误:
defadd_item(item, items=[]):
"""添加项目到列表"""
items.append(item)
returnitems
# 看起来没问题?
print(add_item(1)) # [1]
print(add_item(2)) # 期望[2],实际[1, 2]!
print(add_item(3)) # 期望[3],实际[1, 2, 3]!
# 原因:默认参数在函数定义时只创建一次
# 所有调用共享同一个列表对象导致的问题:
UnboundLocalError(在函数内修改后又被读取)正确做法:
# 方法1:使用None作为默认值
defadd_item(item, items=None):
ifitemsisNone:
items = [] # 每次调用创建新列表
items.append(item)
returnitems
# 方法2:使用不可变类型
defcreate_user(name, permissions=()): # 元组是不可变的
return {"name": name, "permissions": list(permissions)}
# 方法3:使用functools.partial
fromfunctoolsimportpartial
defprocess_data(data, config=None):
ifconfigisNone:
config = {}
# 处理数据
returnprocessed_data
# 创建特定配置的版本
process_with_defaults = partial(process_data, config={"mode": "fast"})
# 方法4:使用数据类
fromdataclassesimportdataclass, field
fromtypingimportList
@dataclass
classShoppingCart:
items: List[str] = field(default_factory=list) # 每次创建新列表
defadd_item(self, item):
self.items.append(item)检测工具:
# 使用pylint检测
# pylint会警告:Dangerous default value [] as argument
# 使用flake8插件
# pip install flake8-bugbear
# 会检测到:B006 Do not use mutable data structures for argument defaults错误示范:
# 捕获所有异常,隐藏真正问题
try:
result = risky_operation()
save_to_database(result)
send_email_notification()
exceptException: # 捕获所有异常
print("出错了") # 什么错?不知道
# 捕获异常但不处理
try:
process_file("data.txt")
except:
pass # 静默失败,难以调试
# 错误的异常顺序
try:
parse_json(data)
exceptExceptionase: # 太宽泛,放在前面
print(f"错误: {e}")
exceptjson.JSONDecodeError: # 永远不会执行
print("JSON解析错误")导致的问题:
正确做法:
# 原则1:只捕获你能处理的异常
try:
config = load_config("config.yaml")
exceptFileNotFoundError:
# 文件不存在,使用默认配置
config = get_default_config()
exceptyaml.YAMLErrorase:
# 配置文件格式错误,记录日志
logger.error(f"配置文件格式错误: {e}")
# 可以选择退出或使用默认值
config = get_default_config()
# 让其他异常(如内存不足)向上传播
# 原则2:从具体到一般
try:
response = requests.get(url, timeout=10)
response.raise_for_status() # 检查HTTP状态
data = response.json()
exceptrequests.Timeout:
logger.warning(f"请求超时: {url}")
returnNone
exceptrequests.HTTPErrorase:
logger.error(f"HTTP错误 {e.response.status_code}: {url}")
returnNone
exceptjson.JSONDecodeError:
logger.error(f"响应不是有效的JSON: {url}")
returnNone
exceptExceptionase:
# 最后捕获一般异常,记录详细信息
logger.exception(f"未知错误: {e}")
raise # 重新抛出,让上层处理
# 原则3:使用else和finally
try:
connection = create_database_connection()
cursor = connection.cursor()
exceptConnectionErrorase:
logger.error(f"数据库连接失败: {e}")
raise
else:
# 只有try成功时才执行
try:
cursor.execute("SELECT * FROM users")
results = cursor.fetchall()
exceptDatabaseErrorase:
logger.error(f"查询失败: {e}")
results = []
finally:
# 确保游标关闭
cursor.close()
finally:
# 无论成功失败都执行
ifconnection:
connection.close()
# 原则4:使用上下文管理器
fromcontextlibimportsuppress
# 忽略特定异常(明确知道可以忽略)
withsuppress(FileNotFoundError):
os.remove("temp_file.txt")
# 使用with语句自动管理资源
withopen("data.txt", "r") asf:
content = f.read() # 文件会自动关闭异常处理工具:
# 自定义异常类,建立异常层次
classAppError(Exception):
"""应用基础异常"""
pass
classValidationError(AppError):
"""数据验证错误"""
pass
classDatabaseError(AppError):
"""数据库错误"""
def__init__(self, message, sql=None, params=None):
self.sql = sql
self.params = params
super().__init__(message)
# 使用时
defsave_user(user_data):
ifnotvalidate_user(user_data):
raiseValidationError("用户数据无效")
try:
db.execute("INSERT INTO users VALUES (?, ?)",
[user_data["name"], user_data["email"]])
exceptsqlite3.Errorase:
raiseDatabaseError("保存用户失败", sql=sql, params=params) frome常见错误:
# 错误1:在函数内修改全局变量
count = 0
defincrement():
count += 1 # UnboundLocalError: local variable 'count' referenced before assignment
# 错误2:循环变量泄漏
foriinrange(5):
process(i)
print(i) # 输出4,i仍然存在!
# 错误3:闭包变量捕获
defcreate_multipliers():
return [lambdax: i*xforiinrange(5)]
multipliers = create_multipliers()
print(multipliers[0](2)) # 期望0,实际8(i=4)
print(multipliers[1](2)) # 期望2,实际8(i=4)
# 错误4:类属性与实例属性混淆
classUser:
users = [] # 类属性
def__init__(self, name):
self.name = name
self.users.append(self) # 修改的是类属性!
user1 = User("张三")
user2 = User("李四")
print(user1.users) # [user1, user2]
print(user2.users) # [user1, user2] 同一个列表!正确做法:
# 解决方案1:明确声明全局变量
count = 0
defincrement():
globalcount # 明确声明使用全局变量
count += 1
# 更好的方案:使用返回值
defincrement_counter(current):
returncurrent+1
count = increment_counter(count)
# 解决方案2:避免循环变量泄漏
foriinrange(5):
process(i)
# i在这里不可访问(Python 3中,for循环变量作用域仅限于循环内)
# 或者使用列表推导式
results = [process(i) foriinrange(5)]
# 解决方案3:闭包变量立即绑定
defcreate_multipliers():
return [lambdax, i=i: i*xforiinrange(5)] # 使用默认参数立即绑定
# 或者使用partial
fromfunctoolsimportpartial
defcreate_multipliers():
return [partial(lambdai, x: i*x, i) foriinrange(5)]
# 解决方案4:正确使用类属性和实例属性
classUser:
all_users = [] # 类属性,所有实例共享
def__init__(self, name):
self.name = name # 实例属性,每个实例独立
self.friends = [] # 实例属性
User.all_users.append(self) # 修改类属性要指定类名
@classmethod
defget_user_count(cls):
returnlen(cls.all_users) # 使用cls访问类属性
# 使用property管理属性访问
classUser:
def__init__(self, name):
self._name = name # 私有属性
@property
defname(self):
returnself._name
@name.setter
defname(self, value):
ifnotvalue:
raiseValueError("姓名不能为空")
self._name = value作用域调试技巧:
# 查看局部变量和全局变量
defmy_function():
x = 10
y = 20
print(locals()) # 输出局部变量
print(globals().keys()) # 输出全局变量名
# 使用dis模块查看字节码
importdis
deftest_scope():
x = 1
definner():
returnx+1
returninner()
dis.dis(test_scope) # 查看变量作用域错误示范:
# 没有类型提示,只能运行时才发现错误
defcalculate_total(price, quantity):
returnprice*quantity
# 调用时
result = calculate_total("100", 2) # 返回"100100",不是200!
result = calculate_total(100, "2") # TypeError: can't multiply sequence by non-int
# 更复杂的例子
defprocess_data(data):
# data是什么类型?字典?列表?字符串?
returndata["value"] *2 # 如果data是列表,KeyError!正确做法:
# 添加类型提示
fromtypingimportList, Dict, Optional, Union
defcalculate_total(price: float, quantity: int) ->float:
"""计算总价
Args:
price: 单价,浮点数
quantity: 数量,整数
Returns:
总价,浮点数
"""
returnprice*quantity
# 复杂类型提示
defprocess_users(
users: List[Dict[str, Union[str, int]]],
filter_active: bool = True
) ->List[str]:
"""处理用户列表
Args:
users: 用户字典列表,每个字典包含name和age
filter_active: 是否只过滤活跃用户
Returns:
用户名列表
"""
iffilter_active:
return [user["name"] foruserinusersifuser.get("active", True)]
return [user["name"] foruserinusers]
# 使用mypy进行静态检查
# pip install mypy
# mypy your_script.py
# 运行时类型检查(使用pydantic)
frompydanticimportBaseModel, validator
classUser(BaseModel):
name: str
age: int
email: str
@validator('age')
defvalidate_age(cls, v):
ifv<0:
raiseValueError("年龄不能为负数")
returnv
# 自动验证类型
user = User(name="张三", age=25, email="zhang@example.com")
# 如果类型错误,会在创建时立即报错错误示范:
# 忘记关闭文件
file = open("data.txt", "r")
content = file.read()
# 忘记file.close(),可能导致资源泄漏
# 在异常情况下文件未关闭
file = open("data.txt", "r")
try:
content = file.read()
process(content) # 可能抛出异常
finally:
file.close() # 正确,但容易忘记导致的问题:
正确做法:
# 方法1:使用with语句(推荐)
withopen("data.txt", "r") asfile:
content = file.read()
# 文件会自动关闭
# 方法2:处理多个文件
withopen("input.txt", "r") asinfile, open("output.txt", "w") asoutfile:
content = infile.read()
outfile.write(content.upper())
# 方法3:使用pathlib(更现代)
frompathlibimportPath
file_path = Path("data.txt")
content = file_path.read_text() # 自动管理文件
# 写入文件
file_path.write_text("新内容")
# 方法4:数据库连接等资源
importsqlite3
withsqlite3.connect("database.db") asconn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
results = cursor.fetchall()
# 连接会自动关闭和提交第一步:代码审查清单每次提交代码前,检查这些问题:
第二步:使用自动化工具
# 安装代码检查工具
pip install pylint flake8 mypy black
# 配置预提交钩子
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
- repo: https://github.com/pycqa/flake8
rev: 6.0.0
hooks:
- id: flake8第三步:建立团队规范
# coding_standards.py
"""
团队编码规范
"""
# 1. 所有函数必须有类型提示
# 2. 所有异常必须明确处理
# 3. 所有资源必须使用with语句
# 4. 变量名必须清晰
# 5. 提交前必须通过mypy检查第四步:定期重构每周花1小时,重构一段旧代码:
“无他,惟手熟尔”!有需要的用起来!
如果你觉得这篇文章有用,欢迎点赞、转发、收藏、留言、推荐❤!
本文分享自 Nicholas与Pypi 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!