《Python编程入门》附录 D:常见错误与解决方案

本附录分两半:报错速查按异常类型列出真实报错原文、成因与修法,覆盖 SyntaxError 到 AssertionError 共十七类;常见困惑 FAQ 解答 a=b 的别名、可变默认参数、多线程没变快、is 与 ==、pip 装错环境、中文乱码、.pyc 缓存、finally 里 return 等问题。

这是全书查阅频率最高的一页,分两半:前半是报错速查,按异常类型给出真实报错原文、成因、修法与对应章节;后半是常见困惑 FAQ,回答那些「代码能跑但结果不对」的问题。

一条贯穿全文的规律:读懂「成因」比记住「修法」更重要。同一个报错在不同上下文里有不同成因,只会照抄修法的人,换个场景又会卡住。下面所有报错原文都在本机 Python 3.14.6 上真实运行取得,为便于阅读只截取关键行。

附录 D 常见错误与解决方案

报错速查

SyntaxError:语法错误

def f()
    return 1

报错原文:SyntaxError: expected ':',箭头 ^ 指向 def f() 行。

成因:源码不符合语法规则,解释器在解析阶段就拒绝执行。箭头指向的位置常是「发现错误处」,真正的问题往往在上一行。
修法:看箭头并往回检查一行;expected ':' 表示函数 / 条件 / 循环头部漏了冒号。
详见:2.2 编辑器、REPL 与第一个脚本 。

IndentationError:缩进错误

def f():
    x = 1
   y = 2

报错原文:IndentationError: unindent does not match any outer indentation level。

成因:同一代码块内混用了空格与 Tab,或缩进层级对不齐。Python 用缩进表示代码块,因此缩进是语法的一部分。
修法:统一用 4 个空格;打开编辑器「显示空白字符」排查混用的 Tab。
详见:4.1 条件、循环与推导式 。

NameError:名字未定义

print(total)

报错原文:NameError: name 'total' is not defined。

成因:使用了一个从未赋值过的名字,常见于变量名拼错、变量在使用之后才定义,或函数内的局部变量被外部访问。
修法:核对拼写;确认变量在使用之前已赋值;跨函数共享时检查作用域与 global / nonlocal。
详见:4.2 函数:参数传递、默认值与作用域 。

TypeError:类型不匹配

print("a" + 1)

报错原文:TypeError: can only concatenate str (not "int") to str。

成因:对不兼容的类型做了操作。信息里「not “int”」明确指出是右边的操作数类型不对。
修法:显式转换,如 "a" + str(1),或用 f-string f"a{1}";调用函数时核对参数类型与数量。
详见:3.1 数值、字符串与 f-string 格式化 。

ValueError:类型对但值不对

print(int("abc"))

报错原文:ValueError: invalid literal for int() with base 10: 'abc'。

成因:类型是 str 没错,但内容无法转成整数。TypeError 是「类型错」,ValueError 是「类型对、值非法」——这是两者最关键的区别。
修法:用 try/except ValueError 捕获并给出友好提示;或先校验再转换(如 s.strip().isdigit())。
详见:7.1 异常层次与 try/except/else/finally 。

KeyError:字典键不存在

d = {"a": 1}
print(d["b"])

报错原文:KeyError: 'b'。

成因:用 d[key] 取一个不存在的键。注意 KeyError 的参数就是那个键本身。
修法:改用 d.get("b", default),或先 if "b" in d 判断;也可用 d.setdefault 兜底。
详见:3.2 列表、元组、字典与集合 。

IndexError:序列下标越界

xs = [1, 2, 3]
print(xs[5])

报错原文:IndexError: list index out of range。

成因:用超出序列长度的下标取值。切片 xs[5:6] 不会报错(返回空列表),但单点取值 xs[5] 会。
修法:用 len(xs) 核对边界;遍历时优先用 for x in xs 而非下标;确需下标时考虑 enumerate。
详见:3.2 列表、元组、字典与集合 。

AttributeError:对象没有该属性

s = "hello"
print(s.push("x"))

报错原文:AttributeError: 'str' object has no attribute 'push'。

成因:访问了对象不具备的方法或属性。这里把列表的 append 与字符串的方法记混了(字符串不可变,没有 push)。
修法:用 dir(obj) 或 help(obj) 查它有哪些方法;确认变量类型是否与预期一致(可能是上游返回了 None)。
详见:6.1 类、实例与属性查找(MRO) 。

ImportError:导入的名字不存在

from math import sqrt, logar

报错原文:ImportError: cannot import name 'logar' from 'math' ... Did you mean: 'log10'?

