Skip to content

Python 常见陷阱指南

本指南汇总了前端开发者在学习 Python 时最容易踩的坑,每个陷阱包含错误示例、原因分析、正确写法和 JS/TS 对比。

1. 可变默认参数陷阱

问题描述

Python 函数的默认参数在函数定义时求值一次,而不是每次调用时求值。

❌ 错误示例

python
def append_to(element, target=[]):
    """错误:默认参数 [] 只创建一次"""
    target.append(element)
    return target

# 第一次调用
result1 = append_to(1)
print(result1)  # [1]

# 第二次调用 (期望返回 [2],实际返回 [1, 2])
result2 = append_to(2)
print(result2)  # [1, 2] - 错误!使用了同一个列表

# 验证是同一个对象
print(result1 is result2)  # True

为什么前端开发者易犯?

JavaScript 中每次调用函数都会重新创建默认参数:

javascript
// JavaScript - 每次调用都创建新数组
function appendTo(element, target = []) {
  target.push(element)
  return target
}

console.log(appendTo(1)) // [1]
console.log(appendTo(2)) // [2] - 正确!

✅ 正确示例

python
def append_to(element, target=None):
    """正确:使用 None 作为哨兵值"""
    if target is None:
        target = []
    target.append(element)
    return target

result1 = append_to(1)
print(result1)  # [1]

result2 = append_to(2)
print(result2)  # [2] - 正确!

最佳实践

  • 避免使用可变对象 (列表、字典、集合) 作为默认参数
  • 使用 None 作为哨兵值,在函数内部创建可变对象
  • 不可变对象 (数字、字符串、元组) 作为默认参数是安全的

2. 闭包变量捕获 (延迟绑定)

问题描述

循环中创建的闭包函数会捕获变量的引用,而不是值,导致所有闭包共享同一个变量。

❌ 错误示例

python
# 创建3个函数,期望分别返回 0, 1, 2
funcs = [lambda: i for i in range(3)]

# 调用所有函数
results = [f() for f in funcs]
print(results)  # [2, 2, 2] - 错误!都返回最后一个值

为什么错误?

lambda 捕获的是变量 i 的引用,而不是值。当循环结束时,i 的值为 2,所有闭包都访问同一个 i

✅ 正确示例

方法 1: 使用默认参数立即绑定

python
funcs = [lambda i=i: i for i in range(3)]

results = [f() for f in funcs]
print(results)  # [0, 1, 2] - 正确!

方法 2: 使用 functools.partial

python
from functools import partial

def return_value(x):
    return x

funcs = [partial(return_value, i) for i in range(3)]
results = [f() for f in funcs]
print(results)  # [0, 1, 2]

JavaScript 对比

javascript
// JavaScript (let 会创建块级作用域)
const funcs = []
for (let i = 0; i < 3; i++) {
  funcs.push(() => i)
}

console.log(funcs.map((f) => f())) // [0, 1, 2] - 正确!

// 但使用 var 会有相同问题
const funcs2 = []
for (var i = 0; i < 3; i++) {
  funcs2.push(() => i)
}

console.log(funcs2.map((f) => f())) // [3, 3, 3] - 相同问题!

3. 整数缓存 (-5 到 256)

问题描述

Python 缓存了小整数 (-5 到 256),这些整数在内存中只有一个实例。

示例

python
# 小整数:使用缓存
a = 100
b = 100
print(a is b)  # True - 指向同一个对象

# 大整数:不使用缓存
x = 1000
y = 1000
print(x is y)  # False - 不同对象 (具体行为依赖解释器)

# 但值相等
print(x == y)  # True

最佳实践

  • 使用 == 比较值相等
  • 使用 is 仅用于检查对象身份 (如 None、单例)
  • 不要依赖整数缓存机制

JavaScript 对比

javascript
// JavaScript 中所有原始值比较都是值比较
const a = 100
const b = 100
console.log(a === b) // true

// 没有 is 运算符

4. 字符串驻留 (String Interning)

问题描述

Python 会自动驻留部分字符串 (如标识符风格的字符串),使它们在内存中只有一个实例。

示例

python
# 标识符风格的字符串:会驻留
a = "hello"
b = "hello"
print(a is b)  # True

# 包含空格或特殊字符:可能不驻留
x = "hello world"
y = "hello world"
print(x is y)  # False (具体行为依赖解释器)

# 但值相等
print(x == y)  # True

最佳实践

  • 比较字符串使用 ==,不要使用 is
  • is 仅用于检查 None、单例或对象身份

5. 列表/字典的浅拷贝 vs 深拷贝

问题描述

