Python3環境で日本語を使うとUnicodeEncodeErrorが出る問題

この記事は以前に投稿したものですが、諸事情でアカウントを作り直したため改めて投稿しています。内容は少し古いかもしれません。

# 背景

私の環境でpython(3.6.2)の文字列に日本語を使うとUnicodeEncodeErrorが出てしまいました。すぐ直せるだろうと軽く考えていたのですが、案外解決に手間取ったので備忘録がてら記事にしておこうと思いました。python2系で同様の問題と解決策を記載したページはいくつかヒットしたのですが、python3系の情報は私の検索の仕方が悪かったのか、まだ情報が少ないようです。
(ないとは言っていません。すでにもっと良質の情報をwebに公開されている先達の方はどうかご容赦ください。)

## 動作に失敗した環境
- MacOS High Sierra上のdockerコンテナ
- dockerイメージはconda/miniconda3をpullし、`conda update —all`したもの
- python version 3.6.2

サンプルコード

```python:jp.py
print('日本語をプリントします')
```

```shell-session
# python ./jp.py
Traceback (most recent call last):
  File "jp.py", line 1, in <module>
    print('\u65e5\u672c\u8a9e\u3092\u30d7\u30ea\u30f3\u30c8\u3057\u307e\u3059')
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-10: ordinal not in range(128)
```

# 最終解決策

まず、`locale -a`コマンドを実行します。利用可能なロケール一覧が表示されます。
私のdockerコンテナでは以下のようになりました。

```shell-session
# locale -a
C
C.UTF-8
POSIX
```

次に、環境変数LC_CTYPEに`locale -a`で表示された利用可能ロケールのうち、UTF-8を含むものをセットします。私の環境では

```shell-session
# export LC_CTYPE="C.UTF-8"
```

または、環境変数LANGに`locale -a`で表示された利用可能ロケールのうち、UTF-8を含むものの前半(\.の前の部分)をセットします。
私の環境では

```shell-session
# export LANG="C.UTF-8"
```

これで日本語が表示されるようになりました。

```shell-session
# python ./jp.py
日本語をプリントします
```

以上です。`locale -a`でUTF-8を含むロケールがなかった場合、あるいはUNIX系OSでない場合の解決方法はわかりません。現状、そのような状況でお困りの方のお役には立てなさそうです。

### 試してみてダメだったこと

- ソースコードのエンコーディングを指定する

```python:jp_1.py
# -*- coding: utf-8 -*-
print('日本語をプリントします')
```

同じエラーが出現しました。

- ソースコードの文字列をunicodeオブジェクトと明示的に表示する

```python:jp_2.py
# -*- coding: utf-8 -*-
print(u'日本語をプリントします')
```

同じエラーがでます。そもそも、Unicodeが表示できない、というエラーなので当たり前ですね。

- ソースコードの文字列をバイト列として扱う

```python:jp_3.py
# -*- coding: utf-8 -*-
print(b'日本語をプリントします')
```
今度は`SyntaxError: bytes can only contain ASCII literal characters.`という別のエラーに変わります。

- sys.stdoutに文字コードを指定する

```python:jp_4.py
# -*- coding: utf-8 -*-
import sys
import codecs
sys.stdout = codecs.getwriter('utf-8')(sys.stdout)
print('日本語をプリントします')
```

実行結果

```shell-session
Traceback (most recent call last):
  File "./jp_4.py", line 7, in <module>
    print('\u65e5\u672c\u8a9e\u3092\u30d7\u30ea\u30f3\u30c8\u3057\u307e\u3059')
  File "/usr/local/lib/python3.6/codecs.py", line 377, in write
    self.stream.write(data)
 TypeError: write() argument must be str, not bytes
```

Python2系では解決策の一つとして記載のあったやり方ですが、今回は上手くいきませんでした。Python3系ではcodecsの扱い方が変わったのでしょうか? 詳しくは追求していませんが、いずれにしてもすべてのソースコードにこのような付加物を付けるのはあまり現実的ではないと思いました。

- 環境変数LANGをja_JPまたはja_JP.UTF-8に設定する

```shell-session
# export LANG="ja_JP" #または"ja_JP.UTF-8"
# python ./jp_1.py
Traceback (most recent call last):
  File "./jp_1.py", line 2, in <module>
    print('\u65e5\u672c\u8a9e\u3092\u30d7\u30ea\u30f3\u30c8\u3057\u307e\u3059')
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-10: ordinal not in range(128)
```

日本語を表示させたいのだからja_JPにすればよいのだろうと安易に考えましたが、ダメでした。最終的に上手くいった方法から考察するに、利用可能ロケールにja_JPがなかったからと思われます。もちろん、`locale -a`で`ja_JP.UTF-8`が表示されるような環境ならこれでも上手くいくと思います。

## 簡単な考察

まず、Pythonインタプリタがなぜ日本語を正しく表示できないのかを考えてみます。エラーメッセージを読むと、`'ascii codec can't encode …`と表示されているので、超意訳すると「Pythonはこういう文字コードも扱えるけど使ってる端末がasciiしか扱えないから表示できないよ」とお節介を焼いていると読めます。ではPythonはどういう手段で端末の扱える文字コードの***codec***を認識しているのでしょうか?
これはPython標準ライブラリドキュメントの[sys.getfilesystemencoding()](https://docs.python.jp/3/library/sys.html#sys.getfilesystemencoding)の項に記載されています。このドキュメントによると、

*  Mac OS X では、エンコーディングは 'utf-8' となります。
-  On Unix, the encoding is the locale encoding.
*  On Windows, the encoding may be 'utf-8' or 'mbcs', depending on user configuration.

との記載があります。従って、MacやWindowsではデフォルト設定でも大丈夫と思われますがそのほかのUNIX系OS(Linux含む)ではlocaleの設定に依存するということが分かります。よって、localeの環境変数が正しく設定されていなかったり、空白だったりするとデフォルトの'ascii'が使用されてしまい、日本語が正しく表示されなくなるということのようです。LC_CTYPEを参照しているところまではドキュメントには記載されていませんでしたので、Python自体の実装を見てみないとそこまでは分からないのかもしれません。