背景

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是用于避免循环引用的利器,实际使用中还是遇到了一些问题,因此这里加以总结与记录。更多的用法与体会随时补充。

参考