我是否应该返回通过引用传递和修改的列表?

Should I ever return a list that was passed by reference and modified?

提问人:Michael Skarn 提问时间:8/6/2019 更新时间:8/6/2019 访问量:1875

问:

我最近发现 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 按引用传递

评论

1赞 DeepSpace 8/6/2019
“通过引用自动传递” 从技术上讲,这是不正确的。Python 通过引用的副本传递内容。这些引用可能是对可变对象或不可变对象的引用,这就是混淆的来源。查看 stupidpythonideas.blogspot.com/2013/11/...
1赞 Mad Physicist 8/6/2019
@DeepSpace。“按参考”和“按值”根本不是唯一可用的选项。
0赞 Rick 8/6/2019
“pythonic”的方式 - 如果你正在编写一个改变 - 是不返回任何东西(除非你有一个很好的理由这样做)。相反,在函数文档字符串中明确说明该函数修改了 .此外,python 既不按值传递对象,也不按引用传递对象,您需要阅读本文listlist
0赞 Rick 8/6/2019
这只是一个玩具示例,还是您实际上是在编写一个修改 ?如果是这样,你为什么认为你需要这样做?list
0赞 Michael Skarn 8/6/2019
@RickTeachey。这只是一个玩具示例,但我确实编写了修改列表的函数。据我所知,人们可以 1.修改函数中的列表(在阅读此处的答案后,为了可读性,该列表也不应返回),或 2.创建列表的副本并返回该副本。我听说前者是一种更便宜的操作,这让我认为在大多数情况下它会更好。

答:

3赞 Mad Physicist 8/6/2019 #1

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

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

显式比隐式更好。

这在标准库中得到了证实。List 方法在 SO 上是臭名昭著的:list.append、insertextendlist.sort 等。

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

这个想法在很大程度上是 Python 思维方式的一部分。以下是 Guido 电子邮件的摘录,解释了原因和原因:

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

评论

0赞 Serge 8/6/2019
这有助于打击臭名昭著的方法链接反模式:)
1赞 jirassimok 8/6/2019
另一方面,返回列表的修改副本也是一种很好的形式。哪种形式更适合给定函数取决于函数和上下文。
0赞 jirassimok 8/6/2019
@Serge方法链接本身并不是一个反模式,但它是有时间和地点的,它不应该在其他地方被滥用(例如附加到列表)。
0赞 Mad Physicist 8/6/2019
@jirassimok。OP 专门描述了就地情况。如果您正在处理副本,您几乎必须归还副本。
0赞 jirassimok 8/6/2019
我本可以更好地表达这个评论。我基本上是想澄清,有时你想做一个修改的副本而不是修改输入,我不想把它作为一个单独的答案添加,因为它并没有真正回答这个问题。
1赞 Green Cloak Guy 8/6/2019 #2

从技术上讲,“最佳实践”是完全不修改它:

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

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

但一般来说,如果你限制自己返回一个新对象或就地修改一个对象,而不是同时修改一个对象,而不是同时修改一个对象,那就更清楚了。

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

评论

0赞 Michael Skarn 8/6/2019
但是,修改原始列表不是比制作副本更快吗?还是差异可以忽略不计?
0赞 Green Cloak Guy 8/6/2019
@MichaelSkarn 是的,而且内存效率更高,但出于某种原因,人们可能仍然需要原始列表(您会注意到该类的几乎每个方法都返回一个新的、更改的实例,而不是修改字符串本身)。str
0赞 Mad Physicist 8/6/2019
@MichaelSkarn。实际上取决于列表有多大,以及修改的内容。 在具有 10**6 个元素的列表中可能比制作一个新元素便宜。如果必须以任何一种方式重新分配缓冲区,调用都不会产生任何影响(由于摊销,这并不总是发生)。x[0] = 3append
0赞 blue_note 8/6/2019 #3

当然,没有严格的应该,但是,函数应该要么做某事,要么返回某事因此,您最好要么在不返回任何内容的情况下修改列表,要么返回一个新列表,保持原始列表不变。

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

1赞 ShadowRanger 8/6/2019 #4

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

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