为什么Python的列表推导式不复制参数,以至于实际对象无法被修改?
也许我喝了太多函数式编程的“ Kool Aid”,但我觉得列表推导式的这种行为似乎是个糟糕的设计选择:
>>> d = [1, 2, 3, 4, 5]
>>> [d.pop() for _ in range(len(d))]
[5, 4, 3, 2, 1]
>>> d
[]
为什么d
没有被复制,而是直接在原来的基础上被修改了呢?列表推导式的目的应该是返回我们想要的列表,而不是在返回一个列表的同时,默默地修改了其他对象。对d
的改变有点隐晦,这看起来不太符合Python的风格。这样做有什么好的用处吗?
为什么让列表推导式的行为和for循环完全一样,而不是让它们更像函数(在函数式编程中,具有局部作用域)?
14 个回答
为什么要创建一个(可能非常耗费资源的)副本,而习惯用法的代码本来就不会有副作用呢?而且,为什么那些偶尔需要副作用的用例(虽然少见,但确实存在)要被禁止呢?
Python首先是一种命令式语言。可变状态不仅被允许,而且是非常重要的——没错,列表推导式是希望保持纯粹的,但如果强制执行这一点,就会和语言的其他部分不一致。那么,d.pop()
这个操作会改变 d
,但只有在不在列表推导式中,并且条件合适的情况下?那样就没有意义了。你可以(而且应该)选择不使用它,但没有人会把更多的规则写死,让这个功能变得更复杂——习惯用法的代码(这才是大家应该关注的代码 ;)
)并不需要这样的规则。它本来就是这样做的,如果需要的话也会有所不同。
在这个表达式中:
[d.pop() for _ in range(len(d))]
你想要哪个变量被隐式复制或作用域限制?在这个理解中,唯一有特殊状态的变量是 _
,而这并不是你想要保护的那个。
我看不出你怎么能让列表推导式有能力识别所有涉及的可变变量,并且能够隐式地复制它们。或者知道 .pop()
会改变它所操作的对象?
你提到函数式语言,但它们通过让所有变量不可变来实现你想要的效果。而Python并不是这样设计的。
在Python中,除非你特别要求,否则它不会自动复制。这是一个非常简单、清晰且容易理解的规则。如果在这个规则上加上一些例外,比如“在列表推导式中的某些情况下除外...”,那就太荒唐了。如果Python的设计是由这样的人来管理的,那Python就会变成一个病态、扭曲、半坏的语言,根本不值得学习。感谢让我再次意识到,这绝对不是事实!
想要复制吗?那就自己做一个复制!在Python中,当你需要进行一些更改,而这些更改不应该反映在原始数据上时,这总是解决方案。也就是说,采用一种干净的方法,你可以这样做:
dcopy = list(d)
[dcopy.pop() for _ in range(len(d))]
如果你特别想把所有内容放在一个表达式里,虽然这样可能不算“干净”的代码,你也可以这么做:
[dcopy.pop() for dcopy in [list(d)] for _ in range(len(d))]
也就是说,通常的技巧是,当你真的想把赋值折叠到列表推导式中时,可以添加一个for
子句,控制变量就是你想要赋值的名称,而“循环”则是在一个包含你想要赋值的单个值的序列上。
函数式编程语言从不改变数据,因此它们也不需要复制(也不需要)。Python不是一种函数式语言,但当然你可以在Python中以“函数式的方式”做很多事情,而且通常这样做会更好。例如,有一个更好的替代方案来替代你的列表推导式(保证结果相同且不影响d
,而且快得多、更简洁、更清晰):
d[::-1]
(我妻子安娜称之为“火星微笑”;-)。切片(而不是切片赋值,那是不同的操作)在核心Python(语言和标准库)中总是会执行复制,当然在一些独立开发的第三方模块中,比如流行的numpy
,它更倾向于将切片视为对原始numpy.array
的“视图”。