3 个棘手的 Python 细微差别 细微差别一 细微差别二 细微差别三 结论

2025-06-07

3 个棘手的 Python 细微差别

细微差别第一

细微差别二

细微差别三

结论

这篇博文改编自我上周在芝加哥 Python 用户组ChiPy的一次演讲。想要了解更多类似的精彩内容,我强烈推荐你参加当地的 Python 聚会,以及PyVideo,这是一个旨在索引所有 Python 聚会或会议演讲录像的社区项目。

细微差别第一

列表不包含对象

我知道这听起来很荒谬。Python 里的一切都是对象。如果声明一个列表,我就会把它填满对象。收集对象正是列表的功能!

有点。让我稍微回顾一下。

在Advent of Code期间,我和几个朋友在我们的友谊 Discord 上分享代码片段并一起调试。其中一个问题是创建一个代表布料的巨大矩阵,你必须通过操作矩阵中的各个元素来确定哪些布料方块被使用了。我有个朋友现在正在学习 Python,设置这个问题比他们想象的要难得多

以下是他们最初设置矩阵的方式。很简单,对吧?


l = [[0] * 5] * 5

print(l)
[[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]]
Enter fullscreen mode Exit fullscreen mode

到目前为止一切顺利。但是当他们要更改第一个嵌套列表中的元素时……

l[0][3] = 1

print(l)
[[0, 0, 0, 1, 0],
[0, 0, 0, 1, 0],
[0, 0, 0, 1, 0],
[0, 0, 0, 1, 0],
[0, 0, 0, 1, 0]]
Enter fullscreen mode Exit fullscreen mode

😧

他们在我们的 Discord 群里发帖,问到底发生了什么事。我的回答和往常一样,基本上就是耸耸肩,问他们为什么不用列表推导式。我们的另一个朋友说了些类似“好像是指针?”之类的话。

我的第二个朋友说得对。Python 不会在列表中存储实际的对象。它存储的references是两个对象。这太奇怪了。

Areference指向内存中的特定位置。该特定位置包含对象。如果更改存储在该位置的对象,则会更改该对象出现的所有实例。

因此,原始矩阵中声明的五个列表l = [[0] * 5] * 5实际上只是五个独立references同一个列表的列表。更改其中一个列表,所有列表都会更改。

那么等一下,我怎样才能使它们成为不同的物体?

好问题!为了实现矩阵中每个元素都能独立操作且不会无意间影响其他元素的目标,我们需要确保为每个嵌套列表在内存中的新位置创建一个新对象。在这种情况下,我们可以使用 copy 库创建一个可以独立更改的新容器对象。

from copy import copy
a = [0, 0, 0, 0, 0]

b = []

# intentionally verbose example to illustrate a point :)
for _ in range(5):
    b.append(copy(a))
Enter fullscreen mode Exit fullscreen mode

当然,我们可以让它更简洁。我第一次就猜对了。你的大部分 Python 问题都可以通过一个合适的列表推导式来解决(不保证满意)。

l = [[0 for i in range(5)] for j in range(5)]

l[0][3] = 1

print(l)

[[0, 0, 0, 1, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]]
Enter fullscreen mode Exit fullscreen mode

细微差别二

默认参数在函数定义时计算,而不是每次调用函数时计算

这是什么意思?举个例子更容易解释。假设出于某种原因,我们想编写一个函数,它接受一个元素并将其附加到现有列表中,但不使用该append函数。

def list_append(element, input_list=[]):
    input_list.extend([element])
    return input_list
Enter fullscreen mode Exit fullscreen mode

现在,这是一个完全人为设计的例子,我们永远不会在实践中实现它,但它对于演示很有用。让我们尝试调用该函数几次。

print(list_append(3))
[3]
print(list_append([5, 7]))
[3, 5, 7]
print(list_append("asdf"))
[3, 5, 7, "asdf"]
Enter fullscreen mode Exit fullscreen mode

当我们多次调用该函数时,它似乎总是添加并返回同一个对象,而不是每次调用该函数时都从一个新的空列表开始。

