Python开发中常见的深坑与避坑指南

Dylan
Dylan
发布时间:2026-10-13| 6人浏览过
原创

本文总结了Python开发中容易忽视的常见陷阱,包括嵌套列表创建导致的共享引用问题、函数默认参数为可变对象的副作用、遍历时修改列表引发的逻辑错误,以及整数和字符串缓存机制对身份比较的影响。此外,还涵盖了变量泄漏、异常处理最佳实践及布尔运算符优先级等实用技巧,帮助开发者写出更健壮的代码。

Python常见陷阱概述

Python以其清晰简洁的语法著称,但若对底层机制了解不足,开发者极易陷入各种隐蔽的“陷阱”。本文将梳理Python中一些常见且深不见底的坑,帮助读者规避风险。

列表创建与引用机制

列表创建与引用机制

嵌套列表的创建误区

利用*号创建嵌套列表


  li = [[]] * 3
  print(li)
  # Out: [[], [], []]

通过上述方法,我们可以得到一个包含3个子列表的嵌套列表。若尝试向第一个子列表添加元素:


  li[0].append(1)
  print(li)
  # Out: [[1], [1], [1]]

观察输出结果会发现,仅向第一个元素添加元素,却导致所有三个子列表都增加了该元素。这是因为[[]]*3并未创建三个独立的列表对象,而是创建了三个指向同一个列表对象的引用。因此,操作其中一个元素时,其他引用也会反映相同变化。其效果等同于以下代码逻辑:


  li = []
  element = [[]]
  li = element + element + element
  print(li)
  # Out: [[], [], []]
  element.append(1)
  print(li)
  # Out: [[1], [1], [1]]

我们可以通过打印元素的内存地址来验证这一现象:


  li = [[]] * 3
  print([id(inner_list) for inner_list in li])
  # Out: [6830760, 6830760, 6830760]

至此原因已明了。正确的解决方式是:


li = [[] for _ in range(3)]

如此便创建了三个相互独立的列表对象。


  print([id(inner_list) for inner_list in li])
  # Out: [6331048, 6331528, 6331488]

列表元素的遍历与引用

列表元素的遍历与引用

应避免使用索引方式遍历列表,例如:


  for i in range(len(tab)):
    print(tab[i])

更推荐的做法是:


  for elem in tab:
  print(elem)

for语句会自动生成迭代器。若需同时获取索引和元素,请使用enumerate函数:


  for i, elem in enumerate(tab):
    print((i, elem))

布尔值与异常处理最佳实践

注意==符号的使用细节


  if (var == True):
    # 当var是:True、1、 1.0、 1L时if条件成立
  if (var != True):
    # 当var不是 True 和 1 时if条件成立
  if (var == False):
    # 当var是 False 或者 0 (or 0.0, 0L, 0j) if条件成立

  if (var == None):
    # var是None if条件成立

  if var:
    # 当var非空(None或者大小为0)对象 string/list/dictionary/tuple, non-0等if条件成立

  if not var:
    # 当var空(None或者大小为0)对象 string/list/dictionary/tuple, non-0等if条件成立

  if var is True:
    # 只有当var时True时 if条件成立 1也不行

  if var is False:
    # 只有当var时False时 if条件成立 0也不行

  if var is None:
  # 和var == None 一致

异常捕获的最佳实践

不够优雅的代码写法:


  if os.path.isfile(file_path):
    file = open(file_path)
  else:
    # do something

更推荐的做法:


  try:
    file = open(file_path)
  except OSError as e:
    # do something

在Python 2.6及以上版本中,写法可以更简洁:


with open(file_path) as file:

采用这种写法的原因在于其通用性更强。例如,若file_path传入None,若不判断则需额外处理,若判断则代码冗余。使用with语句可自动处理文件关闭及异常,更加简洁安全。

类变量初始化与默认参数陷阱

类变量初始化陷阱

切勿在对象的__init__函数之外初始化类属性,这主要引发两个问题:

  • 若类属性被修改,初始值也会随之改变。
  • 若将可变对象设为默认值,所有实例将共享同一个对象。

错误示范(除非你确实需要静态变量):


```
  class Car(object):
    color = "red"
    wheels = [Wheel(), Wheel(), Wheel(), Wheel()]
```

正确的做法:


```
  class Car(object):
    def __init__(self):
      self.color = "red"
      self.wheels = [Wheel(), Wheel(), Wheel(), Wheel()]
```

**函数默认参数**

```
def foo(li=[]):
  li.append(1)
  print(li)

foo([2])
# Out: [2, 1]
foo([3])
# Out: [3, 1]
```

