note.nkmk.me

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

Date: 2018-03-25 / Modified: 2019-08-06 / tags: Python, リスト

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

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

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

ここでは以下の内容について説明する。

  • 重複した要素を削除し、新たなリストを生成
    • 元のリストの順序を保持しない: set()
    • 元のリストの順序を保持する: dict.fromkeys(), sorted()
    • 二次元配列(リストのリスト)の場合
  • 重複した要素を抽出し、新たなリストを生成
    • 元のリストの順序を保持しない
    • 元のリストの順序を保持する
    • 二次元配列(リストのリスト)の場合

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

リストやタプルが重複した要素を持っているかどうかを判定したい場合は以下の記事を参照。

また、一つのリストではなく複数のリスト間で共通する要素や共通しない要素を抽出したい場合は以下の記事を参照。

なお、リストは異なる型のデータを格納可能で、厳密には配列とは異なる。メモリサイズやメモリアドレスを必要とするような処理や大規模なデータの数値計算処理などで配列を扱いたい場合はarray(標準ライブラリ)やNumPyを使う。

スポンサーリンク

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

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

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

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

これをlist()で再びリストにすればOK。タプルの場合はtuple()でタプルにすればよい。

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]

もちろんsetのまま使ってもよい。集合型setについては以下の記事を参照。

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

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

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

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

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

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

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

引数keyには呼び出し可能(コーラブル)なオブジェクトとして指定するので()は書かない。

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

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

二次元配列(リストのリスト)の場合、set()dict.fromkey()を使う方法はエラー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となるため、最終的に生成されるリストの要素として追加されない。

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

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

元のリストから重複している要素のみを抽出する場合は、リスト、タプルのメソッドcount()を使う。count()はリスト・タプルに引数に指定した値の要素が何個あるかを返すメソッド。

リスト内包表記で、set()で得られる一意な要素の中から元のリストに2個以上存在する(=重複している)要素を抽出する。

l = [3, 3, 2, 1, 5, 1, 4, 2, 3]
print([x for x in set(l) if l.count(x) > 1])
# [1, 2, 3]

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

重複した要素を削除する場合と同様に、dict.fromkey()を使うか(Python3.6以降)、sorted() でソートする。

print([x for x in dict.fromkeys(l) if l.count(x) > 1])
# [3, 2, 1]

print(sorted([x for x in set(l) if l.count(x) > 1], key=l.index))
# [3, 2, 1]

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

print([x for x in l if l.count(x) > 1])
# [3, 3, 2, 1, 1, 2, 3]

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

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

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]

もっとスマートな方法があるかも知れない。

重複した状態のまま抽出したいのであれば一次元の場合と同じでOK。

print([x for x in l_2d if l_2d.count(x) > 1])
# [[1, 1], [0, 1], [0, 1], [1, 1], [1, 1]]
スポンサーリンク
シェア
このエントリーをはてなブックマークに追加

関連カテゴリー

関連記事