[解決!Python]リスト(配列)から重複する要素を削除するには解決!Python

リストから重複する要素を取り除くには幾つかの方法がある。set関数を使った手軽なものから自前で重複要素を取り除くコードまで、それらの方法を紹介する。

» 2024年01月30日 05時00分 公開
[かわさきしんじDeep Insider編集部]
「解決!Python」のインデックス

連載目次

# リストの要素の順序が重要でない、かつ要素がハッシュ可能な場合
a = [7, 3, 3, 2, 5, 8, 2, 5, 6, 5]
s = set(a)  # 重複要素を削除した集合を作成
result = list(s)  # その集合からリストを作成
print(result)  # [2, 3, 5, 6, 7, 8]など

# リストの要素の順序が重要な場合

# Python 3.7以降では辞書は要素の挿入順序を維持する
# dict.fromkeysメソッドは引数に指定した反復可能オブジェクトをキーとして
# デフォルトではNoneをその値として辞書を作成する
a = [7, 3, 3, 2, 5, 8, 2, 5, 6, 5]
tmp = dict.fromkeys(a)  # リストの要素がキー、その値は全てNoneの辞書を作成
print(tmp)  # {7: None, 3: None, 2: None, 5: None, 8: None, 6: None}
result = list(tmp)  # list関数に辞書を渡すとキーを要素とするリストが作成される
print(result)  # [7, 3, 2, 5, 8, 6]

# 同様なことをcollections.Counterクラスを用いて行う
from collections import Counter

a = [7, 3, 3, 2, 5, 8, 2, 5, 6, 5]
c = Counter(a)
print(c.items())  # dict_items([(7, 1), (3, 2), (2, 2), (5, 3), (8, 1), (6, 1)])
result = list(c)
print(result)  # [7, 3, 2, 5, 8, 6]

# sorted関数とリストのindexメソッドを組み合わせる
a = [7, 3, 3, 2, 5, 8, 2, 5, 6, 5]
s = set(a)
result = sorted(s, key=a.index)
print(result)

# 上で紹介した方法はいずれもハッシュ可能なオブジェクトのみを対象とする
a = [[0, 1], [2, 3], [2, 3], [4, 5], [0, 1]]
s = set(a)  # TypeError:リストはハッシュ可能ではない
tmp = dict.fromkeys(a)  # TypeError:リストはハッシュ可能ではない

# ハッシュ可能でないオブジェクトが要素の場合は地道に手作業で調べる
a = [[0, 1], [2, 3], [2, 3], [4, 5], [0, 1]]

def remove_repetition(iterable):
    result = []
    for item in iterable:
        if item not in result:
            result.append(item)
    return result

result = remove_repetition(a)
print(result)  # [[0, 1], [2, 3], [4, 5]]


リストから重複する要素を取り除く幾つかの方法

 リストにはさまざまな要素を順序付き(インデックスによる要素の指定が可能な形)で格納できる。また、リストでは格納する要素の重複が許されている。だが、リストに重複する要素が含まれていないようにしたいときもある。

 そうしたときに、重複する要素を取り除くのには幾つかの方法がある。ここでは以下のような方法を紹介する。

  • set関数を使う
  • 辞書(のfromkeysクラスメソッド)を使う
  • collections.Counterクラスを使う
  • 自分でコードを書く

 一番簡単な方法が「一度、集合に変換した後に、それをリストに再変換する」ことだ。集合は要素の重複が許されず、また、重複する要素を含んだリストを与えて集合を作成すると、重複する要素が削除される。

 以下に例を示す。

a = [7, 3, 3, 2, 5, 8, 2, 5, 6, 5]
s = set(a)  # 重複要素を削除した集合を作成
result = list(s)  # その集合からリストを作成
print(result)  # [2, 3, 5, 6, 7, 8]など


 リストaは重複する要素(3と2は2個、5は3個)を含んでいる。これをset関数に渡すことで、重複している分は削除される。その結果をlist関数に渡せば、重複が削除されたリストが得られるというわけだ。

 ただし、集合は順序がないので、集合からリストを作成する際には、元のリストとは要素の順序が異なるものになることがある。上のコード例では「[2, 3, 5, 6, 7, 8]など」とコメントにあるが、読者がコードを実行したときにそれと同じ順序のリストが得られるかどうかは分からない。

 リストの要素の順序が重要な場合には、Python 3.7以降で保証されている「挿入順序が保存される」特性と辞書にもともと備わっている「同じキーを持つ要素の値は上書きされる」特性を使って、リストから辞書を作成し、それを基にリストを再作成するとよい。

 以下のコードについて考えてみよう。

a = [7, 3, 3, 2, 5, 8, 2, 5, 6, 5]
source = [(item, None) for item in a]  # [(7, None), (3, None), (3, None), ...]
print(source)
# 出力結果:
#[(7, None), (3, None), (3, None), (2, None), (5, None),
# (8, None), (2, None), (5, None), (6, None), (5, None)]


 リストaは先ほどと同じもので、重複する要素を含んでいる。次のリストsourceには、リスト内包表記を使って「(リストの要素, None)」というタプルを要素とするリストを代入している。次に、このようにして作成したリストsourceから「リストの要素をキー、値をNone」とする辞書を作成する(値に意味はないので、Noneにしている)。

