变量是引用

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

a和b引用同一个列表。

标识、相等性和别名

每个变量都有标识、类型和值。对象一旦创建,它的标识绝不会
变;你可以把标识理解为对象在内存中的地址。is 运算符比较两个
对象的标识;id()函数返回对象标识的整数表示。

==is之间选择

== 运算符比较两个对象的值(对象中保存的数据),而is 比较对象的
标识(即判定是否是同一个对象)。

检查x是否为None有两种方法:

In [10]: if not x: ...:     print('x is None') ...:
x is None
In [11]: if x is None: ...:     print('x is None') ...:
x is None

is 运算符比 == 速度快,因为它不能重载,所以 Python 不用寻找并调
用特殊方法,而是直接比较两个整数 ID。而 a == b 是语法糖,等同
a.__eq__(b)

继承自 object 的 __eq__ 方法比较两个对象的ID,结果与 is一样。

In [15]: class B(object): ...:     def __init__(self,value): ...:         self._value = value ...:
In [16]: a = B(1)
In [17]: b = B(1)
In [18]: a == b
Out[18]: False
In [19]: a is b
Out[19]: False
In [20]: id(a)
Out[20]: 140510725545488
In [21]: id(b)
Out[21]: 140510722278792
In [22]: a is B
Out[22]: False
In [23]: id(B)
Out[23]: 21494392
In [24]: type(a) == B
Out[24]: True

但是多数内置类型使用更有意义的方式覆盖了__eq__ 方法,会考虑对象属性的值。

元组的相对不可变性

元组保存的是对象的引用。如果引用的元素是可变的,即使元组本身不可变,元素依然可变。

In [25]: t1 = (1, 2, [30, 40])
In [26]: t2 = (1, 2, [30, 40])
In [27]: t1 == t2
Out[27]: True
In [28]: id(t1[-1])  #t1[-1]就是最后那个列表元素
Out[28]: 140510930522568
In [29]: t1[-1].append(99)
In [30]: t1
Out[30]: (1, 2, [30, 40, 99])
In [31]: id(t1[-1])
Out[31]: 140510930522568
In [32]: t1 == t2

这也是有些元组不可散列的原因。

默认做浅复制

复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法。例如:

In [33]: l1 = [3, [55, 44], (7, 8, 9)]
In [34]: l2 = list(l1) # 创建l1的副本   还可以 l2 = l1[:]
In [35]: l2
Out[35]: [3, [55, 44], (7, 8, 9)]
In [36]: l2 == l1
Out[36]: True
In [37]: l2 is l1
Out[37]: False

然而,构造方法或[:]做的是浅复制(即复制的是元素引用)。如果所有元素都是不可变的,那么这样没有问题,还能节省内存。但是,如果有可变的元素,可能就会导致意想不到的问题。

l1 = [3, [66, 55, 44], (7, 8, 9)]
l2 = list(l1) # l2是l1的浅复制副本
l1.append(100) #
l1[1].remove(55) #
print('l1:', l1)
print('l2:', l2)
l2[1] += [33, 22] #
l2[2] += (10, 11) #
print('l1:', l1)
print('l2:', l2)

把代码拷贝到 http://www.pythontutor.com/ 可以可视化内存结构

当执行完第2行代码后,如上所示。l1l2指向不同的列表,但是两者引用同一个列表[66,55,44]和元组(7,8,9)以及int实例。

l1 = [3, [66, 55, 44], (7, 8, 9)]
l2 = list(l1) #
print(l1[0] is l2[0]) #True
print(l1[1] is l2[1]) #True
print(l1[2] is l2[2]) #True

它们三个元素刚开始都引用了同一个对象,但是为啥上图没有体现出来,这里注意一下

把 100 追加到l1 中,对 l2 没有影响。

看到这我明白了,像int这种是字面量就能表示值的不可变类型直接将值放入列表中,没有把内存结构画出来了。

把内部列表l1[1] 中的 55 删除。这对 l2 有影响,因为l2[1] 绑定的列表与 l1[1]是同一个。

对可变的对象来说,如 l2[1] 引用的列表,+= 运算符就地修改列表。这次修改在l1[1] 中也有体现,因为它是l2[1]的别名。

对元组来说,+= 运算符创建一个新元组,然后重新绑定给变量
l2[2]。这等同于 l2[2] = l2[2] + (10, 11)。现在,l1l2
中最后位置上的元组不是同一个对象。

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

