ZetCode

Python 陷阱和边界情况

最后修改于 2025 年 4 月 2 日

本教程涵盖了可能让开发者困惑的常见 Python 陷阱和边界情况。

可变默认参数

Python 对默认参数的处理是来自其他语言的开发者最常见的困惑来源之一。其行为与许多人的预期大相径庭,导致了难以诊断的细微错误。

default_args.py
def append_to(element, to=[]):
    to.append(element)
    return to

print(append_to(1))  # [1]
print(append_to(2))  # [1, 2]

Python 的默认参数仅在函数定义时评估一次。这意味着可变的默认参数会在多次调用之间保持其状态。应使用 None 作为默认值,并在函数内部创建一个新的列表来避免此问题。

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

Python 2 和 Python 3 中,列表推导式的作用域规则发生了显著变化。在处理旧代码或维护跨版本兼容性时,理解这些差异至关重要。

list_comp_scope.py
x = 10
lst = [x for x in range(5)]
print(x)  # Outputs 10 in Python 3, but would be 4 in Python 2

在 Python 3 中,列表推导式有自己的作用域,但在 Python 2 中,它们会“泄漏”到周围的作用域。这个问题在 Python 3 中得到了修复,但在移植代码或阅读旧示例时仍可能引起困惑。

延迟绑定闭包

Python 中的闭包表现出延迟绑定的行为,这常常让开发者措手不及。这种行为在循环中尤其明显,当变量被嵌套函数捕获时。

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

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

Python 闭包是延迟绑定变量的——它们使用的是函数被调用时变量的值,而不是创建时的值。要捕获当前值,请使用默认参数:lambda i=i: i

整数的同一性

Python 对小整数的缓存是一个实现细节,当使用 'is' 运算符而非相等运算符进行比较时,可能会导致令人惊讶的行为。

integer_identity.py
a = 256
b = 256
print(a is b)  # True

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

为了优化,Python 会缓存小整数(-5 到 256),因此它们可能具有相同的标识。对于更大的整数,这并不能保证。请始终使用 == 进行值比较,而不是 'is'。

元组创建的陷阱

Python 创建元组的语法可能会令人困惑,尤其是在处理单元素元组时。其语法与其他序列类型不同,并常常导致细微的错误。

tuples.py
empty = ()
single = (1)       # Not a tuple!
proper_single = (1,)  # Proper single-element tuple

print(type(empty))        # <class 'tuple'>
print(type(single))       # <class 'int'>
print(type(proper_single)) # <class 'tuple'>

在 Python 中,是逗号而不是括号构成了元组。括号中的单个值就是那个值本身。要创建一个单元素元组,需要包含一个尾随逗号。

字典键的顺序

在 Python 3.7 中,字典的排序行为发生了显著变化,这可能会影响那些隐式依赖于先前无序行为或明确需要排序的代码。

dict_order.py
d1 = {'a': 1, 'b': 2}
d2 = {'b': 2, 'a': 1}

print(d1 == d2)  # True (same keys/values)
print(list(d1) == list(d2))  # False in Python <3.7, True in 3.7+

在 Python 3.7 之前,字典不保留插入顺序。虽然如果它们有相同的键/值,比较结果仍然相等,但迭代顺序可能会不同。Python 3.7+ 维护插入顺序。

布尔求值

Python 的真值测试非常灵活,但如果未能完全理解,可能会导致意外行为。许多值在布尔上下文中被评估为 False,这既有用又可能令人惊讶。

boolean.py
values = [0, 0.0, False, '', [], (), {}, None]

for v in values:
    if not v:
        print(f"{v!r} is falsy")

在 Python 中,有几个值在布尔上下文中被评估为 FalseNoneFalse、任何数值类型的零、空序列/集合。这很有用,但如果你没有预料到,也可能导致错误。

字符串驻留

Python 的字符串驻留是一种优化技术,可能会影响同一性比较。虽然通常是透明的,但在使用 'is' 运算符而不是相等比较时,可能会导致令人困惑的行为。

