python理解python里的赋值引用拷贝及作用域内存管理垃圾回收

Posted mick_seu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python理解python里的赋值引用拷贝及作用域内存管理垃圾回收相关的知识,希望对你有一定的参考价值。

项目刚刚开始,师傅让我写一些工具,于是开始接触python,十分好用的脚本。于是我用着用C++的用法用python,出现了不少问题,这里以新手的角度理解下python里的赋值,引用,拷贝以及作用域。

参考自:

python基础(5):深入理解 python 中的赋值、引用、拷贝、作用域(xrzs)

Python的函数参数传递:传值?引用?(winterTTr Blog)


1、抛砖引玉:

value = [0, 1, 2]
value[1] = value
那么 value 此时是?结果并不是我们预想的 [0, [0, 1, 2], 2],而是:

[0, [...], 2]
value[1] = value 导致 value = [0, value, 1],value的第二个元素指向value本身,无穷的递归导致了[...]。


Python 没有「变量」,我们平时所说的变量其实只是「标签」,是引用。

对于 value = [0, 1, 2] ,python并没有将列表 [0, 1, 2] 赋值给 value, 只是让 value 指向了一个之前创建好的 [0, 1, 2] 这样一个列表。当 value[1] = value 时,也只是把 value 指向的列表的第二个元素内容,由 1 改为了 value 本身的引用。


想要达到我们预想的结果,需要用到拷贝。




2、浅拷贝和深拷贝

关于浅拷贝和深拷贝,网上也有很多图文并茂的例子。

我们常用到的拷贝方式:

1)没有限制条件的分片表达式(L[:])能够复制序列,但此法只能浅层复制。

2)字典 copy 方法,D.copy() 能够复制字典,但此法只能浅层复制

3)有些内置函数,例如 list,能够浅拷贝 list(L)

4)copy 标准库模块能够生成完整拷贝:deepcopy 本质上是递归 copy



3、变量vs对象

在python中,“类型”属于“对象”,“变量”是没有“类型”的。所有的变量都可以理解是内存中一个对象的“引用”。所以,我们需要把“变量”和真正的“内存对象”分开。

例如:

nfoo = 1   #一个指向int数据类型的变量nfoo(再次提醒,nfoo没有类型)
lstFoo = [1]   #一个指向list类型的变量lstFoo,这个list中包含一个整数1


4、可更改(mutable)与不可更改(immutable)对象

有了以上概念,我们就需要理解可更改与不可更改对象。在python中,string, tuple 和number是不可更改的对象,而list, dict等则是可以修改的对象。有什么区别呢?

nfoo = 1
nfoo = 2
lstFoo = [1]
lstFoo[0] = 2

number对象不能被修改,nfoo原来指向对象1,后来指向了另一个对象2,可以使用 id(nfoo) 发现 nfoo 指向的对象发生了变化。

lstFoo指向一个列表,可以修改。开始其第一个元素为1,后来修改为2,但lstFoo一直指向同一个列表对象。

对于变量(相对于“对象”),python函数参数传递有时是引用传递,有时是值传递。如果这个变量对应的对象值可变,就认为是引用,函数内外是同一个变量,如果这个变量对应的对象值不可改变,就认为是赋值,函数内外是不同的变量。

下面举例说明这两种对象对函数的参数传递的影响:

# 不可变对象 --> 赋值
def ChangeInt( a ):
    a = 10
nfoo = 2 
ChangeInt(nfoo)
print nfoo #结果是2


传入函数的变量 a 可以认为是一个新的变量,与 nfoo 一同指向对象 2 ,后来  a = 10 ,让这个新的变量指向了对象10,由于是两个变量,函数里 a 的改变并没有改变 nfoo 的指向。

# 可变对象 --> 引用
def ChangeList( a ):
    a[0] = 10
lstFoo = [2]
ChangeList(lstFoo )
print nfoo #结果是[10]

这里,a 和 lstFoo 是一个变量,函数内部的修改影响了函数外的变量。