浅复制没什么问题,但有时我们需要的是深复制(即副本不共享内部对
象的引用)。copy 模块提供的 deepcopycopy函数能为任意对象做深复制和浅复制。

为了演示 copy()deepcopy()的用法,定义了一个简单
的类Bus。这个类表示运载乘客的校车,在途中乘客会上车或下车。

# -*- coding: utf-8 -*
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)

在交互式控制台中,执行下面代码:

>>> import copy
>>> from Bus import Bus
>>> bus1 = Bus(['小明','小红','小黄'])
>>> bus2 = copy.copy(bus1) #浅复制
>>> bus3 = copy.deepcopy(bus1) #深复制
>>> id(bus1),id(bus2),id(bus3) #三个不同的Bus实例
(2651381816680, 2651381846648, 2651382400280)
>>> bus1.drop('小明') #bus1上的小明下车后
>>> bus2.passengers #bus2中也没有他了
['小红', '小黄']
>>> id(bus1.passengers),id(bus2.passengers),id(bus3.passengers) #可以看出,bus1和bus2共享同一个列表对象
(2651382392584, 2651382392584, 2651382272072)
>>> bus3.passengers #而bus的passengers指向另一个列表
['小明', '小红', '小黄']

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

>>> a = [10, 20]
>>> b = [a, 30]
>>> a.append(b)
>>> a
[10, 20, [[...], 30]]
>>> from copy import deepcopy
>>> c = deepcopy(a)
>>> c
[10, 20, [[...], 30]]

深复制有时可能太深了。例如,对象可能会引用不该复制的外部
资源或单例值。我们可以实现特殊方法 __copy__()
__deepcopy__(),控制 copydeepcopy的行为。

当函数的参数作为引用时

Python 唯一支持的参数传递模式是共享传参(call by sharing)。Java 的引
用类型是这样,基本类型按值传参。

共享传参指函数的各个形式参数获得实参中各个引用的副本。也就是
说,函数内部的形参是实参的别名。

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

>>> def f(a, b):
...     a += b
...     return a
...
>>> x = 1
>>> y = 2
>>>> f(x,y)
3
>>> x,y #数字x没变,无法修改那些对象的标识 方法体内是(x = x + y )
(1, 2)
>>> a = [1, 2]
>>> b = [3, 4]
>>> f(a,b)
[1, 2, 3, 4]
>>> a,b #列表a变了
([1, 2, 3, 4], [3, 4])
>>> t = (10,20)
>>> u = (30,40)
>>> f(t, u)
(10, 20, 30, 40)
>>> t,u #元组t没变
((10, 20), (30, 40))

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

我们以上面的的 Bus 类为基础定义一个新类, HauntedBus,然后修改 __init__ 方法。这一次,passengers 的默认值不是None,而是 [],这样就不用像之前那样使用 if判断了。这个“聪明的举动”会让我们陷入麻烦。

class HauntedBus:#主要的问题是传入的参数列表和HauntedBus中的列表会互相影响def __init__(self, passengers=[]):self.passengers = passengers# 老司机,带带我def pick(self, name):self.passengers.append(name)# 这不是去幼儿园的车,我要下车def drop(self, name):self.passengers.remove(name)

控制台测试:

>>> bus1 = HauntedBus(['Alice', 'Bill'])
>>> bus1.passengers
['Alice', 'Bill']
>>> bus1.pick('Charlie')
>>> bus1.drop('Alice')
>>> bus1.passengers #目前没什么问题
['Bill', 'Charlie']
>>> bus2 = HauntedBus()#一开始,bus2是空的,因此默认把的空列表赋值给self.passengers
>>> bus2.pick('Carrie')
>>> bus2.passengers
['Carrie']
>>> bus3 = HauntedBus() #一开始也是空的,因此还是赋值默认的列表
>>> bus3.passengers #但是默认列表竟然不为空
['Carrie']
>>> bus3.pick('Dave')
>>> bus2.passengers #登上Bus3的Dave出现在bus2上
['Carrie', 'Dave']
>>> bus2.passengers is bus3.passengers #问题是,它们指向同一个列表
True
>>> bus1.passengers #但bus1.passengers是不同的列表
['Bill', 'Charlie']
>>> ps = ['jack','rose'] #声明一个列表ps
>>> bus4 = HauntedBus(ps)
>>> bus4.passengers #和列表中的值一样
['jack', 'rose']
>>> ps.append('groves') #ps列表新增一个元素
>>> bus4.passengers #也在bus4上了
['jack', 'rose', 'groves']
>>> bus4.drop('rose') #bus4下了一个人
>>> ps #ps列表页少了个元素
['jack', 'groves']

