为什么这些列表方法(append、sort、extend、remove、clear、reverse)返回 None 而不是结果列表?

Why do these list methods (append, sort, extend, remove, clear, reverse) return None rather than the resulting list?

提问人:scubbo 提问时间:6/26/2012 最后编辑:wjandreascubbo 更新时间:8/10/2023 访问量:8175

问:

我注意到,对列表的许多修改列表内容的操作将返回,而不是返回列表本身。例子:None

>>> mylist = ['a', 'b', 'c']
>>> empty = mylist.clear()
>>> restored = mylist.extend(range(3))
>>> backwards = mylist.reverse()
>>> with_four = mylist.append(4)
>>> in_order = mylist.sort()
>>> without_one = mylist.remove(1)
>>> mylist
[0, 2, 4]
>>> [empty, restored, backwards, with_four, in_order, without_one]
[None, None, None, None, None, None]

这个决定背后的思考过程是什么?

对我来说,这似乎很碍事,因为它阻止了列表处理的“链接”(例如)。我想可能是“The Powers That Be”认为列表理解是一种更好的范式(一个有效的观点),因此不想鼓励其他方法——但阻止直觉方法似乎是不正常的,即使存在更好的替代方案。mylist.reverse().append('a string')[:someLimit]


这个问题专门针对 Python 的设计决策,即从 .append 等可变列表方法返回 None。但是,新手经常编写不正确的代码,这些代码期望 .append(特别是)返回刚刚修改的相同列表。但是,关闭与此问题重复的问题。“代码做错了事情,因为结果是 None 而不是 list”是这些情况下的 OP 应该通过调试独立发现的东西;创建一个适当的 MRE 会留下一个像这样的问题 - 因此,它可以被认为是重复的。

请参阅如何在列表、字典等中收集重复计算的结果(或复制每个修改了元素的列表)? 对于“如何重复追加到列表”的简单问题。(或调试归结为该问题的问题)。这是一本新的经典,专门准备以初学者缺乏的视角来解决该主题。

若要获取列表的修改版本,请参阅:

