重複順列生成 - evalで再帰いらず -

擬似多次元配列 の動作確認をしていて気になった。

>>> g = ((x,y,z) for x in range(2) for y in range(2) for z in range(2))
>>> for pos in g:
        print pos, ma[pos]

(0, 0, 0) 100
(0, 0, 1) -1
(0, 1, 0) -1
(0, 1, 1) -1
(1, 0, 0) -1
以下略

ma は三次元配列。インデクス値 0,1 の範囲で全要素を列挙する。
上のようにジェネレータ式で for を連ねれば難しいこと考えずに達成できる。
だけど問題は配列の次元数は可変だってこと。
もし十次元配列だったら、for を 10個も書くの?
それはあんまりだ。めんどくさ過ぎる。

普通は再帰で考える

どうみても再帰問題です。そのはずなんだけど、どうにも頭が回らない。上手くいかねー。
おかしいなぁ、昔Cで似たようなことやった記憶はあるんだけど。
どうでもよくなりかけた頃、ふとジェネレータ式を見て思いついた。

「そうかジェネレータ式で必要なだけ for を連ねればいいんじゃん!」
え? ……それは面倒だったんじゃないかって? いやいやつまり……

ジェネレータ式を文字列で作って eval すりゃいい

作るべきジェネレータ式はこんなかんじ。

((i0,i1,i2, ... id-1) for i0 in range(n) for i1 in range(n) for i2 in range(n) ... for id-1 in range(n))

次元数 d によって2箇所に可変部分があるけど、文字列操作と考えたら大したことは無い。
n は、インデクスの上限(開区間上限)。
数学的に言うと、重複順列の問題だったみたいなので、それっぽい名前で。

def repeated_permutation_n(dimension, n):
    a = ','.join(map(lambda i:'i%d' % i, xrange(dimension)))
    b = ' '.join(map(lambda i:'for i%d in xrange(%d)' % (i,n), xrange(dimension)))
    return eval('((%s) %s)' % (a,b))

数値以外も列挙したい

順列は別に数値に限った話ではないので、もう少し一般化してみる。

def repeated_permutation(dimension, iterable):
    if isinstance(iterable, str):
        iterable = iterable.replace('\\','\\\\').replace("'",r"\'")
        iterable = "'%s'" % iterable
    elif isinstance(iterable, unicode):
        iterable = iterable.replace(u'\\',u'\\\\').replace(u"'",ur"\'")
        iterable = u"u'%s'" % iterable
    a = ','.join(map(lambda i:'i%d' % i, xrange(dimension)))
    b = ' '.join(map(lambda i:'for i%d in %s' % (i,iterable), xrange(dimension)))
    return eval('((%s) %s)' % (a,b))

列挙される値を数値上限 n から iterable に変更。
iterable が str や unicode のときに若干細工が必要だった以外は特に変わったところ無し。

実行例

>>> for x in repeated_permutation_n(2,3):
	print x
	
(0, 0)
(0, 1)
(0, 2)
(1, 0)
(1, 1)
(1, 2)
(2, 0)
(2, 1)
(2, 2)

>>> for x in repeated_permutation(3, 'AB'):
	print x
	
('A', 'A', 'A')
('A', 'A', 'B')
('A', 'B', 'A')
('A', 'B', 'B')
('B', 'A', 'A')
('B', 'A', 'B')
('B', 'B', 'A')
('B', 'B', 'B')

重複順列の話 その2 へ続く。