问题在于,没有指定初始乘客的HantedBus实例会共享同一个乘客列表。

这是因为 self.passengers 变成了 passengers
参数默认值的别名。出现这个问题的根源是,默认值在定义函数时计算
(通常在加载模块时),因此默认值变成了函数对象的属性。因此,如
果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受
到影响。

可以查看HauntedBus.__init__对象,看看它的__defaults__属性中的那些幽灵学生:

>>> HauntedBus.__init__.__defaults__
(['Carrie', 'Dave'],)

最后,我们可以验证 bus2.passengers是一个别名,它绑定到
HauntedBus.__init__.__defaults__属性的第一个元素上:

>>> HauntedBus.__init__.__defaults__[0] is bus2.passengers
True

可变默认值导致的这个问题说明了为什么通常使用 None 作为接收可变值的参数的默认值。

防御可变参数

如果定义的函数接收可变参数,应该谨慎考虑调用方是否期望修改传入的参数。

>>> bus4 = HauntedBus(ps)
>>> bus4.passengers #和列表中的值一样
['jack', 'rose']
>>> ps.append('groves') #ps列表新增一个元素
>>> bus4.passengers #也在bus4上了
['jack', 'rose', 'groves']
>>> bus4.drop('rose') #bus4下了一个人
>>> ps #ps列表页少了个元素
['jack', 'groves']

问题出现的原因是代码self.passengers = passengers

正确的做法是,校车自己维护乘客的列表:

def __init__(self, passengers=None):if passengers is None:self.passengers = []else:self.passengers = list(passengers)#通过构造函数创建副本

除非这个方法确实想修改通过参数传入的对象,否则在类中
直接把参数赋值给实例变量之前一定要三思,因为这样会为参数对
象创建别名。如果不确定,那就创建副本。这样客户会少些麻烦。

del和垃圾回收

del语句删除名称,而不是对象。del命令可能会导致对象被当作垃圾
回收,但是仅当删除的变量保存的是对象的最后一个引用,或者无法得
到对象时。

为了演示对象生命结束时的情形,下面使用 weakref.finalize
注册一个回调函数,在销毁对象时调用。

>>> import weakref
>>> s1 = {1, 2, 3}
>>> s2 = s1 #s1和s2是别名,指向同一个集合
>>> def bye():
...     print('Gone with the wind...')
...
>>> ender = weakref.finalize(s1, bye) #在s1引用的对象上注册回调bye
>>> ender.alive #调用finalize对象之前, alive属性的值为True
True
>>> del s1 #del不是删除对象,而是删除对象的引用
>>> ender.alive
True
>>> s2 = 'spam' #重新绑定最后一个引用,让{1,2,3}无法获取
Gone with the wind... #对象被销毁,调用了bye回调
>>> ender.alive
False

弱引用

正是因为有引用,对象才会在内存中存在。当对象的引用数量归零后,
垃圾回收程序会把对象销毁。但是,有时需要引用对象,而不让对象存
在的时间超过所需时间。这经常用在缓存中。

弱引用不会增加对象的引用数量。引用的目标对象称为所指对象
(referent)。因此我们说,弱引用不会妨碍所指对象被当作垃圾回收。

下面展示了如何使用 weakref.ref 实例获取所指对象。如果对
象存在,调用弱引用可以获取对象;否则返回 None

>>> a_set = {0, 1}
>>> wref = weakref.ref(a_set) #创建弱引用对象wref
>>> wref #a_set的弱引用对象
<weakref at 0x7f539f165a98; to 'set' at 0x7f5397bbdf28>
>>> wref() #调用 wref() 返回的是被引用的对象,{0, 1}。因为这是控制台会话,所以 {0, 1} 会绑定给 _ 变量
{0, 1}
>>> a_set = {2, 3, 4} #a_set不再指向{0, 1}集合,因此集合的引用数量减少了。但是_ 变量仍然指代它。
>>> wref()# 调用 wref() 依旧返回 {0, 1}。
{0, 1}
>>> wref() is None #计算这个表达式时,{0, 1} 存在,因此 wref() 不是 None。但是,随后 _ 绑定到结果值 False。现在 {0, 1} 没有强引用了。
False
>>> wref() is None
True