5、内存管理与垃圾回收([Python]内存管理

前面我们知道了 python 中变量和对象的区别,变量仅仅是对对象的引用。Python的内存管理通过两个机制,一个是引用计数,一个是垃圾回收。前者负责确定当前变量是否需要释放,后者解决前者解决不了的循环引用以及提供手动释放的方法。


引用计数(reference counting):针对可以重复利用的内存缓冲区和内存,python使用了一种引用计数的方式来控制和判断某快内存是否已经没有再被使用。即每个对象都有一个计数器count,记住了有多少个变量指向这个对象,当这个对象的引用计数器为0时,假如这个对象在缓冲区内,那么它地址空间不会被释放,而是等待下一次被使用,而非缓冲区的该释放就释放。

我们可以通过sys包中的getrefcount()来获取当前对象有多少个引用,这里返回的引用个数分别是2和3,比预计的1和2多了一个,这是因为传递参数给getrefcount的时候产生了一个临时变量。

>>> a = "aaaa"
>>> sys.getrefcount(a)
2
>>> b = a
>>> sys.getrefcount(a)
3
>>> b = ""
>>> sys.getrefcount(a)
2
当一个变量通过另外一个变量赋值,那么它们的对象引用计数就增加1,当其中一个变量指向另外的地方,之前的对象计数就减少1。


垃圾回收(Garbage Collection):python提供了del方法来删除某个变量,这样可以让某个对象引用数减少1。当某个对象引用数变为0时并不是直接将它从内存空间中清除掉,而是采用垃圾回收机制gc模块,当这些引用数为0的变量规模达到一定规模,就自动启动垃圾回收,将那些引用数为0的对象所占的内存空间释放。这里gc模块采用了分代回收方法,将对象根据存活的时间分为三“代”,所有新建的对象都是0代,当0代对象经过一次自动垃圾回收,没有被释放的对象会被归入1代,同理1代归入2代。每次当0代对象中引用数为0的对象超过700个时,启动一次0代对象扫描垃圾回收,经过10次的0代回收,就进行一次0代和1代回收,1代回收次数超过10次,就会进行一次0代、1代和2代回收。而这里的几个值是通过查询gc模块get_threshold()返回(700,10,10)得到的。此外,gc模块还提供了手动回收的函数,即gc.collect()。这里我们不探讨细节。

>>> a = "aa"
>>> sys.getrefcount(a)
2
>>> b = a
>>> sys.getrefcount(a)
3
>>> del b
>>> sys.getrefcount(a)
2
>>> b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'b' is not defined


这里我们可以稍微探讨下浅拷贝与深拷贝的问题。

以list为例,对于浅拷贝,copy.copy(),产生了一块新的内存来存放原list中的每个元素引用,也就是说每个元素的跟原来list中元素地址是一样的。

>>> a = ["aa", "cc"]
>>> id(a)
140533922732616
>>> id(a[0])
140533922708592
>>> import copy
>>> b = copy.copy(a)
>>> id(b)
140533890984264
>>> id(b[0])
140533922708592

我们可以通过修改list中每个元素的引用来改变每个元素,接着上面:

>>> b[0] = "bb"
>>> id(b)
140533890984264
>>> id(b[0])
140533922708704

可以发现第一个元素指向的对象已被修改。

但如果,原list中要是包含list,dict等可变对象,当修改其中一个元素指向的list时,两边都受到了影响。

>>> a = [1, []]
>>> b = copy.copy(a)
>>> b[1].append(2)
>>> id(a[1])
140533922732616
>>> id(b[1])
140533922732616
>>> a
[1, [2]]
>>> b
[1, [2]]

那是因为使用浅拷贝后,a 和 b 里对应元素分别指向同样的对象,直接修改对象对两边都有影响。

但是!!!如果我们将其中一个的元素指向别的对象,原有的联系就会被打破:

>>> b[1] = 10
>>> a
[1, [2]]
>>> b
[1, 10]
>>> id(a[1])
140533922732616
>>> id(b[1])
9290144

这里我们将 b 第二个元素指向的对象从 list 变到数字 10.


深拷贝 copy.deepcopy() 则不一样,它的将会递归浅拷贝,从而生成一个全新的对象,变量里指向的可变对象分别有各自的内存空间!

>>> a = [1, []]
>>> b = copy.deepcopy(a)
>>> b[1].append(2)
>>> a
[1, []]
>>> b
[1, [2]]
>>> id(a[1])
140533890984328
>>> id(b[1])
140533922732616



以上是关于python理解python里的赋值引用拷贝及作用域内存管理垃圾回收的主要内容,如果未能解决你的问题,请参考以下文章

Python中的深浅拷贝,赋值及引用

Python引用拷贝赋值

python变量存储,理解赋值浅拷贝深拷贝

5-14 练习题及答案

Python学习笔记 | 变量 + 引用 + 拷贝 + 作用域

Python里的拷贝