列表/字典的切片或 copy() 方法只创建浅拷贝,嵌套结构仍共享引用。

❌ 错误示例

python
# 浅拷贝
original = [[1, 2], [3, 4]]
shallow = original.copy()  # 或 original[:]

# 修改嵌套列表
shallow[0].append(99)

print(original)  # [[1, 2, 99], [3, 4]] - 原始列表也被修改!
print(shallow)   # [[1, 2, 99], [3, 4]]

✅ 正确示例

python
import copy

original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)

deep[0].append(99)

print(original)  # [[1, 2], [3, 4]] - 原始列表未被修改
print(deep)      # [[1, 2, 99], [3, 4]]

JavaScript 对比

javascript
// JavaScript - 同样的浅拷贝问题
const original = [
  [1, 2],
  [3, 4],
]
const shallow = [...original] // 或 original.slice()

shallow[0].push(99)

console.log(original) // [[1, 2, 99], [3, 4]] - 同样的问题!

// 深拷贝需要 structuredClone 或 JSON 技巧
const deep = structuredClone(original)
deep[0].push(100)
console.log(original) // [[1, 2, 99], [3, 4]] - 未被修改

6. 可变对象作为类属性

问题描述

类属性在所有实例间共享,使用可变对象作为类属性会导致意外共享。

❌ 错误示例

python
class User:
    # 类属性 (所有实例共享)
    permissions = []

user1 = User()
user2 = User()

user1.permissions.append("read")

print(user1.permissions)  # ['read']
print(user2.permissions)  # ['read'] - 错误!user2 也有了 read 权限
print(user1.permissions is user2.permissions)  # True - 同一个列表!

✅ 正确示例

python
class User:
    def __init__(self):
        # 实例属性 (每个实例独立)
        self.permissions = []

user1 = User()
user2 = User()

user1.permissions.append("read")

print(user1.permissions)  # ['read']
print(user2.permissions)  # [] - 正确!

JavaScript 对比

javascript
// JavaScript class 字段提案 (ES2022+)
class User {
  permissions = [] // 每个实例独立 (语法糖,实际在构造函数中初始化)
}

const user1 = new User()
const user2 = new User()

user1.permissions.push('read')

console.log(user1.permissions) // ['read']
console.log(user2.permissions) // [] - 正确!