同样的问题也适用于其他内置数据类型的某些方法,例如 set.discard(请参阅如何使用列表推导式从列表中的集合中删除特定元素)和 dict.update(请参阅为什么 python dict.update() 不返回对象?

同样的道理也适用于设计你自己的 API。请参阅就地操作返回对象是个坏主意吗?

python 列表

评论

0赞 Karl Knechtel 8/13/2022
Смотритетакже: 为什么 + 运算符不更改列表而 .append() 更改列表?
0赞 scubbo 3/9/2023
@KarlKnechtel,我怀疑“这个问题是一个合适的关闭目标......”是要作为评论或元数据留下,而不是作为编辑问题的一部分?无论如何 - 你能详细说明为什么你认为这个问题应该结束吗?我创建了一个MRE,我知道这种行为是什么以及如何解决它——我问的是导致该行为在语言中实现的设计理念,而不是“如何实现我想要的目标”,我怀疑其他人也会觉得这种理念很有趣。
0赞 Karl Knechtel 3/9/2023
不;我的意思是,将其他问题(特别是上一句中描述的那种问题)作为重复的问题是合适的。如果我认为这个问题应该结束,我会投票结束它。
0赞 scubbo 3/10/2023
啊,明白了 - 谢谢!我误解了“关闭目标”的含义。
0赞 Karl Knechtel 3/10/2023
我试图改进措辞,因为它应该对每个人都尽可能清楚。

答:

3赞 icecrime 6/26/2012 #1

有人可能会争辩说,签名本身清楚地表明该函数会改变列表而不是返回一个新列表:如果该函数返回一个列表,它的行为就不那么明显了。

评论

0赞 scubbo 8/4/2022
类型提示(以及 IDE 中签名定义的“窥视”)在当时是否被广泛使用?如果没有,我觉得如果程序员不能用鼠标悬停/快捷键观察方法的签名,那么期望程序员定期考虑方法的签名是不合理的——我本来以为他们会认为签名符合他们的期望。或者是否可以在没有(之前)类型提示的情况下在 IDE 中显示签名定义?我是在类型提示流行之后才真正开始使用 IDE 的,所以我从未对其进行过测试。
0赞 Karl Knechtel 3/10/2023
该模块仅在 2015 年最初发布的 3.5 中添加。IDE 必须从他们自己的数据库开始,其中包含有关内置函数等的类型信息。我认为这个论点只是“知道函数返回是提醒它改变对象”;但我认为逻辑更有可能反过来。除此之外,名称的选择方式应该能够清楚地说明哪些方法是命令,哪些是查询(根据其他地方引用的“命令-查询分离”模型)。typingNone
18赞 Tim Pietzcker 6/26/2012 #2

我不能代表开发人员,但我发现这种行为非常直观。

如果一个方法在原始对象上工作并就地修改它,它不会返回任何内容,因为没有新信息 - 你显然已经有了对(现在已变异的)对象的引用,那么为什么要再次返回它呢?

但是,如果方法或函数创建了一个新对象,那么它当然必须返回它。

所以不返回任何内容(因为现在列表已被反转,但标识器仍然指向该列表),但必须返回新生成的列表,因为仍然指向旧的、未修改的列表。l.reverse()lreversed(l)l

编辑:我刚刚从另一个答案中了解到,这个原则被称为命令-查询分离

评论

2赞 scubbo 6/26/2012
这显然是我需要培养的心态,因为我对“......那为什么要再归还它呢?“是”为什么不呢?“。在我看来,返回引用允许使用不同的技术(“链接”),而不会阻止任何其他技术(顺序修改),因此如果链接本身是“坏的”,它只会“更糟”。然而,在其他评论中提出的论点(由圭多本人提出,同样!)断言链接确实是不好的。感谢您的输入!我真的很喜欢向那些知道自己在说什么的人学习!
2赞 Tim Pietzcker 6/26/2012
@scubbo:Python的禅宗()指出“应该有一种 - 最好只有一种 - 明显的方法来做到这一点。这有助于回答“为什么不呢?尽管我必须承认,在Python中有几个领域,“有不止一种方法可以做到这一点”似乎是指导原则:)import this
1赞 scubbo 10/18/2019
我希望我过去的自己想过包括“圭多的评论”的链接:(另外,蒂姆,你的第一个“另一个答案”链接也链接到维基百科页面(尽管我怀疑你目前也缺乏纠正这一点的上下文)
0赞 smci 12/17/2020
另一种选择是e.g. PHP、Scala、R tidyverse 的“流畅”表示法(调用链),有时也用 JS、Java C++ 实现。“我觉得这种行为非常直观”不是理由,而是一个循环论证;如果你学会了流利的范式,你就不会了。(Fluent 是更差还是更好,例如在可维护性、多线程等方面,这是一个完全不同的非常活跃的争论。
43赞 Jon Clements 6/26/2012 #3

Python 中的一般设计原则是用于就地改变对象以返回 None 的函数。我不确定这是否是我选择的设计选择,但它基本上是为了强调不会返回新对象。

Guido van Rossum(我们的 Python BDFL)在 Python-Dev 邮件列表上陈述了设计选择:

我想再次解释为什么我如此坚持 sort() 不应该 返回 'self'。

这来自一种编码风格(在其他各种语言中很流行,我 相信特别是Lisp陶醉于它)的一系列副作用 在单个对象上可以像这样链接:

x.compress().chop(y).sort(z)

这将与

x.compress()
x.chop(y)
x.sort(z)

我发现链接形式对可读性构成威胁;它要求 读者必须非常熟悉每种方法。这 第二种形式清楚地表明,这些调用中的每一个都作用于同一 对象,所以即使你不了解这个类及其方法 好了,您可以理解第二个和第三个调用适用于 x(并且所有调用都是为了它们的副作用),而不是 别的。

我想为返回新值的操作保留链接, 与字符串处理操作类似:

y = x.rstrip("\n").split(":").lower()

有一些标准库模块鼓励链接 副作用调用(我想到了 PSAT)。不应该有任何新的 的;pstat 在弱时滑过我的过滤器。

评论

2赞 Nathaniel Jones 8/4/2021
“pstat 在弱时从我的过滤器中溜走了。”我喜欢。直到今天,pstats 仍然是逃脱的那个:stats.strip_dirs().add() => <pstats.Stats object at 0x0000018866977640>
0赞 scubbo 8/4/2022
在它写成十年后再回过头来(感谢 Knechtel @Karl出色的说明性编辑!),Guido 对这种范式的回应确实是合理的。然而,在那段时间里爱上了 FP,我觉得它就像一个稻草人 - 如果没有方法就地改变一个对象,但总是返回一个新对象,那么被链接的代码可以用以下方式表示: - 我声称,它被更直观地替换为链。也就是说,Python 不是 FP(主要是故意的?),所以范式不适用。x1 = x.compress(); x2=x1.chop(y); x3 = x2.sort(z)
0赞 scubbo 2/15/2023
或者,重新相位 - “Python 中的一般设计原则是用于就地改变对象以返回 None 的函数”是一个正确的陈述,但无法解释为什么这些操作会就地改变。
2赞 Karl Knechtel 9/14/2022 #4

如果您在请求帮助修复代码后被发送到这里:

将来,请尝试自己寻找代码中的问题,仔细研究代码运行时会发生什么。与其因为出现错误消息而放弃,不如检查每个计算的结果,并查看代码开始工作的地方与预期不同。

如果您有代码调用列表上的方法,例如 或,您会注意到返回值是 ,而列表已就地修改。仔细研究这个例子:.append.sortNone

>>> x = ['e', 'x', 'a', 'm', 'p', 'l', 'e']
>>> y = x.sort()
>>> print(y)
None
>>> print(x)
['a', 'e', 'e', 'l', 'm', 'p', 'x']

y得到了特殊值,因为这是返回的。 改变了,因为排序就位了。Nonex

它故意以这种方式工作,因此代码会中断。查看其他答案,了解 Python 开发人员为什么希望这样做。x.sort().reverse()

解决问题

首先,仔细考虑代码的意图。x 应该改变吗?我们真的需要一个单独的吗?y

让我们先考虑一下。如果应该更改,则自行调用,而无需在任何地方分配结果。.sortxx.sort()

如果需要排序副本,请使用 。有关详细信息,请参阅如何获取列表的排序副本。y = x.sorted()

对于其他方法,我们可以像这样获得修改后的副本:

.clear->这没有意义;列表的“已清除副本”只是一个空列表。只需使用 .y = []

.append-> 可能最简单的方法是使用运算符。要从列表中添加多个元素,请使用 而不是 。要添加单个元素,请先将其包装在列表中: .在 3.5 及更高版本中,另一种方法是使用解包:for , for .另请参阅 如何允许列表 append() 方法返回新列表 和 如何在 Python 中连接两个列表? for 。.extend+ly = x + l.extendey = x + [e]y = [*x, *l].extendy = [*x, e].append.append.extend

.reverse-> 首先,考虑是否需要实际副本。内置提供了一个迭代器,可用于以相反的顺序循环元素。要制作实际副本,只需将该迭代器传递给 : 。有关详细信息,请参阅如何获取列表的反向副本(在 .reverse 之后链接方法时避免单独的语句)?reversedlisty = list(reversed(x))

.remove-> 找出要删除的元素的索引(使用 ),然后使用切片找到该点之前和之后的元素并将它们放在一起。作为功能:.index

def without(a_list, value):
    index = a_list.index(value)
    return a_list[:index] + a_list[index+1:]

(我们可以进行类似的翻译以制作修改后的副本,当然实际上会从列表中返回一个元素。.pop.pop

另请参阅 Python 中不带特定元素的返回列表的快速方法

(如果您计划删除多个元素,强烈建议改用列表推导式 (或 )。这将比在迭代列表时从列表中删除项目所需的任何解决方法都要简单得多。这种方式自然也会给出修改后的副本。filter


当然,对于上述任何一种情况,我们也可以通过显式制作副本,然后在副本上使用就地方法来制作修改后的副本。最优雅的方法将取决于上下文和个人品味。