Pythonの集合同士の差分を取るときに陥った落とし穴を記録しておく

WEBから取ってきたデータと既にDBに入っているデータの差分を取りたいと思うことがある。

辞書

最初は、辞書でデータを扱っていたとしよう。

companies_in_web = [{"cid": 0, "name": "Toyota"}, {"cid": 1, "name": "Nissan"}, {"cid": 2, "name": "Honda"}]
companies_in_db =  [{"cid": 0, "name": "Toyota"}, {"cid": 1, "name": "Nissan"}, {"cid": 4, "name": "Mazda"}]

 
そして、下のような感じで辞書が入ったリストを集合に変換して差分を取りたい。
取れるんじゃないかと思っていた。  

diff = set(companies_in_web) - set(companies_in_db)

 
しかし、残念ながら、これはTypeErrorを吐く。
Pythonのset型の要素はハッシュ可能である必要があり、辞書はハッシュ可能ではないからだ。

https://docs.python.org/ja/3/library/stdtypes.html#set  

# TypeError: unhashable type: 'dict'

ユーザー定義クラス

それでは、辞書ではなく、こういうデータ構造ならどうだろうか。

class Company:

    def __init__(self, cid, name):
        self.cid = cid
        self.name = name

    def __repr__(self):
        return f"Company: cid={self.cid}, name={self.name}"

companies_in_web = [Company(0, "Toyota"), Company(1, "Nissan"), Company(2, "Honda")]
companies_in_db =  [Company(0, "Toyota"), Company(1, "Nissan"), Company(3, "Mazda")]

それでは、差分を取ってみよう。

diff = set(companies_in_web) - set(companies_in_db)
# {Company: cid=0, name=Toyota, Company: cid=1, name=Nissan, Company: cid=2, name=Honda}

 
残念ながら、これは期待した結果ではない。
Company(2, "Honda")だけが差分抽出されてほしいのに、ToyotaNissanも抽出されている。  

ユーザー定義クラス・改

それでは、先ほどのクラスに__eq____hash__関数を実装してみよう。
(追記:ミュータブルなオブジェクトに__hash__を実装するのは適切なのか...)

class Company:

    def __init__(self, cid, name):
        self.cid = cid
        self.name = name

    def __repr__(self):
        return f"Company: cid={self.cid}, name={self.name}"

    def __eq__(self, other):
        if isinstance(other, Company):
            return self.cid == other.cid and self.name == other.name

    def __hash__(self):
        return hash((self.cid, self.name))

companies_in_web = [Company(0, "Toyota"), Company(1, "Nissan"), Company(2, "Honda")]
companies_in_db =  [Company(0, "Toyota"), Company(1, "Nissan"), Company(3, "Mazda")]

差分をとってみる。

diff = set(companies_in_web) - set(companies_in_db)
# {Company: cid=2, name=Honda}

ようやく期待通りのものが抽出された。

集合同士の差分を取る際には、当然要素同士の比較が行われているはずだが、
どうやら__eq____hash__が実装されているか否かによってその比較の挙動が違うようである。

今日はこのような確認だけしておき、次回の記事でオブジェクトの同一性・同値性の詳細について書いていきたい。