Python变量使用避坑指南:3个常见可变对象陷阱

咖啡半糖
咖啡半糖
发布时间:2026-10-13| 2人浏览过
原创

本文深入解析Python编程中因可变数据类型引发的三个典型错误:函数默认参数使用列表、类变量共享状态以及变量赋值导致的引用陷阱。通过对比不可变与可变类型的行为差异,提供正确的代码修正方案,帮助开发者避免内存共享带来的意外副作用。

在Python编程实践中,开发者常会遭遇一些看似无厘头的错误。这通常并非语言本身的缺陷,而是由于忽视了Python特定的语言特性所致。本文将重点探讨在使用Python变量时容易引发的三个令人困惑的错误,旨在提醒开发者在后续编码过程中需格外留意这些细节。

关于Python运行时的新手常见错误,此处不再赘述,感兴趣的可参考:《Python运行的17个时新手常见错误小结》。

1、 在函数定义中将可变数据类型作为默认参数

这看起来似乎合情合理?假设你编写了一个小型函数,用于搜索当前页面上的链接,并可选择性地将结果追加到另一个提供的列表中。

def search_for_links(page, add_to=[]):
  new_links = page.search_for_links()
  add_to.extend(new_links)
  return add_to

从表面看,这段Python代码非常正常,且确实可以运行。然而,其中隐藏着一个关键问题。如果我们在调用函数时显式传递一个列表参数,程序会按预期正常工作。但若我们省略该参数,使用默认值时,就会出现一些令人费解的现象。

请尝试运行以下代码:

def fn(var1, var2=[]):
  var2.append(var1)
  print(var2)
fn(3)
fn(4)
fn(5)

你可能预期的输出结果是:

[3]
[4]
[5]

但实际观察到的结果却是:

[3]
[3,4]
[3,4,5]

这是为什么呢?如你所见,每次调用都使用了同一个列表对象,导致输出呈现累积效果。在Python中,当定义这样的函数时,默认参数列表仅在函数定义阶段被实例化一次,而非每次函数运行时重新创建。这意味着,除非我们提供一个新的对象,否则该函数将始终操作完全相同的列表对象:

fn(3,[4])
[4,3]

修正后的答案符合我们的预期。要达到这种效果,正确的做法是:

def fn(var1, var2=None):
  ifnot var2:
    var2 =[]
  var2.append(var1)

或者在第一个示例中采用以下写法:

def search_for_links(page, add_to=None):
  ifnot add_to:
    add_to =[]
  new_links = page.search_for_links()
  add_to.extend(new_links)
  return add_to

这种写法将列表的实例化推迟到模块加载之后,确保每次运行函数时都会重新创建列表实例。需要注意的是,对于元组、字符串、整型等不可变数据类型,无需考虑此类情况。因此,像下面这样的代码是完全可行的:

def func(message="my message"):
  print(message)

2、 将可变数据类型作为类变量

这一错误与上述最后一个案例非常相似。请思考以下代码:

class URLCatcher(object):
  urls =[]
  def add_url(self, url):
    self.urls.append(url)

这段代码看起来非常规范。我们定义了一个存储URL的对象,当调用add_url方法时,它会将给定的URL添加到存储中。逻辑上似乎无懈可击?让我们看看实际运行结果:

a =URLCatcher()
a.add_url('http://www.google.com')
b =URLCatcher()
b.add_url('http://www.pythontab.com')
print(b.urls)
print(a.urls)

输出结果:

['http://www.google.com','http://www.pythontab.com']
['http://www.google.com','http://www.pythontab.com']

等等,这是怎么回事?!我们的预期并非如此。我们分别实例化了两个独立的对象a和b,并分别为它们添加了不同的URL。为何这两个对象最终都包含相同的两个URL呢?

这与第一个错误案例的根本原因相同。在创建类定义时,URL列表即被实例化。该类的所有实例共享同一个列表对象。虽然在某些特定场景下这种共享机制可能有用,但在大多数情况下,你希望每个对象拥有独立的存储空间。为此,我们需要修改代码:

class URLCatcher(object):
  def __init__(self):
    self.urls =[]
  def add_url(self, url):
    self.urls.append(url)

现在,当创建对象时,URL列表在__init__方法中被实例化。当我们实例化两个独立的对象时,它们将各自使用独立的列表,互不干扰。

3、 可变类型的赋值陷阱

这个问题曾困扰我许久。让我们做一些调整,换用另一种可变数据类型——字典来进行演示。

a ={'1':"one",'2':'two'}

现在,假设我们想在其他地方使用这个字典,同时保持其初始数据的完整性。

b = a
b['3']='three'

这看起来很简单吧?

接下来,让我们查看原本不应被修改的字典a:

{'1':"one",'2':'two','3':'three'}

哇,等一下,我们再来看看b?

{'1':"one",'2':'two','3':'three'}

等等,这是怎么回事?有点混乱……让我们回顾一下,看看其他不可变类型在这种情况下会发生什么,例如一个元组:

c =(2,3)
d = c
d =(4,5)

此时,c的值为(2, 3),而d的值为(4, 5)。

这个函数的结果如我们所料。那么,在之前的例子中到底发生了什么?当使用可变类型时,其行为类似于C语言中的指针。在上述代码中,执行b = a,实际上意味着:b成为a的一个引用。它们都指向Python内存中的同一个对象。这听起来是否有些熟悉?因为这个问题与前述案例本质相似。

列表也会发生同样的情况吗?是的。那么我们该如何解决呢?这需要非常谨慎。如果我们确实需要复制一个列表进行处理,可以这样做:

b = a[:]

这将遍历并复制列表中的每个对象引用,并将它们放入一个新的列表中。但是要注意:如果列表中的每个对象本身也是可变的,我们将再次获得它们的引用,而非完整的副本。

假设在一张纸上列清单。在原来的例子中,相当于A某和B某正在看着同一张纸。如果有个人修改了这个清单,两个人都将看到相同的变化。当我们复制引用时,每个人现在有了他们自己的清单。但是,我们假设这个清单包括寻找食物的地方。如果“冰箱”是列表中的第一个条目,即使它被复制,两个列表中的条目也都指向同一个冰箱。所以,如果冰箱被A修改,吃掉了里面的大蛋糕,B也将看到这个蛋糕的消失。这里没有简单的方法解决它。只要你记住它,并编写代码的时候,使用不会造成这个问题的方式。

字典以相同的方式工作,并且你可以通过以下方式创建一个昂贵副本:

b = a.copy()

再次说明,这只会创建一个新的字典,指向原来存在的相同的条目。因此,如果我们有两个相同的列表,并且我们修改字典a的一个键指向的可变对象,那么在字典b中也将看到这些变化。

可变数据类型的麻烦也是它们强大的地方。以上都不是实际中的问题;它们是一些要注意防止出现的问题。在第三个项目中使用昂贵复制操作作为解决方案在 99% 的时候是没有必要的。

总结

以上就是本文关于浅谈使用Python变量时要避免的3个错误的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续参阅本站:python探索之BaseHTTPServer-实现Web服务器介绍、Python探索之SocketServer详解等,如有不足之处,欢迎留言指出。感谢朋友们对本站的支持!

相关标签:

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

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 算法库。