Pythonの辞書で擬似多次元配列

Pythonでは辞書のキーに tuple を使える。

>>> d = {(1,2,1):100, (2,2,2):200, (2,0,1):300}
>>> d[(1,2,1)]
100

キーとしている tuple を三次元配列のインデクス(三次元空間の座標)とみなせば、d は三次元配列のように見える。
[ ] の中では tuple の ( ) は無くてもいいので省略すると、ちょっと辞書には見えない。なんか C# の多次元配列みたい。

>>> d[0,0,0] = 999
>>> d[1,1,1] = 777
>>> d[0,0,0]
999
>>> d[1,1,1]
777

あくまでも擬似的な物なので、

>>> d[1000000, 1000000, 1000000] = 1

とかやってもメモリが足りなくなることは無い。
辞書だもんね。

>>> len(d)
6
>>> for k, v in d.iteritems(): print k,v
(1, 2, 1) 100
(1000000, 1000000, 1000000) 1
(2, 2, 2) 200
(0, 0, 0) 999
(2, 0, 1) 300
(1, 1, 1) 777

例えば、広大なN次元空間内でそれほど多くない座標を扱うような場合に、速度はともかくメモリ消費を抑えた(擬似)多次元配列として役立ちそう。

ところで1つ不満がある。

>>> d[0,1,2]
Traceback (most recent call last):
  File "<pyshell#63>", line 1, in <module>
    d[0,1,2]
KeyError: (0, 1, 2)

っぽく見えてても、ランダムにアクセスするとボロがでる。
まぁ、こうすればエラーは出ないけど。

>>> d.get((0,1,2), 0)
0 

これじゃあ、せっかくの偽装がバレバレ。
で、クラス化してみる。

class MultiArray(dict):
    def __init__(self):
        super(MultiArray, self).__init__()
    def __getitem__(self, key):
        return self.get(key, 0)

必要最低限だけど。これだけも。

>>> ma = MultiArray()
>>> ma[0,0,0] = 1
>>> ma[1,2,3] = 11
>>> ma[1,0,1] = 100
>>> ma[1,2,3]
11
>>> ma[1,1,1]
0
>>> ma[100,100,100]
0

それっぽい。
デフォルト値指定と次元数のチェックを追加してみた。

class MultiArray(dict):
    def __init__(self, **kw):
        super(MultiArray, self).__init__()
        self.dim = kw.get('dim', 2)
        self.default = kw.get('default', 0)
    def __getitem__(self, key):
        self._check_key(key)
        return self.get(key, self.default)
    def __setitem__(self, key, value):
        self._check_key(key)
        super(MultiArray, self).__setitem__(key, value)
    def _check_key(self, key):
        if len(key) != self.dim:
            raise IndexError()

三次元配列で、デフォルト値 -1 の場合はこんなん。

>>> ma = MultiArray(dim=3, default=-1)
>>> ma[1,2,2] = 10
>>> ma[1,2,2]
10
>>> ma[0,0,0]
-1
>>> ma[0,0,0] = 100
>>> ma[1,0,1] = 200
>>> ma[1,1,1] = 999
>>> g = ((x,y,z) for x in range(2) for y in range(2) for z in range(2))
>>> for key in g: print key, ma[key]

(0, 0, 0) 100
(0, 0, 1) -1
(0, 1, 0) -1
(0, 1, 1) -1
(1, 0, 0) -1
(1, 0, 1) 200
(1, 1, 0) -1
(1, 1, 1) 999

次元数を間違えるとエラー

>>> ma[1,2] = 1
Traceback (most recent call last):
(中略)
IndexError

とりあえず期待通り動いた。
さらに改造するならインデクスの有効範囲とか……その前にインデクスの型チェックが必要?
チェックとか全部無くして、フリーダムな座標表現のままでも面白いけど。