Python でリストを比較する




完全に同じなら True

[1, 2, 3, 4] == [1, 2, 3, 4]
# True

たとえ中身が同じでも 順番が違えば False

[1, 2, 3, 4] == [4, 3, 2, 1]
# False
Python のリストの比較演算子

2. リストの中身を比較したい。

リストの中身を比較したい場合は、 list クラスから set クラスに変換してしまう。

# 一致(等しいかどうか)
set([1, 2, 3, 4]) == set([4, 3, 2, 1])
# True


# 差分(リストの要素の引き算)
list(set([1, 2, 3, 4]) - set([2, 3, 4]))
# [1]


# 重複(共通の要素を抜き出す)
list(set([1, 2, 3, 4]) & set([3, 4, 5, 6]))
# [3, 4]



WARNING

重複している要素があるリストには使えません。

単純に set にいれて比較してしまうのが簡単だけど、 複数の要素があった場合に正しく判定できない。

>>> # 一致(等しいかどうか)
>>> # 比較 False になって欲しい。
>>> set([1, 1, 2, 3]) == set([1, 2, 3])
True
>>>
>>> # 差分(リストの要素の引き算)
>>> # 引き算 1, 2 が欲しい。
>>> list(set([1, 2, 2, 4]) - set([2, 3, 4]))
[1]
>>>
>>> # 重複(共通の要素を抜き出す)
>>> # 共通する要素を取り出す 1, 1 が欲しい
>>> list(set([1, 1, 3]) & set([1, 1, 2]))
[1]
>>>

Python に限らず、数学では一般に集合は重複を許さないらしい。 ちなみに重複を許す集合は、多重集合 multiset と言ったりするらしい。

>>> # set を使うと重複は除外されてしまう。
>>> set([1, 1, 2, 3])
{1, 2, 3}
>>>
>>> set([1, 2, 2, 4])
{1, 2, 4}
>>>
>>> set([1, 1, 3])
{1, 3}
>>>

3. 重複しているリストの中身を比較したい。

重複を許すようなリストを比較する場合は、 次のように関数を定義して、中身を一つ一つ比較する他なさそう。

#
# 新しく定義した関数の使い方
#


# 一致(等しいかどうか)
equal_list([1, 1, 2, 3], [1, 2, 3])
# False

# 差分(リストの要素の引き算)
subtract_list([1, 2, 2, 4], [2, 3, 4])
# [1, 2]
    
    
# 重複(共通の要素を抜き出す)
intersect_list([1, 3, 1], [1, 1, 2])
# [1, 1]
#
# 新しく定義した関数
#

def equal_list(lst1, lst2):
    lst = lst1.copy()
    for element in lst2:
        try:
            lst.remove(element)
        except ValueError:
            break
    else:
        if not lst:
            return True
    return False


def subtract_list(lst1, lst2):
    lst = lst1.copy()
    for element in lst2:
        try:
            lst.remove(element)
        except ValueError:
            pass
    return lst


def intersect_list(lst1, lst2):
    arr = []
    lst = lst1.copy()
    for element in lst2:
        try:
            lst.remove(element)
        except ValueError:
            pass
        else:
            arr.append(element)
    return arr
Python の list.remove メソッド
Python の else 節



色々な書き方はあるのですが、そういったことの比較検討はこちらで行いました。

4. 解説

◯ 設計方針

list.remove メソッドは、要素がなかったときに ValuErorr という 例外を投げてくるので、これを利用します。

lst = [0, 1, 2, 3]
lst.remove(4)
>>> lst = [0, 1, 2, 3]
>>> lst.remove(4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: list.remove(x): x not in list
>>> 

◯ なんで if 文ではなく try 文なの?

投げられた例外, ValueError は try 文で捕まえることができます。 try 文なんてあまり使わないので、見せられると、ちょっと圧迫感があります。

if 文の方がコードも見やすく、短くなります。 なぜ、これではなく

# if 文してから remove
def subtract_list(lst1, lst2):
    lst = lst1.copy()
    for element in lst2:
        if element in lst:
            lst.remove(element)
    return lst

これなのでしょうか?

# try 文してから remove
def subtract_list(lst1, lst2):
    lst = lst1.copy()
    for element in lst2:
        try:
            lst.remove(element)
        except ValueError:
            pass
    return lst

結論だけ言えば if 文を使った書き方だと、2回探してしまうからです。 element in lst2 と書いた時に、 リスト lst2 の中に element が無いか探して、 さらにそれから lst2.remove(element) で、 もう1度 element を探しています。

Python は可読性を重視して設計された言語です。 このような書き方が、果たして許されるのでしょうか? 結論から言えば、この場合は、try 文を使った書き方が望ましいと感じています。

◯ LBYL と EAFP

上の方の if 文を使った書き方は LBYL と呼ばれるアプローチです。

LBYL
「ころばぬ先の杖 (look before you leap)」 の略です。このコーディングスタイルでは、呼び出しや検索を行う前に、 明示的に前提条件 (pre-condition) 判定を行います。 EAFP アプローチと対照的で、 if 文がたくさん使われるのが特徴的です。

下の方は EAFP と呼ばれるアプローチです。

EAFP
あるオブジェクトが正しいインタフェースを持っているかを決定するのにオブジェクトの型を見ないプログラミングスタイルです。 代わりに、単純にオブジェクトのメソッドや属性が呼ばれたり使われたりします (とりあえず単純に呼び出して失敗したら try 文で捕まえる)

◯ Python では EAFP で書くことが多い気がします。

なぜなら C 言語で実行して、失敗したら Python で処理させた方が速いからです (なに言ってるんだ?って感じですよね...)。

いままで「例外」をいつ投げるべきか「try 文」でいつ包むべきかわからず、 完全に 1+1 がわからないのと同じ状態陥っていました。 僕は個人的にこの話が好きです

実は大人もね、1+1がなんで2になるかわからないんだよ。 だけどみんなわかったふりして1+1=2だって、なんとなく覚えてるだけなんだ。
「1+1がわからない…」強迫神経症になった少女を救った、たった一つの言葉

最近、やっとすこしつかめてきました。 try 文とはなにか、いつ使うべきかについて、 いま頑張って書いています。まだ書きかけです。