第四部分第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. Java-类的生命周期浅析

    简述:Java虚拟机为Java程序提供运行时环境,其中一项重要的任务就是管理类和对象的生命周期.类的生命周期.类的生命周期从类被加载.连接和初始化开始,到类被卸载结束.当类处于生命周期中时,它的二级制 ...

  2. 关于layui的日期和时间组件laydate闪屏的坑

    https://blog.csdn.net/liangwenli_/article/details/82786713 jsp页面: <input type="text" cl ...

  3. Apache Kafka(九)- Kafka Consumer 消费行为

    1. Poll Messages 在Kafka Consumer 中消费messages时,使用的是poll模型,也就是主动去Kafka端取数据.其他消息管道也有的是push模型,也就是服务端向con ...

  4. hadoop学习笔记(十):hdfs在命令行的基本操作命令(包括文件的上传和下载和hdfs中的文件的查看等)

    hdfs命令行 ()查看帮助 hdfs dfs -help ()查看当前目录信息 hdfs dfs -ls / ()上传文件 hdfs dfs -put /本地路径 /hdfs路径 ()剪切文件 hd ...

  5. 百炼OJ - 1007 - DNA排序

    题目链接:http://bailian.openjudge.cn/practice/1007 #include<stdio.h> #include<algorithm> usi ...

  6. js变量提升的坑

    关于js变量提升 变量提升 在js函数内部是可以直接修改全局的变量的,个人感觉是不好的设计, 但是确实存在这个概念 原理: 先查看有没有函数变量bb 查看形参有没有bb 查看全局有没有bb 报错, 找 ...

  7. L3-010 是否完全二叉搜索树 (30分)

    题解 判断一棵树是否是完全二叉树: 取队列的头,将头的左右孩子入队,循环每次判断是否为空,如果为空节点,此时退出循环. 然后检查队列中的元素是否全部为空,如果是则说明是完全二叉树,否则不是. 代码 # ...

  8. Educational Codeforces Round 76 (Rated for Div. 2) A. Two Rival Students

    You are the gym teacher in the school. There are nn students in the row. And there are two rivalling ...

  9. Ansible - iventory

    概述 简单的 ansible 准备 ansible 安装 免密登录 约定 因为重点是 iventory, 所以只使用最简单的模块 ping 1. host-simple 概述 简单版 hosts 文件 ...

  10. 【音乐欣赏】《PANTA RHEI》 - MYTH & ROID

    曲名:PANTA RHEI 作者:MYTH & ROID [ti:PANTA RHEI (<异世界超能魔术师>TV动画片头曲)] [ar:MYTH & ROID] [al: ...