Python变量使用避坑指南:3个常见可变对象陷阱
本文深入解析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详解等,如有不足之处,欢迎留言指出。感谢朋友们对本站的支持!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









