内容简介:Python 学习笔记:内存(三)
声明:本篇文章是 雨痕老师的 Python 学习笔记第三版的草稿,请大家帮忙校对,如发现问题请截图发到公众号后台谢谢!—— 小 e
示例运行环境: CPython 3.6.1, macOS 10.12.5
鉴于不同运行环境差异,示例输出结果会有所不同。尤其是 id,以及内存地址等信息。 请以实际运行结果为准。
内存
没有值类型、引用类型之分。事实上,每个对象都很 “重”。即便是简单的整数类型,都有一个标准对象头,保存类型指针和引用计数等信息。如果是变长类型 (比如 str、list 等), 还会记录数据项长度,然后才是对象状态数据。
>> > x = 1234
>> > import sys
>> > sys . getsizeof ( x ) # Python 3 里 int 也是变长结构。
28
总是通过名字来完成 “引用传递”(pass-by-reference)。名字关联会增加计数,反之减少。 如删除全部名字关联,那么该对象引用计数归零,会被系统自动回收。这就是默认的引用计数垃圾回收机制(Reference Counting Garbage Collector)。
关于 Python 是 pass-by-reference,还是 pass-by-value,会有一些不同的说法。归其原因,是因为名字不是内存位置符号造成的。如果变量 (variable) 不包括名字所关联的目标对象,那么就是值传递。因为传递是通过 “复制” 名字来实现的,这类似于复制指针,或许正确说法是 pass-by-object-reference。不过在编码时,我们通常关注的是目标对象,而非名字自身。从这点上来说, 用 “引用传递” 更能清晰解释代码的实际意图。
基于立场不同,对某些问题会有不同的理论解释。有时候,反过来用实现来推导理论,或许更能 加深理解,更好地掌握相关知识。
>> > a = 1234
>> > b = a
>> > import sys
>> > sys . getrefcount ( a ) # getrefcount 自身也会通过参数引目标对象,导致计数 +1 。
3
>> > del a # 删除其中一个名字,减少计数。
>> > sys . getrefcount ( b )
2
所有对象都由内存管理系统在特定区域统一分配。赋值、传参,亦或是返回局部变量都无需关心内存位置,并没有什么逃逸或者隐式复制(目标对象)行为发生。
基于性能考虑,像 Java 、 Go 这类语言,编译器会优先在栈 (stack) 上分配对象内存。但考虑到闭包、接口、外部引用等因素,原本在栈上分配的对象可能会 “逃逸” 到堆 (heap)。这势必会延长对象生命周期,加大垃圾回收负担。所以,会有专门的逃逸分析 (escape analysis),便于 优化代码和算法。
Python 虚拟机虽然也有执行栈的概念,但并不会在栈上为对象分配内存。从这点上来说,可以认为所有原生对象 (非 C、Cython 等扩展) 都在 “堆” 上分配。
>> > a = 1234
>> > def test ( b ) :
print ( a is b ) # 参数 b 和 a 都指向同一对象。
>> > test ( a )
True
>> > def test ( ) :
x = "hello, test"
print ( id ( x ) )
return x # 返回局部变量。
>> > a = test ( ) # 对比 id 结果,确认局部变量被导出。
4371868400
>> > id ( a )
4371868400
将对象多个名字中的某个重新赋值,并不会影响其他名字。
>> > a = 1234
>> > b = a
>> > a is b
True
>> > a = "hello"
>> > a is b
False
>> > a
'hello'
>> > b
1234
>> > a = 1234
>> > def test ( b ) :
print ( b is a ) # True; 此时 b 和 a 指向同一对象。
b = "hello" # 在 locals 中,b 重新关联到字符串。
print ( b is a ) # False; 这时 b 和 a 分道扬镳。
>> > test ( a )
True
False
>> > a # 显然对 a 没有影响。
1234
注意,只有对名字赋值才会变更引用关系。如下面示例,函数仅仅是透过名字引用修改列 表对象自身,而并未重新关联到其他对象。
>> > a = [ 1 , 2 ]
>> > def test ( b ) :
b . append ( 3 ) # 通过名字 b 向列表追加数据。
print ( b ) # 查看修改结果。
>> > test ( a )
[ 1 , 2 , 3 ]
>> > a # a 和 b 指向同一对象, 然也能获得 “同步” 结果。
[ 1 , 2 , 3]
弱引用
如果说,名字与目标对象关联构成强引用关系,会增加引用计数,会影响其生命周期。那么,弱引用 (weak reference) 就是减配版,在保留引用的前提下,不增加计数,也不阻止目标被回收。不过,并不是所有类型都支持弱引用。
class X :
def __del__ ( self ) : # 析构方法,便于观察实例被回收。
print ( id ( self ) , "dead.")
>> > import sys , weakref
>> > a = X ( ) # 创建实例。
>> > sys . getrefcount ( a )
2
>> > w = weakref . ref ( a ) # 创建弱引用。
>> > w ( ) is a # 通过弱引用访问目标对象。
True
>> > sys . getrefcount ( a ) # 弱引用并为增加目标对象引用计数。
2
>> > del a # 解除目标对象名字引用,对象被回收。
4384434048 dead .
>> > w ( ) is None # 弱引用失效。
True
标准库里另有一些相关实用函数,以及弱引用字典、集合等容器。
>> > a = X ( )
>> > w = weakref . ref ( a )
>> > weakref . getweakrefcount ( a )
1
>> > weakref . getweakrefs ( a )
[ < weakref at 0x10548f778 ; to 'X' at 0x10553b668 > ]
>> > hex ( id ( w ) )
'0x10548f778'
弱引用可用于一些类似缓存、监控等场合,这类 “外挂” 场景不应该影响到目标对象。另一个典型应用是实现 Finalizer,也就是在对象被回收时执行额外的 “清理” 操作。
为什么不使用析构方法(__del__)?
很简单,析构方法作为目标成员,其用途是完成该对象内部资源的清理。它无法感知,也不应该处理与之无关的外部场景。但在实际开发中,类似的外部关联会有很多,那么用 Finalizer 才是合理设计,因为这样只有一个不会侵入的观察员存在。
>> > a = X ( )
>> > def callback ( w ) :
print ( w , w ( ) is None )
>> > w = weakref . ref ( a , callback ) # 创建弱引用时,附加回调函数。
>> > del a # 当回收目标对象时,回调函数被调用。
4384343488 dead .
< weakref at 0x1057f2818 ; dead >True
注意,回调函数参数为弱引用而非目标对象。另外,被调用时,目标已无法访问。
抛开对生命周期的影响不说,弱引用最大的区别在于其类函数的 callable 调用语法。不过可用 proxy 来改进,使其和普通名字引用语法保持一致。
>> > a = X ( )
>> > a . name = "Q.yuhen"
>> > w = weakref . ref ( a )
>> > w . name
AttributeError : 'weakref' object has no attribute 'name'
>> > w ( ) . name
'Q.yuhen'
>> > p = weakref . proxy ( a )
>> > p
< __main__ . X at 0x1055ebe58 >
>> > p . name # 直接访问目标成员。
'Q.yuhen'
>> > p . age = 60 # 通过 proxy 直接向目标添加或设置成员。
>> > a . age
60
>> > del a # 同样不会影响目标生命周期。
4387316960dead
.
对象复制
从编程初学者的角度看,基于名字的引用传递要比值传递自然得多。试想,日常生活中, 谁会因为名字被别人呼来唤去就莫名其妙克隆出一个自己来 ? 但在一个有经验的 程序员 眼里,事情恐怕得反过来。
当调用函数时,我们或许仅仅是想传递一个状态,而非整个实体。这好比把自家姑娘生辰八字,外加一幅美人图交给媒人,断没有直接把人领走的道理。另一方面,现在并发编程也算日常惯例,让多个执行单元共享实例引起数据竞争(data race),也是个大麻烦。
对象的控制权该由创建者负责,而不能寄希望于接收参数的被调用方。必要时,可使用不可变类型,或对象复制品作为参数传递。除自带复制方法 (copy) 的类型外,可选择方法包括标准库 copy 模块,或 pickle、json 等序列化 (object serialization) 方案。
不可变类型包括 int、float 、str、bytes、tuple、frozenset 等。因不能改变其状态,所以被多方只读引用也没什么问题。
复制还分浅拷贝(shallow copy)和深度拷贝(deep copy)两类。对于对象内部成员,浅 拷贝仅复制名字引用,而后者会递归复制所有引用成员。
>> > class X : pass
>> > x = X ( ) # 创建实 。
>> > x . data = [ 1 , 2 ]# 成员 data 引用一个列表。
>> > import copy
>> > x2 = copy . copy ( x ) # 浅拷贝。
>> > x2 is x # 复制成功。
False
>> > x2 . data is x . data # 但成员 data 依旧指向原列表,仅仅复制引用 。
True
>> > x3 = copy . deepcopy ( x ) # 深拷贝。
>> > x3 is x # 复制成功。
False
>> > x3 . data is x . data # 成员 data 列表同样被复制。
False
>> > x3 . data . append ( 3 ) # 向复制的 data 表追加数据。
>> > x3 . data
[ 1 , 2 , 3 ]
>> > x . data # 没有影响原列表。
[ 1 , 2]
相比 copy,序列化是将对象转换为可存储和传输的格式,反序列化则正好相反。至于格式, 可以是 pickle 的二进制,也可以是 JSON、XML 等格式化文本。二进制拥有最好的性能, 但从数据传输和兼容性看,JSON 更佳。
二进制序列化还可选择 MessagePack 等跨平台第三方解决方案。
>> > class X : pass
>> > x = X ( )
>> > x . str = "string"
>> > x . list = [ 1 , 2 , 3]
>> > import pickle
>> > d = pickle . dumps ( x )
>> > x2 = pickle . loads ( d )
>> > x2 is x
False
>> > x2 . list is x . list
False
无论是哪种对象复制方案都存在一定限制,具体请参考相关文档为准。
循环引用垃圾回收
引用计数机制虽然实现简单,但可在计数归零时立即清理该对象所占内存,绝大多数时候 都能高效运作。只是当两个或更多对象构成循环引用(reference cycle)时,该机制就会遭 遇麻烦。因为彼此引用导致计数永不会归零,从而无法触发回收操作,形成内存泄漏。为 此,另设了一套专门用来处理循环引用的垃圾回收器(以下简称 gc)作为补充。
单个对象也能构成循环引用,比如列表把自身作为元素存储。
相比在后台默默工作的引用计数,这个可选的附加回收器拥有更多的控制选项,包括将其 临时或永久关闭。
class X :
def __del__ ( self ) :
print ( self , "dead.")
>> > import gc
>> > gc . disable ( ) # 关闭 gc。
>> > a = X ( )
>> > b = X ( )
>> > a . x = b # 构建循环引用。
>> > b . x = a
>> > del a # 删除全部名字后,对象并为被回收,引用计数失效。
>> >del
b
>> > gc . enable ( ) # 重新启用 gc。
>> > gc . collect ( ) # 主动启动一次回收操作,循环引用对象被正确回收。
< __main__ . X object at 0x1009d9160 > dead .
< __main__ . X object at 0x1009d9128 >dead
.
虽然可用弱引用打破循环,但在实际开发时很难这么做。就本例而言,a.x 和 b.x 都需要保证目标存活,这是逻辑需求,弱引用无法确保这点。另外,循环引用可能由很多对象因复杂流程间接造成,很难被发现,自然也就无法提前使用弱引用方案。
在 Python 早期版本里, gc 不能回收包含 __del__ 的循环引用,但现在已不是问题。 另外,iPython 对于弱引用和垃圾回收存在干扰,建议用原生 Shell 或源码文件测试本节代码。
在进程(虚拟机)启动时,gc 默认被打开,并跟踪所有可能造成循环引用的对象。相比引用计数,gc 是一种延迟回收方式。只有当内部预设的阈值条件满足时,才会在后台启动。 虽然可忽略该条件,强制执行回收,但不建议频繁使用。相关细节,后文在做阐述。
对某些性能优先的算法,在确保没有循环引用前提下,临时关闭 gc 可能会获得更好的性能。甚至某些极端优化策略里,会完全屏蔽垃圾回收,通过重启进程来回收资源。
例如,在做性能测试 (比如 timeit) 时,也会关闭 gc,避免回收操作对执行计时造成影响。
更多精彩内容请 关注 :
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Java核心技术及面试指南
金华、胡书敏、周国华、吴倍敏 / 北京大学出版社 / 2018-9-1 / 59.00
本书根据大多数软件公司对高级开发的普遍标准,为在Java 方面零基础和开发经验在3 年以下的初级程序员提供了升级到高级工程师的路径,并以项目开发和面试为导向,精准地讲述升级必备的技能要点。具体来讲,本书围绕项目常用技术点,重新梳理了基本语法点、面向对象思想、集合对象、异常处理、数据库操作、JDBC、IO 操作、反射和多线程等知识点。 此外,本书还提到了对项目开发很有帮助的“设计模式”和“虚拟......一起来看看 《Java核心技术及面试指南》 这本书的介绍吧!