【问题标题】:Correct Style for Python functions that mutate the argument改变参数的 Python 函数的正确样式
【发布时间】:2014-09-24 22:38:14
【问题描述】:

我想编写一个 Python 函数来改变其中一个参数(它是一个列表,即可变的)。像这样的:

def change(array):
   array.append(4)

change(array)

我更熟悉按值传递而不是 Python 的设置(无论您决定如何称呼它)。所以我通常会写这样一个函数:

def change(array):
  array.append(4)
  return array

array = change(array)

这是我的困惑。因为我可以改变论点,所以第二种方法似乎是多余的。但是第一个感觉不对。另外,我的特定函数将有几个参数,其中只有一个会改变。第二种方法清楚地说明了正在更改的参数(因为它已分配给变量)。第一种方法没有给出任何指示。有约定吗?哪个更好'?谢谢。

【问题讨论】:

  • 为什么第一个“感觉不对”?
  • 我想我只是习惯了第二个。正如我所说,当有多个参数时,第一个参数的情况就不太清楚了。
  • 请注意 list.append 本身什么也不返回,因为它改变了值。内置和标准库中的几乎所有内容都是如此。
  • 有一些值得注意的例外,但只有在访问所需的返回值时才会变得麻烦/混乱。例如list.pop 改变列表并返回弹出的值。我相信@abarnert 会同意这些情况是例外,而不是规则。
  • @NeilDuToit:对象一个参数。 (Python 甚至让你在实现中显式声明相应的参数,如self。)

标签: python mutable


【解决方案1】:

第一种方式:

def change(array):
   array.append(4)

change(array)

是最惯用的方式。通常,在 python 中,我们期望一个函数要么改变参数,要么返回一些东西1。这样做的原因是,如果一个函数没有返回任何东西,那么它非常清楚地表明该函数必须有一些副作用才能证明它的存在(例如改变输入)。

另一方面,如果你以第二种方式做事:

def change(array):
  array.append(4)
  return array

array = change(array)

当你没想到可变对象突然发生变化时,你很容易难以追踪它——“但我认为 change 做了一个副本”...

1从技术上讲,每个函数都会返回 something_something_ 恰好是 None ...

【讨论】:

  • +1。但也因为这意味着每条语句只对一件事进行一次变异,这在您可以将变异函数调用、赋值等链接在一起的语言中是不正确的,而且它几乎总是语句中最左边的东西发生变异。
  • 还可能值得一提的约定是,返回已更改副本但不改变任何内容的函数将命名为changed
  • @abarnert -- 我没有关注那个。显然changed 不是一个好名字——一个函数的名字应该更多地说明它做了什么,而不仅仅是“这个函数改变了 foo”......
  • 嗯,change 首先没有任何意义……但是changedchange 的过去分词,所以它会做任何change 会做的事情,除了通过制作和返回一个副本而不是就地变异。对于更有用/更现实的示例,sortsorted
  • 我会写我自己的答案来解释;这里还有很多话要说,但这个答案本身就很好,应该被接受。
【解决方案2】:

Python 中的约定是函数要么 改变一些东西,或者返回一些东西,而不是两者。

如果两者都有用,您通常会编写两个单独的函数,其中一个以主动动词命名的 mutator,例如change,以及以 changed 之类的分词命名的非mutator。

内置和标准库中的几乎所有内容都遵循这种模式。您正在调用的 list.append 方法不返回任何内容。与 list.sort 相同,但 sorted 不理会它的参数,而是返回一个新的排序副本。

对于一些特殊方法有一些例外(例如,__iadd__ 应该发生变异然后返回 self),在少数情况下,显然必须有一个东西发生变异并且 返回不同的东西(如list.pop),对于那些试图将 Python 用作一种特定于域的语言的库,与目标域的习惯用法保持一致比与 Python 的习惯保持一致更重要成语(例如,一些 SQL 查询表达式库)。像所有约定一样,除非有充分的理由不遵守,否则会遵守这一约定。


那么,为什么要这样设计 Python?

嗯,一方面,它使某些错误变得明显。如果您希望一个函数不发生变异并返回一个值,那么很明显您错了,因为您会收到类似AttributeError: 'NoneType' object has no attribute 'foo' 的错误。

这在概念上也很有意义:一个什么都不返回的函数一定有副作用,或者为什么有人会写它?

但还有一个事实是,Python 中的每条语句都只改变一件事——几乎总是语句中最左边的对象。在其他语言中,赋值是一个表达式,变异函数返回self,你可以将一大堆变异链接到一行代码中,这使得一目了然地看到状态变化变得更加困难,原因是详细的,或者在调试器中单步执行。

当然,所有这些都是一种权衡——它使 Python 中的某些代码比 JavaScript 中的代码更冗长——但这是一种深深植根于 Python 设计中的权衡。

【讨论】:

  • note list.pop() 可以返回一个 (new_list, element) 对
  • 我也很想知道为什么__iadd__ 是个例外
  • @joel __iadd__ 只是被指定为修改其参数的众多表达式运算符之一:请参阅In-place Operators。因为每个函数都有一个运算符语法,例如+=,所以它们也需要返回一个值,因为表达式总是有一个值。
【解决方案3】:

同时改变参数并返回它几乎没有意义。它不仅可能使阅读代码的人感到困惑,而且使您容易受到mutable default argument problem 的影响。如果获取函数结果的唯一方法是通过 mutated 参数,则为参数提供默认值是没有意义的。

您的问题中没有显示第三个选项。与其改变作为参数传递的对象,不如制作该参数的 copy 并返回它。这使它成为一个没有副作用的纯函数。

def change(array):
  array_copy = array[:]
  array_copy.append(4)
  return array_copy

array = change(array)

【讨论】:

  • 我会调用函数changed 而不是change。这在某种程度上是一种约定,即返回某些内容的纯函数由名词命名,例如 sumproductsorted(在 sorted 的情况下,它是 nominalised past participle 而不是实际的名词);而执行操作并返回None 的函数由动词命名,例如sortappend。当然也有例外,但约定有助于理解相似函数之间的区别,例如sort/sortedchange/changed
  • @Stef 我不能争辩,但我的惯例是尽可能少地改变这个问题。我会留下一个结束的想法:计算机科学中有 2 个难题:缓存失效、命名事物和 off-by-1 错误。
【解决方案4】:

来自Python documentation

某些操作(例如 y.append(10) 和 y.sort())会改变 对象,而表面上相似的操作(例如 y = y + [10] 和 sorted(y)) 创建一个新对象。通常在 Python 中(以及 标准库中的所有情况)一种改变对象的方法 将返回 None 以帮助避免获得这两种类型的操作 使困惑。所以如果你错误地写 y.sort() 认为它会给 你是 y 的排序副本,你最终会得到 None,这将 可能会导致您的程序生成易于诊断的错误。

但是,有一类操作,其中相同的操作 有时具有不同类型的不同行为:增强型 赋值运算符。例如,+= 改变列表但不改变元组或 ints (a_list += [1, 2, 3] 等价于 a_list.extend([1, 2, 3]) 并改变 a_list,而 some_tuple += (1, 2, 3) 和 some_int += 1 创建新对象)。

基本上,按照惯例,改变对象的函数或方法不会返回对象本身。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-31
    • 1970-01-01
    • 2014-03-04
    • 2020-06-05
    • 1970-01-01
    相关资源
    最近更新 更多