变异、重新绑定、复制值和赋值运算符之间的区别

Posted

技术标签:

【中文标题】变异、重新绑定、复制值和赋值运算符之间的区别【英文标题】:Difference between mutation, rebinding, copying value, and assignment operator 【发布时间】:2012-02-22 20:16:22 【问题描述】:
#!/usr/bin/env python3.2

def f1(a, l=[]):
    l.append(a)
    return(l)

print(f1(1))
print(f1(1))
print(f1(1))

def f2(a, b=1):
    b = b + 1
    return(a+b)

print(f2(1))
print(f2(1))
print(f2(1))

f1 中,参数l 有一个默认值赋值,并且只计算一次,所以三个print 输出1、2 和3。为什么f2 不做类似的事情?

结论:

为了让我所学的内容更易于阅读本主题的未来读者,我总结如下:

我找到了this关于该主题的不错教程。

我做了一些简单的example programs来比较mutationrebindingcopying valueassignment operator。

【问题讨论】:

【参考方案1】:

因为在f2 中,名称b 是反弹的,而在f1 中,对象l 是变异的。

【讨论】:

我认为 OP 需要更好的解释。 你是对的.. 然后我将 OP 指向这个相关问题 -> ***.com/questions/8997559/…【参考方案2】:

在 f1 中,您将值存储在数组中,或者在 Python 中更好地存储在列表中,在 f2 中,您对传递的值进行操作。这就是我对它的解释。我可能错了

【讨论】:

【参考方案3】:

relatively popular SO question 对此进行了详细介绍,但我将尝试在您的特定上下文中解释该问题。


当你声明你的函数时,默认参数会在那个时候被评估。每次调用该函数时它都不会刷新。

你的函数表现不同的原因是你对待它们的方式不同。在f1 中,您正在改变对象,而在f2 中,您正在创建一个新的整数对象并将其分配给b。您不是在此处修改b,而是在重新分配它。现在它是一个不同的对象。在f1 中,您保留相同的对象。

考虑一个替代函数:

def f3(a, l= []):
   l = l + [a]
   return l

它的行为类似于f2,并且不会一直附加到默认列表中。这是因为它创建了一个新的l,而没有修改默认参数中的对象。


python中常见的样式是赋值None的默认参数,然后赋值一个新的列表。这解决了整个歧义。

def f1(a, l = None):
   if l is None:
       l = []

   l.append(a)

   return l

【讨论】:

Re the "Common style in python is ..." bit: 如果我这样做bomb = [1, 2, 3]; print(f1(4, bomb)); print(f1(5, bomb))? @Ben 如果这是个问题,那么您一开始就不应该使用可变数据结构。 我的经验法则是“不要修改从外部传入的值(除非这是函数的重点)”。遵守该规则也使None 默认参数技巧变得不必要。任何时候你可能不小心修改默认值意味着你也可能不小心修改显式传递的值,这通常不会更好。当然,故意修改传递的值是可以的。【参考方案4】:

这是一个有点棘手的案例。当您对 Python 如何处理 namesobjects 有很好的理解时,这是有意义的。如果您正在学习 Python,则应该努力尽快培养这种理解,因为它绝对是您在 Python 中所做的一切的核心。

Python 中的名称类似于 af1b。它们仅存在于特定范围内(即您不能在使用它的函数之外使用b)。在运行时,名称​​引用一个值,但可以随时通过赋值语句重新绑定到一个新值,例如:

a = 5
b = a
a = 7

值是在程序中的某个位置创建的,可以通过名称来引用,也可以通过列表或其他数据结构中的槽来引用。上面的名字a绑定了值5,后来又反弹到了值7。这对value5没有影响,不管有多少个名字,它始终是5当前绑定到它。

另一方面,对b 的赋值将名称b 绑定到当时a 引用的。之后重新绑定名称 a 5 没有影响,因此对名称 b 也没有影响,该名称也绑定到值 5。

赋值总是在 Python 中以这种方式工作。它从不对价值观有任何影响。 (除了某些对象包含“名称”;重新绑定这些名称显然会影响包含名称的对象,但不会影响名称在更改之前或之后引用的值)

每当您在赋值语句的左侧看到一个名称时,您就是在(重新)绑定该名称。每当您在任何其他上下文中看到名称时,您都在检索该名称所引用的(当前)值。


这样,我们就可以看到您的示例中发生了什么。

当 Python 执行一个函数定义时,它会计算用于默认参数的表达式并在某个地方偷偷摸摸地记住它们。在此之后:

def f1(a, l=[]):
    l.append(a)
    return(l)

l 不是任何东西,因为l 只是函数f1 范围内的一个名称,我们不在该函数内。但是,值 [] 存储在某个地方。

当 Python 执行转移到对 f1调用 时,它会将所有参数名称(al)绑定到适当的值 - 调用者传入的值,或定义函数时创建的默认值。因此,当 Python 生物执行调用 f3(5) 时,名称 a 将绑定到值 5,名称 l 将绑定到我们的默认列表。

