紛失届
LOST-011
届出人
夜間設備点検員のユン技師
なくしたもの
タグが一枚もないのに廃棄されないかばんが二つ。11日目になっても13番コンベヤーを回り続けている。
解決の条件
二つのかばんが廃棄されない理由を突き止め、片付けたあと二度と生まれないようにする。
ハヌルソム国際空港 手荷物センター
初日から報告が上がっていた13番コンベヤーの二つのかばんが、今日も回っています。タグは一枚もありません。規則どおりなら、タグのないかばんはその日のうちに廃棄場へ送られるはずです。
ド・ミラン班長が、これまでの記録を三枚、机に広げます。3限目のX線写真、6限目のメモ、7限目の partner の記録です。「今日はあの二つの正体を明かそう。その前に、廃棄の規則を正確に知っておかないとね」
CPythonのすべてのオブジェクトは、自分を指している参照がいくつあるかを数える数字を持っています。これが参照カウント(reference count)です。タグを付ければ1増え、外せば1減り、0になった瞬間にその場で廃棄されます。
import sys
a = []
print(sys.getrefcount(a))
# 2
b = a
print(sys.getrefcount(a))
# 3
c = [a, a]
print(sys.getrefcount(a))
# 5
del b, c
print(sys.getrefcount(a))
# 2sys.getrefcount(a) を呼ぶときに引数として渡した参照まで数えるからです。c = [a, a] はリストの中にタグを2枚差すので、2増えます。かばんの中に差したタグも、タグです。del b, c で名前 b が外れ、リスト c が廃棄されると、その中のタグ2枚も一緒に消えます。数字そのものに頼らないでください。3.14からは、インタープリターが参照の増減の一部を省く最適化をしています。同じコードを関数の中で動かすと、最初の値はCPython 3.14では1、3.13では2です。見るべきは「タグ1枚につき1」という差です。
0になった瞬間に廃棄されることは、__del__ で確かめられます。オブジェクトが廃棄される直前に呼ばれるダンダーメソッドです(信用してはいけない理由は11-4で)。
class Bag:
def __init__(self, label):
self.label = label
def __del__(self):
print("discard", self.label)
a = Bag("A")
b = a
del a
print("after del a")
del b
print("after del b")
# after del a
# discard A
# after del b
def check_in():
tmp = Bag("T")
print("inside")
check_in()
print("back")
# inside
# discard T
# backdel a のあとも、タグ b が残っているので廃棄されません。最後のタグを外す del b で、すぐに "discard A" が表示されます。tmp も外れます。だから "back" より先に廃棄されます。下のGC観測台は、ワーカープロセスで本物のオブジェクトを作り、本物の gc を動かします。かばんごとに「参照」の数(名札+ほかのかばんの中に差してあるタグ)が見えます。どの名札から出発してかばんの中をたどっても届かないかばんは、赤い点線で描かれます。点線タグは、w() → … でいま何を指しているかを示します。
GC観測台
Python
ピンの案内
→ sys.getrefcount の数字は、バージョンや場所(モジュールか関数の中か)によって変わります。絶対値ではなく、タグを1枚付けたり外したりしたときに1ずつ動く「差」を見てください。
三枚の記録をつなぎ合わせると、正体が見えてきます。3限目のX線写真の [[...]] は、かばんが自分自身を抱え込んだ姿でした。7限目の記録では、二つのかばんが partner 属性で互いを指していました。二つをそのままコードにすると、こうなります。
タグボード
ステップ 1 / 0
次に実行する行: 0
実習台を準備中…
いま変わったもの
封印されたかばん(イミュータブル)
del a, b で、名札はすべて外れました。ところがAの中にはBを指すタグが、Bの中にはAを指すタグが残っています。どちらも参照カウントが1なので、いつまでも0になりません。これが循環参照(reference cycle)です。
名前から出発して届かないオブジェクトは、プログラムがもう二度と使う手段のないゴミです。タグボードから消えたのも、そのためです。しかし参照カウントだけでは、このゴミを片付けられません。
そこでCPythonには、二人目の清掃係がいます。循環ガベージコレクター(gc モジュール)です。
gc.collect() はこの作業をいますぐ行わせ、片付けたオブジェクトの数を返します。import gc
gc.disable() # 例が揺れないよう、自動GCをいったん止める
class Bag:
def __init__(self, label):
self.label = label
self.partner = None
def __del__(self):
print("discard", self.label)
a = Bag("A")
b = Bag("B")
a.partner = b
b.partner = a
del a, b
print("tags gone")
print(gc.collect() > 0)
gc.enable()
# tags gone
# discard A
# discard B
# Truedel a, b のあとでは何も廃棄されません。"discard" は gc.collect() の中でようやく表示されます。2行の順番は決まっていません。gc.collect() を呼ばなくてもかまいません。オブジェクトをたくさん作れば、インタープリターが自動で動かします。gc.disable() は例のためだけに使いました。3限目の、自分自身を抱え込んだリストも同じケースです。
import gc
a = []
a.append(a)
print(a, a[0] is a)
# [[...]] True
del a
print(gc.collect())
# 例:23返ってくる数字は、それまでにたまったほかのゴミも一緒に数えるので、状況やバージョンによって違います。このPCで同じファイルを3.13.9で動かすと1、3.14.2で動かすと23になりました。下の観測台は、始めるときに一度掃除しておいてから数えるので、毎回同じ値になります。
GC観測台
Python
循環も最後には片付けられるのだから、問題ないのでしょうか? 二つの点が違います。
循環をなくす方法は二つあります。使い終わったら片方のタグを自分で外す(break のように)か、最初から片方を弱参照にするかです。
弱参照(weak reference)は、参照カウントに数えられない点線タグです。かばんを指しはしますが、つかんではいません。かばんが廃棄されると、点線タグはひとりでに空になります。
import weakref
class Bag:
def __init__(self, label):
self.label = label
a = Bag("A")
w = weakref.ref(a)
print(w() is a)
# True
del a
print(w())
# Nonew() のように呼んで、はじめてかばんが得られます。すでに廃棄されていれば None が返るので、使う前にいつも確かめます。weakref.ref([1, 2]) は TypeError: cannot create weak reference to 'list' object です。int、str、tuple、dict も同じで、自作クラスのインスタンスなら付けられます。GC観測台
Python
弱参照がいちばんよく使われるのはキャッシュです。普通の dict でキャッシュを作ると、入れておいたオブジェクトをキャッシュがいつまでもつかんでいるので、メモリが減りません。WeakValueDictionary は値を点線タグで持つので、ほかで使われなくなれば項目がひとりでに消えます。
import weakref
class Bag:
def __init__(self, label):
self.label = label
desk = weakref.WeakValueDictionary()
c = Bag("C")
desk["C"] = c
print("C" in desk, len(desk))
# True 1
del c
print("C" in desk, len(desk))
# False 0キーの側を点線で持つ WeakKeyDictionary、要素を点線で持つ WeakSet もあります。オブジェクトに別の情報を付けたいけれど、そのオブジェクトの寿命は延ばしたくない、というときに使います。
かばんが廃棄されるときにすることを登録しておきたいなら、weakref.finalize を使います。
import weakref
class Bag:
def __init__(self, label):
self.label = label
d = Bag("D")
f = weakref.finalize(d, print, "discard D")
print(f.alive)
# True
del d
# discard D
print(f.alive)
# Falsefinalize が呼ばれるのは多くても一度で、プログラムが終わるまでかばんが生きていれば、終了時に呼ばれます。次の節の __del__ より頼りになる仕組みです。ただし、渡す関数や引数がそのかばんをつかんでしまうと(例:d.close を渡す)、かばんがいつまでも廃棄されないので注意しましょう。
__del__ を信用しない:後始末は with で (6分)__del__ は後始末のコードを入れるのによさそうに見えますが、三つの理由で信用できません。
__del__ が呼ばれる保証がありません。class Jam:
def __del__(self):
raise RuntimeError("belt jam")
x = Jam()
del x
print("still running")
# (標準エラー)例:Exception ignored while calling deallocator <function Jam.__del__ at 0x...>:
# (標準エラー)... RuntimeError: belt jam
# still runningファイル、ソケット、ロックのように必ず閉じなければならないリソースは、with で閉じます。ブロックを出た瞬間、たとえ例外が起きても __exit__ が呼ばれます(8限目)。
class Belt:
def __enter__(self):
print("belt on")
return self
def __exit__(self, *exc):
print("belt off")
return False
with Belt() as belt:
print("moving")
# belt on
# moving
# belt off
try:
with Belt():
raise ValueError("stuck")
except ValueError as e:
print("caught", e)
# belt on
# belt off
# caught stuckopen() が返すファイルも、with open(path) as f: で使います。後始末のタイミングが、コードの目に見える場所に決まります。
ピンの案内
→ __del__ は「いつ」を約束してくれません。必ず閉じるべきものは with で、オブジェクトの寿命に結び付けたい後始末は weakref.finalize でやりましょう。
None、True、小さな整数のように、どこでも使われるオブジェクトは不死のオブジェクト(immortal object)です(3.12、PEP 683)。参照カウントが固定されていて、いくらタグを付けたり外したりしても廃棄されません。
import sys
print(sys.getrefcount(None))
# 3221225472
x = None
print(sys.getrefcount(None))
# 3221225472
print(sys._is_immortal(None), sys._is_immortal([]))
# True Falsesys._is_immortal は3.14で加わりました。名前がアンダースコアで始まるので、CPythonの実装の細部です。sys.getsizeof は、かばんそのものの重さだけを量ります。中身は数えません。
import sys
print(sys.getsizeof([]), sys.getsizeof([1, 2, 3]))
# 56 88
big = [0] * 1000
print(sys.getsizeof(big), sys.getsizeof([big]))
# 8056 64CPython 64ビットでは、リストは空の本体56バイトに、タグを差す場所一つにつき8バイトが加わります。作り方によっては場所を多めに確保しておくこともあるので、[1, 2, 3] は80ではなく88です。[big] は場所が一つのかばんなので64バイトです。中に入っている big が8056バイトでも、数えません。
フリースレッド版(3.14t)について一言。GILのないビルドでは、複数のスレッドが同時に参照カウントを変えます。そこで、オブジェクトを作ったスレッドは自分の分をすばやく数え、ほかのスレッドは共有の分を別にアトミックに数えます(biased reference counting)。外から見える規則は同じです。この回の例を3.14.3t(sys._is_gil_enabled() が False)で動かしても、参照カウントの値、即時の廃棄、循環を gc.collect() が片付けることまで同じでした。違ったのは、gc.collect() が返した数字だけです。
二つのかばんは、互いの partner で互いをつかみ合う循環参照でした。タグはないのに参照カウントが1ずつ残って廃棄されず、循環GCが動くまでコンベヤーに残っていたのです。班長が gc.collect() を動かすと、13番コンベヤーは空になりました。
二度と生まれないようにするには、相方を点線タグで覚えさせます。10限目の property で包めば、使う側のコードはそのままです。
import weakref
class Bag:
def __init__(self, label):
self.label = label
self._partner = None
@property
def partner(self):
return self._partner() if self._partner is not None else None
@partner.setter
def partner(self, other):
self._partner = weakref.ref(other) # 相方は点線タグで覚える
a = Bag("A")
b = Bag("B")
a.partner = b
b.partner = a
print(a.partner.label, b.partner.label)
# B A
watch = weakref.ref(a)
del a
print(watch() is None, b.partner)
# True Nonedel a の1行で、Aはすぐに廃棄され、Bの partner は None になります。GCを待つ循環が、最初から存在しません。
退勤前、ユン技師がもう1行記録を持ってきます。幽霊かばんBは先月、遺失物ロッカーに保管されたことがあるのに、そのとき誰も取り出せなかったというのです。班長は「それは明日、最後に見よう」と言って、ロッカーの鍵を机に置きます。
解決報告 — LOST-011。幽霊かばん二つは、partner で互いをつかみ合う循環参照だった。gc.collect() で片付け、相方を weakref.ref で覚えるように変えて再発を防止。
a = []、a.append(a) のあとの sys.getrefcount(a) を予想してみてください。モジュールで実行すると何が出て、なぜ2ではないのでしょう?weakref.WeakValueDictionary() に文字列を値として入れてみてください。どんなエラーになり、自作クラスのインスタンスで包むとどうなるでしょう?__del__ で閉じるクラスを作り、それを __enter__・__exit__ で閉じるように変えてみてください。ブロックの中で例外が起きても閉じられるか確かめましょう。班長の業務手帳
参照カウント=かばんに付いたタグの数。0になったその行で、すぐに廃棄される。
sys.getrefcount は引数として渡した参照も数える。値はバージョンごとに違うので、差だけを見る。
互いを抱え込んだかばん(循環参照)は0にならない。循環GCがあとで片付ける。gc.collect() は片付けた数を返す。
自動GCのタイミングや世代の構成はバージョンごとに違う。そこに頼らない。
弱参照は点線タグ。キャッシュには WeakValueDictionary、廃棄時の処理には weakref.finalize。
__del__ は信用しない。閉じるべきリソースは with で。
None のような不死のオブジェクトは廃棄されない。getsizeof はかばんの本体だけを量る。
新しい授業・講座のお知らせを受け取る
この講座に授業が追加されたり、続きの講座が公開されたりしたらメールでお知らせします。
R
ReflexKR
触って学ぶPythonとReflex。すべての例はReflex 0.10.0で実際に動かして確認しています。
ReflexKRはバナー広告なし・無料で運営しています。役に立ったら、コーヒー一杯で応援してください。 ☕ コーヒーで応援する
アフィリエイトと支援について© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
質問と回答
この回について気になることを書き込んでください。すべての言語のページの質問がここに集まります。