教えて!gooに面白い質問が上がっていた。
Win10でpython3.10 勉強中。
map利用例でこんな現象あり、私だけ? それともこれが仕様?
#map(関数、リストやタプルなどのオブジェクト) 要素全てに同じ関数適用できる機能
今回の問題提起は、下記の現象です。
mapを使って、その結果の利用1回目OKなのに、2回目以降使えない。
a = [0,1,2,3]
def multi(x):
y = x*2
return y
aa = map(multi, a)
print(aa) #<map object at 0x000001F335A9A5F0>
print(list(aa)) #map結果利用1回目 [0, 2, 4, 6,]
print(list(aa)) #map結果利用2回目の結果は []
print(len(list(aa))) #0 なんと2回目以降にmap結果を使うことができない!!!
b = list(aa) #代入してみた
print(b) #[] 結果は同じ 2回目以降map結果使えない
print(list(aa)) #[] 結果は同じ 2回目以降map結果使えない
print(aa) #<map object at 0x000001F335A9A5F0>は変わらない
#リスト内包表記では?
print(a) # [0,1,2,3]
b = [i*2 for i in a]
print(b) # [0,2,4,6] リスト内包表記では、何度も使える
#再度map挑戦
aa = map(multi, a)
c = [i for i in list(aa)] #map結果の利用1回目
print(c) #[0,2,4,6] 目的通りの結果
d = [i for i in list(aa)] #map結果利用2回目 下記のように結果は[]
print(d) #[] 使えない
これはなかなか面白い質問だ。そしてこの質問者の「戸惑い」ってのは全くその通りだ。
例えば、Racketだと、
(define *a* '(0 1 2 3))
(define (multi x)
(* x 2))
(define *aa* (map multi *a*))
とした場合、
> (display *aa*)
(0 2 4 6)
> (display *aa*)
(0 2 4 6)
> (display *aa*)
(0 2 4 6)
> (display (length *aa*))
4
>
と、何度もmapを束縛した大域変数 *aa* は使い回しが可能だ(※1)。
これは当然OCamlでも同じで、
let a = [0; 1; 2; 3]
let multi x =
x * 2
let aa = List.map multi a
としたら
utop[0]> List.iter (printf "%d ") aa;;
0 2 4 6 - : unit = ()
utop[1]> List.iter (printf "%d ") aa;;
0 2 4 6 - : unit = ()
utop[2]> List.iter (printf "%d ") aa;;
0 2 4 6 - : unit = ()
utop[3]> printf "%d " (List.length aa);;
4 - : unit = ()
utop[4]>
と、変数に束縛されたmapは何度も使い回しをする事が出来る。
関数型言語観点では、この質問者の直感は正しくて、Pythonのmapは「おかしな挙動」を見せるのだ。
実際は、Pythonのmapはリストのみならずイテレータを引数に取り、無限長の長さを持つイテラブルでも問題なく処理してくれる。
しかしながら、これはホンモノの遅延評価ではない。Racketのstream-mapでも束縛された値は使い回す事が可能だ。
(define *a* (stream 0 1 2 3))
(define (multi x)
(* x 2))
(define *aa* (stream-map multi *a*))
としておいて、
> (display (stream->list *aa*))
(0 2 4 6)
> (display (stream->list *aa*))
(0 2 4 6)
> (display (stream->list *aa*))
(0 2 4 6)
> (display (stream-length *aa*))
4
>
となる。
問題はPythonのmapの場合、「計算結果を消費していく」事にある。つまり、
a = [0, 1, 2, 3]
def multi(x):
return x * 2
aa = map(multi, a)
とした場合、
>>> aa.__next__()
0
>>> aa.__next__()
2
>>> aa.__next__()
4
>>> aa.__next__()
6
>>> aa.__next__()
Traceback (most recent call last):
File "<pyshell#4>", line 1, in <module>
aa.__next__()
StopIteration
>>>
と、実は内部的には破壊的変更が行われてるのだ。
何度も言うが、副作用がある、と言う事は、思わぬ結果を招く、と言う好例の1つだろう。そういう意味では、PythonのmapはLispや他の関数型言語のmapとは明らかに性質が違うんだ。
例えば、OCamlの場合、mapは、そのものではないにせよ、次のような再帰的定義になっている。
let rec our_map proc lst = match lst with
[] -> []
| first::rest -> (proc first) :: (our_map proc rest)
ここには副作用が全くない。
Schemeの場合、mapのリスト引数は可変長引数の為、定義が若干複雑にはなるが(※2)、やっぱり基本的な実装は同じだ。
(define our-map
(letrec ((f (lambda (proc xs acc)
(if (null? xs)
acc
(f proc (cdr xs) `(,@acc ,(proc (car xs)))))))
(g (lambda (proc xss acc)
(if (member '() xss)
(f (lambda (x)
(apply proc x)) acc '())
(g proc (f cdr xss '()) `(,@acc ,(f car xss '())))))))
(case-lambda
((proc arg) (f proc arg '()))
((proc . args) (g proc args '())))))
あくまで再帰的に処理されてるだけで、破壊的変更を含む副作用は行われていない。
仮にPythonのmapも・・・Pythonは再帰が苦手なんで、リスト内包表記を使って書いたとしても、次のように書けば、「計算結果がそのまま残る」事にはなるだろう(※3)。
def our_map(proc, *lst):
return [proc(*item) for item in zip(*lst)]
そして、次のように設定して
a = [0, 1, 2, 3]
def multi(x):
return x * 2
def our_map(proc, *lst):
return [proc(*item) for item in zip(*lst)]
aa = our_map(multi, a)
実行すると、他の関数型言語のように動く。
>>> print(aa)
[0, 2, 4, 6]
>>> print(aa)
[0, 2, 4, 6]
>>> print(aa)
[0, 2, 4, 6]
>>> print(len(aa))
4
>>>
しかし、実際には恐らく次のように書かれている。
def our_map(proc, *lst):
for item in zip(*lst):
yield proc(*item)
これだと次のように設定して、
a = [0, 1, 2, 3]
def multi(x):
return x * 2
def our_map(proc, *lst):
for item in zip(*lst):
yield proc(*item)
aa = our_map(multi, a)
実行すると、質問の状況と同じ様になることが分かるだろう。
>>> print(aa)
<generator object our_map at 0x7fb85fa83120>
>>> print(list(aa))
[0, 2, 4, 6]
>>> print(list(aa))
[]
>>> print(list(aa))
[]
>>> print(len(list(aa)))
0
>>>
繰り返すが、ジェネレータ及びイテレータは、「無限に値を生成する」イテラブルを受け取れるが、同時に破壊的変更で値を生成し、それを消費する。
つまり、これは別にホンモノの遅延評価じゃあないんだ。
その辺をキチンと押さえておこう。
なお、こういう風にジェネレータ/イテレータを束縛して使いまわしたい、と言った場合、無引数ラムダ式で包んで、サンク(thunk)にしておくのがテクニックだと言える。
>>> aa = lambda: map(multi, a)
>>> print(list(aa()))
[0, 2, 4, 6]
>>> print(list(aa()))
[0, 2, 4, 6]
>>> print(list(aa()))
[0, 2, 4, 6]
>>> print(len(list(aa())))
4
>>>
※1: 厳密に言うと、当然、mapを使った「関数実行」が束縛されてるわけではなく、あくまで「関数を実行した結果が」束縛されている。
Pythonの問題は、aaに「イテレータと言う結果が」束縛されてはいるが、これを何らかのカタチで「具現化」(例えば質問のケースではlist化する)したら後が残らない、と言うのがポイントなわけだ。
※2: ここでは、Scheme仕様上の「もっとも基本的な機能だけを使って」実装しているので、若干複雑になっている。
もちろん、Racketの他の機能やSRFI-1の機能を使えばもっと短く書く事は可能だろうが、その「他の機能」自体がmapから作られてるケースが多い為、言わば「鶏が先か、卵が先か」のような、それこそ再帰的な状況になってしまうため、ここでは基本機能のみを使う事とした(要するにmapを自作実装してるのに、組込みのmapからの発展機能を使えば「そりゃないだろ」「反則だろ!」と言う話になる)。
なお、map自体もreduce/foldで実装可能な為、実の事を言えばreduce/foldの方がより基本的な高階関数だ、と言う事になる。
例えば左側から畳み込むとすれば、mapは
(define (our-map proc . args)
(reverse (apply foldl (lambda x
(let* ((pos (- (length x) 1)) (y (take x pos)))
(cons (apply proc y) (last x)))) '() args)))
こう書けるし、右から畳み込むとすれば
(define (our-map proc . args)
(apply foldr (lambda x
(let* ((pos (- (length x) 1)) (y (take x pos)))
(cons (apply proc y) (last x)))) '() args))
と書く事が出来る。
※3: *は、C言語を学んだ人はポインタと勘違いするだろうが、ポインタではない(過去、実際、教えて!gooの質問者で勘違いしてた人がいた)。
しかし、関数定義の際には*がついた引数は可変長引数を現し、つまりある意味リストでパッキングしてるのに、関数の実引数で使う際には「アンパッキング」、つまりリストのカッコを外す作用になってる、と言うのはポインタ並にかなり紛らわしい。
ちなみに、このようなアンパッキングの文法が成立したのはPython3になってから、だ。