变量="标签"

变量a和变量b引用同一个列表:

>>> a = [1, 2, 3]
>>> b = a
>>> a.append(4)
>>> b
[1, 2, 3, 4]

使用"标签"很形象的解释了变量    =========>   列表[1, 2, 3]是一个物品,而a和b都是给这个物品贴上的标签。因此,改变a的内容,b的内容也改变了。

"is"和"=="

有一个人叫做李华,1997年生,身体情况工作信息记录为info,有个小名叫"小华"。

>>> lihua = {'name':'lihua','born':'1997','information':'info'}
>>> xiaohua = lihua
>>> xiaohua is lihua
True
>>> id(xiaohua),id(lihua)
(2072419437304, 2072419437304)
>>> xiaohua['information'] = 'new_info'
>>> lihua
{'name': 'lihua', 'born': '1997', 'information': 'new_info'}

可见xiaohua和lihua指代同一个对象,假如有个冒充者(李华)说他是李华,身份信息一模一样,记为anony。

>>> anony = {'name': 'lihua', 'born': '1997', 'information': 'new_info'}
>>> anony == lihua
True
>>> anony is lihua
False

此时使用"is"和"=="判断结果是不同的。lihua和xiaohua绑定同一个对象,xiaohua是lihua的别名;而lihua和anony绑定不同对象。

"=="比较的是对象的值,而"is"比较对象的标识。

在Python中,对象的标识就是id()函数返回值,而is比较的就是这个返回值的整数表示。在Cpython中,id()返回的是对象的内存地址,在其他Python解释器中可能是别的值。最主要的是,id()函数返回值在对象的生命周期中一定不会改变。

写程序是一般关注值,因此==出现频率较高,而在变量和单例值之间比较时应该使用is。除此之外,is运算符比==快,因为它不能重载,解释器不需要寻找并调用特殊方法,直接比较整数id;a==b是语法糖,等同于a.__eq__(b),继承自object的__eq__方法比较两个对象的id,结果与is一样,而覆盖__eq__方法后结果可能就与is结果不同了。

元组是"可变的"

元组保存对象的引用,如果引用的元素是可变的,即使元组本身不可变,元素依然可变。也就是说,元组的不可变性其实是指tuple数据结构的物理内容(即引用)不可变,与引用的对象无关。

t1 = (1, 2, [30, 40])
t2 = (1, 2, [30, 40])
print(t1 == t2)
True print(id(t1[-1]))
3031106993224 #标识 t1[-1].append(50)
print(t1)
(1, 2, [30, 40, 50]) #修改t1[-1]列表 print(id(t1[-1]))
3031106993224 #标识没变 print(t1 == t2)
False #值改变了,不相等

浅复制与深复制

默认作浅复制

复制列表最简单的方式是使用内置类型的构造方法,以list为例:

l1 = [3, [40, 50], (6, 7, 8)]
l2 = list(l1)
print(l2)
[3, [40, 50], (6, 7, 8)] print(l2 == l1)
True #副本与原副本相等 print(l2 is l1)
False #副本与原副本指代不同的对象

当然,可变序列都可以用 [:] 来复制,而无论是构造方法还是 [:] 复制都是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用),如果元素是可变的,就会出现问题。

l1 = [3, [40, 50], (6, 7, 8)]
l2 = list(l1)
l1.append(99)
l1[1].remove(50)
print('l1:', l1)
print('l2:', l2)
l2[1] += [22, 33]
l2[2] += (9, 10)
print('l1:', l1)
print('l2:', l2) #结果
l1: [3, [40], (6, 7, 8), 99]
l2: [3, [40], (6, 7, 8)] #对比l1,由于浅复制,追加99对l2无影响,而对元组l1里面的可变对象[40, 50]执行删除操作却影响到了l1,说明l2和l1绑定同一个列表
l1: [3, [40, 22, 33], (6, 7, 8), 99]
l2: [3, [40, 22, 33], (6, 7, 8, 9, 10)] #+=操作就地修改列表,因此l2与l1同时被修改,而+=对于元组这种不可变对象来说,会重新创建一个元组,重新绑定给l2,修改后,l2中的那个元组与l1中的不是同一个 print(id(l1[2]))
print(id(l2[2])) print(id(l1[2]))
print(id(l2[2])) #替换为打印id,发现l2的元组id最后改变了
2377750817600
2377750817600
2377750817600
2377749721512