7. 除法运算符差异 (/ vs //)

问题描述

Python 3 的 / 总是返回浮点数,// 才是整数除法 (向下取整)。

示例

python
# / 总是返回浮点数
print(10 / 3)   # 3.3333333333333335
print(10 / 5)   # 2.0 - 即使整除也返回浮点数

# // 整数除法 (向下取整)
print(10 // 3)  # 3
print(10 // 5)  # 2

# 负数的整数除法向下取整
print(-10 // 3)  # -4 (不是 -3!)
print(-10 / 3)   # -3.3333333333333335

JavaScript 对比

javascript
// JavaScript - 只有一种除法运算符
console.log(10 / 3) // 3.3333333333333335

// 整数除法需要手动转换
console.log(Math.floor(10 / 3)) // 3
console.log(Math.trunc(10 / 3)) // 3 (截断,不是向下取整)

最佳实践

  • 需要浮点结果时使用 /
  • 需要整数结果时使用 //
  • 取余数使用 %

8. 缩进错误 (空格 vs Tab 混用)

问题描述

Python 使用缩进表示代码块,混用空格和 Tab 会导致 IndentationError

❌ 错误示例

python
def example():
    print("Line 1")  # 4个空格
	print("Line 2")  # 1个Tab - IndentationError!

✅ 正确示例

python
def example():
    print("Line 1")  # 4个空格
    print("Line 2")  # 4个空格

最佳实践

  • 始终使用空格,不要使用 Tab (PEP 8 推荐)
  • 使用 4 个空格作为缩进单位
  • 配置编辑器自动转换 Tab 为空格
  • 使用 Ruff 或 Black 自动格式化代码

VSCode 配置

json
{
  "editor.insertSpaces": true,
  "editor.tabSize": 4,
  "python.formatting.provider": "black"
}

9. global 和 nonlocal 使用误区

问题描述

函数内部修改外部变量需要使用 globalnonlocal 关键字。

❌ 错误示例

python
count = 0

def increment():
    count = count + 1  # UnboundLocalError!
    # Python 认为 count 是局部变量,但在赋值前被引用

increment()

✅ 正确示例

使用 global (修改全局变量):

python
count = 0

def increment():
    global count
    count = count + 1

increment()
print(count)  # 1

使用 nonlocal (修改闭包变量):

python
def outer():
    count = 0

    def increment():
        nonlocal count
        count += 1

    increment()
    increment()
    return count

print(outer())  # 2

JavaScript 对比

javascript
// JavaScript - 自动查找外部作用域的变量
let count = 0

function increment() {
  count = count + 1 // 直接修改外部变量,无需声明
}

increment()
console.log(count) // 1

最佳实践

  • 避免修改全局变量,使用参数和返回值传递数据
  • 如必须修改,使用 globalnonlocal 明确声明

10. 列表推导式中的变量泄漏 (Python 2 遗留问题)

Python 3 中已修复

在 Python 2 中,列表推导式的循环变量会泄漏到外部作用域,但 Python 3 已修复。

python
# Python 3 - 变量不会泄漏
result = [i * 2 for i in range(3)]

try:
    print(i)  # NameError: name 'i' is not defined
except NameError:
    print("i 不存在于外部作用域")

但生成器表达式和推导式内部的赋值不会泄漏

python
# 推导式内部的 walrus 运算符 (:=) 会创建外部变量
result = [y := i * 2 for i in range(3)]
print(y)  # 4 - walrus 运算符创建的变量会泄漏到外部

11. 字典键必须是可哈希的

问题描述

字典的键必须是不可变对象 (可哈希),列表、字典、集合不能作为键。

❌ 错误示例

python
# 列表不能作为键
d = {}
key = [1, 2, 3]
d[key] = "value"  # TypeError: unhashable type: 'list'

✅ 正确示例

python
# 使用元组 (不可变)
d = {}
key = (1, 2, 3)
d[key] = "value"
print(d)  # {(1, 2, 3): 'value'}

# 或使用字符串
d = {"key": "value"}

JavaScript 对比

javascript
// JavaScript - 对象键会自动转换为字符串
const d = {}
const key = [1, 2, 3]
d[key] = 'value'

console.log(d) // { '1,2,3': 'value' } - 数组被转换为字符串

// Map 可以使用任意值作为键
const map = new Map()
map.set([1, 2, 3], 'value')
console.log(map.get([1, 2, 3])) // undefined - 不同的数组对象!

12. 布尔值的真值测试

问题描述

Python 中很多对象在布尔上下文中会被视为 False (如空列表、空字符串、0、None)。

示例

python
# 以下值在布尔上下文中为 False:
# - None
# - False
# - 0, 0.0, 0j (数字零)
# - '', [], {}, set() (空序列/集合)

values = [None, False, 0, 0.0, '', [], {}, set()]

for value in values:
    if not value:
        print(f"{repr(value)} is falsy")

注意事项

python
# 检查是否为 None,使用 is,不要使用 ==
value = None

if value is None:  # 正确
    print("value is None")

if not value:  # 不推荐:空列表、0 等也会通过
    print("value is falsy")

JavaScript 对比

javascript
// JavaScript 的 falsy 值
// false, 0, -0, 0n, '', null, undefined, NaN

console.log(!![]) // true - 空数组是 truthy!
console.log(!!'') // false

检测工具推荐

使用以下工具自动检测常见陷阱:

1. Ruff (极速 Python 代码检查器)

bash
# 安装
uv add --dev ruff

# 检查代码
ruff check .

# 自动修复
ruff check --fix .

2. Pylint

bash
# 安装
uv add --dev pylint

# 检查代码
pylint my_module.py

3. MyPy (类型检查)

bash
# 安装
uv add --dev mypy

# 类型检查
mypy my_module.py

小结

本章要点

  • 避免使用可变对象作为默认参数,使用 None 作为哨兵值
  • 闭包变量捕获引用而非值,使用默认参数立即绑定
  • 比较值相等使用 ==,检查对象身份使用 is (仅用于 None、单例)
  • 浅拷贝只复制顶层,嵌套结构需要 copy.deepcopy()
  • 类属性在所有实例间共享,实例属性在 __init__ 中初始化
  • / 是浮点除法,// 是整数除法 (向下取整)
  • 始终使用 4 个空格缩进,不要混用 Tab
  • 修改外部变量需要 globalnonlocal 声明
  • 使用 Ruff、Pylint、MyPy 自动检测问题

与 JS/TS 的关键差异

JavaScript/TypeScriptPython差异说明
默认参数每次求值默认参数定义时求值一次Python 需避免可变默认参数
let 创建块级作用域闭包捕获引用Python 需默认参数立即绑定
原始值比较是值比较小整数/字符串可能共享Python 使用 == 比较值
空数组/对象是 truthy空序列/集合是 falsyPython 真值测试更宽泛
只有一种除法运算符/// 两种Python 区分浮点和整数除法

推荐下一步阅读

学习文档整合站点