Python[List/Dict] - getメソッド, setDefault, defaultDict, __missing__

# 組み込みdictのget()・setDefault
Dictを扱うための3つの基本的な演算とは、キーとその値へのアクセス, 代入, 削除です。

例えば、以下のようなフルーツを`counters`とするdictを定義します。

~~~python
counters = {
    'appple': 3,
    'banana': 2
}
~~~

新たに追加するものを`counters`に増やすには、キーがあるかどうか調べ、なければデフォルトのカウンタ値0で挿入し、カウンタの値を1つ増やします。if分でキーが存在する場合にTrueを返すin文を使います。

~~~python
key = 'lemon'
if key in counters:
    count = counters[key]
else:
    count = 0
counters[key] = count + 1
print(counters)
>>>
{'appple': 3, 'banana': 2, 'lemon': 1}
~~~

同じことを実現する別の方法として、存在しないキーの値を得たい場合にKeyError例外を使用するというものがあります。

~~~python
try:
    count = counters[key]
except KeyError:
    count = 0
counters[key] = count + 1
print(counters)
>>>
{'appple': 3, 'banana': 2, 'lemon': 1}
~~~

存在するキーを取得するかデフォルト値を返すというこの処理の流れはとても一般的なので、組み込み型dictにはこの作業を行う`getメソッド`があります。getの第2引数は、第1引数のキーが存在しない場合にデフォルト値です。

~~~python
count = counters.get(key, 0)
counters[key] = count + 1
print(counters)
>>>
{'appple': 3, 'banana': 2, 'lemon': 1}
~~~

このように、単純な方の辞書では、getメソッドを使うのが最短で最も明確なコードになります。
次に、Dictの値がlistのようなもっと複雑な方の場合はどうなるでしょうか。

~~~python
buyer = {
    'appple': ['ken', 'bob'],
    'banana': ['shane', 'mickel']
}

key = 'lemon'
who = 'ema'

if key in buyer:
    names = buyer[key]
else:
    buyer[key] = names = []
names.append(who)
print(buyer)
>>>
{'appple': ['ken', 'bob'], 'banana': ['shane', 'mickel'], 'lemon': ['ema']}
~~~
今回の場合にはキーが存在しない場合には、空リストを代入しなければいけないところが異なります。3重代入文(buyer[key] = names = [])によって、2行ではなく1行でキーを追加します。Dictにデフォルト値が挿入されたら、listがその後の`append`呼び出しで変更されるため、再度代入する必要はありません。

今回も同様にKeyError例外を使うことも可能です。

~~~python
try:
    names = buyer[key]
except KeyError:
    buyer[key] = names = []
names.append(who)
print(buyer)
>>>
{'appple': ['ken', 'bob'], 'banana': ['shane', 'mickel'], 'lemon': ['ema']}
~~~

また、同様にして`getメソッド`で。キーがあればlist値を取得、なければ1回のアクセスと1回の代入を行うこともできます。

~~~python
names = buyer.get(key)
if names is None:
    buyer[key] = names = []
names.append(who)
~~~

getを使ってlist値を取得するこの方式は、if分で代入式を使用すれば、1行短縮することができ、読みやすさが向上します。

~~~python
if(names := buyer.get(key)) is None:
    buyer[key] = names = []
names.append(who)
print(buyer)
>>>
{'appple': ['ken', 'bob'], 'banana': ['shane', 'mickel'], 'lemon': ['ema']}
~~~

dict型には、このパターンの処理をさらに簡潔に書ける`setdefaultメソッド`もあります。

#### - setdefault:  
辞書のキーの値を取得しようとします。キーがないと、このメソッドはそのキーに対して指定されたデフォルト値を割り当て、そのメソッドはそのキーに対する値を返します。

~~~python
names = buyer.setdefault(key, [])
names.append(who)
print(buyer)
>>>
{'appple': ['ken', 'bob'], 'banana': ['shane', 'mickel'], 'lemon': ['ema']}
~~~

欠損した辞書キーを扱うのに`setdefault`を使うことが最短となるのは、デフォルト値の作成が安価で変更可能かつ例外の起こる可能性がないわスかな状況に限られます。そのような非常に特殊な場合には、getを使うためにより多くの文字と行を費やすよりもメソッド名の`setdefault`を使用する価値がありそうです。しかし、そのような状況では`defaultdict`を使用するようにすることです。

# defaultDict
組み込みcollectionsモジュールの`defaultdict`クラスには、キーが存在しない場合のデフォルト値を自動格納してこのユースケースを単純化する機能があります。
例えば、国の情報を格納したdictがあります。

~~~python
countries = {
    'Japan': {'Tokyo', 'Fukuoka'},
    'America': {'New York'}
}
~~~

`setdefault`を用いたクラスでのdictの格納の場合は以下になります。

~~~python
class Countries:
    
    def __init__(self):
        self.data = {}
        
    def add(self, country, city):
        city_set = self.data.setdefault(country, set())
        city_set.add(city)

countries = Countries()
countries.add('Russia', 'Yekaterinburg')
countries.add('Itary', 'Roma')
print(countries.data)
>>>
{'Russia': {'Yekaterinburg'}, 'Itary': {'Roma'}}
~~~

次に`defaultdict`を使用して書き換えてみます。

~~~python
from collections import defaultdict

class Countries:
    
    def __init__(self):
        self.data = defaultdict(set)
        
    def add(self, country, city):
        self.data[country].add(city)

countries = Countries()
countries.add('France', 'Pari')
countries.add('Japan', 'Osaka')
print(countries.data)
>>>
defaultdict(<class 'set'>, {'France': {'Pari'}, 'Japan': {'Osaka'}})
~~~
addの実装が短く単純になりました。


# `__missing__`
ファイルシステムにある写真を管理する処理を作成する時、画像の読み書きができるようにイメージのパス名にマップしてオープンするためのdictが必要です。

~~~python
pictures = {}
path = 'test.png'

if (handle := pictures.get(path)) is None:
    try:
        handle = open(path, 'a+b')
    except OSError:
        print(f'Failed to open path {path}')
        raise
    else :
        pictures[path] = handle
handle.seek(0)
image_data = handle.read()
~~~

dict型のサブクラスで特殊メソッド`__missing__`を実装して欠損キーを扱うロジックを実装できます。

~~~python
path = 'test.png'

def open_picture(profile_path):
    try:
        return open(profile_path, 'a+b')
    except OSError:
        print(f'Failed to open path {profile_path}')
        raise
    
class Picture(dict):
    def __missing__(self, key):
        value = open_picture(key)
        self[key] = value
        return value
    
pictures = Picture()
handle = pictures[path]
handle.seek(0)
image_data = handle.read()
~~~
pictures[path]dictアクセスで、キーのpathがdictにないことがわかると、`__missing__`メソッドが呼ばれる。これは、キーのデフォルト値を作成し、dictに挿入し、それを呼び出し元に返す。同じpathでアクセスした時は、すでに存在しているので、`__missing__`は呼び出されません。