Help us understand the problem. What is going on with this article?

Python2とPython3のstr型/unicode型/bytes型/encode/decodeの違い解説

More than 1 year has passed since last update.

[+]Python2

まず、Python2での挙動を確かめていく。

str型

Python2のstr型はエンコード形式不明の文字列である。

>>> 'あ'
'\xe3\x81\x82'

ここではe38182になっているのでutf-8でエンコードされているとわかる。
しかし、その「utf-8でエンコードされている」という情報は持っていない。
なので、以下のようなエラーが発生する。

>>> 'あ'.decode()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe3 in position 0: ordinal not in range(128)

エンコード情報を持っていないが故に、「ASCIIでエンコードされてるっしょ」という決め打ちでデコードしようとした結果、失敗したのだ。

次のようにすることで、正しくデコードできる。

>>> 'あ'.decode('utf-8')
u'\u3042'

こちらが引数で指定してあげる必要がある。

また、同じく「エンコード情報を持っていない」というのが原因で、次のようなエラーも出る。
以下のpyファイルを作成して、実際に実行させてみよう。

hoge.py
hoge = 'あ'
$ python2 hoge.py
  File "hoge.py", line 1
SyntaxError: Non-ASCII character '\xe3' in file hoge.py on line 1, but no encoding declared;
 see http://python.org/dev/peps/pep-0263/ for details

このエラーは何なのだろうか。
実は、ASCIIで表すことができない文字をソースコード中で使用すると、その(ソースコード中の)文字をどのエンコード形式でエンコードすればいいのかわからず、このエラーが出る。
「ASCIIで表せないけど代替のエンコード形式教えてもらってないよ!」と怒られているのである。

# -*- coding: utf-8 -*-という1文を1行目もしくは2行目に書いて代替のエンコード形式を教えてあげることで、このエラーを回避できる。

hoge2.py
# -*- coding: utf-8 -*-
hoge2 = 'あ'
$ python2 hoge2.py

ちなみに、Python3ではデフォルトのcodingがutf-8になっているため、この対応は必要なくなる。

次に、str型にindexを指定した場合を見てみる。

>>> 'a'[0]
'a'

>>> 'あ'[0]
'\xe3'

>>> 'あ'[1]
'\x81'

>>> 'あ'[2]
'\x82'

str型はエンコード情報を持たないため、'あ'という文字が内部的に何バイトになっているかを知る術が無い。
なので、str型にindexを指定すると、1バイトごとに区切られてしまう

次のように、一度unicode型にデコードして処理したあとエンコードして元に戻してあげる必要がある。
実はASCII以外の場合は、unicode文字列のほうが扱いやすいのである。
(ASCIIは必ず1バイトなので、1バイトごとに切り分けられても問題が無い。)

>>> 'あいう'.decode('utf-8')[0].encode('utf-8')
'\xe3\x81\x82'

>>> '\xe3\x81\x82' == 'あ'
True

unicode型

unicode型はunicodeのコードポイントを羅列した形式の文字列である。

>>> u'あ'
u'\u3042'

>>> u'あいう'
u'\u3042\u3044\u3046'

encodeしてみる。

>>> u'あ'.encode()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character u'\u3042' in position 0: ordinal not in range(128)

>>> u''.encode('utf-8')
'\xe3\x81\x82'

これもまた、ASCII決め打ちしているので引数で指定してあげる必要があるようだ。

次に、以下のpyファイルを作成して実行してみる。

fuga.py
# -*- coding: utf-8 -*-
fuga = u'あ'
print(fuga)
$ python2 fuga.py
あ

$ python2 fuga.py > fuga.txt
Traceback (most recent call last):
  File "fuga.py", line 3, in <module>
    print(fuga)
UnicodeEncodeError: 'ascii' codec can't encode character u'\u3042' in position 0: ordinal not in range(128)

リダイレクトするとエラーになる。
これは何が原因なのだろうか。
「unicode文字列をASCIIにエンコードできません」と怒られているが、ちゃんと# -*- coding: utf-8 -*-って書いてるじゃないか。何故utf-8ではなくASCIIでエンコードしようとしてるんだ。と思うかもしれない。
しかし、# -*- coding: utf-8 -*-の意味はあくまで「ソースコード中の文字列のエンコード形式」だけである。
リダイレクト無しのターミナル直接出力でエラーにならなかったのは、実は「ターミナルのエンコード形式」を自動で取得して自動でエンコードして出力してくれたからなのである。
ターミナルのエンコード形式は次のコマンドで取得できる。

