昨日の記事に引き続き、今度はPython3で文字列を扱う場合の自分なりの方針をまとめてみる。
個人的結論
- ほとんどの場合は文字列を扱い、標準入出力とも文字列でやり取りする。
- ただし外部プログラムからバイト列が渡される、などバイト列を扱わなければいけないこともありうる。逆に言うと、そういう場合を除いてはバイト列を扱わない。
これが良い悪いということではなく、現状自分の書くコードではバイト列を扱わなければならないケースが少ないというだけ。
バイト列と文字列
バイト列は特定のエンコード方式でエンコードされており、リテラルではb'a'
のように表現する。一方、文字列はUnicodeのコードポイントを並べたものであり、リテラルでは'あいう'
のように表現する。
さらっと書いたが、この時点でPython2との扱いの違いが分かる。
- 「Python3のバイト列」は「Python2のバイト文字列」と扱いが似ている。しかし「Python2のバイト文字列」は『文字列』であるが、「Python3のバイト列」は『文字列』ではない全く別の型である。
- 「Python3の文字列」と「Python2のユニコード文字列」は同等と考えてよい。リテラル表記に違いがあり、
u
を文字列の前につけなくてはならなかった「Python2のユニコード文字列」に対して「Python3の文字列」はそれが不要。
(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
型であることが分かる。
>>> type(b'a')
Out[6]: bytes # ≒ Python2のstr型
>>> type('a')
Out[7]: str # ≒ Python2のunicode型
また前述の通り、Python3のバイト列は「文字列」ではない。そのため文字列と連結不可能であるし、サポートしているメソッドも異なる。
この点は割と重要で、Python2だと同じ文字列なのでなんとなく処理が進んでしまい最終的に「UnicodeEncodeErrorががが」となる所が、Python3だと「型が異なることによるエラー」となり、エラー出力/発生箇所が比較的分かりやすい。
>>> 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 の追記
(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
(標準出力)を使えば、文字列ではなくバイト列を扱える。
(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
には文字列は書き込めない。
>>> 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