第四部分第8章,对象引用、可变性和垃圾回收

1. 创建对象之后才会把变量分配给对象

变量是对象的标注,是对象的别名,是对象的引用,并不是对象存储的地方。

例子1. 证明赋值语句的右边先执行

class Gizmo():
def __init__(self):
print('Gizmo id: %d' % id(self)) x = Gizmo()
#这里表明,在尝试求积之前会创建一个新的Gizmo实例。
y = Gizmo() * 10 print(dir())

2. 标识(id())、相等性(==)和别名

例子1. 两个变量指向同一个对象

charles = {'name': 'Charles L. Dodgson', 'born': 1832}

lewis = charles

print(id(charles), id(lewis))
print(charles is lewis) lewis['balance'] = 950
print(charles)

例子2. chrles和lewis绑定同一个对象,alex绑定另外一个具有相同内容的对象

charles = {'name': 'Charles L. Dodgson', 'born': 1832}

alex = {'name': 'Charles L. Dodgson', 'born': 1832}

print(id(charles), id(alex))
#dict类的__eq__方法实现了==运算符
print(charles == alex)
print(charles is alex)

例子1和例子2中,charles和lewis是别名,即两个变量绑定同一个对象。而alex不是charles的别名,因为二者绑定的是不同的对象。alex和charles绑定的对象具有相同的值(==比较的是值),但它们的标识(id)不同。

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

3. ==和is

==运算符比较两个对象的(对象中保存的数据),is运算符比较对象的标识。

通常我们关注的是值,不是id,所以Python中出现的频率比is高。

is运算符比速度快。 a == b是语法糖,等同于a.eq(b)。继承自object的__eq__方法比较两个对象的id,结果与is一样。多数内置类型覆盖了__eq__方法,所以相等性测试可能涉及大量处理工作,例如比较大型集合或嵌套层级深的结构时。

4. 元组tuple的相对不可变性

  1. 元组与多数Python的collections(list、dict、set等等)一样,保存的是对象的引用。
  2. 元组的不可变性是值tuple本身不可变,元素依然可变。
  3. 而str、bytes和array.array等单一类型序列是扁平的,它们保存的不是引用,而是在连续的内存中保存数据本身(字符、字节和数字)。

例子1. 元组中不变的是元素的标识(id),元组的值会随着引用的可变对象的变化而变

t1 = (1, 2, [30, 40])
t2 = (1, 2, [30, 40]) #t1和t2是两个不同的对象,标识不同,但是值相同
print(id(t1), id(t2))
print(t1 == t2) #print('id(t1)', id(t1))
print(id(t1[-1]))
#t1[-1]为列表的引用,可以修改其中的值
t1[-1].append(50)
print(id(t1[-1]))
print(t1)
#print('id(t1)', id(t1))

5. 浅复制(shallow copy)

例子1. 构造方法list和[:]做的是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用)

l1 = [3, [55,44], (7,8,9)]
l2 = list(l1) print(l1 == l2, id(l1), id(l2)) l3 = l2[:]
print(l3 == l2, id(l3), id(l2))

浅复制,如果所有元素都是不可变的,那么这样没有问题,且节省内存。但是,如果有可变的元素,可能会导致意想不到的问题。

例子2. 一个包含另外一个列表和一个元组的列表做浅复制,再做些修改,看看影响。

#可以在Python Tutor网站查看过程
l1 = [3, [66, 55, 44], (7, 8, 9)]
# 1.list()构造方法是浅复制
l2 = list(l1)
# 2.浅复制复制的是最外层容器,保留源容器中元素的引用,所以把100追加到l1中,对l2没有影响。
l1.append(100)
# 3.l1[1]与l2[1]绑定的列表是同一个
l1[1].remove(55)
print('l1:', l1)
print('l2:', l2)
# 4.对列表来说,+=运算符就地修改列表。
print('id(l2[1],列表): ', id(l2[1]))
l2[1] += [33, 22]
print('id(l2[1],列表): ', id(l2[1]))
print('id(l2[2],元组): ', id(l2[2]))
# 5.对元组来说,+=运算符创建一个新的元组,然后重新绑定给变量。
l2[2] += (10, 11)
print('id(l2[2],元组): ', id(l2[2]))
print('l1:', l1)
print('l2:', l2)

对列表来说,+=运算符就地修改列表。对元组来说,+=运算符创建一个新的元组,然后重新绑定给变量。

6.深复制(Deep copy)

  1. 有时候我们需要的是深复制(即副本不共享内部对象的引用)
  2. copy模块提供的deepcopy和copy函数能为任意对象做深复制和浅复制。

例子1. 演示copy()和deepcopy()的用法,定义了Bus类。

