Python 常见陷阱
概述
Python以其简洁易读的语法和丰富的生态系统而闻名,这使其成为初学者的理想选择。然而,与任何编程语言一样,Python也有一些可能导致令人困惑的行为或难以发现的错误的特性。这些"陷阱"可能会让新手感到沮丧,但了解它们将帮助你编写更健壮、更可靠的代码。
本文将介绍Python中最常见的陷阱,解释它们为什么会发生,以及如何避免它们。通过掌握这些知识,你将能够编写更清晰、更少错误的Python代码。
可变默认参数
这可能是Python中最著名的陷阱之一。当你使用可变对象(如列表、字典或集合)作为函数参数的默认值时,它们会在函数定义时被创建一次,而非每次调用函数时都创建。
问题示例
def add_item(item, my_list=[]):
my_list.append(item)
return my_list
print(add_item(1)) # 输出: [1]
print(add_item(2)) # 预期: [2], 但实际输出: [1, 2]
print(add_item(3)) # 预期: [3], 但实际输出: [1, 2, 3]
原因解释
默认参数值是在函数定义时计算的,而不是在函数调用时。当函数被定义时,my_list=[]
创建了一个空列表,后续的每次调用都会使用这个相同的列表对象。
解决方法
使用None
作为默认值,然后在函数内部检查并创建新的列表:
def add_item(item, my_list=None):
if my_list is None:
my_list = []
my_list.append(item)
return my_list
print(add_item(1)) # 输出: [1]
print(add_item(2)) # 输出: [2]
print(add_item(3)) # 输出: [3]
延迟绑定闭包
在循环中创建函数时,所有函数可能会共享相同的变量值,而不是每个函数捕获独立的值。
问题示例
functions = []
for i in range(3):
functions.append(lambda: i)
for f in functions:
print(f()) # 预期: 0, 1, 2, 但实际输出: 2, 2, 2
原因解释
Lambda函数不会立即捕获变量i
的值,而是在被调用时引用当前环境中的i
。由于循环结束时i
的值是2,所以所有函数都返回2。
解决方法
使用默认参数值来捕获循环变量的当前值:
functions = []
for i in range(3):
functions.append(lambda i=i: i) # 默认参数会在定义时绑定
for f in functions:
print(f()) # 输出: 0, 1, 2
浮点数精度问题
Python中的浮点数计算可能会导致精度问题,这是由于浮点数在计算机中的二进制表示方式所致。
问题示例
print(0.1 + 0.2) # 预期: 0.3, 但实际输出: 0.30000000000000004
print(0.1 + 0.2 == 0.3) # 预期: True, 但实际输出: False
解决方法
对于需要精确小数计算的场景,使用decimal
模块:
from decimal import Decimal
print(Decimal('0.1') + Decimal('0.2')) # 输出: 0.3
print(Decimal('0.1') + Decimal('0.2') == Decimal('0.3')) # 输出: True
或者在比较浮点数时使用近似相等的方法:
def is_close(a, b, rel_tol=1e-9):
return abs(a - b) <= rel_tol
print(is_close(0.1 + 0.2, 0.3)) # 输出: True
从Python 3.5开始,标准库提供了math.isclose()
函数,可以替代上面的自定义函数。
可变与不可变对象
Python中的对象分为可变(mutable)和不可变(immutable)两种。理解这一区别对避免意外行为至关重要。
可变对象包括:
- 列表(list)
- 字典(dict)
- 集合(set)
不可变对象包括:
- 整数(int)
- 浮点数(float)
- 字符串(str)
- 元组(tuple)
问题示例
# 列表(可变)
list1 = [1, 2, 3]
list2 = list1
list2.append(4)
print(list1) # 输出: [1, 2, 3, 4]
# 字符串(不可变)
string1 = "hello"
string2 = string1
string2 = string2 + " world"
print(string1) # 输出: hello
解决方法
当需要复制可变对象而不是引用它们时,使用适当的复制方法:
# 浅拷贝
import copy
list1 = [1, 2, [3, 4]]
list2 = copy.copy(list1) # 或 list2 = list1.copy() 或 list2 = list1[:]
list2[0] = 9
print(list1) # 输出: [1, 2, [3, 4]]
list2[2][0] = 9
print(list1) # 输出: [1, 2, [9, 4]] # 注意嵌套列表被修改了
# 深拷贝
list1 = [1, 2, [3, 4]]
list3 = copy.deepcopy(list1)
list3[2][0] = 9
print(list1) # 输出: [1, 2, [3, 4]] # 嵌套列表不受影响
is
与 ==
的区别
is
运算符检查两个变量是否引用同一个对象(身份比较),而==
运算符检查两个变量的值是否相等(值比较)。
问题示例
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # 输出: True(值相等)
print(a is b) # 输出: False(不是同一个对象)
# 整数缓存的特殊情况
x = 256
y = 256
print(x is y) # 输出: True(Python缓存了-5到256的整数)
x = 257
y = 257
print(x is y) # 通常会输出: False(超出缓存范围)
最佳实践
- 使用
is
来检查一个对象是否为None
:if x is None:
- 使用
==
来比较值:if x == 0:
全局变量与局部变量
在函数内部修改全局变量时,需要使用global
关键字声明。
问题示例
counter = 0
def increment():
counter += 1 # 这会引发UnboundLocalError
# increment() # 错误:UnboundLocalError: local variable 'counter' referenced before assignment
原因解释
当你在函数内部尝试修改一个变量时,Python会假定它是一个局部变量。但在实际赋值前引用它会导致错误,因为局部变量'counter'尚未定义。
解决方法
counter = 0
def increment():
global counter
counter += 1
return counter
print(increment()) # 输出: 1
print(increment()) # 输出: 2
类变量与实例变量
类变量由所有类实例共享,而实例变量对每个实例都是唯一的。
问题示例
class Student:
grades = [] # 类变量
def __init__(self, name):
self.name = name # 实例变量
def add_grade(self, grade):
self.grades.append(grade) # 使用类变量
student1 = Student("Alice")
student2 = Student("Bob")
student1.add_grade(90)
print(student2.grades) # 预期: [], 但实际输出: [90]