和 Java 不同,引用计数是 Python 主要的垃圾回收算法(下文均指 CPython 解释器)。需要首先明确的是,包括 Python 在内的任何高级语言的垃圾回收机制都是非常复杂的,实际上,Python 的垃圾回收也包含了分代机制等复杂的概念,本文只关注引用计数这方面的内容。
问题描述
引用计数 GC 算法,简单来说,就是当一个对象被引用的次数为 0 时,才进行回收。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class A: def __init__(self): print('Object created')
def __del__(self): print('Object deleted')
def func(): a = A() return a
b = func()
print('exit')
|
1 2 3 4
| alrisha@Pisces:~/code/python$ python script.py Object created exit Object deleted
|
注意到由于 Python 全局变量的作用域,对象 A 的实例最终在程序结束时才被删除。
引用计数算法有非常多的优点,其占用内存空间小,而由于计数为 0 时即可触发 GC,因此也几乎没有时间上的停顿(几乎没有 STW,Stop the World),而其本身实现也非常容易。但是,它相对于基于 GC Root 的可达性算法(如 JVM)有一个致命缺陷,那就是难以处理循环引用。实际上,Python 引入分代机制的原因之一就是为了解决循环引用的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class A: def __init__(self, obj = None): self.ptr = obj print('Object A created')
def __del__(self): print('Object A deleted')
class B: def __init__(self, obj = None): self.ptr = obj print('Object B created')
def __del__(self): print('Object B deleted')
a = A() b = B()
a.ptr = b b.ptr = a
del a
print('exit')
|
1 2 3 4 5 6
| alrisha@Pisces:~/code/python$ python script.py Object A created Object B created exit Object A deleted Object B deleted
|
可以看到,由于存在循环引用,即使我们手动删除了对象 A 的引用,它依然没有被回收。
当然,在小的 Python 项目中,可能无需关注这些细节,因为 Python 的垃圾回收机制最终几乎总是能处理类似的问题。但当遇到占用内存较大的项目并且存在硬件限制的场景时,循环引用带来的问题甚至可能类似于内存泄漏。由于每轮 GC 必定存在时间间隔,在两次 GC 间隔之间有可能存在非常多的循环引用对象,从而引发 OOM。
解决方案
弱引用(不推荐)
弱引用是解决引用计数法中循环引用问题的一种方法,比较出名的例子就是 C++ 的智能指针。Python 提供了 weakref
模块来实现弱引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import weakref
class A: def __init__(self, obj = None): self.ptr = obj print('Object A created')
def __del__(self): print('Object A deleted')
class B: def __init__(self, obj = None): self.ptr = obj print('Object B created')
def __del__(self): print('Object B deleted')
a = A() b = B()
a.ptr = weakref.ref(b) b.ptr = weakref.ref(a)
del a
print('exit')
|
1 2 3 4 5 6
| alrisha@Pisces:~/code/python$ python script.py Object A created Object B created Object A deleted exit Object B deleted
|
我们可以认为,Python 的引用计数器仅记录强引用对象而不记录弱引用对象,因此恰当地使用弱引用能够很好地解决循环引用问题。
然而,我不推荐在 Python 中使用弱引用来解决循环引用问题。
- 首先,Python 弱引用所使用的装饰器模式 API 在开发中添加了额外的复杂度。
- 其次,我认为 Python 中的弱引用,相当于开发者将对象所有权完全交给了 Python 解释器。这使得对象的生命周期完全不可控(在没有 GC 的 C++中这一点没那么明显)。
- 最后,使用弱引用也给开发者带来不小的心智压力,开发者必须时刻注意弱引用对象的判空,甚至会徒增很多不必要的代码量。
手动关闭对象
传统的方法可能才是最好的选择。面对循环引用手动清理对象引用无疑是最直观有效的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| class A: def __init__(self, obj=None): self.ptr = obj print('Object A created')
def finish(self): self.ptr = None
def __del__(self): self.ptr = None print('Object A deleted')
class B: def __init__(self, obj=None): self.ptr = obj print('Object B created')
def finish(self): self.ptr = None
def __del__(self): print('Object B deleted')
a = A() b = B()
a.ptr = b b.ptr = a
a.finish() b.finish()
del a
print('exit')
|
1 2 3 4 5 6
| alrisha@Pisces:~/code/python$ python script.py Object A created Object B created Object A deleted exit Object B deleted
|
那这里大伙就要问了,既然要手动关闭对象,那我还用 Python 干嘛呢?这不脱裤子放屁嘛?
对于脚本和科研用途来说,确实,这些 GC 上的细节无需特地在意。但对于希望使用 Python 进行大规模工程开发的人来说,这些问题还是非常关键的。另外,上边这个例子还是不够 Pythonic ,我们完全可以用 with
语法糖,通过 Python 类的上下文管理器来实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| class A: def __init__(self, obj=None): self.ptr = obj print('Object A created')
def __enter__(self): pass
def __exit__(self, exc_type, exc_val, exc_tb): self.ptr = None
def __del__(self): print('Object A deleted')
class B: def __init__(self, obj=None): self.ptr = obj print('Object B created')
def __enter__(self): pass
def __exit__(self, exc_type, exc_val, exc_tb): self.ptr = None
def __del__(self): print('Object B deleted')
a = A() b = B()
with a: a.ptr = b with b: b.ptr = a
del a
print('exit')
|
1 2 3 4 5 6
| alrisha@Pisces:~/code/python$ python script.py Object A created Object B created Object A deleted exit Object B deleted
|
所以说,如果希望通过 Python 进行大规模工程开发,还是需要对 Python 的 GC 机制有一定的了解,多用 with
来关联对象的上下文。不要因为 Python 看起来简单而轻视其中的工程细节。