成因:模块本身能导入,但里面没有这个名字。3.14 的报错还会给出「Did you mean」建议。
修法:按提示改正名字;用 dir(module) 或官方文档确认可用名字;循环导入也会表现为 ImportError。
详见:5.1 import 机制与模块搜索路径 。

ModuleNotFoundError:整个模块找不到

import pandasx

报错原文:ModuleNotFoundError: No module named 'pandasx'。

成因:ImportError 的子类,指模块整体不存在。两种典型:包名拼错,或包装在了另一个虚拟环境里。
修法:先核对包名拼写;再用 python -c "import sys; print(sys.executable)" 对照 python -m pip -V 确认环境一致。
详见:2.1 安装 Python 与虚拟环境 。

FileNotFoundError:文件不存在

open("nope.txt", encoding="utf-8")

报错原文:FileNotFoundError: [Errno 2] No such file or directory: 'nope.txt'。

成因:相对路径是相对当前工作目录而非脚本所在目录解析的,这是新手最常踩的坑;也可能确实文件名写错。
修法:用 pathlib.Path(__file__).parent / "nope.txt" 基于脚本定位;写入前先 Path.mkdir(parents=True, exist_ok=True)。
详见:10.2 文件 I/O、pathlib 与编码 。

UnicodeDecodeError:按错误的编码读文件

open("gbk.txt", encoding="utf-8").read()

报错原文:UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd6 in position 0: invalid continuation byte。

成因:文件实际是 GBK 编码,却按 UTF-8 解码。报错里的字节值 0xd6 就是第一个解不出来的字节。
修法:用正确的编码打开,如 encoding="gbk";读取时始终显式写 encoding,别依赖系统默认值。
详见:10.2 文件 I/O、pathlib 与编码 。

ZeroDivisionError:除以零

print(1 / 0)

报错原文:ZeroDivisionError: division by zero。

成因:除数为 0。整数除法 // 与取模 % 遇到 0 同样报此错。
修法:除法前判零;统计平均值时先确认分母非零。
详见:3.1 数值、字符串与 f-string 格式化 。

RecursionError:递归过深

def f(n):
    return f(n + 1)
f(0)

报错原文:RecursionError: maximum recursion depth exceeded,前面有 [Previous line repeated 996 more times]。

成因:递归没有终止条件,或层数超过默认上限(约 1000)。消息里的 996 more times 是重复帧的省略。
修法:补上终止条件;把递归改写成循环或迭代器;确需更深递归时可 sys.setrecursionlimit,但那只是推迟问题。
详见:8.1 可迭代协议、迭代器与生成器 。

RuntimeError:运行期通用错误

import threading
def boom():
    raise RuntimeError("worker failed")
t = threading.Thread(target=boom)
t.start()
t.join()

报错原文:RuntimeError: worker failed,由子线程打印。

成因:不属于其他更具体类别的运行期错误。子线程里未捕获的异常会原样打印到标准错误,但不会让主线程退出——这是线程调试最容易被误导的地方。
修法:子线程里用 try/except 记录异常;或改用 concurrent.futures,异常会在取 result() 时重新抛出。
详见:12.1 GIL 与线程 / 进程模型 。

StopIteration:迭代器已耗尽

it = iter([1])
next(it)
next(it)

报错原文:StopIteration(无附加消息)。

成因:对已经耗尽的迭代器继续调 next()。for 循环内部会捕获它并正常结束,所以手动 next() 才容易撞上。
修法:给 next() 传默认值 next(it, None);或改用 for 循环。生成器里用 return 表示结束,别手动抛 StopIteration(PEP 479 会把它转成 RuntimeError)。
详见:8.1 可迭代协议、迭代器与生成器 。

AssertionError:断言失败

x = 5
assert x > 10, "x must exceed 10"

报错原文:AssertionError: x must exceed 10。

成因:assert 的条件为假。它是调试工具,不是参数校验——python -O 优化模式下所有 assert 会被整体移除。
修法:调试期保留 assert 定位问题;对外部输入做校验要用 if ...: raise ValueError(...)。
详见:14.1 pytest 基础与断言 。

常见困惑 FAQ

为什么 a = b 之后改 a 会影响 b

>>> a = [1, 2]
>>> b = a
>>> b.append(3)
>>> a
[1, 2, 3]

a = b 只是让两个名字指向同一个列表对象,并没有复制内容。要独立副本用 a = b.copy()(浅拷贝)或 copy.deepcopy(b)(深拷贝,嵌套也复制)。详见 3.3 可变与不可变、引用语义与拷贝 。

为什么默认参数用 [] 会串数据

