更改参数的 Python 函数的正确样式

Correct Style for Python functions that mutate the argument

提问人:Neil Du Toit 提问时间:9/25/2014 最后编辑:mgilsonNeil Du Toit 更新时间:1/22/2022 访问量:20384

问:

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

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

change(array)

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

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

array = change(array)

这是我的困惑。由于我只能改变参数,因此第二种方法似乎是多余的。但第一个感觉不对。此外,我的特定函数将具有多个参数,其中只有一个会更改。第二种方法明确了正在更改的参数(因为它已分配给变量)。第一种方法没有给出任何指示。有约定吗?哪个“更好”?谢谢。

python 可变

评论

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

答:

37赞 mgilson 9/25/2014 #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_ 恰好是 None ...

评论

1赞 abarnert 9/25/2014
+1.但也因为这意味着每个语句只改变一件事一次,这在语言中是不正确的,你可以将变异的函数调用、赋值等链接在一起,而且它几乎总是语句中最左边的东西被变异。
1赞 abarnert 9/25/2014
还可能值得一提的是,返回已更改副本而不更改任何内容的函数将被命名为 。changed
2赞 mgilson 9/25/2014
@abarnert -- 我不跟着那个。显然,这并不是一个好名字——一个函数的名称应该更多地说明它的作用,而不仅仅是“这个函数改变了 foo”......changed
4赞 abarnert 9/25/2014
好吧,首先没有任何意义......但是是 的过去分词,所以它可以做任何事情,除了制作和返回一个副本而不是就地变异。对于更有用/更现实的示例,vs.changechangedchangechangesortsorted
2赞 abarnert 9/25/2014
我会写我自己的答案来解释;这里还有更多要说的,但这个答案本身就很好,应该被接受。
30赞 abarnert 9/25/2014 #2

Python 中的约定是函数要么改变某些内容,要么返回某些内容,而不是两者兼而有之。

如果两者都有用,则通常编写两个单独的函数,其中 mutator 以主动动词 (如 ) 命名,非 mutator 以分词 (如 ) 命名。changechanged

内置函数和 stdlib 中的几乎所有内容都遵循这种模式。您调用的方法不返回任何内容。与 — 相同,但不理会其参数,而是返回一个新的排序副本。list.appendlist.sortsorted

对于一些特殊方法(例如,应该变异然后返回),以及一些显然必须有一个东西发生突变而另一个东西被返回的情况(例如),以及对于试图将 Python 用作一种特定于领域的语言的库,其中与目标域的习语保持一致比与 Python 的习语保持一致更重要(例如, 一些 SQL 查询表达式库)。像所有惯例一样,除非有充分的理由不遵守,否则会遵循这一惯例。__iadd__selflist.pop


那么,为什么 Python 是这样设计的呢?

嗯,一方面,它使某些错误变得明显。如果你期望一个函数是非变异的并返回一个值,那么很明显你错了,因为你会得到一个错误,比如 .AttributeError: 'NoneType' object has no attribute 'foo'

这在概念上也很有意义:一个不返回任何内容的函数一定有副作用,否则为什么会有人编写它?

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

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

评论

2赞 joel 6/25/2019
注意可以返回 (new_list, element) 对list.pop()
0赞 joel 6/25/2019
我也想听听为什么是个例外__iadd__
0赞 Mark Ransom 12/24/2021
@joel 只是指定为修改其参数的众多表达式运算符之一:请参阅就地运算符。因为每个函数都有一个运算符语法,比如它们也需要返回一个值,因为表达式总是有一个值。__iadd__+=
6赞 Mark Ransom 7/1/2016 #3

既改变参数又返回参数几乎没有意义。它不仅可能会给阅读代码的人带来困惑,而且会使您容易受到可变默认参数问题的影响。如果获取函数结果的唯一方法是通过 mutated 参数,则为参数提供默认值是没有意义的。

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

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

array = change(array)

评论

0赞 Stef 10/29/2021
我会调用该函数而不是 .这在某种程度上是一种惯例,即返回某物的纯函数由名词命名,例如 or 或(在 的情况下,它是一个名词化的过去分词,而不是一个实际名词);而执行操作并返回的函数由动词命名,例如 或 。当然也有例外,但约定有助于理解类似函数(如 / 或 /)之间的区别。changedchangesumproductsortedsortedNonesortappendsortsortedchangechanged
1赞 Mark Ransom 10/30/2021
@Stef 我不能争辩,但我的惯例是尽可能少地改变这个问题。最后,我给大家留下一个结论:计算机科学中有两个难题:缓存失效、命名和偏离 1 错误。
1赞 Bharel 1/22/2022 #4

来自 Python 文档

某些操作(例如 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 创建新对象)。

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