突变、重绑定、值拷贝和赋值运算符之间的区别

11 投票
5 回答
7251 浏览
提问于 2025-04-17 12:00
#!/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没有类似的表现呢?

总结:

为了让未来阅读这个讨论的朋友更容易理解我学到的内容,我总结如下:

  • 我找到了一篇关于这个主题的不错的教程

  • 我写了一些简单的示例程序,用来比较变更重新绑定复制值赋值操作符之间的区别。

5 个回答

3

这个问题有点复杂,理解起来需要对Python是如何处理名称对象有一定的了解。如果你正在学习Python,尽早掌握这些概念是非常重要的,因为它们是你在Python中做任何事情的核心。

在Python中,名称就像是 af1b 这样的东西。它们只在特定的范围内存在(也就是说,你不能在使用它的函数外部使用 b)。在程序运行时,一个名称 指向 一个值,但可以随时通过赋值语句重新指向一个新值,比如:

a = 5
b = a
a = 7

值是在程序的某个时刻创建的,可以通过名称来引用,也可以通过列表或其他数据结构中的位置来引用。在上面的例子中,名称 a 绑定到值 5,后来又重新绑定到值 7。这对 5没有影响,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 的当前值),看起来像 [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 所指向的值!所以要小心这种情况。

5

因为在 f2 里,名字 b 被重新绑定了,而在 f1 里,对象 l 是被改变了。

6

这个问题在一个比较热门的讨论中讲得很详细,不过我会尽量用你能理解的方式来解释这个问题。


当你定义一个函数时,默认参数是在那个时刻就被计算出来的。也就是说,每次你调用这个函数的时候,它不会重新计算默认参数。

你发现函数行为不一样的原因是因为你对它们的处理方式不同。在f1中,你是在修改这个对象,而在f2中,你是创建了一个新的整数对象并把它赋值给b。这里你并不是在修改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

撰写回答