methaneのブログ

このブログに乗せているサンプルコードはすべてNYSLです。

標準入出力のエンコーディングを指定する3つの方法の使い分け

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

#!/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

http://blog.livedoor.jp/dankogai/archives/51816624.html

もちろん普通そんな書き方はしません。Pythonで複数の方法があるときは、それぞれ別の目的があるのです。ちゃんと適切な方法を使いましょう。
ということで、3つの方法それぞれの用途を解説していきます。

1. PYTHONIOENCODING

Python 2 でも 3 でも、プログラムで Unicode 文字列を出力する際に、ユーザーの環境に応じた適切なエンコーディングを Python が自動的に検出してそのエンコーディングで出力してくれるようになっています。
ですが、常に Python が自動で適切なエンコーディングを特定できるとは限りません。 id:dankogai さんが例に挙げられた locale が使えない状況や、リダイレクト先が期待しているパイプの先が locale と違うエンコーディングを期待している場合などに正しいエンコーディングを Python に教えられるのは実行しているユーザーだけです。なので、 ユーザーが Python プログラムを実行するときに指定するのが PYTHONIOENCODING です。
なので、上のサンプルコードのように Python プログラム自体が特定のエンコーディングを使って出力したい場合に使うものではありません。
ちなみに、リダイレクトされた場合などに適切なエンコーディングを特定できない問題ですが、とりあえず可能な限り strict で安全側に振ってエラーを発生させるようになっていますが、さすがにターミナルで動いてたプログラムが | cat しただけで動かなくなるのはやり過ぎだろうということで、locale依存のエンコーディングを使うようにしようという議論があります。
追記(2012-08-07 13:17)
Python 3.2 では標準入出力がターミナルでない場合でも locale 依存のエンコーディングを使ってくれるようになっていました。

2. buffer を使う

buffer 属性を使うことで、バイトストリームにアクセスすることができます。
Python 3 では文字列(Unicode)とバイト列は完全に別物なので、バッファに書きだすということはテキストIOではなくバイトIOをしていることになります。
何がバイトIOで何がテキストIOなのかは難しい問題ですが、原則的にプログラムが厳密に標準入出力の内容を制御したい場合はバイトIOを使うべきです。例えば、他のプログラムから呼び出され、標準入出力にjsonやxmlを受け取ったり返したりしる場合、エンコードされたxmlやjsonはテキストではなくバイト列だとみなすことができます。
この方法では、入出力しているのはバイト列なので、 Python 3 的にはもちろん print() を使うなんてもっての外です。
実際に用途を見てみないと判断できませんが、 環境に応じたエンコーディングを自動的に使うのではなくプログラム自体でエンコーディングを指定したい場合、たいていこの方法が正しいでしょう。

3. buffer あるいは fileno をベースにテキストIOを開き直す

入出力がバイト列ではなくあくまでもテキストなんだ、という場合は、こちらの方法が正しいでしょう。
例えば設定ファイルでエンコーディングを指定できるプログラムを作る場合に、 Python が選んで自動で設定してくれたテキストIOを無視して自分でテキストIOをセットアップするためにこの方法を使います。
すでにプログラムが標準入出力を使っているかもしれない前提で、正しい標準入出力の開き直し方を紹介しておきます。

buffer を開き直す

今の stdio の buffer オブジェクトを取得するのは TextIOWrapper.detach() を使います。このメソッドは、 flush() と、 buffer の所有権の破棄を行います。つまり、 sys.stdout を置き換える前に def foo(a, b, out=sys.stdout) みたいな関数を定義してしまってあとから実行した場合に、正しくエラーを起こすことができます。
なお、 id:dankogai さんが指摘されていたバッファリングの挙動が違う件は、もともと Python が open() するときにそのファイルがttyかどうかで挙動を指定して TextIOWrapper を作っているためで、合わせるのは簡単です。

def reopen_stdio_with_encoding(encoding='utf-8'):
    import io
    import sys
    def reopen(file):
        return io.TextIOWrapper(file.detach(),
                                line_buffering=file.line_buffering,
                                encoding=encoding)
    sys.stdout = reopen(sys.stdout)
    sys.stderr = reopen(sys.stderr)
    sys.stdin = reopen(sys.stdin)

reopen_stdio_with_encoding()
print("ほげほげ")
fileno を開き直す

一応開き直す前に flush して出力されないままになるデータがないことを保証しましょう。

def reopen_stdio_with_encoding(encoding='utf-8'):
    import sys
    sys.stdin = open(sys.stdin.fileno(), encoding=encoding, closefd=False)
    sys.stdout.flush()
    sys.stdout = open(sys.stdout.fileno(), 'w', encoding=encoding, closefd=False)
    sys.stderr.flush()
    sys.stderr = open(sys.stderr.fileno(), 'w', encoding=encoding, closefd=False)

reopen_stdio_with_encoding()
print("ほげほげ")

こっちは TextIOWrapper とか気にせずに、 open() 関数にすべてをお任せできるので楽ちんです。
しかし、もし stdin のバッファ内にデータが残っていた場合にそれが消えてしまいますし、先ほど挙げた置き換え前の sys.stdout がすでにどこかに保存されている場合にエラーにすることができずもとの stdout が利用されてしまうという欠点があります。どちらかと言えば buffer を使うほうがお行儀が良いです。

将来について

標準入出力のエンコーディングをあとから書き換えたいケースが現実の世界にある問題なので、もっと直接的で、罠のある「開き直し」をせずにエンコーディングを再設定するインタフェースが必要じゃないかという議論が python-ideas で行われています。
しかし、すでにある程度の入出力が実行されている TextIOWrapper に対してエンコーディングの再設定を許すべきかどうか、許さないのであれば TextIOWrapper が初期状態であるかどうかを判定するAPIや、そうでない時に再設定しようとした場合のエラーをどうするべきか、という話の途中で、今のところレスが途絶えてしまっているので、だれかが旗を振って議論を進めないとこのまま立ち消えになってしまうかもしれません。

Python 3 の Unicode とバイト列を分けるという決断は、 PerlRuby から離れて JavaC#Haskell といった固い言語に近づくものなので、現状では一部でいままで簡単だったことが面倒になっていることは否めません。
今後、スクリプト言語としても生産性や簡単さを可能な限り犠牲にしないように、現実のよくある問題をストレートに解決するAPIが、全体の整合性を保ちながら整備されていくことでしょう。