Kesinの知見置き場

知見を共有していきたいじゃないですか

Pool.imap()について

前回multiprocessingのPoolクラスの使い方を書いたのですが、執筆当時はイテレータについてよく分かってなくて、imap()が投げっぱなしだったのでそれについての補足です。

1. イテレータについて

imap()の補足をする前にイテレータについて説明します。Wikipediaによると

イテレータ (Iterator) とは、プログラミング言語において配列やそれに類似するデータ構造の各要素に対する繰返し処理の抽象化である。実際のプログラミング言語では、オブジェクトまたは文法などとして現れる。反復するためのものの意味で反復子(はんぷくし)と訳される。繰返子(くりかえし)という妙訳もある。

ということで、そもそもPython固有の単語ではなくて概念的なことのようです。
とりあえずイテレータというものに触れてみましょう。Pythonではiter()の中にリスト、タプル、辞書などを入れることでイテレータを作成することができます

>>> iterator = iter([0,1,2,3])
>>> print iterator
<listiterator object at 0x1017027d0>

何かオブジェクトが作成されたようですが、printしても中身を表示することができません。実はイテレータはnext()を呼ぶことで順番に値を取り出す仕組みになっていて、for文ではnext()が自動的に呼び出されています。

>>> print iterator.next()
0
>>> print iterator.next()
1
>>> print iterator.next()
2
>>> print iterator.next()
3
>>> print iterator.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

オブジェクトが作られたときではなく、処理を遅らせることができるので,この動作を遅延評価と呼びます。イテレータの優れているところは実際にnext()で値を取り出すまではメモリを消費しないという点です。この仕組があるために、[1, 2, 3, 1, 2, 3…]と無限に循環するようなイテレータを作成してfor文に使用しても問題ありません(当然どこかでbreakしないとプログラムが終了しませんが)。先にメモリを確保する必要のあるリストでは無理な芸当です。他にも遅延評価を上手く使うことで必要のない計算を行わないという処理が簡単にできたりするようです.

2. Pool.imap()について

さて,イテレータについて分かったので本題のPool.imap()を調べます.リファレンスによると1行目に
itertools.imap()と同じです.
とあるので、itertools.imap()を調べると,
iterables の要素を引数として funtion を呼び出すイテレータを作成します。
つまりitertools.imap()とmap()は使い方は同じだけど返ってくるのが
map()→リスト
itertools.imap()→イテレータ
という違いのようです.
そんなわけで前回ではPool.imap()で作成したイテレータをnext()でしか使っていませんでしたがfor文で使っても問題ないようですね.ちなみにmap, imapの引数はiterablesの要素とあるので,リストではなくてイテレータでもOKです.

>>> import os, time, itertools
>>> def print_time():
...     return time.strftime("%M:%S", time.localtime(time.time()))
>>> def print_argument(arg):
...     time.sleep(1)
...     print "pid: %d, argument: %d, time: %s" % (os.getpid(), arg, print_time())
...     return arg
>>> list = range(2,12) 
>>> list
[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
#mapはリストを返す
>>> map(print_argument, list)
pid: 20290, argument: 2, time: 27:55
pid: 20290, argument: 3, time: 27:56
pid: 20290, argument: 4, time: 27:57
pid: 20290, argument: 5, time: 27:58
pid: 20290, argument: 6, time: 27:59
pid: 20290, argument: 7, time: 28:00
pid: 20290, argument: 8, time: 28:01
pid: 20290, argument: 9, time: 28:02
pid: 20290, argument: 10, time: 28:03
pid: 20290, argument: 11, time: 28:04
[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
#imapはイテレータを返す
>>> itertools.imap(print_argument, list)
<itertools.imap object at 0x101702f50>

>>> import multiprocessing
#4プロセスで実行
>>> pool = multiprocessing.Pool(4)
#Pool.imapは引数の評価をプロセスに振り分ける
>>> for i in pool.imap(print_argument, iter(list)):
...     print i
...     time.sleep(2)
pid: 20957, argument: 2, time: 33:42
pid: 20956, argument: 3, time: 33:42
pid: 20955, argument: 4, time: 33:42
pid: 20954, argument: 5, time: 33:42
2
pid: 20956, argument: 7, time: 33:43
pid: 20957, argument: 6, time: 33:43
pid: 20955, argument: 8, time: 33:43
pid: 20954, argument: 9, time: 33:43
pid: 20956, argument: 10, time: 33:44
3
pid: 20957, argument: 11, time: 33:44
4
5
6
7
8
9
10
11

ちなみにPython3では基本的に繰り返しに使用するものはリストではなくイテレータが推奨されているようで,map()や辞書型のitems()など様々なものがイテレータを作るように変更されたようです.

3. multiprocessによるimap()の謎

この記事を書くためにイテレータについて勉強するうちに新たに疑問として沸き上がってきたことがありました。それはマルチプロセスではどうやって遅延評価されるのかということです。
もしイテレータを1つ評価してからfor文の処理をするという流れだとすると、for文の処理に時間がかかる場合にマルチプロセスの利点が無くなりそうです。逆にイテレータを最初に全て評価するならばマルチプロセスの利点が生かせるけど、それでは普通のmap()と同じになりそうです。
実は結論は前の実行結果で既に出ていて、Pool.imap()ではおそらくイテレータを評価しながらfor文の処理も同時に行っているようです。ソースを読んだわけではないのでこの動作で正しいのかちょっと怪しいですが。imap()を使うとイテレータのメリットを生かしつつマルチプロセスの利点も生かすことができそうです。

恥ずかしながらイテレータについてほとんど知らなかったので、今回の記事を書くために色々調べて勉強になりました。map処理は今まで普通のmap()とかPool.map()を使ってたけど、これからはimap()に変えようかな。