变异、重新绑定、复制值和赋值运算符之间的区别
#!/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
不做类似的事情?
结论:
为了使我学到的知识更容易为该线程的未来读者导航,我总结如下:
#!/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))
In f1
the argument l
has a default value assignment, and it is only evaluated once, so the three print
output 1, 2, and 3. Why f2
doesn't do the similar?
Conclusion:
To make what I learned easier to navigate for future readers of this thread, I summarize as the following:
I found this nice tutorial on the topic.
I made some simple example programs to compare the difference between mutation, rebinding, copying value, and assignment operator.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
相对流行的 SO 问题对此进行了详细介绍,但我会尝试在您的特定情况下解释这个问题。
当您声明函数时,默认参数将在此时被评估。每次调用该函数时它不会刷新。
您的函数表现不同的原因是您以不同的方式对待它们。在
f1
中,您正在改变对象,而在f2
中,您正在创建一个新的整数对象并将其分配给b
。您不是在这里修改b
,而是重新分配它。现在它是一个不同的对象。在f1
中,您保留相同的对象。考虑一个替代函数:
其行为类似于
f2
并且不会继续附加到默认列表。这是因为它正在创建一个新的l
,而没有修改默认参数中的对象。python 中常见的风格是分配默认参数
None
,然后分配一个新列表。这解决了整个歧义。This is covered in detail in a relatively popular SO question, but I'll try to explain the issue in your particular context.
When your declare your function, the default parameters get evaluated at that moment. It does not refresh every time you call the function.
The reason why your functions behave differently is because you are treating them differently. In
f1
you are mutating the object, while inf2
you are creating a new integer object and assigning it intob
. You are not modifyingb
here, you are reassigning it. It is a different object now. Inf1
, you keep the same object around.Consider an alternative function:
This behaves like
f2
and doesn't keep appending to the default list. This is because it is creating a newl
without ever modifying the object in the default parameter.Common style in python is to assign the default parameter of
None
, then assign a new list. This gets around this whole ambiguity.因为在
f2
中,名称b
被重新绑定,而在f1
中,对象l
被突变。Because in
f2
the nameb
is rebound, whereas inf1
the objectl
is mutated.这是一个有点棘手的案例。当您很好地理解 Python 如何处理名称和对象时,这是有意义的。如果您正在学习 Python,您应该努力尽快加深对 Python 的理解,因为它绝对是您在 Python 中所做的一切的核心。
Python 中的名称类似于
a
、f1
、b
。它们仅存在于特定范围内(即您不能在使用 b 的函数之外使用它)。在运行时,名称引用一个值,但可以随时使用赋值语句重新绑定到新值,例如:值在程序中的某个时刻创建,并且可以通过名称引用,也可以通过列表或其他数据结构中的槽。在上面的名称
a
绑定到值 5,然后反弹到值 7。这对 值 5 没有影响,它始终是值 5无论当前有多少个名字与其绑定。另一方面,对
b
的赋值会将名称b
绑定到a
引用的值那个时间点。之后重新绑定名称a
对 值 5 没有影响,因此对也绑定到该值的名称b
没有影响5.在 Python 中赋值总是以这种方式进行。它永远对价值观没有任何影响。 (除了某些对象包含“名称”;重新绑定这些名称显然会影响包含该名称的对象,但不会影响更改之前或之后名称引用的值)
每当您在赋值语句,您正在(重新)绑定名称。每当您在任何其他上下文中看到某个名称时,您都会检索该名称引用的(当前)值。
有了这个,我们就可以看到您的示例中发生了什么。
当Python执行函数定义时,它会评估用于默认参数的表达式,并将它们偷偷地记住在旁边的某个地方。之后:
l
就什么都不是了,因为l
只是函数f1
范围内的一个名称,而我们并不在其中。功能。然而,值[]
被存储在某个地方。当 Python 执行转移到对
f1
的调用时,它将所有参数名称(a
和l
)绑定到适当的值 - 调用者传入的值,或者定义函数时创建的默认值。因此,当 Python 执行调用f3(5)
时,名称a
将绑定到值 5,名称l
将绑定到我们的默认列表。当 Python 执行
l.append(a)
时,看不到任何赋值,因此我们指的是l
和a
的当前值。因此,如果这对l
有任何影响,它只能通过修改l
引用的值来实现,而且确实如此。列表的append
方法通过在末尾添加一个项目来修改列表。因此,在此之后,我们的列表值,仍然是存储为f1
的默认参数的相同值,现在已经有了 5(a< 的当前值) /code>) 附加到它后面,看起来像
[5]
。然后我们返回
l
。但我们修改了默认列表,因此它将影响以后的任何调用。而且,我们还返回了默认列表,因此对我们返回的值的任何其他修改都将影响以后的任何调用!现在,考虑
f2
:这里,和以前一样,值 1 被保存在某个地方作为
b
的默认值,当我们开始执行f2( 5)
调用名称a
将绑定到参数 5,名称b
将绑定到默认值1
>。但随后我们执行赋值语句。
b
出现在赋值语句的左侧,因此我们重新绑定名称b
。首先,Python 计算出b + 1
,即 6,然后将b
绑定到该值。现在,b
绑定到值 6。但函数的默认值并未受到影响:1 仍然是 1!希望事情已经澄清了。为了理解 Python,你确实需要能够根据引用值的名称进行思考,并且可以反弹以指向不同的值。
也许还值得指出一个棘手的案例。我上面给出的规则(关于赋值总是绑定名称而不影响值,因此如果有任何其他影响名称的事情,它必须通过更改值来实现)对于标准赋值来说是正确的,但并不总是对于“增强”赋值运算符例如
+=
、-=
和*=
。不幸的是,它们的作用取决于您使用它们的目的。在:
这通常表现得像:
即它计算一个新值并将
x
重新绑定到该值,对旧值没有影响。但如果x
是一个列表,那么它实际上会修改x
引用的值!所以要小心这种情况。This is a slightly tricky case. It makes sense when you have a good understanding of how Python treats names and objects. You should strive to develop this understanding as soon as possible if you're learning Python, because it is central to absolutely everything you do in Python.
Names in Python are things like
a
,f1
,b
. They exist only within certain scopes (i.e. you can't useb
outside the function that uses it). At runtime a name refers to a value, but can at any time be rebound to a new value with assignment statements like:Values are created at some point in your program, and can be referred to by names, but also by slots in lists or other data structures. In the above the name
a
is bound to the value 5, and later rebound to the value 7. This has no effect on the value 5, which is always the value 5 no matter how many names are currently bound to it.The assignment to
b
on the other hand, makes binds the nameb
to the value referred to bya
at that point in time. Rebinding the namea
afterwards has no effect on the value 5, and so has no effect on the nameb
which is also bound to the value 5.Assignment always works this way in Python. It never has any effect on values. (Except that some objects contain "names"; rebinding those names obviously effects the object containing the name, but it doesn't affect the values the name referred to before or after the change)
Whenever you see a name on the left side of an assignment statement, you're (re)binding the name. Whenever you see a name in any other context, you're retrieving the (current) value referred to by that name.
With that out of the way, we can see what's going on in your example.
When Python executes a function definition, it evaluates the expressions used for default arguments and remembers them somewhere sneaky off to the side. After this:
l
is not anything, becausel
is only a name within the scope of the functionf1
, and we're not inside that function. However, the value[]
is stored away somewhere.When Python execution transfers into a call to
f1
, it binds all the argument names (a
andl
) to appropriate values - either the values passed in by the caller, or the default values created when the function was defined. So when Python beings executing the callf3(5)
, the namea
will be bound to the value 5 and the namel
will be bound to our default list.When Python executes
l.append(a)
, there's no assignment in sight, so we're referring to the current values ofl
anda
. So if this is to have any effect onl
at all, it can only do so by modifying the value thatl
refers to, and indeed it does. Theappend
method of a list modifies the list by adding an item to the end. So after this our list value, which is still the same value stored to be the default argument off1
, has now had 5 (the current value ofa
) appended to it, and looks like[5]
.Then we return
l
. But we've modified the default list, so it will affect any future calls. But also, we've returned the default list, so any other modifications to the value we returned will affect any future calls!Now, consider
f2
:Here, as before, the value 1 is squirreled away somewhere to serve as the default value for
b
, and when we begin executingf2(5)
call the namea
will become bound to the argument 5, and the nameb
will become bound to the default value1
.But then we execute the assignment statement.
b
appears on the left side of the assignment statement, so we're rebinding the nameb
. First Python works outb + 1
, which is 6, then bindsb
to that value. Nowb
is bound to the value 6. But the default value for the function hasn't been affected: 1 is still 1!Hopefully that's cleared things up. You really need to be able to think in terms of names which refer to values and can be rebound to point to different values, in order to understand Python.
It's probably also worth pointing out a tricky case. The rule I gave above (about assignment always binding names with no effect on the value, so if anything else affects a name it must do it by altering the value) are true of standard assignment, but not always of the "augmented" assignment operators like
+=
,-=
and*=
.What these do unfortunately depends on what you use them on. In:
this normally behaves like:
i.e. it calculates a new value with and rebinds
x
to that value, with no effect on the old value. But ifx
is a list, then it actually modifies the value thatx
refers to! So be careful of that case.在 f1 中,您将值存储在数组中,或者更好的是在 Python 中存储在列表中,而在 f2 中,您对传递的值进行操作。这就是我对此的解释。我可能错了
In f1 you are storing the value in an array or better yet in Python a list where as in f2 your operating on the values passed. Thats my interpretation on it. I may be wrong
其他答案解释了为什么会发生这种情况,但我认为如果您想获得新对象,应该对要做什么进行一些讨论。许多类都有方法
.copy()
,允许您创建副本。例如,如果我们重写f1
,那么无论我们调用它多少次,它都会继续返回
[1]
。还有库 https://docs.python.org/3/library/copy .html 用于管理副本。另外,如果您要循环访问容器的元素并逐一改变它们,那么使用推导式不仅更具 Python 风格,而且可以避免改变原始对象的问题。例如,假设我们有以下代码:
这会将
scaled_data
设置为[0.166666666666666666, 0.38709677419354843, 0.8441754916792739]
;每次将scaled_data
的值设置为缩放版本时,您也会更改data
中的值。如果您这样做,则将
scaled_data
设置为[0.16666666666666666, 0.3333333333333333, 0.5]
因为您不是在改变原始对象,而是在创建一个新对象。Other answers explain why this is happening, but I think there should be some discussion of what to do if you want to get new objects. Many classes have the method
.copy()
that allows you create copies. For instance, if we rewritef1
asthen it will continue to return
[1]
no matter how many times we call it. There is also the library https://docs.python.org/3/library/copy.html for managing copies.Also, if you're looping through the elements of a container and mutating them one by one, using comprehensions not only is more Pythonic, but can avoid the issue of mutating the original object. For instance, suppose we have the following code:
This will set
scaled_data
to[0.16666666666666666, 0.38709677419354843, 0.8441754916792739]
; each time you set a value ofscaled_data
to the scaled version, you also change the value indata
. If you instead havethis will set
scaled_data
to[0.16666666666666666, 0.3333333333333333, 0.5]
because you're not mutating the original object but creating a new one.