Python 会自动管理内存,因此大多数对象不需要显式 cleanup。 但如果一个对象拥有外部资源(文件、socket、锁), 你必须释放它。

为什么 __del__() 不是 Cleanup

Python 在销毁对象时会调用它的 __del__() method。 这看起来似乎正是释放资源的位置:

# cleanup.py
from typing import ClassVar

class Counter:
    # Number of objects of this class
    count: ClassVar[int] = 0

    def __init__(self, name: str) -> None:
        self.name = name
        print(name, "created")
        Counter.count += 1

    def __del__(self) -> None:
        print(self.name, "deleted")
        Counter.count -= 1
        if Counter.count == 0:
            print("Last Counter object deleted")
        else:
            print(Counter.count,
                  "Counter objects remaining")

    def __repr__(self) -> str:
        return f"Counter({self.name!r}{self.count})"

counters = []
for name in ["First", "Second", "Third"]:
    counters.append(Counter(name))
#: First created
#: Second created
#: Third created

for c in counters:
    print(c)
    del c
#: Counter('First' 3)
#: Counter('Second' 3)
#: Counter('Third' 3)
print("End of delete loop")
#: End of delete loop

循环内部的 del c 并不会删除对象。 它只是解除名称 c 的绑定,并释放这个名称持有的那一个 reference。 counters list 仍然引用着每一个 Counter, 所以循环期间没有任何 Counter 的 reference count 降到零。 这就是为什么循环运行时不会出现任何 deleted 行, 为什么每一个 __repr__() 都报告 count 为 3, 以及为什么 End of delete loop 会在任何 deletion 之前打印。

Python 会在之后、interpreter shutdown 时销毁这些对象, 因为那时它会拆除 global counters list。 这个 list 持有仅剩的 reference,所以它消失时,其中对象也随之消失。 代码清单在 End of delete loop——程序最后一条 statement——处结束, 而每个 __del__() 都只在之后才打印。 如果直接运行 cleanup.py, 上面最后一行之后还会出现三组内容:

Third deleted
2 Counter objects remaining
Second deleted
1 Counter objects remaining
First deleted
Last Counter object deleted

这只是某一台机器上的一次运行结果。 如果在本书的 output checker 下运行同一个文件——它会在单个 process 中执行每章—— 三个对象的 finalization 顺序恰好相反。

三个 __del__() 的执行顺序属于 implementation detail。 它取决于 interpreter 在 shutdown 时如何拆除 counters list, 不同 CPython build 之间都可能不同。 其他实现,例如采用 tracing garbage collector 的 PyPy, 可能以完全不同的顺序销毁对象, 甚至在退出之前根本不执行这些 finalizer。

因此 __del__() 很脆弱: 语言既不保证它什么时候运行,也不保证它一定运行。 在 interpreter shutdown 期间, 一个 __del__() 所引用的 global 可能已经消失。 Python documentation 警告:

警告:由于 __del__() method 被调用时所处的环境并不可靠, 其执行期间发生的异常会被忽略,并改为向 sys.stderr 打印警告。尤其需要注意:

在直接运行的情况下,是 shutdown 过程销毁对象, 这正是上面 warning 所描述的不可靠时刻。 当时 Counter 和 print() 恰好仍然可用,所以输出看起来很正常, 但没有任何东西保证允许这种结果的 teardown order。 __del__() 应尽量少做事情,而且代码不应依赖它。

真正会在生产环境里付出高昂代价的,是异常被吞掉:

# del_swallows.py

class Resource:
    def __init__(self, name: str) -> None:
        self.name = name

    def __del__(self) -> None:
        raise RuntimeError(f"{self.name} not released")

resource = Resource("db")
del resource
print("still running")
#: still running

资源释放失败,但 stdout 什么也没说。 一个带 Exception ignored 标签的 traceback 会输出到 sys.stderr, 可是什么都不会向上传播:没有 caller 能捕获它,没有 finally 会因此运行, exit status 仍然是 0,而一个只断言 stdout 的测试仍然会通过。 相比之下,在 with block 中调用 close() 如果失败,会明确地把错误暴露出来。

Reference Cycle 会延迟销毁

不可预测的时机并不是 __del__() 唯一的问题。 如果对象直接或间接引用自身, 它不会在变得 unreachable 的那一刻立即消失:

# cycle.py
import gc

class Node:
    peer: Node

    def __init__(self, name: str) -> None:
        self.name = name

    def __del__(self) -> None:
        print(self.name, "finalized")

def self_link() -> None:
    node = Node("a")
    node.peer = node

gc.disable()
self_link()
print("unreachable, but still alive")
#: unreachable, but still alive
gc.collect()
#: a finalized
gc.enable()
print("after collect")
#: after collect