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

发布于 2024-12-29 19:38:28 字数 822 浏览 4 评论 0原文

#!/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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

柠檬 2025-01-05 19:38:28

相对流行的 SO 问题对此进行了详细介绍,但我会尝试在您的特定情况下解释这个问题。


当您声明函数时,默认参数将在此时被评估。每次调用该函数时它不会刷新。

您的函数表现不同的原因是您以不同的方式对待它们。在 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

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 in f2 you are creating a new integer object and assigning it into b. You are not modifying b here, you are reassigning it. It is a different object now. In f1, you keep the same object around.

Consider an alternative function:

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

This behaves like f2 and doesn't keep appending to the default list. This is because it is creating a new l 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.

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

   l.append(a)

   return l
眼中杀气 2025-01-05 19:38:28

因为在 f2 中,名称 b 被重新绑定,而在 f1 中,对象 l 被突变。

Because in f2 the name b is rebound, whereas in f1 the object l is mutated.

沙与沫 2025-01-05 19:38:28

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

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

a = 5
b = a
a = 7

值在程序中的某个时刻创建,并且可以通过名称引用,也可以通过列表或其他数据结构中的槽。在上面的名称 a 绑定到值 5,然后反弹到值 7。这对 5 没有影响,它始终是值 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< 的当前值) /code>) 附加到它后面,看起来像 [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 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 use b 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:

a = 5
b = a
a = 7

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 name b to the value referred to by a at that point in time. Rebinding the name a afterwards has no effect on the value 5, and so has no effect on the name b 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:

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

l is not anything, because l is only a name within the scope of the function f1, 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 and l) 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 call f3(5), the name a will be bound to the value 5 and the name l 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 of l and a. So if this is to have any effect on l at all, it can only do so by modifying the value that l refers to, and indeed it does. The append 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 of f1, has now had 5 (the current value of a) 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:

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

Here, as before, the value 1 is squirreled away somewhere to serve as the default value for b, and when we begin executing f2(5) call the name a will become bound to the argument 5, and the name b will become bound to the default value 1.

But then we execute the assignment statement. b appears on the left side of the assignment statement, so we're rebinding the name b. First Python works out b + 1, which is 6, then binds b to that value. Now b 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:

x += y

this normally behaves like:

x = x + y

i.e. it calculates a new value with and rebinds x to that value, with no effect on the old value. But if x is a list, then it actually modifies the value that x refers to! So be careful of that case.

﹉夏雨初晴づ 2025-01-05 19:38:28

在 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

无人问我粥可暖 2025-01-05 19:38:28

其他答案解释了为什么会发生这种情况,但我认为如果您想获得新对象,应该对要做什么进行一些讨论。许多类都有方法.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.166666666666666666, 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] 因为您不是在改变原始对象,而是在创建一个新对象。

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 rewrite f1 as

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

then 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:

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

This will set scaled_data to [0.16666666666666666, 0.38709677419354843, 0.8441754916792739]; each time you set a value of scaled_data to the scaled version, you also change the value in data. If you instead have

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

this will set scaled_data to [0.16666666666666666, 0.3333333333333333, 0.5] because you're not mutating the original object but creating a new one.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文