Python 控制台会自动把 _ 变量绑定到结果不为 None 的表达式结果上。

WeakValueDictionary简介

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

实现一个简单的类,表示各种奶酪。

# -*- coding: utf-8 -*
import weakref
class Cheese:def __init__(self, kind):self.kind = kinddef __repr__(self):return 'Cheese(%r)' % self.kindif __name__ == '__main__':stock = weakref.WeakValueDictionary() #构造WeakValueDictionary 实例catalog = [Cheese('Red Leicester'), Cheese('Tilsit'), Cheese('Brie'), Cheese('Parmesan')]for cheese in catalog:stock[cheese.kind] = cheese  #stock 把奶酪的名称映射到 catalog 中 Cheese 实例的弱引用上print(cheese) #Cheese('Parmesan')print(sorted(stock.keys())) #['Brie', 'Parmesan', 'Red Leicester', 'Tilsit']del catalog #删除 catalog 之后,stock 中的大多数奶酪都不见了,这是WeakValueDictionary 的预期行为。为什么不是全部呢?print(sorted(stock.keys())) #['Parmesan']del cheeseprint(sorted(stock.keys())) #[]

临时变量引用了对象,这可能会导致该变量的存在时间比预期长。通常,这对局部变量来说不是问题,因为它们在函数返回时
会被销毁。但是在上面示例中,for 循环中的变量 cheese 是全局变量,除非显式删除,否则不会消失。

弱引用的局限

不是每个 Python 对象都可以作为弱引用的目标(或称所指对象)。基本
的 list 和 dict 实例不能作为所指对象,但是它们的子类可以轻松地
解决这个问题:

class MyList(list):"""list的子类,实例可以作为弱引用的目标"""a_list = MyList(range(10))
# a_list可以作为弱引用的目标
wref_to_a_list = weakref.ref(a_list)

set 实例可以作为所指对象,用户定义的类型也没问题。但是,inttuple 实例不能作为弱引用的目标,甚至它们的子类也不行。

Python对不可变类型施加的把戏(选读)

对元组t 来说,t[:]不创建副本,而是返回同一个对
象的引用。此外,tuple(t)获得的也是同一个元组的引用。

>>> t1 = (1, 2, 3)
>>> t2 = tuple(t1)
>>> t2 is t1
True
>>> t3 = t1[:]
>>> t3 is t1
True

strbytesfrozenset 实例也有这种行为。注意,frozenset
实例不是序列,因此不能使用fs[:]fs 是一个 frozenset
例)。但是,fs.copy()具有相同的效果:它会欺骗你,返回同一个
对象的引用,而不是创建一个副本:

>>> t1 = (1, 2, 3)
>>> t3 = (1, 2, 3)
>>> t3 is t1
False
>>> s1 = 'ABC'
>>> s2 = 'ABC'
>>> s2 is s1 #s1和s2指代同一个字符串
True

共享字符串字面量是一种优化措施,称为驻留(interning)(这不和Java中的常量池技术类似吗)。CPython 还会在小的整数上使用这个优化措施,防止重复创建“热门”数字,如0、-1 和 42。(Java中的Integer也会缓存-128到127之间的数值)。

千万不要依赖字符串或整数的驻留!比较字符串或整数是否
相等时,应该使用 ==,而不是 is。驻留是 Python 解释器内部使用
的一个特性。

