避免学习 Python 编码时常犯的 3 个错误

这些错误造成了需要数小时才能解决的大问题。
606 位读者喜欢这篇文章。
3 mistakes to avoid when learning to code in Python

Opensource.com

承认自己做错事永远都不容易,但是犯错是任何学习过程的一部分,从学习走路到学习新的编程语言(例如 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']

等等,什么?!我们没料到会这样。我们实例化了两个单独的对象,abA 被赋予了一个 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% 的情况下是不必要的。你的程序可以而且可能应该被修改,以便首先不需要这些副本。

祝你编码愉快!欢迎在评论中提出问题。

标签
User profile image.
Peter 是一位充满热情的开源爱好者,在过去的 10 年里一直在推广和使用开源产品。他曾在许多不同的领域做过志愿者,最初在 Ubuntu 社区,后来涉足音频制作领域,之后又开始写作。

7 条评论

我发现,在一些迭代过程中加入一些打印语句通常可以帮助我发现一些逻辑错误。通常,意想不到的错误起初会让人非常困惑。

你好 Pete,
如果真的需要,你可以完全避免浅拷贝

import copy
b = copy.deepcopy(a)

非常有用,谢谢

非常好的帖子。我来自 C 背景,并且犯过将列表分配给其他变量并对其进行修改的错误。将可变类型视为指针似乎是个好主意。

在第一种情况下,def 语句中任何默认值的参数在定义函数时都会被求值。此 a_list 在定义时(而不是在每次调用时)绑定到一个新实例化的列表。

无论默认值是否可变,这都是正确的。但这仅对可变对象/类型产生可见差异。

类变量也是如此。实例变量必须在调用 __init__()(或 __new__())期间实例化(或者在实例化后“猴子补丁”到对象中)。

最后一个案例的解释也很差。Python 只执行一种形式的绑定(“赋值”)。所有名称(“变量”)都是对对象的引用。区别在于将名称绑定到不可变对象(替换引用)和绑定引用对象(可变对象引用的对象)。如所示,这在很大程度上(实际上)是关于知道何时复制对象的内容与何时绑定对其的引用。

Python 是一种动态语言。它没有与静态语言相同意义上的“变量”。所有“变量”都是对对象的引用。对象具有类型,引用都是通用的(无类型)。

对于刚开始使用 Python 的人来说,非常精彩且真正有帮助。但有一个小错误。你的第二个示例错误地假设它与可变性有关。类变量的重点恰恰在于在类类型的对象实例之间保持相同和共享。它有点像全局对象类型状态。

知识共享许可协议本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.