$ echo $LANG
en_US.UTF-8

utf-8と出力された。
つまり、ソースコード中のu'あ'はprintの際にutf-8に自動でエンコードされていたのだ。

しかし、$ python2 fuga.py > fuga.txtとリダイレクトするとどうだろう。
この時に重要になるのはターミナルのエンコード形式ではなく、fuga.txtのエンコード形式なのである。
なので、「リダイレクト先のfuga.txtのエンコード形式を教えてもらってないってことはASCIIかな」ということで相変わらずの決め打ちASCIIエンコードをかました結果、「ASCIIでエンコードできません」というエラーに遭遇することになる。

ここまで聞けば対処法はもうお分かりだろう。

fuga2.py
# -*- coding: utf-8 -*-
fuga2 = u'あ'
print(fuga2.encode('utf-8'))
$ python2 fuga2.py > fuga2.txt

そう。ソースコード中で(エンコード可能な形式で)エンコードしてしまえば良いのだ。

豆知識

PYTHONIOENCODINGという環境変数にエンコード形式を格納することで、リダイレクト時のエンコード形式を指定することができる。

$ export PYTHONIOENCODING='utf-8'
$ python2 fuga.py > fuga.txt

[+]Python3

それでは、Python3ではどうなのだろうか。

str型

>>> 'あ'
'あ'

>>> 'あ' == u'\u3042'
True

Python2ではstr型は何らかの形式でエンコードされた文字列という定義だったため、
あくまでも「文字列」ではなく「データ」として認識されていた。
しかし、Python3ではunicode文字列と固定されているので、一文字を一文字として認識することができる
なので当然、次のようなこともstr型の状態ですんなりできる。

>>> 'あいう'[0]
'あ'

>>> 'あいう'[1]
'い'

>>> 'あいう'[2]
'う'

エンコードするには、Python2同様encode関数を使用する。

>>> 'あ'.encode()
b'\xe3\x81\x82'

>>> 'あ'.encode('utf-16')
b'\xff\xfeB0'

ここで出てくるのがPython3で新たに加わった「bytes型」である。
(ちなみに、b'\xff\xfeB0' == b'\xff\xfe\x42\x30'である。)

bytes型

bytes型とは、「1バイト区切りのデータの羅列を表す型」のことである。

Python3では、どんな形式でエンコードされていようとも、エンコードされていればそれは文字ではなく「データ」として認識される。

index指定してみる。

>>> 'あ'.encode('utf-8')[0]
227

>>> hex('あ'.encode('utf-8')[1])
'0x81'

>>> hex('あ'.encode('utf-8')[2])
'0x82'

>>> 'あ'.encode('utf-8')[3]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: index out of range

お分かりだろうか。
bytes型にindexを指定すると1バイトごとに区切られ、更にそれは「数値」になる。

足してみる。

>>> b'\x01' + b'\x02'
b'\x01\x02'

index指定する前はまだ「バイトデータ」のまま。
なので、文字列連結のような動作をする。

もちろん次のように、バイトデータをデコードすることでunicode文字列を生成することもできる。

>>> b'\xe3\x81\x82'.decode('utf-8')
'あ'

[+]まとめ

Python2でのencode/decodeは「str型(何らかの形式でエンコードされた文字列)」と「unicode型(unicode文字列)」を行き来するモノ。
Python3でのencode/decodeは「str型(unicode文字列)」と「bytes型(バイトデータ)」を行き来するモノ。
文字列かバイトデータかの区別だけで済むようになったため、3系では文字列周りが凄くシンプルになった。

miyagi1024
SoftwareDvelopment, LinuxKernelReading, MalwareAnalysis, Exploit, AtCoder / seccamp'17'18, SecHack365'18, GlobalCybersecurityCamp'18
https://miyagi1024.github.io
ipfactory
メンバーが各々の技術分野を追求するサークル、「IPFactory」のOrganizationです。それぞれのアウトプット活動を促進するために発足されました。
https://twitter.com/_ipfactory_
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした