承认自己做错事永远都不容易,但是犯错是任何学习过程的一部分,从学习走路到学习新的编程语言(例如 Python)都是如此。
以下是我在学习 Python 时犯的三个错误的列表,列出来是为了让新的 Python 程序员能够避免犯同样的错误。这些错误要么是我长期以来一直犯的,要么是造成了需要数小时才能解决的大问题。
年轻的程序员们请注意,其中一些错误会浪费一下午的时间!
1. 在函数定义中使用可变数据类型作为默认参数
这看起来很有道理,对吧?你有一个小函数,比如说,在当前页面上搜索链接,并可选择将其附加到另一个提供的列表。
def search_for_links(page, add_to=[]):
new_links = page.search_for_links()
add_to.extend(new_links)
return add_to
从表面上看,这看起来是完全正常的 Python 代码,实际上也确实如此。它可以工作。但是它存在问题。如果我们为 add_to 参数提供一个列表,它会按预期工作。但是,如果我们让它使用默认值,就会发生一些有趣的事情。
尝试以下代码
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):
if not var2:
var2 = []
var2.append(var1)
或者,在我们的第一个示例中
def search_for_links(page, add_to=None):
if not 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.')
b = URLCatcher()
b.add_url('http://www.bbc.co.')
b.urls
['http://www.google.com', 'http://www.bbc.co.uk']
a.urls
['http://www.google.com', 'http://www.bbc.co.uk']
等等,什么?!我们没料到会这样。我们实例化了两个单独的对象,a 和 b。A 被赋予了一个 URL,b 被赋予了另一个 URL。为什么两个对象都拥有两个 URL 呢?
事实证明,这与第一个示例中的问题有点类似。URLs 列表是在创建类定义时实例化的。该类的所有实例都使用同一个列表。现在,在某些情况下,这可能是有利的,但在大多数情况下,你不想这样做。你希望每个对象都有一个单独的存储。要做到这一点,我们可以像这样修改代码
class URLCatcher(object):
def __init__(self):
self.urls = []
def add_url(self, url):
self.urls.append(url)
现在,URLs 列表是在创建对象时实例化的。当我们实例化两个单独的对象时,它们将使用两个单独的列表。
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% 的情况下是不必要的。你的程序可以而且可能应该被修改,以便首先不需要这些副本。
祝你编码愉快!欢迎在评论中提出问题。
7 条评论