過去の日記

2012-11-19 [長年日記]

Pythonで多次元の辞書を扱う [Python]

nodes[0][9][3]=3
nodes[0][9][4]=5

みたいな感じに使いたい*1
今、添え字は数字にしてあるけど、実際はこの部分は辞書*2のキーの扱いにしたくて、つまりは文字列とかもつっこみたい。
検索すると、辞書のキーにはタプルが使えるので、

nodes[(0, 9, 3)]=2
nodes[(0, 9, 4)]=3

みたいに書けますよ、というのはでてくるけど、

for i in nodes[(0,9)]:
    print i #=>3(改行)4(改行) とでてほしい

みたいな書き方はできない。
これは今回の目的にはそぐわない。


ちょっと横道にそれるけど、実はdefaultdicみたいな書き方もしたかったりする。

print nodes[0][9][8] #=>0 とか。これをKeyErrorにしたくない

という感じ。

from pprint import pprint
from collections import defaultdict

nodes=defaultdict(int)
nodes[0] = 5
nodes[2] = 10
pprint(nodes) #=> defaultdict(<type 'int'>, {0: 5, 2: 10})
pprint(nodes[4]) #=>0
pprint(nodes) #=> defaultdict(<type 'int'>, {0: 5, 2: 10, 4: 0})

となる。
おさらい終わり。


nodes = defaultdict(dict)

と書いてみる。

nodes[0][0]=5
nodes[0][2]=10

ok。2次元配列みたいに代入できる。
nodes[0]に初めてアクセスしたときに空の辞書がセットされるからだ。
でもこれだと、

pprint(nodes[0][4]) #occurs KeyError

うん当然。nodes[0]は普通の辞書なわけだから。

じゃあdefaultdictのデフォルト値がdefaultdictになればいいんじゃない?

nodes = defaultdict(defaultdict(int)) #occurs TypeError: first argument must be callable

defaultdict(int)がcallableじゃないっていわれる。
ふむ。
callableならいいんだね? ならばlambdaだ。

nodes = defaultdict(lambda : defaultdict(int))
nodes[0][0]=5
nodes[0][2]=10
pprint(nodes[0][4]) #=>0

いいかんじ。


次は3次元。

nodes = defaultdict(lambda : defaultdict(lambda : defaultdict(int)))
nodes[0][1][0]=5
nodes[0][1][2]=10
pprint(nodes[0][1][4]) #=>0


ふむ……。なるほど。

nodes = defaultdict(lambda : defaultdict(lambda : defaultdict(int)))
                                         ^^^^^^^^^^^^^^^^^^^^^^^^^

ここの部分が、一段上のdefaultdicに与えてやった初期値に等くなる。
defaultdic.default_factoryで引っ張ってこれそう。

def multi_dimension_dict(dimension):
    nodes = defaultdict(int)
    for i in range(dimension-1):
        lm = nodes.default_factory
        nodes = defaultdict(lambda : defaultdict(lm))
    return nodes
nodes = multi_dimension_dict(2)
nodes[0][0]=5
nodes[0][2]=10
pprint(nodes[0]) #=>defaultdict(<type 'int'>, {0: 5, 2: 10})
pprint(nodes[0][4]) #=> 0
pprint(nodes[0]) #=>defaultdict(<type 'int'>, {0: 5, 2: 10, 4: 0})
nodes = multi_dimension_dict(3)
nodes[0][9][5]=3
pprint(nodes[0][9][5]) #=> 3
pprint(nodes[0][9]) #=>defaultdict(<function <lambda> at 0x108067938>, {5: 3})
pprint(nodes[0]) #=>defaultdict(<function <lambda> at 0x103d7a938>, {9: defaultdict(<function <lambda> at 0x103d7a938>, {5: 3})})

よしよし。

nodes = multi_dimension_dict(3)
nodes['aa']['bb']['cc']=4
nodes['aa']['bb']['dd']=3
pprint(nodes['aa']['bb']) #=>defaultdict(<function <lambda> at 0x109a51b18>, {'cc': 4, 'dd': 3})
sum=0
for key3rd in nodes['aa']['bb']:
    sum += nodes['aa']['bb'][key3rd]
pprint(sum) #=>7

nodes['dd']['aa']['cc']=8
nodes['dd']['bb']['dd']=6
sum=0
for i,n in nodes.iteritems():
    for j,o in n.iteritems():
        for k,p in o.iteritems():
            sum += nodes[i][j][k] #sum += p と等価
pprint(sum) #=>21(4+3+8+6)

まあいいんじゃないですか?

でもこれだとintしか入れられない。

def multi_dimension_dict(dimension, callable_obj=int):
    nodes = defaultdict(callable_obj)
    for i in range(dimension-1):
        p = nodes.copy()
        nodes = defaultdict(lambda : defaultdict(p.default_factory))
    return nodes

にしたらいいかも!!


問題は、

nodes = multi_dimension_dict(3)
nodes[0][1]=3
nodes[0][1][3]=0 #occurs TypeError

とか次元を間違えて代入しちゃうとリカバリが効かないこと。
これは解決方法が見つからないので使う時に注意するしかないかなー。

*1 「多次元の辞書」っていうよりは、「スパース(疎)な多次元配列」といった方が感覚に近い。

*2 Rubyで言うとHash。