Python でのリスト内包表記の二重ループと JavaScript での flatMap
を比較します。順を追ってコードを構築します。
題材
ネストしたデータと、フラットなデータを考えます。
ネスト | 1 | 2 3 | 4 5 6 | |||
---|---|---|---|---|---|---|
フラット | 1 | 2 | 3 | 4 | 5 | 6 |
構成を文字列で表現して、ネストしたリストに変換します。
-
1,2+3,4+5+6
→[[1], [2, 3], [4, 5, 6]]
コードを少し変形してフラット化します。
-
[[1], [2, 3], [4, 5, 6]]
→[1, 2, 3, 4, 5, 6]
Python
REPL での実行状況を示します。>>>
はプロンプトで、コードではありません。
変換元の文字列を a
とします。
>>> a = "1,2+3,4+5+6"
まず ,
で区切ります。
>>> b = a.split(",")
>>> b
['1', '2+3', '4+5+6']
各項目を +
で区切ります。
>>> c = [s.split("+") for s in b]
>>> c
[['1'], ['2', '3'], ['4', '5', '6']]
各項目を数値に変換します。リスト内包表記をネストさせます。
>>> d = [[int(x) for x in xs] for xs in c]
>>> d
[[1], [2, 3], [4, 5, 6]]
ここまでの処理をまとめます。c
や b
を定義に置き換えます。
>>> [[int(x) for x in xs] for xs in [s.split("+") for s in a.split(",")]]
[[1], [2, 3], [4, 5, 6]]
xs
に入るのは s.split("+")
であることに着目すれば、コードが整理できます。
>>> [[int(x) for x in s.split("+")] for s in a.split(",")]
[[1], [2, 3], [4, 5, 6]]
リスト内包表記がネストしていることは、結果のリストがネストしていることに対応します。
ネストを解消して二重ループにすれば、結果はフラットなリストになります。単に内部の [
]
を外すだけでなく、for
の順番が入れ変わっていることに注意してください。(通常の二重ループと同じ順番)
>>> [int(x) for s in a.split(",") for x in s.split("+")]
[1, 2, 3, 4, 5, 6]
コードの変形によって結果が変化することを示すのが狙いです。
JavaScript
Node.js の REPL での実行状況を示します。>
はプロンプトで、コードではありません。
まずステップごとに処理します。map
を使います。
> a = "1,2+3,4+5+6"
'1,2+3,4+5+6'
> b = a.split(",")
[ '1', '2+3', '4+5+6' ]
> c = b.map(s => s.split("+"))
[ [ '1' ], [ '2', '3' ], [ '4', '5', '6' ] ]
> d = c.map(xs => xs.map(x => parseInt(x)))
[ [ 1 ], [ 2, 3 ], [ 4, 5, 6 ] ]
メソッドチェーンで処理をつなぎます。
> a.split(",").map(s => s.split("+")).map(xs => xs.map(x => parseInt(x)))
[ [ 1 ], [ 2, 3 ], [ 4, 5, 6 ] ]
コードを整理します。
> a.split(",").map(s => s.split("+").map(x => parseInt(x)))
[ [ 1 ], [ 2, 3 ], [ 4, 5, 6 ] ]
map
がフラットなメソッドチェーンではなくネストしていることは、結果の配列がネストしていることに対応します。
外側の map
を flatMap
に置き換えれば結果がフラットになります。
> a.split(",").flatMap(s => s.split("+").map(x => parseInt(x)))
[ 1, 2, 3, 4, 5, 6 ]
まとめ
コードのどこを変更しているかに注意します。
>>> a = "1,2+3,4+5+6"
>>> [[int(x) for x in s.split("+")] for s in a.split(",")]
[[1], [2, 3], [4, 5, 6]]
>>> [int(x) for s in a.split(",") for x in s.split("+")]
[1, 2, 3, 4, 5, 6]
> a = "1,2+3,4+5+6"
'1,2+3,4+5+6'
> a.split(",").map(s => s.split("+").map(x => parseInt(x)))
[ [ 1 ], [ 2, 3 ], [ 4, 5, 6 ] ]
> a.split(",").flatMap(s => s.split("+").map(x => parseInt(x)))
[ 1, 2, 3, 4, 5, 6 ]
参考
flatMap
がリスト内包表記の二重ループに対応することは、調べてみると以前から指摘されています。
PythonでflatMapを使いたくなって調べてみたら、PythonにはなんとflatMapがなく、そういう時はネストしたリスト内包表記使うといろいろなところに書いてあった
おまけ
参考までに Python で別の書き方を見てみます。
map
Python でも map
を使ってみます。JavaScript と違ってメソッドではなく関数です。中間過程でリストが生成されないことに注意が必要です。
>>> a = "1,2+3,4+5+6"
>>> b = a.split(",")
>>> c = map(lambda s: s.split("+"), b)
>>> c
<map object at 0x7ff9100af0d0>
>>> d = map(lambda xs: map(int, xs), c)
>>> d
<map object at 0x7ff9102583d0>
>>> list(map(list, d))
[[1], [2, 3], [4, 5, 6]]
処理をまとめます。
>>> d = map(lambda xs: map(int, xs), map(lambda s: s.split("+"), a.split(",")))
>>> list(map(list, d))
[[1], [2, 3], [4, 5, 6]]
コードを整理します。
>>> d = map(lambda s: map(int, s.split("+")), a.split(","))
>>> list(map(list, d))
[[1], [2, 3], [4, 5, 6]]
flatMap
の代わりに結果をフラット化して取り出します。
>>> d = map(lambda s: map(int, s.split("+")), a.split(","))
>>> from itertools import chain
>>> list(chain.from_iterable(d))
[1, 2, 3, 4, 5, 6]
フラット化については以下の記事を参考にしました。
ジェネレーターをネストさせるときは yield from
を使用すれば効率が上がります。
ジェネレーター内包表記
内包表記で [
]
の代わりに (
)
で囲めばジェネレーターになります。
中間でリストを作らずに内包表記が使えます。
>>> a = "1,2+3,4+5+6"
>>> d = ((int(x) for x in s.split("+")) for s in a.split(","))
>>> d
<generator object <genexpr> at 0x7ff91023eb20>
>>> list(map(list, d))
[[1], [2, 3], [4, 5, 6]]
>>> e = (int(x) for s in a.split(",") for x in s.split("+"))
>>> e
<generator object <genexpr> at 0x7ff910418f40>
>>> list(e)
[1, 2, 3, 4, 5, 6]
関数の引数をジェネレーター内包表記で書けます。括弧を二重にする必要はありません。
>>> list(int(x) for s in a.split(",") for x in s.split("+"))
[1, 2, 3, 4, 5, 6]
>>> sum(int(x) for s in a.split(",") for x in s.split("+"))
21
関連記事
flatMap
をジェネレーターに関連付けて考察した記事です。要点は一対多の結果を列挙することです。