home changes contents help options

088:多次元配列をループする

2次元のリストの全要素にアクセスするには二重の for 文を用います。次のコードは 3x3 のリストの全要素にアクセスする例です。

>>> L2 = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> for x in L2:
...   for y in x:
...     print y
...
1
2
3
4
5
6
7
8
9

次元が増えた場合も、同様に for 文を増やしていくことで取り出すことができます。 3x3x2 のリストの例です。

>>> L3 = [[[1, 2], [3, 4]], [[5, 6], [7, 8]], [[9, 10], [11, 12]]]
>>> for x in L3:
...   for y in x:
...     for z in y:
...       print z
...
1
2
3
4
5
6
7
8
9
10
11
12

リスト自身に変更も加える場合は、添え字でアクセスしたほうが便利かもしれません。 range 関数は連番を発生させるのに便利な組み込み関数です。

>>> L2 = [[1, 2, 3], [4, 5, 6], [7, 8 ,9]]
>>> for i in range(3):
...   for j in range(3):
...     L2[i][j] = L2[i][j] * i
...
>>> L2
[[0, 0, 0], [4, 5, 6], [14, 16, 18]]

リストが何重になっているか事前に分かる場合は上記のコードらで問題ありません。しかし、何重なのか不明の場合、 [1, [2, 3]?, [[4, 5, 6]]?] のような要素とリストが混ざっているリストの場合、含まれるデータの型が分からないといった場合は、話がややこしくなってきます。たとえば、全要素にアクセスするジェネレータとして次のようなものを作ってみるとします。

def traverse_unsafe(iterable):
    try:
        # イテレート可能かどうか確認
        i = iter(iterable)
    except TypeError, e:
        # イテレート不可、データなのでそのまま返す
        value = iterable
        yield value
    else:
        # イテレート可、中身を取り出して返す
        for i2 in i:
            for value in traverse_unsafe(i2):
                yield value
>>> for i in traverse_unsafe([1, [2, 3], [[4, 5, 6]]]):
...   print i
...
1
2
3
4
5
6

一見、うまく動いているように見えます。しかし、 Python の基本型、標準モジュールが提供するクラスにはイテレート可能である型が意外に多い、という落とし穴があります。たとえば、文字列はイテレート可能かつ1字の文字列(イテレート可能)を返すので、先ほどの traverse_unsafe ジェネレータにかけると無限ループに陥り、スタックを使い切ってしまいます。

>>> for s in traverse_unsafe(['foo', ['bar', 'baz']]):
...   print s
  File "<stdin>", line 12, in traverse_unsafe
      (中略)
  File "<stdin>", line 12, in traverse_unsafe
RuntimeError: maximum recursion depth exceeded

結論、この手の関数・ジェネレータには汎用性を求めすぎないほうがよいと思われます。入力の幅が広い関数・ジェネレータの作成には困難が伴います。この traverse_unsafe ジェネレータはそのまま使うのではなく具体的なコードを作成する際の参考・出発点程度に止めておくべきです。入力をチェックする関数でラップしたりするのもよいでしょう。

少しだけ改良を加えてみます。文字列を含むリストにも対処するために、型チェックを入れると次のようになります。組み込み型の str, unicode 型が入ってきた場合の問題を解決しただけなので、このジェネレータを異常動作させうる入力はまだまだ存在します。

def traverse_unsafe2(iterable):
    # 文字列かどうか確認
    if isinstance(iterable, basestring):
        # 文字列なのでそのまま返す
        string = iterable
        yield string
    else:
        try:
            # イテレート可能かどうか確認
            i = iter(iterable)
        except TypeError, e:
            # イテレート不可、データなのでそのまま返す
            value = iterable
            yield value
        else:
            # イテレート可、中身を取り出して返す
            for i2 in i:
                for value in traverse_unsafe2(i2):
                    yield value
>>> list(traverse_unsafe2(['foo', ['bar', 'baz']]))
['foo', 'bar', 'baz']