Python 学习笔记:内存(三)

栏目: Python · 发布时间: 6年前

内容简介:Python 学习笔记:内存(三)

声明:本篇文章是 雨痕老师的 Python 学习笔记第三版的草稿,请大家帮忙校对,如发现问题请截图发到公众号后台谢谢!—— 小 e

示例运行环境: CPython 3.6.1, macOS 10.12.5

鉴于不同运行环境差异,示例输出结果会有所不同。尤其是 id,以及内存地址等信息。  请以实际运行结果为准。

内存

没有值类型、引用类型之分。事实上,每个对象都很 “重”。即便是简单的整数类型,都有一个标准对象头,保存类型指针和引用计数等信息。如果是变长类型 (比如 str、list 等), 还会记录数据项长度,然后才是对象状态数据。

Python 学习笔记:内存(三)

>> > 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

所有对象都由内存管理系统在特定区域统一分配。赋值、传参,亦或是返回局部变量都无需关心内存位置,并没有什么逃逸或者隐式复制(目标对象)行为发生。

基于性能考虑,像 JavaGo 这类语言,编译器会优先在栈 (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

Python 学习笔记:内存(三)

>> > 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   # 同样不会影响目标生命周期。

4387316960

dead

.

对象复制

从编程初学者的角度看,基于名字的引用传递要比值传递自然得多。试想,日常生活中, 谁会因为名字被别人呼来唤去就莫名其妙克隆出一个自己来 ? 但在一个有经验的 程序员 眼里,事情恐怕得反过来。

当调用函数时,我们或许仅仅是想传递一个状态,而非整个实体。这好比把自家姑娘生辰八字,外加一幅美人图交给媒人,断没有直接把人领走的道理。另一方面,现在并发编程也算日常惯例,让多个执行单元共享实例引起数据竞争(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

]

Python 学习笔记:内存(三)

相比 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,避免回收操作对执行计时造成影响。

更多精彩内容请 关注 扫码

Python 学习笔记:内存(三)


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

编程真好玩

编程真好玩

[英] 乔恩·伍德科克 / 余宙华 / 南海出版公司 / 2017-8-1 / 88.00元

在美国,编程已进入幼儿园和中小学课堂,是备受欢迎的课程之一。 在英国,编程被列入国家教学大纲,成为6~15岁孩子的必修课。 在芬兰,编程理念融入了小学的各门课程,孩子们可以随时随地学编程。 编程已经成为世界的通用语言,和听、说、读、写、算一样,是孩子必须掌握的技能。 Scratch是美国麻省理工学院设计开发的可视化少儿编程工具,全球1500多万孩子正在学习使用。它把枯燥乏味......一起来看看 《编程真好玩》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器