リストの要素を消したいとき、remove()を使うべきか、del を使うべきか迷ったことはありませんか?
さらに調べていくと、del は変数そのものを消せるとも書いてあります。変数を消すとメモリが空くのか、データはどこへ行くのか、気になってしまいますよね。
この記事では、Pythonのdel文を基本の使い方から順番に見ていきます。後半では、del してもデータがすぐには消えない理由を、参照カウントとガベージコレクションという仕組みからほどいていきます。サンプルの実行結果は、手元のPython 3.11で確認したものです。
del文とはどんな命令なのか¶
まずは、del がどんな命令なのかをはっきりさせておきましょう。del は英語の delete(削除する)を縮めた言葉です。
del は関数ではなく、if や for と同じ文(ステートメント)の一種です。そのため del(x) のようにカッコで呼ぶ必要はなく、del x と書くのが基本の形になります。
Pythonの公式リファレンスでは、名前を del すると、その名前とオブジェクトの結びつきが取り除かれると説明されています。ここで大事なのは、del が消すのはデータそのものではなく、名前との結びつきだという点です。
この違いは、あとで紹介するメモリの話でとても重要になります。今は、del は名札をはがす命令なんだな、とだけ覚えておいてください。
Pythonの変数が名札のようなものだという話は、こちらの記事で詳しく解説しています。 【関連記事】Pythonの変数は箱じゃない?メモリ上のラベルという正体を理解する
del文で消せるもの一覧¶
del が使える場所は、思っているより幅広いです。代表的な使い方を表にまとめたので、全体像をつかんでおきましょう。
| 書き方 | 消えるもの | 存在しないときのエラー |
|---|---|---|
| del x | 変数 x という名前 | NameError |
| del nums[0] | リストの0番目の要素 | IndexError |
| del nums[1:3] | リストのスライスの範囲 | なし(範囲外でも空振りする) |
| del d["key"] | 辞書のキーと値の組 | KeyError |
| del obj.attr | オブジェクトの属性 | AttributeError |
| del a, b | 複数の名前を左から順に | 先に失敗した時点で止まる |
公式リファレンスにも、複数の対象を並べた場合は左から右へ順番に消していくと書かれています。また、属性や要素の削除は、対象となるオブジェクト側に処理が任されるという説明もあります。
つまり、del nums[0] と書いたとき、実際に要素を詰める作業はリスト自身が行っているわけです。だからこそ、型によって使えるかどうかが変わってきます。
リストと辞書から要素を消してみよう¶
ここからは、実際にコードを動かして挙動を確かめていきます。まずはリストと辞書で、よく使う形を試してみましょう。
nums = [10, 20, 30, 40, 50]
del nums[0]
print(nums) # [20, 30, 40, 50]
del nums[1:3]
print(nums) # [20, 50]
d = {"a": 1, "b": 2}
del d["a"]
print(d) # {'b': 2}
try:
del d["z"]
except KeyError as e:
print("KeyError", e) # KeyError 'z'
del nums[1:3] のようにスライスを指定すると、範囲の要素をまとめて消せます。1番目と2番目が消えて、残りが前に詰められているのがわかりますね。
辞書の場合は、存在しないキーを del するとKeyErrorになります。ある場合だけ消したいときは、if "z" in d で確かめるか、次の章で紹介する pop() を使うと安全です。
タプルや文字列にはdelが使えない¶
ここで、ひとつ試してほしいことがあります。タプルや文字列の要素に del を使うと、どうなるでしょうか。
答えはTypeErrorです。タプルや文字列は中身を変えられないイミュータブルな型なので、要素を取り除くこと自体ができません。
イミュータブルの性質については、こちらの記事で型ごとにまとめています。 【関連記事】Pythonのミュータブルとイミュータブルとは?変更できる型・できない型の違い
remove()・pop()・clear()との違い¶
リストの要素を消す方法は、del だけではありません。似た役割のメソッドがいくつかあるので、違いを比べておきましょう。
| 方法 | 指定するもの | 消した値を返すか | 向いている場面 |
|---|---|---|---|
| del nums[i] | 位置(インデックス) | 返さない | 位置がわかっていて値は不要 |
| nums.pop(i) | 位置(省略で末尾) | 返す | 取り出した値を使いたい |
| nums.remove(x) | 値 | 返さない | 位置はわからないが値はわかる |
| nums.clear() | なし | 返さない | 中身をすべて空にしたい |
使い分けのポイントは、位置で消すのか、値で消すのかです。そして、消した値をあとで使いたいなら pop() を選びます。
辞書にも pop() があり、d.pop("z", None) のように2つ目の引数を渡すと、キーがなくてもエラーになりません。存在するかどうかわからないキーを消すときは、del よりこちらのほうが短く書けます。
del nums と nums.clear() は別物¶
似ているようで、まったく意味が違う組み合わせもあります。それが del nums と nums.clear() です。
del nums は、nums という名札をはがすだけです。一方の nums.clear() は、リストそのものの中身を空にします。
ほかの変数が同じリストを指していた場合、この違いがはっきり出ます。clear() ならそちらから見ても空になりますが、del nums ならリストは無傷のまま残るのです。
リストのメソッドをまとめて復習したい方は、こちらの記事もどうぞ。 【関連記事】Pythonのリストメソッドとは?append()やsort()など主要メソッドの使い方
delしてもデータがすぐ消えない理由¶
いよいよ、この記事の本題です。del したのにデータが残っているように見える現象を、仕組みから見ていきましょう。
CPython(いちばん普及している標準のPython)は、参照カウントという方法でメモリを管理しています。オブジェクトごとに、いま何枚の名札が貼られているかを数えておく仕組みです。
名札が0枚になったとき、そのオブジェクトはもう誰にも使われていないと判断され、片付けられます。逆に言えば、名札が1枚でも残っている限り、データは消えません。
__del__が呼ばれるタイミングを確かめる¶
本当にそうなのか、コードで確かめてみましょう。del は、オブジェクトが片付けられる直前に呼ばれる特殊メソッドです。
class Item:
def __init__(self, name):
self.name = name
def __del__(self):
print(f"{self.name} を片付けました")
a = Item("A")
b = a # 同じオブジェクトに名札がもう1枚
del a
print("del a のあと")
del b
print("del b のあと")
実行すると、del a のあと、A を片付けました、del b のあと、の順に表示されます。del a の時点ではまだ b という名札が残っているので、オブジェクトは片付けられません。
公式リファレンスにも、del x は x.del() を直接呼ぶわけではなく、参照カウントを1つ減らすだけだという注意書きがあります。del が動くのは、カウントが0になったときです。
sys.getrefcount()で数を見てみる¶
名札の枚数は、sys.getrefcount() で確かめることもできます。ただし、この関数は引数として渡すときに一時的な参照が1つ増えるため、ふつうは思ったより1つ多い数が返ってきます。
手元の3.11で c = [] のあとに調べると2、d = c を足すと3になりました。とはいえ、数値はPythonのバージョンや実行環境で変わることがあるので、増えた減ったを観察する道具くらいに考えておくのがおすすめです。
循環参照とガベージコレクション¶
参照カウントには、ひとつ弱点があります。オブジェクト同士がお互いを指し合っていると、名札が0枚にならないのです。
これを循環参照と呼びます。たとえば、p が q を指し、q が p を指している状態で del p, q をしても、お互いの名札が残ったままになります。
そこで登場するのが、gcモジュールが担うガベージコレクタです。公式ドキュメントでは、このコレクタは参照カウントを補うもので、循環参照を見つけて回収する役割だと説明されています。
先ほどの Item クラスで、循環参照を作って試してみました。次のコードでは、del の直後には何も表示されず、gc.collect() を呼んだところで2つとも片付けられます。
import gc
p = Item("P")
q = Item("Q")
p.other = q
q.other = p
del p, q
print("循環 del のあと") # まだ片付けの表示は出ない
gc.collect() # P を片付けました / Q を片付けました
ふだんは gc.collect() を自分で呼ばなくても、ガベージコレクタが自動で動いて回収してくれます。ただ、いつ動くかは決まっていないので、del に大事な後片付けを任せるのは避けたほうが安全です。
ファイルやネットワーク接続のように確実に閉じたいものは、with文を使うのがPythonらしい書き方です。後片付けのタイミングを、自分ではっきり決められるからです。
実務でdelに頼りすぎた失敗談¶
ここで、私自身の失敗をひとつ紹介させてください。私は10年ほどエンジニアとして開発に関わってきましたが、del の意味を取り違えて半日を溶かしたことがあります。
大きなCSVを読み込む集計処理で、メモリ不足が起きたときのことです。読み込み直後の巨大なリストを del したのに、使用メモリがまったく減らず、首をかしげました。
原因は、集計の途中で作った辞書の中に、同じリストへの参照が残っていたことでした。del で名札を1枚はがしても、別の場所に名札が残っていれば、データは生き続けるのです。
それ以来、メモリを空けたいときは、まずどこから参照されているのかを洗い出すようにしています。そもそも巨大なデータを一度に持たない設計にするほうが、del で後始末するより確実だと学びました。
ループの中でdelするときの落とし穴¶
初心者がとてもつまずきやすいのが、for文でリストを回しながら要素を del する書き方です。消した瞬間に後ろの要素が前に詰められるので、次に見るはずだった要素が飛ばされてしまいます。
対策としては、インデックスを後ろからたどって消すか、残したい要素だけで新しいリストを作る方法があります。私の経験では、消すより残すものを選ぶ発想のほうが、バグを生みにくいと感じています。
この落とし穴は、リストの追加と削除をまとめた記事で、サンプルコードつきで詳しく解説しています。remove() や pop() との使い分けとあわせて確認してみてください。 【関連記事】Pythonのリストの追加と削除とは?append・extend・insert・remove・pop・delの違い
辞書をループで回しながら del するのも要注意です。ループ中に辞書の大きさが変わると、RuntimeErrorになります。
辞書の場合は list(d) でキーの一覧をコピーしてから回すか、内包表記で新しい辞書を作りましょう。辞書そのものの扱い方は、こちらの記事でまとめています。 【関連記事】Pythonの辞書(dict)とは?キーと値でデータを管理する基本
exceptの変数はPythonが自動でdelしている¶
最後に、del がひそかに使われている意外な場所を紹介します。それが、例外を受け取る except 節の変数です。
except ValueError as e と書いたとき、変数 e は except のブロックを抜けると自動で消されます。公式リファレンスでは、ブロックの最後に del e が実行されたかのように扱われると説明されています。
次のコードで、ブロックの外から e を使おうとすると、NameErrorになることを確かめてみましょう。
try:
int("abc")
except ValueError as e:
print("変換できません:", e)
print(e) # NameError: name 'e' is not defined
なぜわざわざ消すのでしょうか。公式リファレンスによると、例外オブジェクトはエラーが起きた場所の情報を抱えており、e が残るとそこから循環参照が生まれてしまうからです。
つまり、Python自身も、不要になった名札をはがしてメモリの後片付けを助けているわけです。あとで例外の内容を使いたいときは、別の変数に代入しておくと失敗しません。
まとめ¶
今回は、Pythonのdel文について解説しました。最後に要点を振り返っておきましょう。
del は、変数やリストの要素、辞書のキー、属性を消すための文です。リストから消すときは、位置で消すなら del、値で消すなら remove()、値を使いたいなら pop() と使い分けます。
そして、del が消すのはデータではなく名前との結びつきです。名札がすべてなくなったときに初めて、参照カウントやガベージコレクションの仕組みでデータが片付けられます。
del したのにメモリが減らない、del が呼ばれない。そんなときは、ほかに名札が残っていないかを探すところから始めてみてください。
ここまでお読みいただきありがとうございました。