Python 编程的常见陷阱与奇巧淫技

Posted on Tue 27 January 2026 in Tech

Abstract Python 编程的常见陷阱与奇巧淫技
Authors Walter Fan
Category tech note
Status v1.0
Updated 2026-01-27
License CC-BY-NC-ND 4.0

Python:最容易上手,也最容易写烂的语言

Python 大概是世界上"入门门槛最低"的编程语言之一。

不信你看:

  • 不用声明类型
  • 不用写分号
  • 不用编译
  • 缩进即作用域
  • 标准库啥都有

一个从没写过代码的人,花半小时就能跑起一个"Hello World",花一下午就能写一个爬虫。这是 Python 的魅力:它让编程变得"不那么可怕"

但问题也来了:入门容易,精通很难;能跑起来,跑稳很难

我见过太多这样的代码:

  • 测试环境跑得好好的,上线就崩
  • 小数据量没问题,数据一大就卡死
  • 逻辑看着没毛病,但结果就是不对
  • 代码能用,但没人敢改

这些问题往往不是"不会写",而是踩了 Python 的隐藏陷阱

Python 的设计哲学里有很多"和直觉不一样"的地方:

你以为的 实际上
默认参数每次调用都初始化 只在函数定义时初始化一次
is== 差不多 一个比地址,一个比值
拷贝就是独立副本 浅拷贝只复制一层
lambda 里的变量是"快照" 是引用,循环结束才求值

这些坑,不踩不知道,踩一次记一辈子。

所以这篇文章的目的很明确

  • 陷阱:帮你避开那些"看起来应该行,但就是不行"的经典坑
  • 奇巧淫技:教你一些"原来还能这么写"的骚操作
  • 工程建议:给你一套可落地的"写好 Python"的检查清单

读完你会发现:Python 很简单,但写稳定、高效、可维护的 Python 一点都不简单。


一、10 大陷阱:这些坑你迟早会踩

陷阱 1:可变默认参数(新手杀手 No.1)

def append_to_list(item, target=[]):
    target.append(item)
    return target

你以为每次调用都会得到一个新列表?

print(append_to_list(1))  # [1]
print(append_to_list(2))  # [1, 2]  ???
print(append_to_list(3))  # [1, 2, 3]  ??????

原因:默认参数只在函数定义时计算一次,不是每次调用时。target=[] 那个 [] 是同一个对象。

正确写法

def append_to_list(item, target=None):
    if target is None:
        target = []
    target.append(item)
    return target

经验法则:默认参数永远不要用可变对象(list、dict、set)。用 None 代替,函数内再初始化。


陷阱 2:闭包变量绑定(循环里创建 lambda 的噩梦)

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

print([f() for f in funcs])  # [2, 2, 2]  不是 [0, 1, 2]

原因:lambda 里的 i 不是"创建时的值",而是对变量 i引用。循环结束时 i == 2,所以三个 lambda 都返回 2。

正确写法:用默认参数"捕获"当时的值

funcs = []
for i in range(3):
    funcs.append(lambda i=i: i)  # 注意 i=i

print([f() for f in funcs])  # [0, 1, 2]

或者用 functools.partial

from functools import partial

funcs = [partial(lambda x: x, i) for i in range(3)]
print([f() for f in funcs])  # [0, 1, 2]

陷阱 3:is vs ==(小整数缓存的坑)

a = 256
b = 256
print(a is b)  # True

a = 257
b = 257
print(a is b)  # False  ???

原因:Python 会缓存 -5 到 256 之间的小整数。is 比较的是对象身份(内存地址),不是值。

规则

  • 比较值:用 ==
  • 比较身份(是不是同一个对象):用 is
  • 判断 None:用 is None(因为 None 是单例)
# 正确
if x is None:
    pass

# 错误(虽然通常也能用,但不规范)
if x == None:
    pass

陷阱 4:浅拷贝 vs 深拷贝(嵌套结构的隐形炸弹)

