背景
Python的GC主要由引用计数、标记清理、分代收集等方式构成,大部分对象内存管理可以通过简单的引用计数完成,但引用计数存在天然缺陷,即无法处理循环引用。
Python通过标记清理来处理循环引用,不过这会引入额外开销。在实际开发中通过weakref创建弱引用,可以在一定程度上避免不必要的循环引用。
weakref用法
弱引用的创建使用有两种方式,这里以具体的示例来进行说明,
class Foo(object):
pass
weakref.ref
函数定义,
weakref.ref(object[, callback])
使用方式,
>>> foo = Foo()
>>> ref = weakref.ref(foo)
>>> print ref()
<__main__.Foo object at 0x101823490>
>>> del foo
>>> print ref()
通过weakref.ref创建的弱引用,在使用时需要显示使用()来获取到真实的对象,之后才能访问其字段与方法。当原对象销毁后,()返回的就会是None。
weakref.proxy
weakref.ref的一个不方便之处在于每次使用都需要先进行()操作,弱引用与普通引用在代码使用层面产生了差异。如果不希望进行这样的操作,那么可以使用weakref.proxy方法,其定义如下,
weakref.proxy(object[, callback])
使用方式,
>>> foo = Foo()
>>> proxy = weakref.proxy(foo)
>>> print proxy
<__main__.Foo object at 0x101823490>
>>> del foo
>>> print proxy
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ReferenceError: weakly-referenced object no longer exists
weakref.proxy创建的Proxy对象在使用上与普通代码没有区别,但也存在问题,如上所示,当原对象销毁后再访问Proxy对象,则会抛ReferenceError。
为了处理这种情况,一定程度上还是需要一个判定Proxy指向对象是否有效,目前想到两种方式,
- 使用try...except
def exists(proxy):
try:
proxy.__name__
return True
except ReferenceError:
return False
- 使用dir函数
def exists(proxy):
return bool(dir(proxy))
weakref.proxy方式存在的另一问题是难以通过Proxy对象访问到原对象,这在某些场景下会不方便,因为无法对ProxyType调用weakref.proxy,
>>> foo = Foo()
>>> proxy = weakref.proxy(foo)
>>> weakref.proxy(proxy)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: cannot create weak reference to 'weakproxy' object
这个问题又如何解决呢?在StackOverflow上找到了一个解决方法,
def proxy(instance):
if isinstance(instance, weakref.ProxyType):
return weakref.proxy(instance.__repr__.__self__)
else:
return weakref.proxy(instance)
>>> foo = Foo()
>>> p = proxy(foo)
>>> q = proxy(p)
>>> p
<weakproxy at 0x10181e838 to Foo at 0x101823590>
>>> q
<weakproxy at 0x10181e838 to Foo at 0x101823590>
Python内存管理
Python对象何时销毁?
上面介绍了weakref模块提供的两个主要接口的用法,那么在实际代码中如何去使用呢?在回答这个问题之前,先解决另外一个问题,Python对象何时销毁?
引用计数为0时销毁
如前文所说,大部分Python对象的内存管理通过引用计数完成,当引用计数为0时,对象即会销毁,通过如下代码可以进行验证,
import gc
def is_in_gc(klass):
for x in gc.get_objects():
if isinstance(x, klass):
return True
return False
>>> foo = Foo()
>>> is_in_gc(Foo)
True
>>> foo = None
>>> is_in_gc(Foo)
False
从上面的示例中可以看到,在赋值None发生之后,引用计数就发生了变化,随即触发了销毁操作。
触发gc时销毁循环引用
class Foo(object):
def __init__(self):
self.bar = Bar(self)
class Bar(object):
def __init__(self, foo):
self.foo = foo
>>> foo = Foo()
>>> is_in_gc(Foo), is_in_gc(Bar)
(True, True)
>>> foo = None
>>> is_in_gc(Foo), is_in_gc(Bar)
(True, True)
>>> gc.collect()
4
>>> is_in_gc(Foo), is_in_gc(Bar)
(False, False)
当两个对象互相引用时,就只能等待gc处理了。除了手动触发gc之外,gc又是在何时发生的呢?
大部分Python对象的创建都会经过_PyObject_GC_Malloc这个方法,在这个方法中可以看到gc调用,
PyObject *
_PyObject_GC_Malloc(size_t basicsize)
{
PyObject *op;
PyGC_Head *g;
if (basicsize > PY_SSIZE_T_MAX - sizeof(PyGC_Head))
return PyErr_NoMemory();
g = (PyGC_Head *)PyObject_MALLOC(
sizeof(PyGC_Head) + basicsize);
if (g == NULL)
return PyErr_NoMemory();
g->gc.gc_refs = GC_UNTRACKED;
generations[0].count++; /* number of allocated GC objects */
if (generations[0].count > generations[0].threshold &&
enabled &&
generations[0].threshold &&
!collecting &&
!PyErr_Occurred()) {
collecting = 1;
collect_generations();
collecting = 0;
}
op = FROM_GC(g);
return op;
}
Python对象的weakref何时清理?
上面我们已经弄清楚了Python对象何时销毁,那么关联到对象的weakref又是何时清理的?在对象销毁时通过调用PyObject_ClearWeakRefs进行清理,
# funcobject.c
static void
func_dealloc(PyFunctionObject *op)
{
_PyObject_GC_UNTRACK(op);
if (op->func_weakreflist != NULL)
PyObject_ClearWeakRefs((PyObject *) op);
Py_DECREF(op->func_code);
Py_DECREF(op->func_globals);
Py_XDECREF(op->func_module);
Py_DECREF(op->func_name);
Py_XDECREF(op->func_defaults);
Py_XDECREF(op->func_doc);
Py_XDECREF(op->func_dict);
Py_XDECREF(op->func_closure);
PyObject_GC_Del(op);
}
在各种Python类型的xxx_dealloc函数中会清理weakref。
弱引用的使用场景
在简单介绍了Python内存管理中对象清理的机制之后,就可以实际来看弱引用的使用场景了。
- 存在一对一或一对多的循环引用
class Parent(object):
def __init__(self):
self.children = [Child(self)]
class Child(object):
def __init__(self, parent):
self.parent = weakref.proxy(parent)
上述例子中,对于其余代码部分,如果Child相关类无需对外暴露,那么就特别适合weakref。
- 注意手动解引用
weakref的失效依赖于对象实际销毁,对象销毁又是由引用计数、以及gc.collect两者触发的。gc触发的时机未知,而引用计数变为0的时机则可控。因此在代码层实现上述结构时,可以为对象实现销毁接口,手动控制。
- 避免传递weakref
如果将weakref不断传递,则容易遇到ReferenceError。实践所得,避免无谓传递可以避免随机遇到异常。
避免循环引用的其它方式
除了weakref之外,可以通过其它方式避免循环引用,
- 不直接持有引用,通过ID进行索引
总结
weakref是用于避免循环引用的利器,实际使用中还是遇到了一些问题,因此这里加以总结与记录。更多的用法与体会随时补充。