这是因为函数的默认参数是在函数首次声明时创建并求值的。因此,每次重启应用时,它都会被求值,并且会从一个新的列表开始。但是,每次在应用运行时调用函数时,它都会基于同一个可变对象进行操作,并且会不断增长,直到应用再次重启。

以下是我们修复此问题的方法:

def list_append(element, input_list=None):
    if not input_list:
        input_list = []
    return input_list.extend([element])

print(list_append(3))
[3]
print(list_append([5, 7]))
[5, 7]
print(list_append("asdf", ["bcde"]))
["bcde", "asdf"]
Enter fullscreen mode Exit fullscreen mode

每次函数运行时,都会评估函数范围内声明的元素,因此在上面的例子中,每次都会从一个新的空列表开始,而不是在函数调用之间传递相同的默认参数。

顺便说一句,这不仅仅是列表的问题。在将可变对象作为默认参数传递给函数时,务必谨慎;所有可变对象都会遇到同样的问题。

细微差别三

==对比is

is==都是 Python 中的运算符,用于比较两个对象,并根据结果返回布尔值。这有点棘手,但==它用于检查相等性,而用于is检查同一性

还记得我们之前讨论的第一个细节references,或者说内存中包含对象的特定位置吗?它们在这里又很重要了!

相等意味着两个对象的值匹配。身份意味着你比较的两个对象是完全相同的对象,并且存在于内存中的同一位置。你可以使用 Python 内置函数 检查对象在内存中的位置id()。因此,当你使用运算符比较两个对象时is,你检查的是这两个对象是否具有相同的id,而不是相同的value

一个在 Python 中经常看到的怪癖是,内存中只有一个True布尔对象和一个布尔值对象。 的每次出现都与 的其他所有出现具有相同的 ID 。然而,有些值在计算结果为相等,无需共享其标识。这就是为什么 Python 爱好者在比较事物时经常使用 运算符不是运算符。FalseTrueTrueTrueisTrueFalse==

用文字解释的话,可能会有点令人困惑,而且充满专业术语。以下是一些例子。

布尔值的相等性与同一性:

print(1 == True)
True
print(1 is True)
False
a = True
print(a is True)
True


print(0 == False)
True
print(0 is False)
False
b = False
print(b is False)
True
Enter fullscreen mode Exit fullscreen mode

相等与恒等 - 等价变量

# a and b variables point to the same id
a = [1, 2, 3, 4, 5]
b = a
print(a == b)
True
print(a is b)
True
print(id(a), id(b))
4437740104, 4437740104

# a and b are different container objects, but the values of their contents are identical
a = [1, 2, 3, 4, 5]
b = [1, 2, 3, 4, 5]
print(a == b)
True
print(a is b)
False
print(id(a), id(b))
4437740104, 4442640968
Enter fullscreen mode Exit fullscreen mode

结论

我不确定我在这里能得出一个统一的结论。以上只是三件在 Python 中可能会让你犯错的事情,它们很难在 Google 上找到,因为它们会导致意想不到的行为,却不会给你一个友好的错误信息。

我想如果要我给你一个建议的话,那就是:
1.)references有点奇怪。你通常不需要考虑它们,但它们值得理解;
2.) 始终使用列表推导式

与往常一样,如果您有任何疑问,或者您认为我遗漏了关于这三个主题的任何重要内容,请在评论中告诉我。如果您对 Python 有什么不明白的地方,需要我解释一下,也请告诉我。谢谢!

文章来源:https://dev.to/thejessleigh/ Three-python-nuances-i-wish-id-known-earlier-547c
PREV
🚗 Sidecar 用于代码拆分 代码拆分 关于代码拆分的真相 听起来不太好 蝙蝠侠和罗宾 Sidecar 实现细节 缺点 未来 总体
NEXT
适用于小型个人网站的快速简易 .htaccess .htaccess 是什么?.htaccess(以及共享主机)的局限性和缺点 常见用例速查表 调试和其他一般技巧 其他资源