string_interning.py
a = "hello"
b = "hello"
print(a is b)  # True (usually)

a = "hello world"
b = "hello world"
print(a is b)  # False (usually)

为了优化,Python 可能会驻留小字符串(如标识符),使它们共享内存。但这并不能保证——不要依赖 'is' 进行字符串比较,应始终使用 ==

列表乘法

将包含可变对象的列表相乘可能会产生意外的共享行为。在尝试初始化多维结构时,这是一个常见的错误来源。

list_multiplication.py
lst = [[]] * 3
lst[0].append(1)
print(lst)  # [[1], [1], [1]]

将包含可变对象的列表相乘,会创建指向同一对象的多个引用。要创建独立的副本,请使用列表推导式:[[] for _ in range(3)]

循环的垃圾回收

Python 的垃圾回收器会处理引用循环,但在处理复杂的对象关系或实现 __del__ 方法时,理解这一行为很重要。

garbage_collection.py
class Node:
    def __init__(self):
        self.parent = None
        self.children = []

parent = Node()
child = Node()
child.parent = parent
parent.children.append(child)

del parent, child  # Cycle exists - will be collected by GC

Python 的引用计数无法处理引用循环。垃圾回收器会处理这些,但如果 GC 被禁用或涉及 __del__ 方法,它们可能会导致内存泄漏。尽可能避免循环引用。

运算符优先级

Python 的运算符链式调用可能导致表达式的求值结果与初看起来的不同。对于比较运算符尤其如此。

precedence.py
result = False == False in [False]  # True
# Equivalent to: False == False and False in [False]

Python 中的比较运算符会自然地链式调用,这可能导致令人惊讶的结果。表达式 'False == False in [False]' 会被评估为 'False == False and False in [False]'。使用括号来明确意图。

类变量与实例变量

在 Python 中,类变量和实例变量的区别对于正确的面向对象设计至关重要,但当涉及到可变的类变量时,其行为可能会令人惊讶。

class_vars.py
class Dog:
    tricks = []  # Class variable

    def __init__(self, name):
        self.name = name

    def add_trick(self, trick):
        self.tricks.append(trick)

d1 = Dog('Fido')
d2 = Dog('Buddy')
d1.add_trick('roll over')
d2.add_trick('play dead')

print(d1.tricks)  # ['roll over', 'play dead']

类变量由所有实例共享。如果你修改一个可变的类变量,它会影响所有实例。对于实例特定的可变属性,应在 __init__ 中使用实例变量(self.tricks = [])。

导入系统的怪癖

Python 的导入系统有几种可能会让开发者感到惊讶的行为,尤其是在模块重载和模块级代码的执行方面。

imports.py
# module.py
print("Module is being imported!")

# main.py
import module  # Prints message
import module  # No message - module is cached in sys.modules

Python 模块在每个解释器会话中只加载一次(缓存在 sys.modules 中)。模块中的顶层代码仅在首次导入时运行。要重新加载,请使用 importlib.reload(),但这对于复杂的模块可能会很棘手。

异常作用域

Python 3 改变了在 try/except 块中处理异常变量的方式,这可能会影响那些试图在 except 块完成后检查异常的代码。

exception_scope.py
e = 42
try:
    # ... some code that raises ValueError
    raise ValueError("oops")
except ValueError as e:
    pass

print(e)  # NameError: name 'e' is not defined

在 Python 3 中,异常变量在 except 块之后被删除,以避免引用循环。如果之后需要异常对象,请在 except 块中将其赋给另一个变量。

来源

Python 语言参考

本教程涵盖了开发者应该注意的常见 Python 陷阱和边界情况。

作者

我的名字是 Jan Bodnar,我是一名充满热情的程序员,拥有丰富的编程经验。我从 2007 年开始撰写编程文章。至今,我已创作了超过 1400 篇文章和 8 本电子书。我拥有超过十年的编程教学经验。

列出所有 Python 教程