社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

Python到底是引用传递还是值传递?网上大多数教程都讲错了

Crossin的编程教室 • 3 年前 • 469 次点击  

入门教程、案例源码、学习资料、读者群

请访问: python666.cn


大家好,欢迎来到 Crossin的编程教室 !


还记得上一次关于变量作用域文章最后的问题吗?

Python常见问题:全局变量和局部变量

def func(m):
   m[0] = 20
   m = [4, 5, 6]
   return m

l = [1, 2, 3]
func(l)
print('l =', l)

实际的输出我想大家都尝试过了吧,应该是:
[20, 2, 3]

和80%人想象中的结果不一样。

这是为什么呢?

在 Python 的官方文档 FAQ 里有这样一句话

Remember that arguments are passed by assignment in Python.
要记住,Python 里的参数是通过赋值传递的。

https://docs.python.org/3/faq/programming.html#how-do-i-write-a-function-with-output-parameters-call-by-reference

所以要弄清楚参数传递,先得弄清 Python 的赋值。

或许在很多人的直观印象中,变量是一个容器;给变量赋值,就像是往一个存储的容器中填入一个数据;再次赋值就是把容器中的数据换掉。

然而,

在 Python 中,这种理解是错的!
在 Python 中,这种理解是错的!
在 Python 中,这种理解是错的!

若是想要个形象的类比,Python 中的变量更像是是个标签;给变量赋值,就是把标签贴在一个物体上;再次赋值就是把标签贴在另一个物体上。

体会下这两种设计的差异:

· 前者,变量是一个固定的存在,赋值只会改变其中的数值,而变量本身没有改动。
· 后者,变量不存在实体,它仅仅是一个标签,一旦赋值就被设置到另一个物体上,不变的是那些物体。

这些“物体”就是对象。Python 中所有东西都是对象,包括函数、类、模块,甚至是字符串’hello’,数字1、2、3,都是对象。

用个例子来说明:

a = 1
b = 2
c = 1
# 再次赋值
a = b

在这个代码里,a 和 c 其实指向的是同一个对象—整数 1。给 a 赋值为 b 之后,a 就变成了指向 2 的标签,但 1 和 c 都不会受影响。

示意图:

更有说服力一点的验证:

a = 1
print('a', a, id(a))
b = 2
print('b', b, id(b))
c = 1
print('c', c, id(c))
# 再次赋值
a = b
print('a', a, id(a))

输出:

a 1 4301490544
b 2 4301490576
c 1 4301490544
a 2 4301490576

id() 可以认为是获取一个对象的地址。可以看出,a 和 c 开始其实是同一个地址,而后来赋值之后,a 又和 b 是同一个地址。

每次给变量重新赋值,它就指向了新的地址,与原来的地址无关了。

回到函数的调用上:
Python 里的参数是通过赋值传递的




    
def fn(x):
   x = 3

a = 1
fn(a)
print(a)

输出结果为 1,a 没有变化。

调用 fn(a) 的时候,就相当于做了一次 x = a,把 a 赋值给了 x,也就是把 x 这个标签贴在了 a 的对象上。只不过 x 的作用域仅限于函数 fn 内部。

当 x 在函数内部又被赋值为 3 时,就是把 x 又贴在了 3 这个对象上,与之前的 a 不在有关系。所以外部的 a 不会有任何变化。

把其中的数值换成其他对象,效果也是一样的:

def fn(x):
   x = [4,5,6]

a = [1,2,3]
fn(a)
print(a)

输出结果为 [1,2,3],a 没有变化。(记住这个例子,最后我们还会提到)

那上次的题目又是怎么回事?

我们再来看一个赋值:

a = [1,2,3]
print('a', a, id(a))
b = a
print('b', b, id(b))
b[1] = 5
print('a', a, id(a))
print('b', b, id(b))

输出:

a [1, 2, 3] 4490723464
b [1, 2, 3] 4490723464
a [1, 5, 3] 4490723464
b [1, 5, 3] 4490723464

这个是不是好理解一点?b 赋值为 a 后,和 a 指向同一个列表对象。[1] 这个基于 index 的赋值是 list 对象本身的一种操作,并没有给 b 重新贴标签,改变的是对象本身。所以 b 指向的还是原来的对象,此对象的改动自然也会体现在 a 身上。同理,b.append(7) 这样的操作也会是类似的效果。

再来回顾下原问题呢:

def func(m):
   m[0] = 20
   # m = [4, 5, 6]
   return m

l = [1, 2, 3]
func(l)
print('l =', l)

去掉那句 m=[4,5,6] 的干扰,函数的调用就相当于:

l = [1, 2, 3]
m = l
m[0] = 20

l 的值变成 [20,2,3] 没毛病吧。而对 m 重新赋值之后,m 与 l 无关,但不影响已经做出的修改。

这就是这道题的解答。上次留言里有些同学已经解释的很准确了。

另外说下,函数的返回值 return,也相当于是一次赋值。只不过,这时候是把函数内部返回值所指向的对象,赋值给外面函数的调用者:

def fn(x):
   x = 3
   print('x', x, id(x))
   return x

a = 1
a = fn(a)
print('a', a, id(a))

输出:

x 3 4556777904
a 3 4556777904

函数结束后,x 这个标签虽然不存在了,但 x 所指向的对象依然存在,就是 a 指向的新对象。

所以,如果你想要通过一个函数来修改外部变量的值,有几种方法:

  1. 通过返回值赋值

  2. 使用全局变量

  3. 修改 list 或 dict 对象的内部元素

  4. 修改类的成员变量

有相当多的教程把 Python 的函数参数传递分为可变对象和不可变对象(这个概念下次来说)来说明,然后类比到 C++ 的值传递和引用传递。我很反对这样去理解:

  1. 对于没有学过 C++ 的人来说,这个解释属于循环论证,还是没说清问题。

  2. Python 本来就不存在值传递/引用传递的概念,这个比较没有意义。

  3. 这个类比实际上是错误的。就算类比,也应该是相当于 C++ 里的指针值传递。

  4. 用可变对象/不可变对象来划分很容易产生误解,比如我们前面例子中的 x=[4,5,6],它是可变对象,但一样不影响外部参数的值。

这点前面贴出的官方文档里也直说了:

Since assignment just creates references to objects, there’s no alias between an argument name in the caller and callee, and so no call-by-reference per se.
赋值是创建了一份对象的引用(也就是地址),形参和实参之间不存在别名的关系,本质上不存在引用传递。

网上很容易搜到“参数是可变对象就相当于引用传递”这种错误的理解。也不知道他们是对 Python 的参数传递有什么误解,还是对C++的引用传递有什么误解。结果就是,让很多初学者从网上看了几篇教程之后,更糊涂了。

所以呢,找到一个靠谱的教程是非常重要滴😏

感谢转发和点赞的各位~

_往期文章推荐_

老婆问我:ChatGPT会改变世界吗?




如需了解付费精品课程及教学答疑服务
请在Crossin的编程教室内回复: 666

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/153279