class Bus:
"""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)

创建一个Bus类的实例bus1,一个浅复制副本bus_shallow, 一个深复制副本bus_deep

class Bus:
"""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
bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
#copy()浅复制
bus_shallow = copy.copy(bus1)
#deepcopy()深复制
bus_deep = copy.deepcopy(bus1)
#bus1和bus_shallow元素中passenger是同一个列表的引用,而bus_deep中passengers是指向另一个列表
bus1.drop('Claire')
print(bus_shallow.passengers)
print(bus_deep.passengers) print(id(bus1.passengers), id(bus_shallow.passengers), id(bus_deep.passengers))

例子2.deepcopy解决循环引用问题

如果对象有循环引用,那么这个算法会进入无限循环。deepcopy函数会想办法复制对象,解决循环引用。

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

通过别名共享对象还能解释Python中传递参数的方式,以及使用可变类型作为参数默认值引起的问题。接下来讨论这些问题。

7. 函数的参数作为引用的时候

  1. 基本类型按值传参,引用类型是共享传参(call by sharing)。
  2. 共享传参指函数的形式参数获得实参中引用的副本。也就是说,函数内部的形参是实参的别名。这种方案的结果是,函数可能会修改作为参数传入的可变对象,但是无法修改那些对象的id(不能把一个对象替换成另外一个对象)。

例子1. 函数可能会修改接收到的任何可变对象

本例子用+=运算符,实际传入的实参可能会受到影响

def f(a, b):
a += b
return a
#数字没变
x = 1
y = 2
print(f(x,y), x, y)
#列表变了
a = [1, 2]
b = [3, 4]
print(f(a, b), a, b)
#元组没变
t = (10, 20)
u = (30, 40)
print(f(t, u), t, u)

例子2. 说明可变默认值的危险,类HauntedBus(幽灵巴士),从Bus类而来。

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

题外话:可选参数可以有默认值,这是Python函数定义的一个很棒的特性,这样API在进化的同时能保证向后兼容。

class 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和bus2相互影响
bus1 = HauntedBus()
bus1.pick('Bill')
bus2 = HauntedBus()
print(bus2.passengers)
bus2.pick('Allen')
print(bus1.passengers)

使用默认值的HauntedBus实例会共享同一个乘客列表。

这个问题的根源是,默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性。

因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。

例子3. 查看例子2中的函数对象的属性,证明默认值变成了函数对象的属性

class 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()
bus1.pick('Bill')
bus2 = HauntedBus()
print(bus2.passengers)
bus2.pick('Allen')
print(bus1.passengers) print(dir(HauntedBus.__init__))
#输出(['Bill', 'Allen'])
print(HauntedBus.__init__.__defaults__) #bus2.passengers是一个别名,绑定到HauntedBus.__init__.__defaults__属性的第一个元素上
print(HauntedBus.__init__.__defaults__[0] is bus2.passengers)

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

例子4. TwilightBus,下车的学生从篮球队中消失了。

class TwilightBus:
"""让乘客销声匿迹的校车""" def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = passengers def pick(self, name):
self.passengers.append(name) def drop(self, name):
self.passengers.remove(name)

这里的问题是,校车为传给构造方法的列表创建了别名,即self.passengers = passengers。正确的做法是,校车自己维护乘客列表。 方法是在__init__中,传入passengers参数时,应该把参数值的副本赋值给self.passengers,即self.passengers = list(passengers)代替self.passengers = passengers。

def __init__(self, passengers):
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers)
  1. list()构造函数创建了另外一个对象,维护相同值,这个就是副本。注意,tuple(t)获得的是同一个对象。这些是CPython实现的细节。
  2. 更加灵活。传给passengers参数的值可以是元组或者其他可迭代对象。

8. del和垃圾回收

  1. del语句删除名称,而不是对象。
  2. del命令可能会导致对象被当作垃圾回收,仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时。
  3. 重新绑定也可能导致对象的引用计数归零,导致对象被回收。
  4. 有个__del__特殊方法。即将销毁实例时,Python解释器会调用__del__方法,给实例最后的机会,释放资源(如回收内存)。自己很少需要实现__del__方法,因为很难用对。
  5. 在CPython中,垃圾回收使用的主要算法是引用计数。但Python的其他实现有更复杂的垃圾回收程序,而且不依赖引用计数。这意味着,对象的引用数为0时可能不会立即调用__del__方法。

例子1. 演示对象生命结束时的情形。并说明del不会删除对象。对象不可获取,从而被删除。

import weakref
s1 = {1, 2, 3}
s2 = s1 def bye():
print('Gone with the wind') wek_ref = weakref.finalize(s1, bye)
print(wek_ref.alive)
del s1
print(wek_ref.alive) s2 = 'spam'
print(wek_ref.alive)