d = dict(source)
print(d)  # {7: None, 3: None, 2: None, 5: None, 8: None, 6: None}


 先ほども述べたように、辞書は要素の追加順序を覚えている。そのため、リストsourceの要素が順番に辞書に追加されていく。キーが重複したとき(元のリストの要素において重複するものが現れたとき)には値が上書きされるが、それらは全てNoneとなっているので、結局のところ、辞書にはリストの要素の並び順で元のリストの要素がキー、値がNoneとなる要素が重複なしに追加されていく。そして、できた辞書をlist関数に渡せば、順序が維持されたリストが手に入る(list関数に辞書を渡すと、そのキーを要素とするリストが作成される。要素が必要なときには辞書のvaluesメソッドやitemsメソッドを使えるが、ここでは紹介しない)。

result = list(d)
print(result)  # [7, 3, 2, 5, 8, 6]


 こうした処理を全て自前で行おうとすると、上のようなコードを書くことになるが、辞書が持つfromkeysクラスメソッドを使うと、同じことを簡単に行える。

# dict.fromkeysメソッドは引数に指定した反復可能オブジェクトをキーとして
# デフォルトではNoneをその値として辞書を作成する
a = [7, 3, 3, 2, 5, 8, 2, 5, 6, 5]
tmp = dict.fromkeys(a)  # リストの要素がキー、その値は全てNoneの辞書を作成
print(tmp)  # {7: None, 3: None, 2: None, 5: None, 8: None, 6: None}
result = list(tmp)  # list関数に辞書を渡すとキーを要素とするリストが作成される
print(result)  # [7, 3, 2, 5, 8, 6]


 fromkeysクラスメソッドに、キーを含んだ反復可能オブジェクトを渡すと、キーに対応する値を(デフォルトでは)Noneとする辞書が作成される。後はこれをlist関数に渡すだけでよい。

 同様なことはcollections.Counterクラスを用いても行える。以下に例を示す。

from collections import Counter

a = [7, 3, 3, 2, 5, 8, 2, 5, 6, 5]
c = Counter(a)
print(c.items())  # dict_items([(7, 1), (3, 2), (2, 2), (5, 3), (8, 1), (6, 1)])
result = list(c)
print(result)  # [7, 3, 2, 5, 8, 6]


 Counterクラスのインスタンスを作成すると、そのときに渡した反復可能オブジェクトに含まれる要素がキー、その登場回数が値となるような辞書が得られる(上の「print(c.items())」行の出力結果を参照)。この辞書をlist関数に渡せば、上の例と同様に値は無視されて、辞書のキーを要素とするリストが得られる。

 あるいはsorted関数と、リストのindexメソッドを組み合わせて使うことも考えられる。sorted関数は受け取った反復可能オブジェクトをソートして得られたリストを返送する。このときに要素の大小関係を決定する関数をkeyパラメーターに指定できる。それに元のリストのindexメソッドを指定することで、元リスト内での要素の並び順でソートするようになる(indexメソッドはリスト内での要素の位置を表す値を返す。要素が重複しているときには先頭に近い要素のインデックスを返す)。

 以下に例を示す。

a = [7, 3, 3, 2, 5, 8, 2, 5, 6, 5]
s = set(a)
result = sorted(s, key=a.index)
print(result)


 sorted関数でソートする際に、a.indexメソッドがどんな値を返しているのかを確認してみよう。

for item in s:
    print(f'value: {item}, key: {a.index(item)}')
# 出力結果:
#value: 2, key: 3
#value: 3, key: 1
#value: 5, key: 4
#value: 6, key: 8
#value: 7, key: 0
#value: 8, key: 5


 sorted関数では上の出力結果の「key:」の値を使ってソートする。そのため、元のリストaにおける要素の並び順を維持したリストが得られるというわけだ。

 ここまで見てきた方法は集合か辞書を利用したものだった。よって、これらの方法は元のリストの要素がハッシュ可能でなければ使えない。例えば、リストを要素とするリストでは、リストがハッシュ可能でないことから、例外が発生する。

a = [[0, 1], [2, 3], [2, 3], [4, 5], [0, 1]]
s = set(a)  # TypeError:リストはハッシュ可能ではない
tmp = dict.fromkeys(a)  # TypeError:リストはハッシュ可能ではない


 そのような場合には、自前でリストから重複する要素を取り除くコードを書くことになるだろう。以下に例を示す。

def remove_repetition(iterable):
    result = []
    for item in iterable:
        if item not in result:
            result.append(item)
    return result


 ここではリストの要素を1つずつ調べて、まだ見たことがない要素であれば、結果のリストにそれを追加して、既に登場している要素については何もしない、という関数を定義している。辞書も集合も使っていないので、この関数であれば、リストの要素が何であるかによらずに重複を排除できる。

 以下に実行例を示す。

a = [[0, 1], [2, 3], [2, 3], [4, 5], [0, 1]]

result = remove_repetition(a)
print(result)  # [[0, 1], [2, 3], [4, 5]]


 リスト内包表記を使って、remove_repetition関数の内部を書き換えることも可能だろうが、ここでは取り上げない(読者が自分で考えてみよう)。

「解決!Python」のインデックス

解決!Python

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。