该代码行为符合预期,但若不传递参数呢?


```
foo()
# Out: [1] As expected...

foo()
# Out: [1, 1] Not as expected...
```

这是因为函数参数类型在定义时已确定,而非运行时。因此,多次函数调用时,li指向的是同一个列表对象。解决此问题的方法如下:


```
def foo(li=None):
  if not li:
    li = []
  li.append(1)
  print(li)

foo()
# Out: [1]

foo()
# Out: [1]
```

虽然这解决了上述问题,但对于其他对象(如零长度字符串),输出结果可能不符合预期。


```
x = []
foo(li=x)
# Out: [1]

foo(li="")
# Out: [1]

foo(li=0) 
# Out: [1]
```

最常用的解决办法是检查参数是否为None:


```
def foo(li=None):
  if li is None:
    li = []
  li.append(1)
  print(li)

foo()
# Out: [1]
```

**在遍历时修改**

遍历时修改列表的风险

for语句在遍历对象时会生成迭代器。若在遍历过程中修改对象,会产生意想不到的结果:


  alist = [0, 1, 2]
  for index, value in enumerate(alist):
    alist.pop(index)
  print(alist)
  # Out: [1]

第二个元素未被删除,因为迭代是按顺序遍历索引的。上述循环执行两次,结果如下:


  # Iteration #1
  index = 0
  alist = [0, 1, 2]
  alist.pop(0) # removes '0'

  # Iteration #2
  index = 1
  alist = [1, 2]
  alist.pop(1) # removes '2'

  # loop terminates, but alist is not empty:
  alist = [1]

若要避免此问题,可创建另一个列表进行遍历:


  alist = [1,2,3,4,5,6,7]
  for index, item in reversed(list(enumerate(alist))):
    # delete all even items
    if item % 2 == 0:
      alist.pop(index)
  print(alist)
  # Out: [1, 3, 5, 7]

整数与字符串缓存机制

整数和字符串的定义机制

Python预先缓存了一个区间的整数以减少内存操作,但这有时会导致奇特的错误,例如:


  >>> -8 is (-7 - 1)
  False
  >>> -3 is (-2 - 1)
  True

另一个例子


  >>> (255 + 1) is (255 + 1)
  True
  >>> (256 + 1) is (256 + 1)
  False

通过不断测试会发现,(-3, 256)区间内的整数比较均返回True,有的甚至扩展到(-8, 257)。默认情况下,[-5, 256]会在解释器首次启动时创建并缓存,从而导致上述奇怪行为。这是一个常见但易被忽略的坑。解决方案是始终使用equality(==)运算符而非identity(is)运算符比较值。

Python还保留对常用字符串的引用,在比较字符串身份(即使用is)时可能产生类似混淆行为。


>>> 'python' is 'py' + 'thon'
True  

Python字符串被缓存,所有Python字符串都是该对象的引用。对于不常见的字符串,即使字符串相等,比较身份也会失败。


>>> 'this is not a common string' is 'this is not' + ' a common string'
False
>>> 'this is not a common string' == 'this is not' + ' a common string'
True

变量泄漏与运算符优先级

因此,如同整数规则一样,应始终使用equal(==)运算符而非identity(is)运算符比较字符串值。 列表推导和循环中的变量泄漏问题

有一个例子:


  i = 0
  a = [i for i in range(3)]
  print(i) # Outputs 2

在Python 2中,列表推导式会改变i变量的值,而Python 3修复了这个问题:


  i = 0
  a = [i for i in range(3)]
  print(i) # Outputs 0

类似地,for循环对其迭代变量没有私有作用域:


  i = 0
  for i in range(3):
    pass
  print(i) # Outputs 2

这种行为在Python 2和Python 3中均存在。

为避免变量泄漏问题,请在列表推导和for循环中使用新的变量名。

or操作符的优先级陷阱

例如


if a == 3 or b == 3 or c == 3:

这个很简单,但再看一个:


if a or b or c == 3: # Wrong

这是由于or的优先级低于==,因此表达式将被评估为if (a) or (b) or (c == 3):。正确的方法是明确检查所有条件:


if a == 3 or b == 3 or c == 3: # Right Way

或者,可以使用内置函数any()代替链接or运算符:


if any([a == 3, b == 3, c == 3]): # Right

或者,为了使其更有效率:


if any(x == 3 for x in (a, b, c)): # Right

更加简短的写法:


if 3 in (a, b, c): # Right

总结

以上就是本文的全部内容,希望这些内容对大家的学习或工作具有一定的参考价值,感谢支持。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。