浅复制容易,但有时会出现不想要也很意外的结果,就需要深复制。

为任意对象作浅复制和深复制

copy模块提供copy用于浅复制和deepcopy用于深复制(副本不共享内部对象的引用)。

定义一个类bus表示校车,有乘客上车下车方法:

import copy

class Bus:

    def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers) def pick(self, name):
self.passengers.append(name) def drop(self, name):
self.passengers.remove(name) bus1 = Bus(['Alice', 'Bob', 'David']) #原校车
bus2 = copy.copy(bus1) #copy方法复制的校车
bus3 = copy.deepcopy(bus1) #deepcopy方法复制的校车 print(id(bus1), id(bus2), id(bus3))
bus1.drop('David') #bus1的David下车
print(bus2.passengers) print(id(bus1.passengers), id(bus2.passengers), id(bus3.passengers))
print(bus3.passengers) #结果
2765338083960 2765338084072 2765338083456 #三个不同的Bus对象
['Alice', 'Bob'] #bus2的David消失了
2765338349448 2765338349448 2765337978376 #bus1,bus2共享同一个列表对象,bus3则有另一个列表
['Alice', 'Bob', 'David'] #bus3没有改变

一般来说,深复制不是一件简单的事情。如果对象有循环引用,那么这个朴素算法会进入无限循环。deepcopy函数会记住已经复制的对象,因此能优雅的处理循环引用:

from copy import deepcopy

a = [10, 20]
b = [a , 30]
a.append(b)
print(a)
c = deepcopy(a)
print(c) [10, 20, [[...], 30]]
[10, 20, [[...], 30]]

深复制有时处理得太深,对象可能会引用不该复制的外部资源或单例值,此时可以实现特殊方法__copy__()和__deepcopy__(),控制copy和deepcopy的行为。

函数的参数作为引用

python唯一支持的方式是共享传参。类似于java的引用传参。它是指函数各个形式参数获得实参中各个引用的副本,即函数内部形参是实参的别名。

这样,函数可能会修改作为参数传入的可变对象,但是无法修改那些对象标识(即不能把一个对象替换为另一个对象)

不要使用可变类型作为参数默认值

可选参数可以有默认值,但应该避免使用可变对象作为参数默认值。如果使用可变参数,后果见例子:

定义一辆校车,passenger默认值不用None而用[ ]

class Bus:

    def __init__(self, passengers=[]):
self.passengers = passengers def pick(self, name):
self.passengers.append(name) def drop(self, name):
self.passengers.remove(name) bus1 = Bus(['Alice', 'Bob']) #1车原两人
print(bus1.passengers)
bus1.pick('Jane')
bus1.drop('Alice')
print(bus1.passengers) #上一人下一人 bus2 = Bus() #2车空车
bus2.pick('David')
print(bus2.passengers) #上一人 bus3 = Bus() #3车空车
bus3.pick('Mike')
print(bus2.passengers) #上一人 print(bus2.passengers is bus3.passengers)
print(bus1.passengers)

奇怪的现象出现了:

['Alice', 'Bob']
['Bob', 'Jane']
['David']
['David', 'Mike']
True
['Bob', 'Jane']

1车正常行驶,3车出现”幽灵学生“,上二车的David出现在了3车。事实上,可看到bus2和bus3引用的是同一个乘客列表。

实例化Bus时,如果传入乘客可以正常运作,但是不为Bus指定乘客的话,奇怪的事情发生,这是因为self.passengers变成了passengers参数默认值的别名。默认值在定义函数时计算,因而默认值变为了函数对象的属性,如果默认值是可变对象,那么后续函数调用都会受到影响。审查Bus.__init__对象

print(Bus.__init__.__defaults__)
#'David', 'Mike'成为默认乘客
(['David', 'Mike'],)

所以,如果定义的函数接受可变参数,应该慎重考虑调用方是否期望修改传入的参数。例如校车写成深复制那一节的样子。

del和垃圾回收

del语句删除名称,或者说删除标签。(删除一个物品的标签,而不是删除这个物品)del命令可能导致对象被当作垃圾回收,即满足下列条件之一时:

