Python3で文字列を処理する際の心掛け


[昨日](http://qiita.com/FGtatsuro/items/cf178bc44ce7b068d233)の記事に引き続き、今度はPython3で文字列を扱う場合の自分なりの方針をまとめてみる。

## 個人的結論

- ほとんどの場合は文字列を扱い、標準入出力とも文字列でやり取りする。
- ただし外部プログラムからバイト列が渡される、などバイト列を扱わなければいけないこともありうる。逆に言うと、そういう場合を除いてはバイト列を扱わない。

これが良い悪いということではなく、現状自分の書くコードではバイト列を扱わなければならないケースが少ないというだけ。

## バイト列と文字列

バイト列は特定のエンコード方式でエンコードされており、リテラルでは`b'a'`のように表現する。一方、文字列はUnicodeのコードポイントを並べたものであり、リテラルでは`'あいう'`のように表現する。

さらっと書いたが、この時点でPython2との扱いの違いが分かる。

- 「Python3のバイト列」は「Python2のバイト文字列」と扱いが似ている。しかし「Python2のバイト文字列」は『文字列』であるが、「Python3のバイト列」は『文字列』ではない全く別の型である。
- 「Python3の文字列」と「Python2のユニコード文字列」は同等と考えてよい。リテラル表記に違いがあり、`u`を文字列の前につけなくてはならなかった「Python2のユニコード文字列」に対して「Python3の文字列」はそれが不要。

```bash:
(py3.4)~ » ipython
   (省略)
>>> b'a' # バイト列
Out[1]: b'a'

# 非ASCII文字を含む場合、リテラル表記が使えない
# 文字列を特定のエンコード方式でエンコードする必要がある
>>> b'あ' 
  File "<ipython-input-2-c12eb8e58bcd>", line 1
    b'あ'
        ^
SyntaxError: bytes can only contain ASCII literal characters.

>>> 'あ'.encode('utf-8') # 文字列->バイト列(エンコード)
Out[3]: b'\xe3\x81\x82'


>>> 'あ' # 文字列
Out[4]: 'あ'

>>> b'\xe3\x81\x82'.decode('utf-8') # バイト列->文字列(デコード)
Out[5]: 'あ'


# Python2(再掲)
(py2.7)~ » ipython
   (省略)
>>> 'あ' # バイト文字列
Out[1]: '\xe3\x81\x82'

>>> u'あ' # ユニコード文字列
Out[2]: u'\u3042'

>>> 'あ'.decode('utf-8') (or unicode('あ', 'utf-8')) # バイト文字列->ユニコード文字列(=デコード)
Out[3]: u'\u3042'

>>> u'あ'.encode('utf-8') # ユニコード文字列->バイト文字列(=エンコード)
Out[4]: '\xe3\x81\x82'
```

`type`関数で確認すると、バイト列は`bytes`型/文字列は`str`型であることが分かる。

```bash:
>>> type(b'a')
Out[6]: bytes # ≒ Python2のstr型

>>> type('a')
Out[7]: str # ≒ Python2のunicode型
```

また前述の通り、Python3のバイト列は「文字列」ではない。そのため文字列と連結不可能であるし、サポートしているメソッドも異なる。
この点は割と重要で、Python2だと同じ文字列なのでなんとなく処理が進んでしまい最終的に「UnicodeEncodeErrorががが」となる所が、Python3だと「型が異なることによるエラー」となり、エラー出力/発生箇所が比較的分かりやすい。

```bash:
>>> s = 'str' # 文字列

>>> b = b'byte' # バイト列

>>> s + b # 文字列 + バイト列はエラー
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-20-5fe2240a1b50> in <module>()
----> 1 s + b

TypeError: Can't convert 'bytes' object to str implicitly

>>> s.find('t') # 文字列はfindメソッドをサポートしている
Out[11]: 1

>>> b.find('y') # バイト列はfindメソッドをサポートしていない。
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-24-e1b070a5aaba> in <module>()
----> 1 b.find('y')

TypeError: Type str doesn't support the buffer API
```

またPython3.2から、標準出力がターミナル以外に接続している時にもlocaleの値から適切なエンコード方式を選択するようだ。そのため、Python2ではUnicodeEncodeErrorが出た以下のケースも正常に動作する。


(ref.) http://methane.hatenablog.jp/entry/20120806/1344269400 の追記

```bash:
(py3.4)~ » cat test.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

print('あ' + 'いう')


# ターミナルで実行 (標準入出力をターミナルに接続している)
(py3.4)~ » python test.py
あいう

# ファイルにリダイレクト (標準入出力をターミナル以外に接続している)
(py3.4)~ » python test.py > test.txt
(py3.4)~ » cat test.txt
あいう
```

## もうUnicodeEncodeErrorも恐くない

とか露骨に死亡フラグを立てなくても、UnicodeEncodeErrorに遭遇する可能性は未だある。
例えばcronから実行する場合は、localeからエンコード方式を選択できずにASCIIでエンコード/デコードを試み、大抵UnicodeEncodeErrorを拝む羽目になる。

(ref.) http://www.python.jp/pipermail/python-ml-jp/2014-November/011311.html
(投稿がものすごくタイムリー)

こういったことを考えると、localeを当てにせず常に環境変数`PYTHONIOENCODING`でエンコード方式を指定するというのが良いかもしれない。

(ref.) http://methane.hatenablog.jp/entry/20120806/1344269400
(ref.) http://www.python.jp/pipermail/python-ml-jp/2014-November/011314.html

## ではバイト列を扱う場合はどうすれば良いか

`sys.stdin.buffer`(標準入力)/`sys.stdout.buffer`(標準出力)を使えば、文字列ではなくバイト列を扱える。

```bash:
(py3.4)~ » cat test.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys

#print('あ' + 'いう') # printはsys.stdoutに書き込む
sys.stdout.write('あ' + 'いう' + '\n') # sys.stdoutに文字列を書き込む
sys.stdout.buffer.write(('あ' + 'いう' + '\n').encode('utf-8')) # sys.stdout.bufferにバイト列を書き込む

# ターミナルで実行
(py3.4)~ » python test.py
あいう
あいう

# ファイルにリダイレクト
(py3.4)~ » python test.py > test.txt
(py3.4)~ » cat test.txt
あいう
あいう
```

繰り返しになるが、Python3ではバイト列と文字列は全く別のものである。そのため、文字列を書き込む`sys.stdout`にはバイト列は書き込めず、バイト列を書き込む`sys.stdout.buffer`には文字列は書き込めない。

```bash:
>>> import sys

# テキストストリーム (ref. https://docs.python.org/3/library/io.html#io.TextIOWrapper)
>>> type(sys.stdout) 
Out[2]: _io.TextIOWrapper

# バイトストリーム (ref. https://docs.python.org/3/library/io.html#io.BufferedWriter)
>>> type(sys.stdout.buffer)
Out[3]: _io.BufferedWriter 

# テキストストリームにバイト列は書き込めない
>>> sys.stdout.write('a'.encode('utf-8')) 
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-4-581ae8b6af82> in <module>()
----> 1 sys.stdout.write('a'.encode('utf-8'))

TypeError: must be str, not bytes

# バイトストリームに文字列は書き込めない
>>> sys.stdout.buffer.write('a') 
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-5-42da1d141b96> in <module>()
----> 1 sys.stdout.buffer.write('a')

TypeError: 'str' does not support the buffer interface

```