import copy

original = [[1, 2], [3, 4]]
shallow = original.copy()  # 或 list(original) 或 original[:]
deep = copy.deepcopy(original)

original[0][0] = 999

print(shallow)  # [[999, 2], [3, 4]]  被改了!
print(deep)     # [[1, 2], [3, 4]]    没被改

原因:浅拷贝只复制"第一层",嵌套的对象还是共享引用。

什么时候用深拷贝

  • 嵌套结构(list of list、dict of dict)
  • 你要完全独立的副本

什么时候浅拷贝就够

  • 扁平结构(一维列表、简单字典)
  • 只读访问

陷阱 5:列表乘法创建引用(又一个新手杀手)

# 想创建一个 3x3 的二维数组
matrix = [[0] * 3] * 3
print(matrix)  # [[0, 0, 0], [0, 0, 0], [0, 0, 0]]  看起来没问题?

matrix[0][0] = 1
print(matrix)  # [[1, 0, 0], [1, 0, 0], [1, 0, 0]]  三行都被改了!

原因[[0] * 3] * 3 创建的是三个指向同一个列表的引用,不是三个独立的列表。

正确写法

# 用列表推导式,每次都创建新列表
matrix = [[0] * 3 for _ in range(3)]

matrix[0][0] = 1
print(matrix)  # [[1, 0, 0], [0, 0, 0], [0, 0, 0]]  正确!

记住:* 复制的是引用,不是对象本身。


陷阱 6:字典迭代时修改(运行时炸弹)

d = {'a': 1, 'b': 2, 'c': 3}

for key in d:
    if d[key] == 2:
        del d[key]  # RuntimeError: dictionary changed size during iteration

正确写法:先收集要删的 key,再删

# 方法一:收集后删除
to_delete = [k for k, v in d.items() if v == 2]
for k in to_delete:
    del d[k]

# 方法二:构造新字典
d = {k: v for k, v in d.items() if v != 2}

陷阱 7:字符串拼接的性能陷阱

# 错误:每次 += 都会创建新字符串,O(n²)
result = ""
for s in many_strings:
    result += s

# 正确:用 join,O(n)
result = "".join(many_strings)

字符串是不可变的,+= 会创建新对象。当 many_strings 很大时,性能差距是数量级的。


陷阱 8:浮点数精度问题(钱算错了别怪我)

print(0.1 + 0.2)  # 0.30000000000000004  ???
print(0.1 + 0.2 == 0.3)  # False  ??????

原因:浮点数在计算机里是二进制表示的,0.1 在二进制里是无限循环小数,存储时会有精度损失。

解决方案

# 方法一:用 decimal(金融计算必用)
from decimal import Decimal
print(Decimal('0.1') + Decimal('0.2'))  # 0.3

# 方法二:用 math.isclose 比较(Python 3.5+)
import math
print(math.isclose(0.1 + 0.2, 0.3))  # True

# 方法三:用整数运算(以分为单位存储金额)
price_in_cents = 100 + 200  # 3 块钱

涉及钱的计算,永远不要直接用 float


陷阱 9:except 捕获太宽(吞掉所有异常)

# 错误:什么异常都吞了,包括 KeyboardInterrupt、SystemExit
try:
    do_something()
except:
    pass

# 也不好:Exception 太宽
try:
    do_something()
except Exception:
    pass

# 正确:只捕获你能处理的
try:
    do_something()
except (ValueError, KeyError) as e:
    handle_error(e)

我的原则:捕获异常要像开枪一样精准,别用散弹枪。


陷阱 10:GIL 与多线程(以为能加速,结果更慢)

import threading

counter = 0

def increment():
    global counter
    for _ in range(1000000):
        counter += 1

