可变默认参数的隐患

在Python中,函数默认参数只会在函数定义时被计算一次,而不是每次调用时重新计算。这在使用可变对象(如列表、字典)作为默认参数时会带来意想不到的问题。

# 有问题的写法
def append_to_list(value, target_list=[]):
    target_list.append(value)
    return target_list

# 测试
print(append_to_list(1))  # [1]
print(append_to_list(2))  # [1, 2] - 不是预期的[2]!

正确的做法是使用None作为默认值:

# 正确的写法
def append_to_list(value, target_list=None):
    if target_list is None:
        target_list = []
    target_list.append(value)
    return target_list

# 测试
print(append_to_list(1))  # [1]
print(append_to_list(2))  # [2]

类变量与实例变量的混淆

很多开发者在使用类变量时会遇到共享状态的问题,特别是当类变量包含可变对象时。

class MyClass:
    shared_list = []  # 类变量
    
    def add_item(self, item):
        self.shared_list.append(item)

# 测试
obj1 = MyClass()
obj2 = MyClass()

obj1.add_item('a')
obj2.add_item('b')

print(obj1.shared_list)  # ['a', 'b']
print(obj2.shared_list)  # ['a', 'b'] - 两个实例共享同一个列表!

解决方案是在实例初始化时创建实例变量:

class MyClass:
    def __init__(self):
        self.shared_list = []  # 实例变量
    
    def add_item(self, item):
        self.shared_list.append(item)

# 测试
obj1 = MyClass()
obj2 = MyClass()

obj1.add_item('a')
obj2.add_item('b')

print(obj1.shared_list)  # ['a']
print(obj2.shared_list)  # ['b']

循环导入的陷阱

在大型项目中,循环导入是一个常见问题。当模块A导入模块B,同时模块B又导入模块A时,会导致ImportError。

常见场景:

  • 在模块顶层进行相互导入
  • 在类定义或函数参数中使用其他模块的类型注解

解决方法:

  1. 延迟导入:在函数内部导入需要的模块
  2. 重构代码:将公共代码提取到第三个模块
  3. 使用类型字符串:在类型注解中使用字符串
# 有问题的写法
# module_a.py
from module_b import B

class A:
    def process_b(self, b: B):  # 这里直接使用了B类
        pass

# module_b.py  
from module_a import A

class B:
    def process_a(self, a: A):  # 这里直接使用了A类
        pass
# 解决方案 - 使用字符串类型注解
# module_a.py
class A:
    def process_b(self, b: 'B'):  # 使用字符串类型
        pass

# 在需要的地方延迟导入
from module_b import B

列表推导式中的变量作用域

Python 3.x中列表推导式有自己的作用域,但在某些情况下仍然会"泄漏"变量。

# Python 3.x
x = 'outer'
result = [x for x in range(3)]
print(x)  # 'outer' - 在Python 3中正常

# 但在生成器表达式中
x = 'outer'
gen = (x for x in range(3))
print(x)  # 2 - 变量被修改了!

最佳实践是避免在推导式中使用与外层相同的变量名。

字典键的意外修改

当使用可变对象作为字典键时,如果修改了这些对象,会导致字典行为异常。

# 错误示例
my_dict = {}
key_list = [1, 2, 3]
my_dict[key_list] = 'value'  # TypeError: unhashable type: 'list'

# 但使用元组时
key_tuple = (1, 2, [3, 4])  # 包含可变对象的元组
my_dict[key_tuple] = 'value'  # 这里不会报错

# 但如果修改了元组中的列表
key_tuple[2].append(5)  # 现在字典的行为变得不可预测

迭代过程中修改集合

在迭代集合的同时修改它会导致RuntimeError。

# 错误示例
items = [1, 2, 3, 4, 5]
for item in items:
    if item % 2 == 0:
        items.remove(item)  # RuntimeError

# 解决方案1:创建副本
for item in items[:]:  # 使用切片创建副本
    if item % 2 == 0:
        items.remove(item)

# 解决方案2:使用列表推导式
items = [item for item in items if item % 2 != 0]

# 解决方案3:反向迭代(适用于删除操作)
for i in range(len(items)-1, -1, -1):
    if items[i] % 2 == 0:
        del items[i]

浮点数精度问题

浮点数在计算机中的表示存在精度限制,这在进行相等比较时会导致问题。

# 问题示例
print(0.1 + 0.2 == 0.3)  # False
print(0.1 + 0.2)  # 0.30000000000000004

# 解决方案
import math

# 方法1:使用误差范围
def almost_equal(a, b, tolerance=1e-9):
    return abs(a - b) < tolerance

# 方法2:使用decimal模块(适合金融计算)
from decimal import Decimal
result = Decimal('0.1') + Decimal('0.2')
print(result == Decimal('0.3'))  # True

总结思考

在Python开发过程中,这些陷阱往往源于对语言特性的深入理解不足。通过实际项目中的踩坑经验,我总结了几个关键原则:

  • 总是使用不可变对象作为函数默认参数
  • 明确区分类变量和实例变量的使用场景
  • 对于复杂的模块依赖,考虑使用延迟导入
  • 在修改集合时,优先考虑创建新对象而不是原地修改
  • 对于精度要求高的计算,使用decimal模块

这些经验都是通过真实的项目教训积累而来,希望可以帮助大家避免重蹈覆辙。