我们把s1引用传给finalize函数了,虽然s1,s2都不指向{1, 2, 3}了,为什么{1, 2, 3}对象被销毁了?

这是因为finalize持有{1, 2, 3}的弱引用,为了监控对象和调用回调。

9. 弱引用

弱引用不会增加对象的引用数量。

弱引用不会妨碍所指对象被当做垃圾回收。

如果弱引用指向的对象死亡,弱引用就返回None。

弱引用在缓存应用中很有用,因为我们不想仅因为被缓存引用着而始终保存缓存对象。

例子1. 弱引用是可调用对象,返回的是被引用的对象;如果所指对象不存在了,返回None



Note: 这里隐式赋值给_, _是强引用。

例子1的wearef.ref是底层接口,这里为了做演示。多数程序最好使用WeakKeyDictionary、WeakValueDictionary、WeakSet和finalize(在内部使用弱引用),不要自己手动创建并处理weakref.ref实例。

WeakValueDictionary简介:中文电子书P370

弱引用的局限:中文电子书P372

10. Python对不可变类型施加的把戏。

这是CPython的实现细节。中文电子书P374。

  1. list(l1),l1[:]返回的都是一个新的对象,tuple(t1)得到同一个元组。
  2. str、bytes、fronzenset实例也有这种行为。
  3. 注意,frozenset实例不是序列,因此不能使用fs[:]。但是,fs.copy()是善意的谎言,返回同一个对象的引用而不是创建一个副本。为的是接口的兼容性,节省内存,提高解释器的速度,使得fronzenset的兼容性比set强。
  4. 两个不可变对象是同一个对象还是副本,对用户来说没什么区别。

另外,

总结:

变量保存的是引用,这一点对Python编程有很多实际的影响。

  1. 简单的赋值不创建副本。
  2. 对+=或*=所做的增量赋值来说,如果左边的变量绑定的是不可变对象,会创建新对象;如果是可变对象,就原地修改。
  3. 为现有的变量赋予新值,不会修改之前绑定的变量。这就重新绑定:变量绑定了其他的对象。如果变量是之前那个对象的最后一个引用,对象会被当作垃圾回收。
  4. 函数的参数以引用的形式传递。这意味着,函数可能会修改通过参数传入的可变对象。解决与否看需求。如果不想被修改,在函数本地创建副本,或者传入的参数使用不可变对象。(例如传入元组而不是列表)。
  5. 使用可变类型作为函数参数的默认值有危险,因为如果使用默认值初始化实例,在实例中修改了参数,会影响到以后再使用默认值初始化的实例的调用。
  6. 在CPython中,对象的引用数量归零后和除了循环引用之外没有其他引用,两个对象都会被销毁。
  7. 某些情况下,可能需要保存对象的引用,但不保存对象本身。例如,有一个类想要记录所有实例。这个需求可以使用弱引用实现,这是一种底层机制,是weakref模块中WeakValueDictionary、WeakKeyDictionary和WeakSet等有用的集合类以及weakref.finalize函数的底层支持。
  8. 在Python中比较的是对象的值, is才是比较对象的引用或者标识(id)。而在Java中,比较的是对象(不是基本类型)的引用。
  9. 当然,自己可以在类中定义__eq__方法,决定==如何比较对象。如果不覆盖__eq__方法,从object几成的方法就是比较对象的ID。
  10. Python支持重载运算符,不支持函数重载。
  11. 处理不可变的对象时,变量保存的是真正的底下那个还是共享对象的引用无关紧要。如果a == b成立,而且两个对象都不会变,那么它们就可能是相同的对象。这就是为什么字符串可以安全使用驻留(intering)。仅当对象可变时,对象标识(id)才重要。
  12. 可变对象是导致多线程编程难以处理的主要原因,因为某个线程改动对象后,如果不正确地同步,那就会损坏数据。但是过度同步又会导致死锁。
  13. Python没有直接销毁对象的机制,这其实是一个好的特性:如果随时可以销毁对象,那么指向对象的强引用怎么办?
  14. 在CPython中, 垃圾回收依靠引用计数。
