0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

組み込みdictのget()・setDefault

Dictを扱うための3つの基本的な演算とは、キーとその値へのアクセス, 代入, 削除です。

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

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

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

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例外を使用するというものがあります。

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引数のキーが存在しない場合にデフォルト値です。

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

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

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例外を使うことも可能です。

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回の代入を行うこともできます。

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

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

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:

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

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があります。

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

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

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を使用して書き換えてみます。

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が必要です。

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__を実装して欠損キーを扱うロジックを実装できます。

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__は呼び出されません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?