【问题标题】: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. 创建列表的副本并将其返回。我听说前者是一种更便宜的操作,这让我认为在大多数情况下它会更好。

标签: python pass-by-reference pass-by-value


【解决方案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.。因此,您最好要么修改列表而不返回任何内容,要么返回一个新列表,保持原始列表不变。

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

    【讨论】:

      猜你喜欢
      • 2015-09-25
      • 2012-01-13
      • 2015-07-17
      • 1970-01-01
      • 1970-01-01
      • 2016-12-02
      • 2017-03-10
      • 2015-05-19
      • 2015-04-10
      相关资源
      最近更新 更多