Pythonでリスト(配列)から重複した要素を削除・抽出

Modified: | Tags: Python, リスト

Pythonで、リスト(配列)から、

  • 重複した要素を削除(一意な要素・ユニークな要素のみを抽出)
  • 重複した要素を抽出

して、新たなリストを生成する方法について説明する。

なお、リストではなくタプルの場合も同様の考え方で実現可能。

リストやタプルが重複した要素を持っているかどうかを判定したい場合、一つのリストではなく複数のリスト間で共通する要素や共通しない要素を抽出したい場合は以下の記事を参照。

なお、リストは異なる型のデータを格納可能で、厳密には配列とは異なる。配列を扱いたい場合はarray(標準ライブラリ)やNumPyを使う。

numpy.ndarrayに対する一意な要素の抽出については以下の記事を参照。

重複した要素を削除し、新たなリストを生成

元のリストの順序を保持しない: set()

元のリストの順序を保持する必要がない場合は、集合型setを生成するset()を使う。

set型は重複した要素をもたないデータ型で、set()にリストなどを渡すと、重複する値は無視されて一意な値のみが要素となるset型のオブジェクトを返す。

これをlist()で再びリストにする。タプルにしたい場合はtuple()を使えばよい。もちろんsetのままでもよい。

l = [3, 3, 2, 1, 5, 1, 4, 2, 3]

print(set(l))
# {1, 2, 3, 4, 5}

print(list(set(l)))
# [1, 2, 3, 4, 5]

元のリストの順序を保持する: dict.fromkeys(), sorted()

元のリストの順番を保持したい場合は、辞書型dictのクラスメソッドfromkeys()、または組み込み関数sorted()を使う。

dict.fromkeys()は引数に指定したリストやタプルなどをキーとした新たな辞書オブジェクトを生成する。第二引数を省略した場合、値はNone

辞書のキーは重複した要素をもたないので、set()と同じく重複する値は無視される。さらに辞書オブジェクトをlist()の引数に渡すと辞書のキーを要素とするリストが得られるので、これを利用する。

l = [3, 3, 2, 1, 5, 1, 4, 2, 3]

print(dict.fromkeys(l))
# {3: None, 2: None, 1: None, 5: None, 4: None}

print(list(dict.fromkeys(l)))
# [3, 2, 1, 5, 4]

dict.fromkeys()で引数のシーケンスの順序が保持されることが保証されているのはPython3.7(CPythonは3.6)から。それより前のバージョンでは以下のように組み込み関数sorted()を使う。

要素をソートしたリストを返すsortedの引数keyにリスト・タプルのメソッドindex()を指定する。

index()は値のインデックス(リスト中の何番目の要素か)を返すメソッドで、sorted()keyに指定することで、元のリストの順番を基準に並べ替えられる。

print(sorted(set(l), key=l.index))
# [3, 2, 1, 5, 4]

二次元配列(リストのリスト)の場合

二次元配列(リストのリスト)の場合、set()dict.fromkeys()を使う方法はエラーTypeErrorになる。

l_2d = [[1, 1], [0, 1], [0, 1], [0, 0], [1, 0], [1, 1], [1, 1]]

# l_2d_unique = list(set(l_2d))
# TypeError: unhashable type: 'list'

# l_2d_unique_order = dict.fromkeys(l_2d)
# TypeError: unhashable type: 'list'

これは、リストなどのハッシュ不可能なオブジェクトはset型の要素やdict型のキーにできないから。

以下のような関数を定義する。元のリストの順番は保持され、一次元のリストやタプルに対しても動作する。

def get_unique_list(seq):
    seen = []
    return [x for x in seq if x not in seen and not seen.append(x)]

print(get_unique_list(l_2d))
# [[1, 1], [0, 1], [0, 0], [1, 0]]

print(get_unique_list(l))
# [3, 2, 1, 5, 4]

リスト内包表記を使っている。

