OPINION
不要使用 Python 的列表乘法 ([n] * N)
这是一个陷阱
无论您是新手还是经验丰富的 Python 程序员,您都可能使用过列表乘法和/或在那些“酷 Python 特性”风格的文章中阅读过它。
这是因为它无疑是旨在让您的 Python 生活更轻松的那些很酷的功能之一。
列表乘法的好处是它抽象了初始化列表的过程。
而不是使用迭代方法或列表推导:
# Iterative approach
my_list = []
for _ in range(N):
my_list.append(n)# List comprehension
my_list = [n for _in range(N)]
您可以通过以下方式获得相同的结果:
my_list = [n] * N
你看,我按以下顺序学习编程语言:
C -> C++ -> MATLAB -> R -> Python.
在 Python 之前,我使用的任何其他编程语言都无法远程提供如此简洁和直观。
然而,随着我开始编写越来越复杂的代码,列表乘法开始让我感到不安。
我记得有一次我花了一整个下午调试代码才发现问题源于使用 * 运算符创建不正确的列表。
因此,我觉得有必要讨论这个问题,因为我知道一些开发人员在创建列表时仍然没有注意到星号运算符的权衡。
列表乘法有什么问题
让我们考虑以下代码:
>>> my_list = [0] * 3
>>> my_list[0] = 1
>>> my_list
[1, 0, 0]
这是你所期望的。到现在为止还挺好。
现在,让我们尝试使用相同的方法创建一个二维数组:
>>> my_list = [[0] * 3] * 5
>>> my_list[0][0] = 1
>>> my_list
[[1, 0, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0]]
唔!这可能不是你想要的。
如何通过初始化 3D 数组进一步推动它:
>>> my_list = [[[0] * 3] * 5] * 2
>>> my_list[0][0][0] = 1
>>> my_list
[[[1, 0, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0]], [[1, 0, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0]]]
预期的输出将是更新子列表 [0, 0, 0] 的第一个值。但是,似乎更新已在所有子列表中复制。
那么,为什么会这样呢?
列表乘法如何工作?
要了解以前的行为,有必要重新访问 Python 的常见问题解答,其中说:[0]
“原因是使用 * 复制列表不会创建副本,它只会创建对现有对象的引用。”
让我们将其转换为代码,以更好地了解 Python 是如何在底层运行的:
- List multiplication:
my_list = [[0] * 5] * 5
for i in range(5):
print(id(my_list[i]))
Output:
2743091947456
2743091947456
2743091947456
2743091947456
2743091947456
- Using for loops
my_list = []
for _ in range(5):
my_list.append([0] * 5)for i in range(5):
print(id(my_list[i]))print(my_list)
Output:
2743091947456
2743095534208
2743095532416
2743095534336
2743095532288
- Interpretation
与 for 循环不同,通过运算符 * 复制的所有列表都指向相同的内存地址。这意味着影响一个嵌套列表的任何更改都会影响所有其他列表,这显然违背了我们的初衷。
现在问题变成了:
- 为什么第一个示例 ([n] * N) 工作得很好,尽管列表的所有元素都引用同一个对象?
事实证明,这种行为背后的原因(如 Python 的 wikibook 中所述)是列表是可变项而 int、str 等是不可变的事实。看看这个:[0]
而且由于不可变对象无法更改,因此当您更新列表中的项目时,Python 会创建对该对象的新(不同)引用。
>>> my_list = [0] * 3
>>> id(my_list[0])
1271862264016
>>> my_list[0] = 1
>>> id(my_list[0])
1271862264048
Workaround
使用列表推导可以快速轻松地解决此问题。当然,这是对标准 for 循环的补充。
>>> my_list = [[0] * 3 for _ in range(5)]
>>> my_list[0][0] = 1
>>> my_list
[[1, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]
而且,我们可以看到为每个列表分配了不同的内存地址。
>>> my_list = [[0] * 3 for _ in range(5)]
>>> [id(l) for l in my_list]
[1271867906112, 1271864321536, 1271864322048, 1271864326912, 1271864322560]
更重要的是,这种方法适用于所有场景。
那么,为什么不坚持使用它并安全地使用它,而不是在使用列表乘法之前三思而后行呢?
Conclusion
我不是 Pythonic 语法糖的忠实粉丝。[0]
是的,我同意它使代码简洁明了。
然而,软件行业的简洁性 == 可读性何时出现?
事实上,我使用 Python 编写的代码越多,我发现自己越倾向于使用 Python 标准语法并放弃使用快捷方式。
归根结底,重要的是性能、可维护性和可读性。不是你有多少行代码。
如果你不能没有捷径,至少要阅读你正在使用的语法糖的好、坏和丑。在这种情况下,可能需要对软件工程概念(即数据结构、内存分配……)有适度的理解。
Happy coding!
文章出处登录后可见!