身の程知らずにもPyCon JP 2012で講演することになったこともあって、日頃空気のようにPerlやJSや時々Rubyで書いていることをあえてPython 3で書いている今日この頃なのですが、これははまった。

こんな解決策でいいのかな、と思いつつも、「Pythonチュートリアル」の訳者@kamosawaのお墨付きも得たので一応まとめておくことに。

結論

というわけでこうなりました。

import sys
sys.stdin =  open('/dev/stdin',  'r', encoding='UTF-8')
sys.stdout = open('/dev/stdout', 'w', encoding='UTF-8')
sys.stderr = open('/dev/stderr', 'w', encoding='UTF-8')

無理矢理開き直すなんてずいぶんと乱暴だと私も思うのですが、ぐぐれど探せどこれしか解答が思いつかず。sys.std(out|err)の方は sys.stdout = codecs.getwriter('UTF-8')(sys.stdout.buffer) という方法を見つけたのですが、sys.stdin = codecs.getreader('UTF-8')(sys.stdin.buffer) とかでうまく行かず…

動機

例えば、こんなキラキラスクリプトを考えてみます。

import sys

for line in sys.stdin:
    chars = list(line.rstrip())
    print('☆'.join(chars))

LANG=ja_JP.UTF-8とかLANG=en_US.UTF-8とかとなっていれば、素直に動いてくれます。

% echo 'dankogai' | python3.2 foo.py
d☆a☆n☆k☆o☆g☆a☆i
% echo '小飼弾' | python3.2 kira3.py
小☆飼☆弾

ところが、LC_ALL=C、つまりlocaleが設定されていない環境ではこうなってしまいます。

% echo 'dankogai' | env LC_ALL=C python3.2 kira3.py
Traceback (most recent call last):
  File "kira3.py", line 4, in 
    print('\u2606'.join(chars))
UnicodeEncodeError: 'ascii' codec can't encode character '\u2606' in position 1: ordinal not in range(128)
% echo '小飼弾' | env LC_ALL=C python3.2 kira3.py
Traceback (most recent call last):
  File "kira3.py", line 2, in 
    for line in sys.stdin:
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.2/lib/python3.2/encodings/ascii.py", line 26, in decode
    return codecs.ascii_decode(input, self.errors)[0]
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128)

"dankogai"と"小飼弾"を食わせた時のエラーの違いにご注目ください。"dankogai"の方は「読めたけど書けない」のに対し、「小飼弾」の方はそもそも読めない。デフォルトの US-ASCII では、バイト列として読もうにもMSBが立っているだけで殺されてしまうのです。これでは「とりあえず生のまま読んでからuline = line.decode('UTF-8')」とかするというわけにもいきませぬ。Python 2では以下でうまく行ってたのですが…

import sys

for line in sys.stdin:
    chars = list(line.rstrip().decode('utf-8'))
    print(u'☆'.join(chars).encode('utf-8'))

こういうlocaleに頼れない環境は、レンタルサーバーなどではむしろ当然とも言えます。にもかかわらずぐぐってもこれといった解答に出会えなかったのは、まだPython 3がさほど普及していないってことでしょうか。App Engineも2.Xのままだし…

もっといい案あれば是非。

Dan the Mammal

See Also:

追記:

確認しました。が、openしなおしより長いってもなんだか…

import sys,io
sys.stdin  = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')
sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='utf-8')              
sys.stderr = io.TextIOWrapper(sys.stderr.buffer,encoding='utf-8')              

あと、挙動も完全互換ではありません。openしなおし版はnewlineごとにループが周りますが、こちらはbufferが埋まるかeofが来るかまで待ち受け状態。

追^2記: ありがとうございます。

Python 3 の標準入出力のエンコーディング - methaneの日記
/dev/stdin だとWindowsでは動かないので、 open(sys.stdin.fileno(), ...) がいいと思います。

というわけでまずはポータビリティ向上版を以下に。

import sys
sys.stdin =  open(sys.stdin.fileno(),  'r', encoding='UTF-8');
sys.stdout = open(sys.stdout.fileno(), 'w', encoding='UTF-8');
sys.stderr = open(sys.stderr.fileno(), 'w', encoding='UTF-8');
なので、常に utf-8 を使って欲しい環境では、 PYTHONIOENCODING=utf-8 を指定しておくことをおすすめします。

そこまでおっしゃるなら、というわけで書いたのが以下。

#!/usr/bin/env python3.2
# -*- coding: utf-8 -*-
import sys, os
if 'PYTHONIOENCODING' in os.environ:
  for line in sys.stdin:
    chars = list(line.rstrip())
    print('☆'.join(chars))
else:
  os.environ['PYTHONIOENCODING'] = 'UTF-8'
  sys.argv.insert(0, sys.executable)
  os.execvp(sys.argv[0], sys.argv)

…強引さが増してるぞおいwllevalのようなsandbox環境では余計動かないしww