欢迎访问Python每天3分钟系列。

每天花3分钟时间,学习或温习一个Python知识点。

图片

今天是第039篇:

参数传递和内存结构 – 修正版

昨天讲的话题,有的麦友表示代码没有输出结果,看不懂。也有表示还不够清楚,所以今天重新发一次,添加了代码结果,也大幅完善了说明。

“本文是我的电子书麦叔带你学Python中相关的章节复制和修改。

“完整电子书可以在麦叔编程公众号上回复book1获取。

当调用函数的时候,需要传递参数,变量从一个函数传递到了另一个函数。

这个传递过程发生了?被传递的变量是被复制了一份呢?还是两个函数共用同一个对象呢?

分清楚变量和对象

首先我们要分清楚变量对象的区别和关系。在这行代码name="麦叔"中实际上创建了两个东西:

  • 一个是name变量,它在内存中有一块小的空间,里面保存了变量所代表的对象的内存地址。这个地址可以改变。
  • 一个是字符串”麦叔”,它在内存中有自己的存放空间,里面存放具体的数据。

如下图所示:

图片

做一个类比:有一个冰箱,上面有一个便签贴,上面写着麦叔001。这个便签条就是变量,而冰箱就是对象。

在编程的世界中,便签条上除了有个标识符麦叔001,还有一个地址记录了麦叔001所代表的冰箱存放的位置。

图片

这个图中贴了好几个便签条。在编程中也可以这么干,可以有好多变量实际上指向了同一个对象。这种情况下,如果通过任何一个变量修改了对象的内容,那就都修改了,因为毕竟都是同一个对象。

可变数据类型和不可变数据类型

我们再来理解一下变量和对象的内存结构,尤其是list等集合类型的内存结构:

name = '麦叔'
print(id(name))
name = '张三'
print(id(name))

函数id()返回一个对象的内存地址,上面的代码打印结果如下:

4348256848
4348256944

上面打印的内存地址是不同的,这表明麦叔张三是两个不同的对象。

在内存里,它的过程是这样的:

  • 变量name有一个自己的空间(蓝色方块),存放它所代表的对象的地址(黄色方块)。
  • 一开始,变量名 name 指向了“麦叔”对象;后来变量名name指向了“张三”。
  • 这时候“麦叔”不再有变量指向它,因而成为孤魂对象,这种对象会被Python的垃圾回收器销毁掉,因为没用了嘛。
图片
不可变对象内存结构

要点总结:

对于字符串,数字等数据类型,每次我们修改一个变量的值的时候,Python会创建一个新的对象,把变量指向这个新的对象。

所以字符串等被称为不可变对象,因为对象一旦创建就不能修改。修改实际上是创建了新的对象。

来看看复杂的数据类型,比如 list:

cities = ['北京', '上海', '广州', '深圳']
print(id(cities))
cities.append('雄安')
print(id(cities))

打印结果:

4307939968
4307939968

上面创建了一个列表cities,然后给cities中间添加了城市雄安。两次打印内存地址是完全相同的,说明是同一个对象。

这说明了:list, set, dict等对象的内容是可以被修改的,并不会因为修改而从新创建对象。所以它们被称为可变对象。

如下图所示

  • cities是一个普通的变量,和前面的name没有什么区别,它里面保存了list对象的地址。
  • list对象就是下图中间的部分,它里面存放了很多东西,所谓存放只不过是保存了具体对象的地址。就好像有班级花名册,记录同学名字和住址。
  • 当list中添加或者减少一个对象时,只是更新花名册上的名字就可以了,不需要每次都新建一个花名册。
图片

下面列举了Python中的可变对象(Mutable Types)和不可变对象(Immutable Type):

图片

参数传递和引用次数

理解了变量的内存存储结构,我们再来看本文开始的的问题,参数传递到底是传递了什么?是复制了一份吗?

先说结论:参数和返回值的传递都是传递的变量指向的内存地址

import random
name = '麦叔'
print(f'name的地址:{id(name)}')

def shuai_score(person):
    print(f'name的地址:{id(person)}')
    score = random.randint(1, 10)
    print(f'score的地址:{id(score)}')
    return score

mscore = shuai_score(name)
print(f'mscore的地址:{id(mscore)}')

打印结果:

name的地址:4345848400
name的地址:4345848400
score的地址:4344381712
mscore的地址:4344381712

代码说明:

  • name被传给了shuai_score函数,传递的是内存地址,所以两次打印地址是相同的。
  • 返回的score也是内存地址,所以两次打印地址也是相同的。

再来看下一个例子:

name = 'zhangsan'
print(f'外部name的地址:{id(name)}')

def hello(name):
    print('----------')
    print(f'传进来name的地址:{id(name)}')
    name = 'lisi'
    print(f'修改过的name的地址:{id(name)}')
    print('----------')

hello(name)

print(f'外部name的新地址:{id(name)}')

打印结果:

外部name的地址:4339450800
----------
传进来name的地址:4339450800
修改过的name的地址:4339450928
----------
外部name的新地址:4339450800

代码说明:

  • 最开始name是zhangsan,地址是4339450800。这个name是全局变量
  • 调用hello(name)的时候,全局变量name被传递给了函数hello(),这时候hello创建了一个局部变量,碰巧也叫name。这两个虽然都叫name,但是不同的变量。但它们同时指向了对象zhangsan,所以在函数打印时的时候,内存地址也是4339450800。
  • 在函数内局部变量的值改成了lisi,这时候Python创建了一个新的对象,并把局部变量name指向了这个新的对象。所以再次打印地址成了4339450928。
  • 这个过程并不会影响外部的全局变量,所以外部再次打印,还是4339450800。

简单来说:

  • 全局变量 name 指向zhangsan,这一直都没有变过;
  • 局部变量 name 本来是指向zhangsan的,但是后来指向了lisi,这并不影响全局变量还是指向张三。

再来看一个可变对象的例子:

cities = ['北京', '上海', '广州', '深圳']
print(f'全局cities的地址:{id(cities)}')

def add_city(cities):
    print(f'局部cities的地址:{id(cities)}')
    cities.append('雄安')
    print(f'局部cities的地址:{id(cities)}')

add_city(cities)
print(cities)
print(f'全局cities的地址:{id(cities)}')

打印结果:

全局cities的地址:4378653312
局部cities的地址:4378653312
局部cities的地址:4378653312
['北京', '上海', '广州', '深圳', '雄安']
全局cities的地址:4378653312

代码说明:

  • cities变量被传递给add_city函数,这时候局部变量和全局变量同时指向同一个list对象。
  • 在add_city中增加了雄安,但因为list是可变对象,所以这个修改不会改变list对象地址,所以全局变量和局部变量仍然指向同一个对象。
  • 所以多次打印cities的地址,都是同一个。函数中的改变,也影响了外部的变量的值,因为本来就是同一个对象嘛。
声明:本网站资源来源于网络收集,如有侵权,请联系站长进行删除处理。 分享目的仅供大家学习和交流,请不要用于商业用途,否则后果自负。本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解。本站资源售价只是赞助,收取费用仅维持本站的日常运营所需。反馈邮箱:1159995880@qq.com