open('test.txt', 'wt', encoding='utf-8').write('1,2,3
')

这段代码在CPython是安全的,一旦引用数量归零,就立即销毁对象,因为文件对象的引用数量会在write方法返回后归零,Python在销毁内存中表示文件的对象之前,会立即关闭文件。然而在JPython和IronPython中却不安全,因为它们使用的是宿主运行时(Java VM和.NET CLR)中的垃圾回收程序,不依靠引用计数,更加复杂。所以,在任何情况下,应该改为以下的代码:

with open('test.txt', 'wt', encoding='utf-8') as fp:
fp.write('1,2,3')

Fluent_Python_Part4面向对象,08-ob-ref,对象引用、可变性和垃圾回收的更多相关文章

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

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

  2. 流畅的python第八章对象引用,可变性和垃圾回收

    变量不是盒子 在==和is之间选择 ==比较两个对象的值,而is比较对象的标识 元组的相对不可变姓 元组与多数的python集合(列表,字典,集,等等)一样,保存的是对象的引用.如果引用的元素是可变的 ...

  3. 基于Python对象引用、可变性和垃圾回收详解

    基于Python对象引用.可变性和垃圾回收详解 下面小编就为大家带来一篇基于Python对象引用.可变性和垃圾回收详解.小编觉得挺不错的,现在就分享给大家,也给大家做个参考. 变量不是盒子 在示例所示 ...

  4. PythonI/O进阶学习笔记_6.对象引用,可变性和垃圾回收

    前言: 没有前言了- -......这系列是整理的以前的笔记上传的,有些我自己都忘记我当时记笔记的关联关系了. 记住以后 笔记记了就是用来复习的!!!不看不就啥用没了吗!!! content: 1.p ...

  5. Python 对象引用、可变性和垃圾回收

    p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 25.0px Helvetica } 变量不是盒子 在示例所示的交互式控制台中,无法使用"变量是盒 ...

  6. gj7 对象引用、可变性和垃圾回收

    7.1 python变量到底是什么 #python和java中的变量本质不一样,python的变量实质上是一个指针 int str, 便利贴 a = 1 a = "abc" #1. ...

  7. Python进阶:set和dict/对象引用、可变性和垃圾回收/元类编程/迭代器和生成器

    frozenset:不可变集合,无序,不重复 dict上的特性: 1. dict的key或者set的值 都必须是可以hash的(不可变对象 都是可hash的, str, fronzenset, tup ...

  8. Python对象的引用、可变性和垃圾回收

    1.标识.相等性和别名 别名的例子 >>> charles = {'name': 'Charles L. Dodgson', 'born': 1832} >>> l ...

  9. 0030 Java学习笔记-面向对象-垃圾回收、(强、软、弱、虚)引用

    垃圾回收特点 垃圾:程序运行过程中,会为对象.数组等分配内存,运行过程中或结束后,这些对象可能就没用了,没有变量再指向它们,这时候,它们就成了垃圾,等着垃圾回收程序的回收再利用 Java的垃圾回收机制 ...

随机推荐

  1. python之路之考试题目

  2. optm.adam

    optm.adam 待办 https://www.cnblogs.com/dylancao/p/9878978.html 这个优化包 理解求导过程,来理解神经网络.

  3. NOIP做题练习(day5)

    A - 中位数图 题面 题解 先找出题意中的\(b\)所在的位置. 再以这个位置为中心,向右\(for\)一遍有多少个大于/小于该数的数 大于就\(++cs\) 小于就\(--cs\). 因为这个数是 ...

  4. GitHub的安装和第一次上传本地项目

    网站的新用户注册:http://www.github.com 安装:下载之后安装,一路下一步就可以了,安装完成后打开Git Bash,进入bash界面. 邮箱注册: $ git config --gl ...

  5. Linux - Shell - shell 执行方式

    概述 shell 的执行方式 背景 偶尔执行个 shell 脚本 一般都用 './script' 执行 最近忽然看到 有不同的执行方式, 感觉有必要整理一下, 然后和大家分享 准备 os centos ...

  6. Linux中 /boot 目录介绍

    转自https://blog.csdn.net/dulin201004/article/details/7396968 一./boot/目录中的文件和目录 Linux系统在本地启动时,目录/boot/ ...

  7. hdu1874 (spfa 最短路)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1874 很简单的最短路问题,刚刚学习spfa,其实很简单,思想和一维动态规划差不多,数组d[i]表示起点 ...

  8. 关于DLL搜索路径的顺序问题

    DLL的动态链接有两种方法.一种是加载时动态链接(Load_time dynamic linking).Windows搜索要装入的DLL时,按以下顺序:应用程序所在目录→当前目录→Windows SY ...

  9. node学习之express(1)

    1.前提是你安装了node,npm 2.此次我学习的网站是 汇智网 3.创建一个项目学习: npm init 按照提示,输入/不输入 项目的一些信息 安装express模块:npm install e ...

  10. 前端——语言——Core JS——《The good part》读书笔记——第四章节(Function)

    本章介绍Function对象,它是JS语言最复杂的内容. Java语言中没有Function对象,而是普通的方法,它的概念也比较简单,包含方法的重载,重写,方法签名,形参,实参等. JS语言中的Fun ...