默认值在函数定义时求值一次,那个列表被所有调用共享,因此 add(1)、add(2)、add(3) 返回的是同一个列表。正确写法是把默认值设成 None,函数体内再创建:

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

详见 4.2 函数:参数传递、默认值与作用域 。

为什么多线程没让程序变快

本机实测:两段纯计算的 CPU 任务,顺序执行约 0.447s,开两个线程并行约 0.458s,几乎没有变化。原因是 GIL(全局解释器锁)保证同一时刻只有一个线程执行 Python 字节码。

想让 CPU 密集任务真正并行,要用多进程(multiprocessing / ProcessPoolExecutor);多线程只在等 I/O(网络、磁盘)时有优势。详见 12.1 GIL 与线程 / 进程模型 。

为什么 is 和 == 不一样

== 比较值,is 比较是不是同一个对象(内存身份)。

>>> [1, 2, 3] == [1, 2, 3]
True
>>> [1, 2, 3] is [1, 2, 3]
False
>>> a = 1000; b = int("1000")
>>> a == b, a is b
(True, False)

只用 is 判断 None、True、False 这类单例;其余一律用 ==。小整数(−5 到 256)会被缓存,导致 256 is 256 为 True,别依赖这种实现细节。详见 3.3 可变与不可变、引用语义与拷贝 。

为什么 pip install 装到别的环境去了

最常见的三种原因:用了系统 pip 而非虚拟环境里的 pip;python 与 pip 指向不同环境;多个 Python 版本共存。判断基准是让解释器自己报告:

python -c "import sys; print(sys.executable)"
python -m pip -V

始终用 python -m pip install ...,能保证 pip 与当前解释器同源。详见 2.1 安装 Python 与虚拟环境 。

为什么中文读出来是乱码

要么文件不是 UTF-8,要么按错误的编码去读(见上文 UnicodeDecodeError)。读写文件时永远显式指定 encoding="utf-8",不要依赖 locale.getpreferredencoding()——它在不同平台上结果不同。详见 10.2 文件 I/O、pathlib 与编码 。

为什么改了代码却没生效(.pyc 缓存)

Python 会把导入的模块编译成 __pycache__/*.pyc 缓存,正常情况会比对源文件修改时间自动重编译,所以「改了不生效」几乎总是别的原因:进程没重启、from module import name 导入的是旧对象、或编辑器保存失败。排查手段:确认进程真重启了;必要时 python -B 禁用字节码缓存,或删掉 __pycache__/。详见 5.1 import 机制与模块搜索路径 。

为什么 finally 里的 return 会有警告

3.14 起在 finally 块里写 return / break / continue 会触发 SyntaxWarning(PEP 765),因为这会吞掉异常与返回值。实测下面这段代码运行时打印 SyntaxWarning: 'return' in a 'finally' block,最终返回 2,try 里的 return 1 被无声丢弃:

def f():
    try:
        return 1
    finally:
        return 2

把清理逻辑放在 finally,不要在里面改变控制流。详见 7.1 异常层次与 try/except/else/finally 。

为什么 for 循环里删元素会漏掉

>>> xs = [1, 2, 3, 4]
>>> for x in xs:
...     xs.remove(x)
>>> xs
[2, 4]

边遍历边删除会让下标错位、跳过元素。正确做法是遍历副本 for x in xs[:],或直接构造新列表 xs = [x for x in xs if not should_remove(x)]。详见 3.2 列表、元组、字典与集合 。

小结

  • 报错速查里十七类异常,根因可归为四族:语法(SyntaxError / IndentationError)、名字(NameError / AttributeError)、类型与值(TypeError / ValueError / KeyError / IndexError)、环境(ImportError / ModuleNotFoundError / FileNotFoundError / UnicodeDecodeError)。
  • 读懂「成因」比背「修法」重要:TypeError 与 ValueError、ImportError 与 ModuleNotFoundError 的区别,是排错时最先要分清的。
  • 变量别名、可变默认参数、GIL 三件事解释了绝大多数「代码能跑但结果不对」的困惑。
  • 排查环境问题时,永远让解释器自己报告——sys.executable、python -m pip -V 比任何猜测都可靠。
  • 「改了不生效」「循环漏元素」这类静默错误最难查,因为它们不报错;养成「不边遍历边修改」「不依赖可变默认值」的习惯能提前避开。
  • 若这里的十七类没覆盖你的报错,先在 附录 A 语法速查表 定位写法,再回对应章节看完整推导。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「python」更多文章

  1. 《Python高级编程》目录
  2. 《Python高级编程》11.3 PEP 流程与版本迁移策略
  3. 《Python高级编程》11.2 嵌入式与自由线程运行时