threads = [threading.Thread(target=increment) for _ in range(2)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print(counter)  # 不是 2000000,而是一个随机的小于 2000000 的数

原因

  1. GIL(全局解释器锁):CPython 同一时刻只有一个线程执行 Python 字节码,多线程不能利用多核 CPU
  2. counter += 1 不是原子操作:包含读取、加 1、写回三步,线程可能在任何一步被切换

解决方案

# CPU 密集型:用多进程
from multiprocessing import Process, Value

# 或者用 threading.Lock 保护共享变量
import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(1000000):
        with lock:
            counter += 1

# IO 密集型:多线程或 asyncio 仍然有效
import asyncio

记住:Python 多线程适合 IO 密集型(网络请求、文件读写),CPU 密集型用多进程或者换语言。


二、10 小窍门:让代码更优雅的骚操作

窍门 1:海象运算符 :=(Python 3.8+)

# 之前
line = file.readline()
while line:
    process(line)
    line = file.readline()

# 之后
while (line := file.readline()):
    process(line)

# 在条件表达式中避免重复计算
if (n := len(some_list)) > 10:
    print(f"List is too long ({n} elements)")

特别适合"赋值并判断"的场景。


窍门 2:字典合并(Python 3.9+)

# 旧写法
merged = {**dict1, **dict2}

# 新写法(更清晰)
merged = dict1 | dict2

# 就地更新
dict1 |= dict2

窍门 3:collections 里的宝藏

from collections import defaultdict, Counter, namedtuple, deque

# defaultdict:自动初始化
word_count = defaultdict(int)
for word in words:
    word_count[word] += 1  # 不用先 if word not in word_count

# Counter:计数神器
counter = Counter(words)
print(counter.most_common(3))  # 前 3 个高频词

# namedtuple:轻量级数据类
Point = namedtuple('Point', ['x', 'y'])
p = Point(1, 2)
print(p.x, p.y)  # 比用 dict 或 tuple 更清晰

# deque:高效的双端队列
q = deque(maxlen=3)  # 固定长度,自动弹出旧元素
q.append(1)
q.append(2)
q.append(3)
q.append(4)  # 1 被自动弹出
print(q)  # deque([2, 3, 4], maxlen=3)

窍门 4:itertools 里的黑魔法

from itertools import chain, groupby, islice, cycle, combinations

# chain:扁平化多个可迭代对象
for item in chain([1, 2], [3, 4], [5]):
    print(item)  # 1, 2, 3, 4, 5

# groupby:分组(注意要先排序!)
data = [('a', 1), ('a', 2), ('b', 3), ('b', 4)]
for key, group in groupby(data, key=lambda x: x[0]):
    print(key, list(group))
# a [('a', 1), ('a', 2)]
# b [('b', 3), ('b', 4)]

# islice:切片迭代器(不用全部加载到内存)
first_10 = islice(huge_iterator, 10)

# combinations:组合
list(combinations([1, 2, 3], 2))  # [(1, 2), (1, 3), (2, 3)]

窍门 5:上下文管理器(with 的威力)

# 自定义上下文管理器(类方式)
class Timer:
    def __enter__(self):
        self.start = time.time()
        return self

    def __exit__(self, *args):
        self.elapsed = time.time() - self.start
        print(f"耗时: {self.elapsed:.2f}s")

with Timer():
    do_something_slow()

# 更简洁:用 contextlib
from contextlib import contextmanager

@contextmanager
def timer():
    start = time.time()
    yield
    print(f"耗时: {time.time() - start:.2f}s")

with timer():
    do_something_slow()

窍门 6:__slots__ 省内存的大杀器

# 普通类:每个实例都有一个 __dict__
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

# 使用 __slots__:省掉 __dict__,内存占用大幅下降
class Point:
    __slots__ = ['x', 'y']

    def __init__(self, x, y):
        self.x = x
        self.y = y

当你要创建百万级对象时,__slots__ 能省 40%-50% 内存。

代价:不能动态添加属性。


窍门 7:functools 的实用工具

from functools import lru_cache, partial, reduce

# lru_cache:自动缓存(适合纯函数)
@lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# partial:固定部分参数
def power(base, exponent):
    return base ** exponent

square = partial(power, exponent=2)
print(square(5))  # 25

# reduce:累积操作
from functools import reduce
product = reduce(lambda x, y: x * y, [1, 2, 3, 4])  # 24

窍门 8:一行流(Pythonic 但别过度)

# 交换变量
a, b = b, a

# 条件表达式
result = "yes" if condition else "no"

# 链式比较
if 0 < x < 10:
    pass

# 多重赋值
a = b = c = 0

# 解包
first, *middle, last = [1, 2, 3, 4, 5]
# first=1, middle=[2,3,4], last=5

# 字典推导式反转 key-value
inverted = {v: k for k, v in original.items()}

# any / all
if any(x > 10 for x in numbers):
    print("有大于10的")

if all(x > 0 for x in numbers):
    print("全是正数")

窍门 9:dataclasses(Python 3.7+)

from dataclasses import dataclass, field

@dataclass
class User:
    name: str
    age: int
    tags: list = field(default_factory=list)  # 可变默认值要用 field

    def is_adult(self) -> bool:
        return self.age >= 18

user = User("Alice", 25)
print(user)  # User(name='Alice', age=25, tags=[])

namedtuple 更灵活,比手写 __init__ 更省事。


窍门 10:f-string 的高级用法

name = "Alice"
score = 95.5678

# 基本用法
print(f"Hello, {name}!")

# 格式化数字
print(f"Score: {score:.2f}")  # 95.57

# 对齐
print(f"{name:>10}")  # 右对齐,宽度10
print(f"{name:<10}")  # 左对齐
print(f"{name:^10}")  # 居中

# 调试神器(Python 3.8+)
x = 10
y = 20
print(f"{x=}, {y=}")  # x=10, y=20

# 表达式
print(f"{2 + 2 = }")  # 2 + 2 = 4

三、性能速查表

场景
字符串拼接 += 循环 "".join(list)
成员检查 x in list x in set
计数 手写 dict Counter
缓存计算 每次重算 @lru_cache
大量小对象 普通 class __slots__namedtuple
读大文件 readlines() 逐行迭代 for line in file

TL;DR(太长不看版)

10 大陷阱速记

# 陷阱 正确做法
1 可变默认参数 None,函数内初始化
2 闭包变量绑定 lambda i=i: i 捕获值
3 is vs == 比值用 ==,判 None 用 is
4 浅拷贝嵌套结构 copy.deepcopy()
5 列表乘法创建引用 列表推导式 [[] for _ in range(n)]
6 迭代时修改字典 先收集再删,或构造新字典
7 字符串 += 拼接 "".join(list)
8 浮点数精度 Decimal 或整数运算
9 except 太宽 只捕获你能处理的异常
10 GIL 多线程 CPU 密集用多进程,IO 密集用 asyncio

10 小窍门速记

# 窍门 一句话
1 海象运算符 := 赋值并判断
2 字典合并 \| {**a, **b} 更清晰
3 collections Counter、defaultdict、deque
4 itertools chain、groupby、islice
5 上下文管理器 @contextmanager 简化 with
6 __slots__ 百万对象省 40% 内存
7 functools lru_cache、partial
8 一行流 解包、链式比较、any/all
9 dataclasses 比 namedtuple 更灵活
10 f-string f"{x=}" 调试神器

四、工程建议:写出"能上线"的 Python

知道陷阱和技巧还不够,真正的挑战是把它们融入日常习惯。这里给你一套可落地的建议。

4.1 代码风格:让别人能接手

# 推荐的工具链
pip install black isort flake8 mypy

# 格式化
black your_code.py
isort your_code.py

# 静态检查
flake8 your_code.py
mypy your_code.py
  • black:自动格式化,别再纠结缩进和换行
  • isort:自动整理 import 顺序
  • flake8:检查代码风格和常见问题
  • mypy:类型检查(配合 type hints)

我的原则:让机器能检查的,就别靠人肉 review

4.2 类型提示:给未来的自己留条活路

# 不推荐
def process(data):
    return data.get('name')

# 推荐
from typing import Optional

def process(data: dict[str, str]) -> Optional[str]:
    return data.get('name')

类型提示的好处:

  • IDE 能自动补全
  • mypy 能提前发现错误
  • 三个月后你还能看懂自己写的代码

4.3 测试:别等上线才发现问题

# 用 pytest,简单直接
def test_append_to_list():
    result = append_to_list(1)
    assert result == [1]

    # 验证不会有"可变默认参数"的问题
    result2 = append_to_list(2)
    assert result2 == [2]  # 应该是新列表,不是 [1, 2]

最小测试覆盖:

  • 核心逻辑:必须测
  • 边界条件:空值、None、超大数据
  • 已知陷阱:可变默认参数、闭包变量等

4.4 日志:出了问题能查

import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

def do_something(data):
    logger.info(f"开始处理,数据量: {len(data)}")
    try:
        result = process(data)
        logger.info(f"处理完成,结果: {result}")
        return result
    except Exception as e:
        logger.exception(f"处理失败: {e}")
        raise

线上出问题时,日志是你唯一的眼睛。

4.5 依赖管理:别让环境变成玄学

# 推荐用 poetry 或 pip-tools
pip install poetry
poetry init
poetry add requests

# 或者用 pip-tools
pip install pip-tools
pip-compile requirements.in
pip-sync requirements.txt
  • 锁定依赖版本
  • 区分开发依赖和运行时依赖
  • 用虚拟环境隔离项目

Checklist:写 Python 前/后自检

写代码时(对照 10 大陷阱)

  • [ ] 陷阱 1:函数的默认参数有没有用可变对象?(用 None 代替)
  • [ ] 陷阱 2:循环里创建 lambda/闭包有没有正确捕获变量?(i=i
  • [ ] 陷阱 3:比较值用 ==,比较 None 用 is None
  • [ ] 陷阱 4:嵌套数据结构需要独立副本时用了 deepcopy
  • [ ] 陷阱 5:用 [[]] * n 创建二维数组了吗?(改用列表推导式)
  • [ ] 陷阱 6:迭代时没有修改正在迭代的 dict/list?
  • [ ] 陷阱 7:字符串大量拼接用 "".join() 了?
  • [ ] 陷阱 8:涉及金额计算用了 Decimal 而不是 float
  • [ ] 陷阱 9:异常捕获是否足够精准?(别用裸 except
  • [ ] 陷阱 10:CPU 密集型任务用多进程而不是多线程?

提交前

  • [ ] 跑了 black / isort 格式化?
  • [ ] 跑了 flake8 / mypy 检查?
  • [ ] 核心逻辑有单元测试覆盖?
  • [ ] 关键路径有日志输出?
  • [ ] 依赖版本锁定了(requirements.txt / poetry.lock)?

Code Review 时

  • [ ] 有没有"我以为"的隐式假设?
  • [ ] 边界条件处理了吗(空值、None、超大数据)?
  • [ ] 异常处理合理吗?
  • [ ] 性能敏感的地方考虑过复杂度吗?
  • [ ] 有没有用到本文提到的陷阱写法?

扩展阅读


@startmindmap
* Python 陷阱与技巧
** 常见陷阱
*** 可变默认参数
*** 闭包变量绑定
*** is 与 == 区别
*** 浅拷贝深拷贝
*** 列表乘法引用
** 实用技巧
*** 海象运算符
*** 字典合并操作
*** collections 模块
*** itertools 模块
*** 上下文管理器
** 性能优化
*** join 拼接字符串
*** set 检查成员
*** lru cache 缓存
** 工程实践
*** 代码格式化工具
*** 类型提示
*** pytest 测试
@endmindmap

Python 陷阱与技巧思维导图


本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。