《流畅的Python》读书笔记——Python对象引用、可变性和垃圾回收相关推荐

  1. 流畅的python 对象引用 可变性和垃圾回收

    对象引用.可变性和垃圾回收 变量不是盒子 人们经常使用"变量是盒子"这样的比喻,但是这有碍于理解面向对象语言中的引用式变量.Python 变量类似于 Java 中的引用式变量,因此 ...

  2. 流畅的Python读书笔记-第八章-对象引用、可变性和垃圾回收

    第8章:对象引用,可变性和垃圾回收 在Python里面变量不是盒子,而是便利贴,类似于Java中的引用变量,因此最好把它们理解为附加在对象上的标注. 因为变量不过是标注,因此无法阻止为对象贴上多个标注 ...

  3. 《Fluent Python》学习笔记:第 8 章 对象引用、可变性和垃圾回收

    本文主要是 Fluent Python 第 8 章的学习笔记.这部分主要是介绍了变量.引用.对象.深拷贝.浅拷贝.垃圾回收等.本章虽然枯燥,但是非常有用. <Fluent Python>学 ...

  4. 记录学习《流畅的python》的一些知识-----对象引用,可变性和垃圾回收

    记录我学习<流畅的python>的过程--对象引用,可变性和垃圾回收 2021.9.22 1.变量不是盒子 2.标识.相等性和别名 3.默认做浅复制 4.函数的参数作为引用时 5.del和 ...

  5. [流畅的Python][8][对象引用、可变性和垃圾回收]

    第8章 对象引用.可变性和垃圾回收 "你不开心,"白骑士用一种忧虑的声调说,"让我给你唱一首歌安 慰你吧--这首歌的曲名叫作 :<黑线鳕的眼睛>." ...

  6. 与孩子一起学编程python_与孩子一起学编程(Python读书笔记3)

    第十一章 嵌套与可变循环 Python 3.X里 print()函数默认是自动换行的,所以本章代码会有很多问题,实际上 print()函数里有一个默认参数 end, 默认情况下: end= " ...

  7. python基础编程:基于Python对象引用、可变性和垃圾回收详解

    下面小编就为大家带来一篇基于Python对象引用.可变性和垃圾回收详解.小编觉得挺不错的,现在就分享给大家,也给大家做个参考.一起跟随小编过来看看吧 变量不是盒子 在示例所示的交互式控制台中,无法使用 ...

  8. Python读书笔记-每日篇-20190222|激活码生成器(redis存储)

    问题描述: 做为 Apple Store App 独立开发者,你要搞限时促销,为你的应用生成激活码(或者优惠券),使用 Python 如何生成 200 个激活码(或者优惠券),并将生成的激活码保存到R ...

  9. Python读书笔记-每日篇-20190221|激活码生成器(mysql存储)

    问题描述: 做为 Apple Store App 独立开发者,你要搞限时促销,为你的应用生成激活码(或者优惠券),使用 Python 如何生成 200 个激活码(或者优惠券),并将生成的激活码保存到M ...

  10. 第八章《对象引用、可变性和垃圾回收》(下)

    对<流畅的python>的读书笔记以及个人理解 9-20 接着上一篇的内容,上一篇的链接:https://blog.csdn.net/scrence/article/details/100 ...

最新文章

  1. 连接阿里云和容器技术生态 - 阿里云开源容器项目汇总
  2. Oracle 创建dblink
  3. java+character类使用_Java Character类应用实例
  4. LeetCode 594. Longest Harmonious Subsequence
  5. 设计师中国风作品必备汉字毛笔字偏旁部首大集合
  6. 解决RPM包依赖的几种方法
  7. onActivityResult在setResult之前被调用
  8. JavaScript开发规范要求
  9. 使用易宝支付接口实现java网上支付功能。
  10. linux服务器离线安装python第三方库
  11. Docker Compose 配置文件 docker-compose.yml 详解
  12. Bonny校园app使用体验
  13. 过去66年的66项最佳发明(1994~2019)
  14. 浅谈C++中的多线程(一)
  15. 互联网中B端客户和C端客户的区别?
  16. 机房环境监控的现状与发展趋势!
  17. kali linux 安装驱动安装教程,Kali Linux 安装英伟达显卡过程实录
  18. 传统OA厂商举步维艰
  19. Vue:滚动页面到指定位置实现避坑笔记
  20. [论文速览] Dataset and Neural Recurrent Sequence Labeling Model for Open-Domain Factoid Question Answeri

热门文章

  1. 20190925 On Java8 第二十二章 枚举
  2. 20172330 2018-2019-1 《程序设计与数据结构》实验一报告
  3. spring boot RESTFul API拦截 以及Filter和interceptor 、Aspect区别
  4. Jenkins学习之旅
  5. 读《Node入门》笔记
  6. BZOJ 2434 阿狸的打字机(ac自动机+dfs序+树状数组)
  7. ES6 Number
  8. 前端面试题之手写事件模型及事件代理/委托
  9. YTU 2578: 分数减法——结构体
  10. css3中的文字效果