当 Python 执行 l.append(a) 时,看不到赋值,所以我们指的是 la 的当前值。因此,如果这对l 有任何影响,它只能通过修改l 所指的值来实现,而且确实如此。列表的append 方法通过在末尾添加一个项目来修改列表。因此,在此之后,我们的列表值 仍然是存储为 f1 的默认参数的相同值,现在附加了 5(a 的当前值),看起来喜欢[5]

然后我们返回l。但是我们已经修改了默认列表,所以它会影响以后的任何调用。而且,我们返回了默认列表,所以对我们返回的值的任何其他修改都会影响任何未来的调用!

现在,考虑f2

def f2(a, b=1):
    b = b + 1
    return(a+b)

这里,和以前一样,值 1 被隐藏在某个地方作为 b 的默认值,当我们开始执行 f2(5) 时,调用名称 a 将绑定到参数 5,并且名称b 将绑定到默认值1

然后我们执行赋值语句。 b 出现在赋值语句的左侧,因此我们重新绑定了名称 b。首先 Python 计算出 b + 1,即 6,然后将 b 绑定到该值。现在b 绑定了值6。但函数的默认值没有受到影响:1 仍然是1!


希望这可以解决问题。为了理解 Python,您确实需要能够根据引用值的名称进行思考,并且可以反弹以指向不同的值。

可能还值得指出一个棘手的案例。我上面给出的规则(关于赋值总是绑定名称而不影响值,所以如果有任何其他影响名称,它必须通过改变值来实现)对于标准赋值是正确的,但对于“增强”赋值运算符并不总是如此比如+=-=*=

不幸的是,这些功能的作用取决于您使用它们的目的。在:

x += y

这通常表现为:

x = x + y

即它计算一个新值并将x 重新绑定到该值,对旧值没有影响。但是如果x 是一个列表,那么它实际上修改了x 所指的值!所以要小心这种情况。

【讨论】:

非常感谢您花费大量墨水来解释细节。我很感激。此外,我还找到了 this 关于该主题的不错的教程。另外,我做了一些简单的example programs 来比较变异、重新绑定、赋值运算符和复制值之间的区别。 也许是 OT,但这不是语言中的错误设计吗?仅仅通过阅读它,我无法推断 [] 在任何地方都存在 - 我只是假设它的范围很广,除非“l”被存储在某个地方。 (有点l =新数组)。 [] 的边缘存在对我来说似乎是病态的。 @AlienLifeForm:我不这么认为。默认值或生成它的代码必须存储在旁边的某个地方。默认表达式不必是简单的文字,它可以是具有任意复杂运行时和副作用的任何 Python 表达式。如果将其存储为每次调用函数时都静默执行的代码,那么直观性会大大降低。事实上,默认值基本上是附加到函数对象的数据;事实上,它们实际上可以作为函数的 func_defaults 属性访问。 很好的答案。例如,也许您可​​以参考一个简单的示例,说明在 C++ 中何时不以这种方式使用赋值运算符。 (我的意思是,只是举个例子,不要浪费你的时间来解释它。)我正在尝试考虑这样的案例,但我现在是一个初学者。 @EricAuld 嗯。自从我做了很多 C 或 C++ 以来已经有一段时间了,但我认为主要的复杂性是内存位置通过指针(以及在一定程度上是引用)明确地成为语言的一部分。所以在某种程度上,当你分配简单的整数时,你必须处理“真正改变”的值,因为当你这样做时,内存位置的值真的会改变,而 C/C++ 可以看到并谈论内存位置。我不太确定如何制作一个不会给 Python 讨论增加太多噪音的好示例。【参考方案5】:

其他答案解释了为什么会发生这种情况,但我认为如果你想获得新对象,应该讨论一下做什么。许多类都有方法.copy(),允许您创建副本。例如,如果我们将f1 重写为

def f1(a, l=[]):
    new_l = l.copy()
    new_l.append(a)
    return(new_l)

那么无论我们调用多少次,它都会继续返回[1]。还有用于管理副本的库https://docs.python.org/3/library/copy.html

此外,如果您循环遍历容器的元素并一一改变它们,使用推导式不仅更符合 Python 风格,而且可以避免改变原始对象的问题。例如,假设我们有以下代码:

data = [1,2,3]
scaled_data = data
for i, value in enumerate(scaled_data):
     scaled_data[i] = value/sum(data)

这会将scaled_data 设置为[0.16666666666666666, 0.38709677419354843, 0.8441754916792739];每次将值 scaled_data 设置为缩放版本时,也会更改 data 中的值。如果你有

data = [1,2,3]
scaled_data = [x/sum(data) for x in data]

这会将scaled_data 设置为[0.16666666666666666, 0.3333333333333333, 0.5],因为您没有改变原始对象,而是创建了一个新对象。

【讨论】:

以上是关于变异、重新绑定、复制值和赋值运算符之间的区别的主要内容,如果未能解决你的问题,请参考以下文章

JS 中的赋值运算符是(重新)绑定还是突变?

r值和l值之间的差异[重复]

python中的两个赋值运算符有啥区别? [复制]

c++ 拷贝构造函数与赋值运算符重载函数的区别是

复制构造函数与赋值运算符(=)有何不同

js 值和引用