1.删除的变量保存的是对象的最后一个引用

2.无法得到对象

重新绑定也可能会导致对象的引用数量归零,导致对象销毁。

python采用引用计数算法来进行垃圾回收,每个对象都会统计有多少个引用指向自己,当引用计数器归零时,对象就立即销毁。python2.0采用分代垃圾回收算法,用于处理循环引用。

见下例:

import weakref

s1 = {1, 2, 3}
s2 = s1 def bye():
print('bye') ender = weakref.finalize(s1, bye) #注册一个回调函数,在{1,2,3}销毁时使用
print(ender.alive)
del s1
print(ender.alive)
s2 = 'helloworld'
print(ender.alive) #结果发现del s1后,对象仍然存活,而s2重新绑定了对象,于是无法获取对象,导致对象被销毁
True
True
bye
False

弱引用

有引用时对象才会在内存中存在。当对象的引用数量归零后,垃圾回收程序会把对象销毁。

弱引用不会增加对象引用数量,引用的目标对象称为所指对象,因此,弱引用不会妨碍所指对象被当作垃圾回收(任何无引用的时候)。弱引用在缓存中很有用,因为我们不想因为被缓存引用着而始终保存缓存对象。

python提供weakref模块来控制弱引用。

weakref.ref

import weakref
import sys set1 = {1, 2}
print(sys.getrefcount(set1)) #打印引用计数
wref = weakref.ref(set1) #创建弱引用
print(wref) #打印弱引用
print(sys.getrefcount(wref))
set2 = wref() #!!!弱引用时可调用对象,返回的是被引用的对象,若所指对象不存在则返回None
print(set2 is set1)
print(sys.getrefcount(set1))
set1 = None
set2 = None
print(wref)

结果:

2
<weakref at 0x0000024BADFA0408; to 'set' at 0x0000024BADEE99E8>
2
True
3 #调用弱引用返回被引用对象绑定到set2,所以引用显示为3
<weakref at 0x0000024BADFA0408; dead> #弱引用失效

初始引用为2的原因是:当使用某个引用作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用。

weakref.WeakValueDictionary

WeakValueDictionary类实现一种可变映射,里面的值是对象的弱引用,被引用对象在程序其他地方被当作垃圾回收后,对应的键会自动从WeakValueDictionary中删除。

import weakref

class Cheese:
def __init__(self, kind):
self.kind = kind def __repr__(self):
return 'Cheese(%r)' % self.kind stock = weakref.WeakValueDictionary()
catalog = [Cheese('A'), Cheese('B'), Cheese('C'), Cheese('D'), Cheese('E'), Cheese('A')]
for cheese in catalog:
stock[cheese.kind] = cheese print(sorted(stock.keys()))
del catalog
print(sorted(stock.keys()))
del cheese
print(sorted(stock.keys()))

结果:

['A', 'B', 'C', 'D', 'E']
['A']
[]

删除引用后['A']奶酪还在,是因为临时变量引用了对象,这可能导致该变量存在的时间比预期长。通常,这对局部变量来说不是问题,因为它们在函数返回时会被销毁。示例中是全局变量,需显式删除才会消失。

Weak模块还有proxy,WeakSet,WeakKeyDictionary等

//proxy(obj[,callback])函数来创建代理对象。使用代理对象就如同使用对象本身一样,而不需要像ref那样显示调用

//WeakKeyDictionary的键是弱引用,它的实例可以为应用中其他部分拥有的对象附加元数据,这样就无需为对象添加属性

//WeakSet类保存元素弱引用的集合类,元素没有强引用时,集合会把它删除

以上来自《流畅的python》第8章

