リストに要素を足したいとき、appendを使えばいいことはすぐに覚えますよね。ところが少し進むと、extendやinsertも出てきて、どれを使えばいいのか迷いはじめます。
削除のほうも、remove、pop、delと似たような道具が3つも並んでいます。名前だけでは違いがわかりにくく、なんとなく使っている方も多いのではないでしょうか。
今回は、リストに要素を追加する方法と削除する方法を、違いがわかるように並べて整理します。サンプルの実行結果は、手元のPython 3.11で確かめたものです。
あわせて、初心者がよく踏む落とし穴も紹介します。とくに、ループの中で要素を消すと一部が消えずに残る現象は、知らないとなかなか原因に気づけません。
リストの追加と削除の道具を一覧で見てみよう¶
まずは、今回登場する道具をまとめて眺めておきましょう。追加が3つ、削除が4つで、どれも元のリストを直接書き換えるのが共通点です。
それぞれの役割を、表にまとめてみました。
| 書き方 | 種類 | 何をするか | 返り値 |
|---|---|---|---|
| append(x) | 追加 | 末尾にxを1つ足す | None |
| extend(iterable) | 追加 | 末尾に中身をまとめて足す | None |
| insert(i, x) | 追加 | i番目の前にxを差し込む | None |
| remove(x) | 削除 | 値がxの要素を1つ消す | None |
| pop(i) | 削除 | i番目を取り出して消す | 取り出した要素 |
| del a[i] | 削除 | i番目や範囲を消す | なし(文なので値を返さない) |
| clear() | 削除 | 中身をすべて消す | None |
表の右端に注目してください。値を返すのはpop()だけで、ほかのメソッドはNoneを返すことになっています。
これはPythonの公式チュートリアルにも書かれている設計方針です。リストのように中身を書き換えられるデータ構造では、書き換えるだけのメソッドは値を返さないことになっています。
リストそのものの性質や、タプルとの違いをおさらいしたい方は、こちらの記事も参考にしてください。
【関連記事】Pythonのリスト(list)とタプル(tuple)、どっちを使う?
要素を追加する3つのメソッド¶
ここからは、追加のメソッドを1つずつ見ていきます。最初に、3つをまとめて動かしたコードを確認しておきましょう。
fruits = ['りんご', 'みかん']
# 末尾に1つ足す
fruits.append('ぶどう')
print(fruits) # ['りんご', 'みかん', 'ぶどう']
# 末尾に複数をまとめて足す
fruits.extend(['もも', 'なし'])
print(fruits) # ['りんご', 'みかん', 'ぶどう', 'もも', 'なし']
# 先頭(0番目の前)に差し込む
fruits.insert(0, 'いちご')
print(fruits) # ['いちご', 'りんご', 'みかん', 'ぶどう', 'もも', 'なし']
どれも、新しいリストを作るのではなく、fruitsそのものが変わっている点がポイントです。
appendは末尾に1つだけ足す¶
appendは、いちばんよく使う追加の方法です。引数に渡したものを、そのまま1つの要素として末尾に加えます。
ループの中で結果を集めていくときには、ほぼ必ずと言っていいほど登場します。空のリストを用意して、条件に合ったものをappendしていく書き方は、Pythonの定番パターンです。
なお、この書き方の多くはリスト内包表記で1行にまとめられます。慣れてきたら、こちらの記事の書き方も試してみてください。
【関連記事】Pythonのリスト内包表記を使いこなせ!
extendは中身をばらして足す¶
extendは、リストやタプルなど、順番に取り出せるものを受け取って、その中身を1つずつ末尾に加えます。2つのリストを1つにつなげたいときに便利です。
appendとの違いは、リストを渡したときにはっきり表れます。appendに[3, 4]を渡すと、[1, 2, [3, 4]]のようにリストが丸ごと1つの要素として入ります。
一方で、extendに同じ[3, 4]を渡すと、結果は[1, 2, 3, 4]です。入れ子にしたいのか、平らにつなげたいのかで使い分けましょう。
ちなみに、a += [3, 4]と書いても、extendと同じように元のリストへ中身が足されます。どちらを使っても動きは同じなので、読みやすいほうを選んで大丈夫です。
insertは好きな位置に差し込む¶
insertは、1つ目の引数で位置を、2つ目の引数で値を指定します。指定した番号の要素の前に、新しい値が差し込まれる仕組みです。
insert(0, x)なら先頭に、insert(len(a), x)ならappendと同じく末尾に入ります。番号がリストの長さより大きくてもエラーにはならず、末尾に追加されるだけです。
便利な反面、insertは使いどころに少し注意が必要です。理由は、後半の速さの話であらためて説明します。
要素を削除する4つの方法¶
続いて、削除の方法を見ていきましょう。値を目印に消すのか、位置を目印に消すのかが、使い分けの最大のポイントです。
まずは、それぞれの動きをコードで確かめてみます。
numbers = [3, 1, 2, 1]
# 値が1の要素を、最初の1つだけ消す
numbers.remove(1)
print(numbers) # [3, 2, 1]
items = [10, 20, 30]
# 最後の要素を取り出す
last = items.pop()
print(last, items) # 30 [10, 20]
# 0番目の要素を取り出す
first = items.pop(0)
print(first, items) # 10 [20]
data = [0, 1, 2, 3, 4, 5]
# 1番目を消す
del data[1]
print(data) # [0, 2, 3, 4, 5]
# スライスで範囲をまとめて消す
del data[1:3]
print(data) # [0, 4, 5]
removeは値で探し、popとdelは位置で指定する。この違いを押さえておけば、迷うことはぐっと減ります。
removeは最初に見つかった1つだけを消す¶
removeは、指定した値と等しい要素を前から探して、最初に見つかった1つだけを消します。上の例でも、1が2つあったのに、消えたのは前のほうだけでした。
同じ値をすべて消したいときに、removeを1回呼ぶだけでは足りません。この点は、あとのループの落とし穴でも関わってきます。
また、リストに存在しない値を渡すと、ValueErrorになります。手元の環境では、list.remove(x): x not in listというメッセージが表示されました。
消したい値が入っているかわからないときは、if x in a: で先に確かめてから消すと安全です。
popは消した値を受け取れる¶
popは、指定した位置の要素を取り除いて、その値を返してくれます。引数を省略すると、最後の要素が対象になります。
消すだけでなく、消した値をそのあと使いたい場面で活躍します。たとえば、やることリストから1件ずつ取り出して処理していく、といった使い方です。
ただし、空のリストに対してpopを呼ぶとIndexErrorになります。手元ではpop from empty listというメッセージが出たので、空になりうる場合は事前にチェックしておきましょう。
delとclearは位置や範囲でまとめて消す¶
delは、メソッドではなくPythonの文です。del a[i]で1つ、del a[1:3]のようにスライスを使えば範囲をまとめて消せます。
スライスの書き方に自信がない方は、こちらで基本をおさらいしておくと、delの使い方もすっと頭に入ります。
【関連記事】Pythonのスライスとは?
clearは、リストの中身をすべて消して空にするメソッドです。del a[:]と同じ動きになります。
返り値がNoneになる落とし穴¶
ここからは、初心者がつまずきやすいポイントを見ていきます。1つ目は、appendなどの返り値です。
次のようなコードを書いたことはありませんか。
names = ['佐藤', '鈴木']
names = names.append('高橋')
print(names) # None
appendは、リストを書き換えるだけで、新しいリストは返しません。そのため、結果をnamesに代入し直すと、namesの中身がNoneに置き換わってしまいます。
この間違いは、エラーがその場では出ないのが厄介です。少し先でnamesを使おうとしたときに、NoneTypeに関するエラーが出て、ようやく気づくことになります。
私は10年ほどエンジニアとして開発に関わってきましたが、新人のコードレビューでこの1行を見つけたことは一度や二度ではありません。エラーが出た場所と原因の場所が離れているため、本人はずっと別の行を疑っていました。
覚え方はシンプルです。書き換えるメソッドは、代入しないと決めておきましょう。sort()とsorted()の関係も、これとまったく同じ考え方です。
ループの中で削除すると要素が残る落とし穴¶
2つ目の落とし穴は、forループで回しながら同じリストから要素を消すケースです。偶数だけを取り除くつもりで、次のように書いたとします。
nums = [1, 2, 2, 3, 4]
for n in nums:
if n % 2 == 0:
nums.remove(n)
print(nums) # [1, 2, 3] ← 2が1つ残っている
偶数をすべて消したはずなのに、2が1つ残ってしまいました。エラーも出ないので、気づかないまま先へ進んでしまいがちです。
なぜ要素が飛ばされるのか¶
forループは、内部で今何番目を見ているかを数えながら進んでいます。途中で要素を消すと、後ろの要素が1つずつ前に詰められます。
1つ目の2を消した瞬間、2つ目の2がその位置へ詰めてきます。ところがループは次の番号へ進むので、詰めてきた2はチェックされないまま飛ばされてしまうのです。
私自身、駆け出しのころに同じミスをして、テストデータでは正しく動いたのに本番で消し残しが出たことがあります。偶数が連続しないデータでたまたま試していたせいで、問題が表に出なかったのです。
新しいリストを作るのが安全¶
この落とし穴を避けるいちばん簡単な方法は、元のリストを書き換えずに新しいリストを作ることです。残したい要素だけを集めれば、消し漏れは起きません。
方法ごとの安全さを、表で比べてみましょう。
| 書き方 | 結果 | おすすめ度 |
|---|---|---|
| ループ中にremoveする | 要素が飛ばされて消し残しが出る | 避ける |
| 内包表記で新しいリストを作る | 正しく[1, 3]になる | いちばん安全 |
| コピーを回して元を消す | 正しく動くが少し読みにくい | 必要なときだけ |
内包表記なら、nums = [n for n in nums if n % 2 != 0]の1行で済みます。残す条件を書くので、何をしたいのかも読み取りやすくなります。
どうしても元のリストを書き換えたい場合は、for n in nums[:]:のようにコピーを回しながら元を消す方法もあります。コピーと元のリストの関係については、こちらの記事が参考になります。
【関連記事】Pythonの浅いコピーと深いコピーの違い
先頭の追加と削除が遅くなる理由¶
最後に、速さの話をしておきます。リストは、末尾への追加と末尾からの削除はとても速く動きます。
一方で、先頭への追加や先頭からの削除は、データが多いほど遅くなります。先頭を変えると、ほかの要素をすべて1つずつずらす必要があるからです。
公式チュートリアルでも、先頭から取り出すような使い方にはリストは向いていないと説明されています。代わりにすすめられているのが、collectionsモジュールのdequeです。
dequeは、両端への追加と削除が速くなるように作られたデータ構造です。appendleft()で先頭に足し、popleft()で先頭から取り出せます。
試しに手元で、10万件のデータに対して先頭への追加と削除を1万回くり返してみました。環境によって数字は変わりますが、リストでは0.6秒ほど、dequeでは0.003秒ほどと、はっきり差が出ました。
順番待ちの列のように、先に入れたものから取り出す処理では、最初からdequeを選んでおきましょう。dequeのほかの使い方は、こちらの記事でまとめています。
【関連記事】Pythonのcollectionsとは?
まとめ¶
リストの追加と削除には、似た道具がいくつもあります。追加なら、1つ足すappend、まとめて足すextend、位置を決めて差し込むinsertの3つです。
削除は、値で探すremove、位置で取り出すpop、位置や範囲で消すdel、全部消すclearを使い分けます。消した値を使いたいならpop、と覚えておくと迷いません。
そして、appendなどの返り値はNoneなので、結果を代入し直さないこと。ループ中に同じリストから消さず、新しいリストを作ること。この2つを守るだけで、リストまわりのバグはかなり減らせます。
まずは手元のコードを見返して、ループの中でremoveしている箇所がないか探してみてはいかがでしょうか。
ここまでお読みいただきありがとうございました。