我应该返回一个通过引用传递并修改的列表吗?

Posted

技术标签:

【中文标题】我应该返回一个通过引用传递并修改的列表吗?【英文标题】:Should I ever return a list that was passed by reference and modified? 【发布时间】:2019-12-13 04:42:03 【问题描述】:

我最近发现python中的列表是通过引用自动传递的(除非使用了表示法array[:])。例如,这两个函数做同样的事情:

def foo(z):
    z.append(3)

def bar(z):
    z.append(3)
    return z

x = [1, 2]
y = [1, 2]
foo(x)
bar(y)
print(x, y)

在此之前,我总是返回我操作过的数组,因为我认为我必须这样做。现在,我知道它是多余的(也许效率低下),但似乎返回值通常是代码可读性的好习惯。我的问题是,执行这两种方法是否有任何问题/最佳实践是什么?我缺少第三种选择吗?很抱歉,如果以前有人问过这个问题,但我找不到任何能真正回答我问题的东西。

【问题讨论】:

"通过引用自动传递" 从技术上讲这是不正确的。 Python 使用引用的副本来传递事物。这些引用可能指向可变或不可变对象,这就是混淆的来源。见stupidpythonideas.blogspot.com/2013/11/… @DeepSpace。那,“按引用”和“按值”根本不是唯一可用的选项。 “pythonic”方式——如果你正在编写一个改变list的函数——就是不返回任何东西(除非你有很好的理由这样做)。相反,在函数文档字符串中特别说明该函数修改了list。此外,python 既不是通过值也不是通过引用传递对象,您需要read this article。 这只是一个玩具示例,还是您实际上正在编写一个修改list 的函数?如果是这样,你为什么认为你需要这样做? @RickTeachey。这只是一个玩具示例,但我确实编写了修改列表的函数。据我所知,可以 1. 修改函数中的列表(在阅读了此处的答案后,为了便于阅读,它永远也不应该返回),或者 2. 创建列表的副本并将其返回。我听说前者是一种更便宜的操作,这让我认为在大多数情况下它会更好。 【参考方案1】:

此答案的前提是,已经决定是就地修改您的输入还是返回副本。

正如您所指出的,是否返回修改后的对象是一个见仁见智的问题,因为结果在功能上是等效的。一般来说,返回一个就地修改的列表被认为是一种好的形式。根据Zen of Python(项目#2):

显式优于隐式。

这在标准库中得到了证实。列表方法在 SO 上因此而臭名昭著:list.append, insert, extendlist.sort 等。

Numpy 也经常使用这种模式,因为它经常处理复制和返回不切实际的大型数据集。一个常见的例子是数组方法numpy.ndarray.sort,不要与***函数numpy.sort混淆,后者返回一个新副本。

这个想法在很大程度上是 Python 思维方式的一部分。这是Guido's email 的摘录,解释了原因和原因:

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

【讨论】:

这有助于对抗臭名昭著的方法链反模式:​​) 另一方面,返回列表的修改后的副本也是一种不错的形式。对于给定的函数,哪种形式更好取决于函数和上下文。 @Serge 方法链本身并不是一种反模式,但它有时间和地点,它不应该在其他地方被滥用(例如附加到列表)。 @jirassimok。 OP专门描述了就地情况。如果您正在处理副本,您几乎必须退回副本。 我可以更好地表达该评论。我基本上是想澄清有时您想要制作修改后的副本而不是修改输入,我不想将其添加为单独的答案,因为它并没有真正回答问题。【参考方案2】:

Python 内置函数通常不会同时执行这两种操作,以避免混淆函数/方法是修改其参数还是返回新值。就地修改时,不执行return(使其隐式返回None)。例外情况是变异函数返回的对象不是变异对象(例如dict.popdict.setdefault)。

遵循相同的模式通常是个好主意,以避免混淆。

【讨论】:

【参考方案3】:

“最佳实践”在技术上是完全不修改事物:

def baz(z):
    return z + [3]

x = [1, 2]
y = baz(x)
print(x, y)

但一般来说,如果您将自己限制为返回一个新对象或就地修改一个对象,而不是同时进行两者,则更清楚。

标准库中有一些例子既可以就地修改对象又返回something(最重要的例子是list.pop()),但这是一种特殊情况,因为它不返回被修改的对象

【讨论】:

修改原始列表不会比制作副本更快吗?或者差异可以忽略不计? @MichaelSkarn 是的,内存效率更高,但出于某种原因,人们可能仍然想要原始列表(您会注意到 str 类的几乎每个方法都返回一个新的、已更改的实例而不是修改字符串本身)。 @MichaelSkarn。真的取决于列表有多大,以及修改是什么。 x[0] = 3 在具有 10**6 个元素的列表上可能比制作一个新元素便宜。如果必须以任何一种方式重新分配缓冲区(由于摊销,这并不总是发生),调用 append 不会有任何区别。【参考方案4】:

当然没有严格的应该,但是,一个函数should either do something, or return something.。因此,您最好要么修改列表而不返回任何内容,要么返回一个新列表,保持原始列表不变。

注意:该列表并非完全通过引用传递。实际传递的是引用的值。如果您重新分配,请记住这一点

【讨论】:

以上是关于我应该返回一个通过引用传递并修改的列表吗?的主要内容,如果未能解决你的问题,请参考以下文章

在 C# 中,我应该按值传递参数并返回相同的变量,还是按引用传递?

c# 中是不是默认通过引用传递数组或列表?

如何通过引用传递列表?

JavaScript - 通过引用传递时清空数组/对象问题

C语言有引用传递吗

在 C# 中通过引用传递对象和对象列表