ここでは、以下の性質を利用している。

  • and演算子のショートサーキット(短絡評価)でX and YXFalseであればYは評価されない(実行されない)こと
  • append()メソッドがNoneを返すこと

元のリストseqの要素がseenに存在しない場合(x not in seenTrue)、and以降のnot seen.append(x)が評価される。seen.append(x)が実行され、seenにその要素が追加される。append()メソッドがNoneを返し、NoneFalseであるため、not seen.append(x)Trueと評価される。リスト内包表記の条件式がTrueとなり、最終的に生成されるリストの要素として追加される。

元のリストseqの要素がseenに存在する場合は、x not in seenFalseとなり、リスト内包表記の条件式がFalseとなるため、最終的に生成されるリストの要素として追加されない。

そのほか、結果はソートされるが、NumPyの関数np.unique()で引数axisを設定する方法もある。

重複した要素を抽出し、新たなリストを生成

元のリストの順序を保持しない

元のリストから重複している要素のみを抽出する場合は、collections.Counter()を使う。要素をキー、その個数を値とするcollections.Counter(辞書のサブクラス)を返す。

import collections

l = [3, 3, 2, 1, 5, 1, 4, 2, 3]

print(collections.Counter(l))
# Counter({3: 3, 2: 2, 1: 2, 5: 1, 4: 1})

辞書のサブクラスなのでitems()でキーと値を取り出せる。個数が2個以上のキーを抽出すればよい。

print([k for k, v in collections.Counter(l).items() if v > 1])
# [3, 2, 1]

元のリストの順序を保持する

上の例のように、Python 3.7以降はcollections.Counterのキーは元のリストなどの順序を保持する。

それより前のバージョンでは、重複した要素を削除する場合と同様にsorted()でソートすればよい。

l = [3, 3, 2, 1, 5, 1, 4, 2, 3]

print(sorted([k for k, v in collections.Counter(l).items() if v > 1], key=l.index))
# [3, 2, 1]

重複した状態のまま抽出したい場合は、単純に元のリストから個数が2個以上の要素を残せばよい。順序も保持される。

cc = collections.Counter(l)
print([x for x in l if cc[x] > 1])
# [3, 3, 2, 1, 1, 2, 3]

二次元配列(リストのリスト)の場合

二次元配列(リストのリスト)に対しては、元のリストの順番を保持しない場合と保持する場合でそれぞれ以下のような関数が考えられる。一次元のリストやタプルに対しても動作する。

l_2d = [[1, 1], [0, 1], [0, 1], [0, 0], [1, 0], [1, 1], [1, 1]]

def get_duplicate_list(seq):
    seen = []
    return [x for x in seq if not seen.append(x) and seen.count(x) == 2]

def get_duplicate_list_order(seq):
    seen = []
    return [x for x in seq if seq.count(x) > 1 and not seen.append(x) and seen.count(x) == 1]

print(get_duplicate_list(l_2d))
# [[0, 1], [1, 1]]

print(get_duplicate_list_order(l_2d))
# [[1, 1], [0, 1]]

print(get_duplicate_list(l))
# [3, 1, 2]

print(get_duplicate_list_order(l))
# [3, 2, 1]

重複した状態のまま抽出したいのであれば、元のリストから個数が2個以上の要素を残す。

print([x for x in l_2d if l_2d.count(x) > 1])
# [[1, 1], [0, 1], [0, 1], [1, 1], [1, 1]]

なお、count()の計算量はO(n)であるため、上で示したcount()を繰り返し実行する関数は非常に効率が悪い。もっとスマートな方法があるかも知れない。

collections.Counterは辞書のサブクラスなので、collections.Counter()にリストなどのハッシュ不可能なオブジェクトを要素とするリストやタプルを渡すとエラーになり使えない。

# print(collections.Counter(l_2d))
# TypeError: unhashable type: 'list'

関連カテゴリー

関連記事