python对象引用和垃圾回收的更多相关文章

  1. python内存管理&垃圾回收

    python内存管理&垃圾回收 引用计数器 环装双向列表refchain 在python程序中创建的任何对象都会放在refchain连表中 name = '张三' age = 18 hobby ...

  2. Python中的垃圾回收与del语句

    python中的垃圾回收采用计数算法 一个对象如果被引用N次,则需要N次(即计算引用次数为零时)执行del 才能回收此对象. a = 100 b = a del a print(b) print(a) ...

  3. Python中的垃圾回收机制

    Python的垃圾回收机制 引子: 我们定义变量会申请内存空间来存放变量的值,而内存的容量是有限的,当一个变量值没有用了(简称垃圾)就应该将其占用的内存给回收掉,而变量名是访问到变量值的唯一方式,所以 ...

  4. python del和垃圾回收

    1. del是删除对象 2. python中的垃圾回收是删除引用计数

  5. Python 中的垃圾回收机制(转载)

    from: https://foofish.net/python-gc.html GC作为现代编程语言的自动内存管理机制,专注于两件事:1. 找到内存中无用的垃圾资源 2. 清除这些垃圾并把内存让出来 ...

  6. python进阶之垃圾回收

    内存使用: 程序在运行的过程需要开辟内存空间,比如创建一个对象需要一片内存空间,定义变量需要内存空间.有内存的开辟,势必也要有内存的释放,不然只进不出那不是貔貅了吗?即使有开辟有释放在短期内还是会有垃 ...

  7. python中的垃圾回收机制及原理

    序言: 来一起看看: 不同于C/C++,像Python这样的语言是不需要程序员写代码来管理内存的,它的GC(Garbage Collection)机制 实现了自动内存管理.GC做的事情就是解放程序员的 ...

  8. python里面的垃圾回收机制

    文章链接:https://www.jianshu.com/p/1e375fb40506 Garbage collection(GC) 现在的高级语言如java,c#等,都采用了垃圾收集机制,而不再是c ...

  9. 【转载】Python中的垃圾回收机制

    GC作为现代编程语言的自动内存管理机制,专注于两件事:1. 找到内存中无用的垃圾资源 2. 清除这些垃圾并把内存让出来给其他对象使用.GC彻底把程序员从资源管理的重担中解放出来,让他们有更多的时间放在 ...

随机推荐

  1. 内存吞金兽(Elasticsearch)的那些事儿 -- 认识一下

    背景及常见术语 背景 Elasticsearch 是一个开源的搜索引擎,建立在一个全文搜索引擎库 Apache Lucene 基础之上. Lucene 可以说是当下最先进.高性能.全功能的搜索引擎库- ...

  2. NOIP 模拟 $34\; \rm Rectangle$

    题解 \(by\;zj\varphi\) 对于没有在同一行或同一列的情况,直接枚举右边界,左边界从大到小,用树状数组维护上下边界即可. 而对于有多个在一列或一行的情况,这些点将左右分成了几个区间,枚举 ...

  3. wpf 中 theme 的使用 和 listview 模板的使用.

    theme 文件 <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentatio ...

  4. C 静态存储动态存储

    首先,我们可以把程序所占的内存空间分为三个部分:(可以根据静态资源区.栈区.堆区来划分) 静态存储:程序运行期间由系统分配固定得到存储空间(栈): 动态存储:开发者根据自身需要进行动态分配的存储空间( ...

  5. (强制)要求覆写equals必须覆写hashCode(原理分析)

    hashCode和equals hashCode和equals用来标识对象,两个方法协同工作可用来判断两个对象是否相等.众所周知,根据生成的哈希将数据散列开来,可以使存取元素更快.对象通过调用Obje ...

  6. Hibernate的一级缓存和二级缓存

    Fist level cache: This is enabled by default and works in session scope. Read more about hibernate f ...

  7. GROUP BY 语句用于结合合计函数,根据一个或多个列对结果集进行分组

    1 drop table orders; 2 create table orders ( 3 o_id int auto_increment primary key, 4 orderdate date ...

  8. Spring Boot集成Redis集群(Cluster模式)

    目录 集成jedis 引入依赖 配置绑定 注册 获取redis客户端 使用 验证 集成spring-data-redis 引入依赖 配置绑定 注册 获取redis客户端 使用 验证 异常处理 同样的, ...

  9. vue@cli3 项目模板怎么使用public目录下的静态文件,找了好久都不对,郁闷!

    作为图片最好放在static目录下,但是vue@cli3没有static,网上都说放在public目录下,行,那就放吧,可问题是图片放了怎么使用 第一次尝试 肯定用绝对路径这就不说了,用相对路径,we ...

  10. canvas——动画实例

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...