続(々): 結局は psutil の方がより救世主なの (psuti.Process の nice)の続

プロセスの優先度を変更する」のではなくて「サブプロセスを起こす際に優先度を指定したい」話。

「Unix vs Windows」という文脈で語る際、Windows に対して「nice (/renice)がないでやんの、ヴァーカヴァーカ」とは言えなくて、実際は「CreateProcess に直接優先度を指定できる」という Unix なプロセス API にはない特徴があり、実はこれについては微妙に引き分けだったりする。(Unix 系の場合は、システムコールレベルでのサポートではなくて、nice コマンド経由でプロセスを実行出来るようになっているので、エンドユーザから見た場合は違いはないとも言える。)

Python は、よほどのことがない限りは、ほとんどのものが「Unix での考え方の方に従う」ように作らていて、ゆえに、subprocess モジュールが(Windows で出来るからという理由で)「プロセス生成時に優先度を指定出来る」ようには作られてなくて、これは psutil も同じである。

この事実がまぁ悩ましくてな。「世界は Unix で出来ている」ならば、これでいいってわけなのよな:

1 # -*- coding: utf-8 -*-
2 import subprocess
3 
4 # ...
5 subprocess.check_call(["nice", "-20", "my_command", "arg"])

もちろん Windows には nice コマンドがないので、こんなことは出来ない。CreateProcess で指定できるのになんでだよ、てことになってしまう。

python が nice の存在を暗に仮定している、あるいは Windows の CreateProcess で優先度指定が出来ることを黙殺している、という事実は、優先度指定をプロセス生成時に行いたいケースにおいては「subprocess モジュールが提供する安直ヘルパ関数(とか)の全滅」を意味していて、そうしたい場合、「subprocess.Popen」にまで遡る必要がある。けどまぁ出来る:

例として「check_call」が「優先度をアイドルにセットする」ことまでやってしまう、てのを
 1 from __future__ import unicode_literals
 2 
 3 import sys
 4 import subprocess
 5 
 6 
 7 if hasattr("", "decode"):
 8     _encode = lambda s: s.encode(sys.getfilesystemencoding())
 9 else:
10     _encode = lambda s: s
11 
12 
13 def _filter_args(*cmd):
14     """
15     do filtering None, and do encoding items to bytes
16     (in Python 2).
17     """
18     return list(map(_encode, filter(None, *cmd)))
19 
20 
21 def check_call(*popenargs, **kwargs):
22     """
23     Basically do simply forward args to subprocess#check_call, but this
24     does two things:
25     * It does encoding these to bytes in Python 2.
26     * It does omitting `None` in *cmd.
27 
28     """
29     import psutil
30     def _call(*popenargs, timeout=None, **kwargs):
31         with psutil.Popen(*popenargs, **kwargs) as p:
32             try:
33                 p.nice(psutil.IDLE_PRIORITY_CLASS)
34                 return p.wait(timeout=timeout)
35             except:  # Including KeyboardInterrupt, wait handled that.
36                 p.kill()
37                 # We don't call p.wait() again as p.__exit__ does that for us.
38                 raise
39     def _check_call(*popenargs, **kwargs):
40         retcode = _call(*popenargs, **kwargs)
41         if retcode:
42             cmd = kwargs.get("args")
43             if cmd is None:
44                 cmd = popenargs[0]
45             raise subprocess.CalledProcessError(retcode, cmd)
46         return 0
47     cmd = kwargs.get("args")
48     if cmd is None:
49         cmd = popenargs[0]
50     _check_call(_filter_args(cmd), **kwargs)

ローカル関数の「_call」と「_check_call」が subprocess モジュールからコピーしてきたもので、前者内を「psutil.Process」に置き換えた上で即座に「.nice(…)」を呼び出している。

正直 psutil が「プロセス生成時に指定できる」ように考えてくれるのが一番ハッピーな気がするんだけれど、まぁ…「安直ヘルパに依存しなければいいだけ」と言っちゃえばそれだけとも言えるので、歯痒いねぇ…。

まぁ psutil なしの場合はさらに状況はヒドいわけだから、やっぱり結局は「psutilさn、ありがとぅーありがとぅー」ではある、と。うーむ。