Python 3.4 で入った変更をみて驚いてしまった。
multiprocessing.cpu_count() なんか結構気軽に使っていた。けれど What’s New In Python 3.4 で説明されている変更:
は、ここで説明しているよりも遥かに大きな改造をやらかしていることに気付いて、ちょっとイラっときた。
まず元の multiprocessing.cpu_count がこんなだったことを今更知った:
1 def cpu_count():
2 '''
3 Returns the number of CPUs in the system
4 '''
5 if sys.platform == 'win32':
6 try:
7 num = int(os.environ['NUMBER_OF_PROCESSORS'])
8 except (ValueError, KeyError):
9 num = 0
10 elif 'bsd' in sys.platform or sys.platform == 'darwin':
11 comm = '/sbin/sysctl -n hw.ncpu'
12 if sys.platform == 'darwin':
13 comm = '/usr' + comm
14 try:
15 with os.popen(comm) as p:
16 num = int(p.read())
17 except ValueError:
18 num = 0
19 else:
20 try:
21 num = os.sysconf('SC_NPROCESSORS_ONLN')
22 except (ValueError, OSError, AttributeError):
23 num = 0
24
25 if num >= 1:
26 return num
27 else:
28 raise NotImplementedError('cannot determine number of cpus')
- 驚愕ポイント1: をげ、こやつ、外部プロセス起こすん?
- 驚愕ポイント2: うぉ、Windows は環境変数読むだけだと?
- 驚愕ポイント3: をぃをぃ、NotImplementedError てなんだよ
最後のはドキュメントされてたが気付いてなかった。
3つ目のを除くと、まずこれが言える: 「Python 2.6~3.3 までの multiprocessing.cpu_count は、何度も呼び出すならキャッシュするとかした方が良い。また、場合によっては信頼出来ない値を返す。」
What’s New の表現ではわからないが、 :issue:`17914` が発端で行われた改造は、トータルではこうである:
- cpu_count なんて情報は「os」モジュールで取得出来るべきだ ⇒ os に実装移動
- cpu_count の実装は、(ゆえに) posixmodule.c に移動 (つまり C 実装に変更)
- そしてその C 実装の中身はもとの multiprocessing.cpu_count とは似ても似つかない
(上のリンクでも読めるが) C 実装はこうだ:
1 /*[clinic input]
2 os.cpu_count
3
4 Return the number of CPUs in the system; return None if indeterminable.
5 [clinic start generated code]*/
6
7 static PyObject *
8 os_cpu_count_impl(PyModuleDef *module)
9 /*[clinic end generated code: output=c59ee7f6bce832b8 input=d55e2f8f3823a628]*/
10 {
11 int ncpu = 0;
12 #ifdef MS_WINDOWS
13 SYSTEM_INFO sysinfo;
14 GetSystemInfo(&sysinfo);
15 ncpu = sysinfo.dwNumberOfProcessors;
16 #elif defined(__hpux)
17 ncpu = mpctl(MPC_GETNUMSPUS, NULL, NULL);
18 #elif defined(HAVE_SYSCONF) && defined(_SC_NPROCESSORS_ONLN)
19 ncpu = sysconf(_SC_NPROCESSORS_ONLN);
20 #elif defined(__DragonFly__) || \
21 defined(__OpenBSD__) || \
22 defined(__FreeBSD__) || \
23 defined(__NetBSD__) || \
24 defined(__APPLE__)
25 int mib[2];
26 size_t len = sizeof(ncpu);
27 mib[0] = CTL_HW;
28 mib[1] = HW_NCPU;
29 if (sysctl(mib, 2, &ncpu, &len, NULL, 0) != 0)
30 ncpu = 0;
31 #endif
32 if (ncpu >= 1)
33 return PyLong_FromLong(ncpu);
34 else
35 Py_RETURN_NONE;
36 }
「CPU 数が決定できない場合」の振る舞いとしては What’s New でも説明されているし上の実装からもわかる通り、これ (os.cpu_count()) は None を返す。そしてこれは multiprocessing.cpu_count() がこの場合に NotImplementedError を返すのとは違っている。:issue:`17914`ではまさにこの振る舞いについて議論していた。
os.cpu_count() が NotImplementedError ではなく None を返すこと自体は歓迎すべきことだ。問題は、(versionchangedとかで) 変更を説明することなくこの変更が行われていること。この修正も望ましいもの、ではある。つまりこれは、「CPU 数が決定できないなら多重度 1 として振舞う」という当たり前のことをするものだから。こんなもんがいちいち NotImpl で死んでたら、クロスプラットフォームなアプリケーションが遠い夢となる。←間違えた…。この修正は後方互換だった。寝ぼけてました。
versionadded とか versionchanged 、あるいは What’s New が重要なのはさ、「その新しい Python を使いたいユーザ」が「やったね!」と思うためだけにあるのではなくて、それよりも古い Python を使っているユーザへの注意喚起にもなったりするわけだからさ、やっぱ漏れなく書かれて欲しいなぁ、と思った…、というハナシ。そしてなんで multiprocessing.cpu_count の NotImplementedError もやめてしまうという決断をしてくれなかったんだろう、と思った、ってハナシ。
なお、結局どうすべきか、てのは Python 3.5 までの現在のところは多分:
- Python 3.4 以降は os.cpu_count() を優先して使うべし
- Python 3.3 までは multiprocessing.cpu_count を(諦めて)使う:
- 必要ならば NotImplementedError に備えつつ
- 信頼出来ないかもしれないことに慄きつつ
- 何度も繰り返し呼び出すならキャッシュとかした方がいいかもってことを頭の片隅に置きつつ
てことだろう。とっても些細なことかもしれないがなんだか気になった。