Pillow (PIL) の Image.show 問題、に対する「やり過ぎ」解

Pillow (PIL) の Image.show 問題」は穏健なことしか書いてない。

「情報」として「自分で拡張しなはれや」のレベルでいいのだ、という意味で、「Pillow (PIL) examples」に書く内容としてはこれだけで十分だ。これ以降の話になればこれは「ブログ的」な話、もしくは、「抜本対策を Pillow 本家に提案しちまえ」のレベルになってくる。

何が不十分かってのは:

  1. ping の /n 値を増やすことは、show 呼び出しが本当に待機してしまうことを意味する (/n 10 は show のたびに 10 秒ほんとうに待たされる。)
  2. ので「別に temporary を消さなくてもいいじゃん」と妥協することになる。

まぁそういうことなんだけれど、ちゃんとしたいと願う場合にこの問題が厄介なのは、現実には「DOS そのものとの戦い」になること。

Python からプロセスを起動するのに、subprocess モジュールを使うのは、これは Windows の場合は CreateProcess API を呼び出すことに相当する(正確には「呼び出せる」ことに相当)。この API の場合は DOS を介在させずに実行形式ファイルを起動出来る。対して、os モジュールの system は、Unix においてもそうであるようにこれは DOS を介する。Windows API 的には ShellExecuteEx。そして今の場合に、なぜに os.system を使う必要があるかといえば、「DOS の START に画像ファイルへのパスを与えると、良きに計らってくれるから」であるだけでなく、START を使うと呼び出し元から切り離せる、から。(振り逃げ。)

細かい説明より先に見てもらったほうがいいか:

 1 from PIL import ImageShow
 2 
 3 class _WindowsViewer3(ImageShow.Viewer):
 4     format = "BMP"
 5     def get_command(self, file, **options):
 6         import os, atexit
 7         fn_enc = str([ord(c) for c in file])
 8         atexit.register(
 9             os.system, '''\
10 start "" /MIN python -c \
11     "import os, time ; \
12      time.sleep(10) ; \
13      os.remove(str().join(map(chr, {})))"'''.format(fn_enc))
14         return ("""start "Pillow" /WAIT "%s" """ % (file, ))
15 
16 ImageShow.register(_WindowsViewer3, order=-1)

sleep がないから ping で、なんてことをするんではなくて、「python なんだからシステムに python が存在することを知っている」ことに依存して、python で sleep もファイル削除もやっちまえ、というところまでは至ってシンプルな発想、てことには異論ある? ないよね。

滅茶苦茶なのが「その python を呼び出すことそのものこそが難解パズルなのだ」ということ。「Pillow (PIL) の Image.show 問題」でも言った通り、DOS のコマンドラインパースは仕様も振る舞いもブチ壊れているので、まともに引用符を扱うのが極めて困難だ。そういうわけで、こういうバカげたことをやっている:

 1 Python 2.7.9 (default, Dec 10 2014, 12:28:03) [MSC v.1500 64 bit (AMD64)] on win32
 2 Type "help", "copyright", "credits" or "license" for more information.
 3 >>> file = "a.BMP"
 4 >>> [ord(c) for c in file]
 5 [97, 46, 66, 77, 80]
 6 >>> "".join(map(chr, [ord(c) for c in file]))
 7 'a.BMP'
 8 >>> str().join(map(chr, [ord(c) for c in file]))
 9 'a.BMP'
10 >>> fn_enc = str([ord(c) for c in file])
11 >>> '''"os.remove(str().join(map(chr, {})))"'''.format(fn_enc)
12 '"os.remove(str().join(map(chr, [97, 46, 66, 77, 80])))"'

こうすることで python -c 内で引用符を避けている。泣き叫ぶがいいさ、これが DOS だ。大好きになるがいい。

こうやって答え出しちゃってからコピペして使うのは簡単だけれど、これを生み出すまでのデバッグは死ぬぞ、だって「(python -c 実行結果が)見えない」んだから。

で、この答え、価値あんの? 無論、「ない」。単なるデバッグ目的の show() ごときにかけるコストではないわ。


2017-07-03 追記:
DOS がどーしようもない、この「やり過ぎ解」に価値がない、という二点で揺らぐわけではないんだけれど、前者についてはちと言いすぎがやはり気になったので一応。

「まともに引用符を扱うのが極めて困難」は別に出来ないってことではなくて、出来ます。けどそれ自体がもうパズル。仕様が不可解で無駄に頭を使う。borne-shell みたいに簡潔じゃない。borne-shell でも複雑になれば記述は複雑にはなるけれど、規則的には「開いたら閉じる」に従うだけなので、書く方は別に悩まない。読むほうは大変だけど:

1 me@host: ~$ echo "abc'\"d"
2 abc'"d
3 me@host: ~$ echo "abc'\"d" | sed 's@'"'"'@@'
4 abc"d
5 me@host: ~$ echo "abc'\"d" | sed 's@'"'"'"@@'
6 abcd
7 me@host: ~$ printf "abc'\"d\n" | sed 's@'"'"'"@@'
8 abcd

's@'"'"'@@' 部分が読み手には大変なんだけれど、これを書く側は単に「いったんシングルクオートの方を閉じて('s@')」からダブルクートでシングルクオートを囲んで("'")再びシングルクートで囲む('@@')、としてるだけなのね。

対して、DOS はここまで単純な規則として憶えられるものではなく、上の例を(今の場合 MSYS コマンド群へのパスが通っているとして)CMD.EXE から実行するとこんなハメになる:

1 c:\Users\hhsprings>printf "abc'\"d\n"
2 abc'"d
3 c:\Users\hhsprings>printf "abc'\"d\n" | sed 's@'"'"'"@@'
4 abc'"d
5 printf: warning: ignoring excess arguments, starting with `|'

何が起こっているかというと、この場合パイプ以降も全部 printf への引数だとみなされてしまっている。こうなったらもうダメで、無駄な格闘がはじまる。

要するにこの種のバトルがバカバカしいわけです。たぶんね、これでも大丈夫と思う:

1         atexit.register(
2             os.system, '''\
3 start "" /MIN python -c \
4     "import os, time ; \
5      time.sleep(10) ; \
6      os.remove(\\"{}\\")"'''.format(file))

ただこれが「大丈夫」(かもしれない)なのは python -c が賢いからというだけであって、これ以外のコマンドだとおそらくうまくいかんと思う。


2017-07-21 追記:
「DOS の start に頼らないとデタッチ出来ない」みたいな言い方してたのがずっと気になってたのよね、無論「出来ます」。とはいえここいらの話は昔から「ヘンな難しさ」があったんだよね、「だって Windows ですから」。

本質的には「やり過ぎ」であることには違いはないけれど、「python の力だけで」こんなんでオッケー:

 1 from PIL import ImageShow
 2 
 3 class _WindowsViewer4(ImageShow.Viewer):
 4     format = "BMP"
 5     def get_command(self, file, **options):
 6         import os, subprocess, atexit, win32process
 7         atexit.register(
 8             subprocess.Popen,
 9             ["python",
10              "-c",
11              "import time, os ; time.sleep(20); os.remove('%s')" % file.replace(os.sep, "/")],
12             creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | win32process.DETACHED_PROCESS,
13             close_fds=True)
14         return ("""start "Pillow" /WAIT "%s" """ % (file, ))
15 
16 ImageShow.register(_WindowsViewer4, order=-1)

無論「画像の表示」の方は結局「start」任せね。