このブログを検索

第4章 文字列

4.1. 没頭する前に、退屈でも理解しておくべきことがある


そう考える人は少数ですが、テキストというものは非常に複雑です。アルファベットの話からはじめましょう。パプアニューギニア・ブーゲンビル島で使われているのは、世界で最も文字数が少ないアルファベットです。そのロトカス・アルファベットはわずか12文字で成り立っています(A、E、G、I、K、O、P、R、S、T、U、V)。他の言語を見てみると、中国語、日本語、韓国語には何千もの文字があります。英語はもちろん26文字で、大文字小文字を区別すれば52文字あり、さらにいくつかの記号(!@#$%&)があります。

"テキスト"というと、"コンピュータ画面に表示される文字と記号"のことだと考えるでしょう。しかし、コンピュータは、文字と記号ではなくビットとバイトを扱っています。コンピュータ上に現れるどのテキストも、実際は特定の文字コードで保存されています。かなり大雑把に言うと、文字エンコーディングは、画面上に表示されるものと、コンピュータがメモリやディスクに実際に格納しているものを対応させているのです。文字コードにはさまざまなものがあって、例えばロシア語、中国語、英語に対して最適化されているものであったり、複数の言語に対して適用できるものもあります。

現実はもっと複雑です。多くの文字は複数のエンコーディングで共通ですが、各エンコーディングは文字をメモリやディスクに保存するときに、異なるバイト列を使うかもしれません。文字エンコーディングは暗号鍵の一種と考えることもできます。何かバイト列が与えられたとして(ファイルでもWebページでも何でも)、それが"テキスト"だというのであれば、文字エンコーディングを調べてバイトを文字にデコードします。もし、与えられたキーが間違っていたりキーがない場合は、コードを自力でクラッキングする、という面倒なことになります。間違ったキーを使った場合の結果は、むちゃくちゃなものになってしまいます。

アポストロフィがあるべきところにクエスチョンマークがある奇妙なWebページを見たことがあるでしょう。そういうものは大抵、ページを書いた人が文字エンコーディングを正しく宣言しなかったことが原因でブラウザ側が理解できず、期待どおりの文字とそうではない文字が混ざっているのです。英語の場合はイライラするだけですみますが、他の言語では全く読めなくなる可能性もあります。

文字エンコーディングは、世界中の主要言語に対してそれぞれ設定されています。各言語は異なっているものですし、歴史的にはメモリとディスク領域は高価だったので、各文字エンコーディングはある特定の言語に対して最適化されていました。つまり、各エンコーディングは同じ数字(0-255)を使って、その言語の文字を表しているのです。

例えば、有名なasciiエンコーディングでは、英語の文字を数字の0から127として格納しています(65が"A"、97が"a"、など)。英語で使われるアルファベットはシンプルなので、128より少ない数ですべてを表すことができるのです。2進数を知っている人は、1バイト(=8ビット)の7までで表現できると解るでしょう。(※2^7=128)

フランス語、スペイン語、ドイツ語といった西欧語では、英語よりも文字数が多くなります。もっと正確に言うと、スペイン語における ñ のようなディアクリティカルマーク(発音区別符号)が多いのです。最も一般的な西欧語エンコーディングはCP-1252で、Windowsで広く使われていることから"Windows-1252"と呼ばれることもあります。

CP-1252エンコーディングは数字0-127はasciiと共有していて、128-255の範囲には「nの上に波(241)」、「uの上にドット2つ(252)」などがあります。これも1バイトのエンコーディングで最大の数は255です。

中国語、日本語、韓国語といった文字が非常に多い言語では、マルチバイトの文字セットが必要になります。つまり、各"文字"は、2バイト数(0-65535)で表現されます。しかし、異なるマルチバイト エンコーディング同士でも、シングルバイトのときと同じような問題を抱えています。同じ数字が異なる文字を意味することがある、という問題です。より多くの文字を表現したいので数字の範囲がより広い、というだけです。

ネットワークを使わない世界で、"テキスト"が自分でタイプしてときどき印刷するものであればほとんど問題ありませんでした。"plain text"というものは特に存在していなかったからです。ソースコードはasciiで、他の人もみなワードプロセッサを使っていました。ワードプロセッサでは独自の(テキストではない)フォーマットを定義して、リッチスタイルと一緒に文字エンコーディング情報を記録していたのです。人が文書を読むときは、書いた人と同じワードプロセス プログラムを使うので、おおむね上手くいっていました。

では、emailやWebのようなグローバルネットワークが出現したあとのことを考えてみましょう。多くの"plain text"が地球上を飛び交っています。1つ目のコンピュータ上で書かれ、2つ目のコンピュータから発信され、3つ目のコンピュータで受信され表示されるのです。コンピュータは数字を見ているだけですが、その数字は違うものを指すかもしれないのです。

ああ! どうしたら良いんでしょう! そもそも、plain textのすべての部分でエンコーディング情報が付くようにシステム設計がなされるべきなのです。覚えていますか?エンコーディングというのは、コンピュータが読める数字から人間が読める文字へと暗号を解読する鍵なのです。暗号解読の鍵が無いということは、文字化けが起きたり、意味がわからないものになったり、もっと悪い結果にもなります。

次に、同じ場所に複数の文字を保存することを考えてみましょう。例えば、同じデータベースのテーブルが、これまで受信したメールをすべて持っているとします。

正しく表示するためには、テキストの各部分に対応する文字エンコーディングを保持していることが、ここでも必要です。難しいことでしょうか?メールデータベースを検索するときには、複数のエンコーディング間を急いで変換することになります。楽しそうじゃないですか?

文書が複数の言語で書かれた可能性を考えます。いくつかの言語の文字が、同じ文書の中で隣り合っています。(ヒント:プログラムはこのような手法をよく使って"モード"を切り替えます。ロシア語のkoi8-rを使っていたら、241は「Я」になり、次にMacのギリシア語モードになったら241は「ώ」になります。)
もちろん、このような文書であっても検索したくなるでしょう。

さぁ、沢山泣きましょう。あなたの知っている文字列の話はすべて間違っているのです。"plain text"というものは存在しません。

4.2. Unicode


Unicodeを入力せよ

すべての言語のすべての文字を表現するために設計されたシステムが、Unicodeです。アルファベット、文字、表意文字を4バイトの数字で表します。

各数字は、文字を一意的に表しており、世界中のいずれかの言語で使われます(すべての数字が使われているわけではありませんが、65535よりも多く使われるため、2バイトでは不十分です)。
複数の言語で使われる文字には同じ数字を割り当てますが、語源的に同じでないときは別の数字を割り当てます。いずれにしても、1つの文字に対して数字は必ず1つで、1つの数字に対して文字は必ず1つです。すべての数字は常に1つのものを指しています。つまり、"入力モード"を記録しないでもよいということです。U+0041は必ず「A」を示していて、これは「A」が存在しない言語であっても同じです。

一見すると素晴らしいアイデアです。1つのエンコーディングがすべてを統一していて、複数の言語が同じ文書にある場合でも、入力モードを切り替える必要はもうありません。ところが、すぐに明らかな疑問が湧き上がるでしょう。4バイト?1文字ごとに4バイト?これは極めて無駄です。特に、英語やスペイン語のような言語では1バイト以下(数字256まで)ですべての文字を表現できるのです。中国語のような表意文字であっても1文字2バイト以上は必要ないので、やはり無駄だとわかります。

1文字につき4バイト使うUnicodeエンコーディングは、UTF-32と呼ばれています。32ビット=4バイトだからです。UTF-32は一意的なエンコーディングで、各Unicode文字(4バイトの数字)を受け取り、その数字に対応する文字を表現します。

この方法にはいくつか利点がありますが、最も重要な利点はN番目の文字を一定の時間で探すことができる、ということです。なぜなら、N番目の文字は4xN番目のバイトから始まるからです。欠点もいくつかありますが、最も顕著なものは、どんな1文字にも4バイト使ってしまうということです。

数多くのUnicode文字があっても、最初の65535個以上を使うことは、ほとんどありません。そのため、UTF-16と呼ばれるもう1つのUnicodeエンコーディングがあります(16ビット=2バイト)。UFT-16では0から63353番までの文字を2バイトでエンコーディングしているので、稀にしか使われない65535より大きな「アストラル界」のUnicodeが必要があるときは、泥臭い手法を使います。
最も顕著な利点は、UTF-16はUTF-32よりも2倍も省スペースであることです。各文字は4バイトではなく2バイトだけが必要です(例外を除く)。文字列に「アストラル界」の文字がないと想定するなら、文字列の中からN番目の文字を一定時間で探すことはこのエンコーディングでも簡単です(そうではないとわかる直前までは、よい想定です)。

UTF-32とUTF-16どちらにも、一見ではわからない欠点があります。異なるコンピュータ システムは、それぞれ別の方法で個々のバイトを保存しています。つまり、U+4E2DというUTF-16文字は、システムがビッグエンディアンかリトルエンディアンかによって「4E 2D」か「2D 4E」のどちらかで保存されているのです。(UTF-32では他の順序になる可能性もあります)。文書が自分のコンピュータの中でだけ読まれるのであれば、同じコンピュータの別のアプリケーション内ではすべて同じバイトオーダーを使うので心配する必要はありません。
しかし、システム間で文書を受け渡したい場合、おそらくワールド・ワイド・ウェブを使うことになりますが、バイトが保存された順番を示す必要が出てきます。

そうしないと、受け取った側のシステムでは2バイトの文字列「4E 2D」がU+4E2DなのかU+2D4Eなのか、知ることができないからです。

この問題を解決するため、複数バイトのUnicodeエンコードでは、"バイトオーダーマーク"を定義します。これは出力されない特別な文字で、文書の最初に入れることでバイトがどの順序で入っているかを示します。

UTF-16では、バイトオーダーマークはU+FEFFです。受け取ったUTF-16ドキュメントがバイト「FF FE」で始まる場合はバイトは順方向で、「FE FF」で始まる場合はバイト順は逆方向だとわかるのです。

しかし、特にascii文字を大量に扱うときは、UTF-16であっても理想的だとは言えません。考えてみると、中国語のウェブページもascii文字を大量に含んでいます。出力される中国語の文字を囲んでいるすべての要素と属性がascii文字なのです。

N番目の文字を一定時間で探すことができるのはよいことですが、「アストラル界」の文字の問題が厄介なものとして残っています。つまり、すべての文字が2バイトであるという確証がないため、別のインデックスを用意しないとN番目の文字を一定時間で探すことはできないのです。そして実際のところ、世の中で使われるasciiテキストというのは、確かに多いのです・・・。

これらの問題を考え抜いた人たちがいて、たどり着いた答はこれです。


UFT-8


UFT-8はUnicodeの長さを変えるシステムです。つまり、文字によってバイト数が違うのです。ascii文字(A-Zなど)に対しては、UTF-8では1文字1バイトを使います。実際、最初の128文字(0-127)ではUTF-8とasciiはすべて同じバイトを使用しているので、区別がつきません。

"Extended Latin" の ñ やöは2バイトです。(バイトはUTF-16にあるような単純なUnicodeコードポイントではなく、複雑なビットも含まれています。)中国語の文字で「中」であれば3バイトです。ほとんど使用されない「アストラル界」の文字は4バイトを使います。

UTF-8の欠点: 文字のバイト数が異なるため、N番目の文字を探すのはO(N)操作になります(※線形操作。Nに比例した計算量)。つまり、文字列が長ければ長いほど、文字検索に時間がかかるのです。さらに、文字をバイトにエンコード、デコードするときにビットをいじくり回さなければなりません。


UTF-8の利点: 一般的にascii文字に対して、非常に強力なエンコーディングです。拡張ラテン文字ではUFT-16と遜色がありません。中国語に関してはUTF-32よりも優れています。また、ビット操作の性質でバイト順の問題がありません(ここでは数学的な証明をしないので信じてもらうしかありませんが)。UTF-8でエンコードされた文書は、どのコンピュータでも同じバイトの流れを使うのです。

4.3. 没頭しよう


Python3では、すべての文字列はUnicode文字の列です。UTF-8でエンコードされているPython文字列というものは存在しません。また、CP-1252でエンコードされているPython文字列というものも、存在しません。この文字列はUTF-8だろうか?という質問は意味がありません。UTF-8というものは、文字をバイト列に変換する方法なのですから。特定の文字エンコーディングで、文字列をバイト列に変えたいときは、Python3が助けてくれます。同様に、バイト列を文字列に変換したいときもPython3が助けてくれます。バイトは文字ではありません。バイトはバイトなのです。文字は抽象概念です。文字列は、そのような概念が並んでいるものなのです。

>>> s = '深入 Python'    # ①
>>> len(s)               # ②
9
>>> s[0]                 # ③
'深'
>>> s + ' 3'             # ④
'深入 Python 3'

①クォーテーションで囲み、文字列を作ります。Pythonではシングルクォートでもダブルクォートでもどちらも使えます。

②ビルトインのlen()関数は、文字列の長さ、つまり文字数を返します。この関数はリスト、タプル、セット、辞書の長さを知るために使ったものと同じです。文字列は文字のタプルのようなものだと言えます。

③リストの要素を取得するときと同様に、インデックス表記によって文字列から文字を取り出すことができます。

④リストのときと同様に、文字列を+演算子で結合することができます。

4.4. 文字列フォーマット


humanize.pyをもう1度見てみましょう。

SUFFIXES = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],         # ①
            1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']}

def approximate_size(size, a_kilobyte_is_1024_bytes=True):
    '''Convert a file size to human-readable form.                          # ②

    Keyword arguments:
    size -- file size in bytes
    a_kilobyte_is_1024_bytes -- if True (default), use multiples of 1024
                                if False, use multiples of 1000

    Returns: string

    '''                                                                     # ③
    if size < 0:
        raise ValueError('number must be non-negative')                     # ④

    multiple = 1024 if a_kilobyte_is_1024_bytes else 1000
    for suffix in SUFFIXES[multiple]:
        size /= multiple
        if size < multiple:
            return '{0:.1f} {1}'.format(size, suffix)                       # ⑤

    raise ValueError('number too large')

①'KB' , 'MB', 'GB'・・・は文字列です。

②docstring関数は文字列を表します。これは複数行に渡っていて、3連続のクォートで開始と終了を示します。

③この3連続クォートがdocstringの終点を表します。

④もう1つ文字列があります。例外が出たときに、人間が読めるエラーメッセージとして渡されるものです。

⑤ここには・・・おやおや、これはいったい何でしょうか?Python3では文字列から値への変換をサポートしています。ここでは複雑な表記法が使われていますが、最も基本的な使い方は、1文字の列に値を入れることです。

>>> username = 'mark'
>>> password = 'PapayaWhip'                             # ①
>>> "{0}'s password is {1}".format(username, password)  # ②
"mark's password is PapayaWhip"

①私のパスワードはPapayaWhipではありませんよ。

②ここでは多くのことをやっています。まず、これは文字列リテラルを呼び出すメソッドです。文字列はオブジェクトで、オブジェクトはメソッドを持っています。2つ目に、この記述全体で文字列を判定します。3つ目に、{0}と{1}は置換領域で、format()メソッドに渡された引数と置き換えられます。

4.4.1. コンパウンド・フィールド名

先ほどの例は最も単純な場合で、置き換えの領域は整数だけでした。整数置換の領域は、format()メソッドの引数のリストに対するインデックスとして扱われています。つまり、{0}は第1の引数(username)で置き換えられて、{1}は第2の引数(password)で置き換えられるのです。引数の数だけ場所インデックスを指定できて、引数はいくつでも取ることができます。そして、置換領域はもっと強力なことができます。

>>> import humansize
>>> si_suffixes = humansize.SUFFIXES[1000]      # ①
>>> si_suffixes
['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
>>> '1000{0[0]} = 1{0[1]}'.format(si_suffixes)  # ②
'1000KB = 1MB'

①humansizeモジュールの関数を一切呼び出さずに、データ構造の1つを受け取ることができます。SI単位(1000倍ごとの単位)のリストです。

②複雑に見えますが、そうでもありません。{0}はformat()メソッドに渡す最初の引数si_suffixesを参照します。つまり{0[0]}はformat()メソッドで渡される「最初の引数si_suffixes」の「最初の要素」、つまり'KB'になります。一方、{0[1]}は同じリストの2番目の要素、'MB'を指しています。波括弧の外側のすべてのもの(1000、= 記号、スペース)は変更されません。最終的に得られるのは'1000KB = 1MB'という文字列になります。

この例が示しているのは、フォーマットを設定するPython構文を使うと、データ構造の要素とプロパティにアクセスできるということです。これをコンパウンドフィールド名と呼びます。

このような状況で使うことができます。
  • リストを渡して、インデックスで要素にアクセスする(先ほどの例)
  • 辞書を渡して、キーで値にアクセスする
  • モジュールを渡して、名前で変数と関数にアクセスする
  • クラスインスタンスを渡して、名前を使ってプロパティとメソッドにアクセスする
  • これらを自由に組み合わせて使うこともできます
ちょっと驚くかもしれませんが、すべてを組み合わせた例を見てみましょう。
>>> import humansize
>>> import sys
>>> '1MB = 1000{0.modules[humansize].SUFFIXES[1000][0]}'.format(sys)
'1MB = 1000KB'
このように動作します:
  • sysモジュールはPythonで動作しているインスタンスの情報を保持しています。インポートしたら、sysモジュール自身をformat()メソッドの引数として渡すことができます。置換領域の{0}はsysモジュールを参照します。
  • sysモジュールは、このPythonインスタンスにインポートされたすべてのモジュールの辞書です。キーはモジュール名の文字列で、値はモジュールオブジェクトです。つまり、置換領域{0.modules}ではインポートされたモジュールの辞書を参照しています。
  • sys.modules['humansize']は、つい先ほどインポートされたhumansizeモジュールです。置換領域の{0.modules[humansize]}ではhumansizeモジュールを参照しています。構文の違いに注意しましょう。通常のPythonコードでは、名前の前後に引用符('humansize'というように)が必要ですが、置換領域では辞書のキー名の部分で引用符を省略してもかまいません(humansize となる)。PEP3101 文字列フォーマットの応用 から引用:キー項目のパースのルールは極めてシンプルです。数から始まれば数値として扱い、そうでなければ文字列として扱うのです。
  • sys.modules['humansize'].SUFFIXES はhumansizeモジュールの冒頭で定義された辞書で、 {0.modules[humansize].SUFFIXES} はその辞書を参照しています。
  • sys.modules['humansize'].SUFFIXES[1000] はSI単位のリスト、['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']です。つまり、置換領域 {0.modules[humansize].SUFFIXES[1000]}はこのリストを参照します。
  • sys.modules['humansize'].SUFFIXES[1000][0] はSI単位系の最初の要素、'KB'です。最終的に、置換領域の全体 {0.modules[humansize].SUFFIXES[1000][0]} が2文字の文字列 KB に置換されることになります。

4.4.2. フォーマット指定子

ちょっと待ってください! まだ続きがあります。humansize.pyのコードの不思議な行をもう少し見てみましょう。

if size < multiple:
    return '{0:.1f} {1}'.format(size, suffix)

{1}はformat()メソッドに渡される2番目の引数の単位と置き換えられます。しかし、{0:.1f}というのは何でしょうか?{0}は知っていますが、{:.1f} がわからないでしょう。2番目の部分(コロンとそのあと)はフォーマット指定子を定義していて、置換された変数がどうフォーマットされるのかという追加情報を与えます。

👉 フォーマット指定子は、置換テキストをいろいろな役立つ方法に置き換えることができます。Cにおけるprintf()関数のようなものです。ゼロやスペースを加えたり、文字列を並べたり、小数点精度を管理したり、数値を8進数に変えたりすることもできます。

置換領域では、コロンがフォーマット指定子の開始マークになります。フォーマット指定子".1"は、"小数点第一位に丸め込む"という意味になります(小数点のあと1桁だけ表示)。フォーマット指定子"f"が意味するのは"固定小数点"です(指数表記や他の10進数表記ではありません)。このようなわけで、698.24というサイズと'GB'という単位があったときは、小数点第二位で四捨五入されたあとに単位が付けられて、698.2GBが返ります。

>>> '{0:.1f} {1}'.format(698.24, 'GB')
'698.2 GB'

フォーマット指定子の詳細はPython公式ドキュメントのFormat Specification Mini-Languageに嫌というほど書いてあるので、そちらを参照してください。

4.5 その他の文字列メソッド

フォーマットの他にも、文字列には数々の役に立つ技があります。

>>> s = '''Finished files are the re-  # ①
... sult of years of scientif-
... ic study combined with the
... experience of years.'''
>>> s.splitlines()                     # ②
['Finished files are the re-',
 'sult of years of scientif-',
 'ic study combined with the',
 'experience of years.']
>>> print(s.lower())                   # ③
finished files are the re-
sult of years of scientif-
ic study combined with the
experience of years.
>>> s.lower().count('f')               # ④
6

①Python対話型シェルに複数行の文字列を入力することができます。三重クォーテーションで複数行文字列を開始したら、ENTERを押して改行し、続きを書きます。終了の三重クォーテーションを入力して文字列が終わり、次のENTERでコマンドを実行します(この場合、文字列をsに代入します)。

②splitlines()メソッドは複数行の文字列を受け取って、行ごとのリストにして返します。行の最後の改行記号は含まれていないことに注意します。

③lower()メソッドはすべての文字列を小文字に変換します(同様に、upper()メソッドは文字列を大文字に変換します)。

④count()メソッドは、文字の出現回数をカウントします。実際に"f"は文中に6つあります!

もう1つ、よくある事例を挙げておきます。キー、値のペアのリストでkey1=value1、key2=value2があるとして、これを分けて辞書の形式{key1:value1, key2:value2}にしてましょう。

>>> query = 'user=pilgrim&database=master&password=PapayaWhip'
>>> a_list = query.split('&')                                        # ①
>>> a_list
['user=pilgrim', 'database=master', 'password=PapayaWhip']
>>> a_list_of_lists = [v.split('=', 1) for v in a_list if '=' in v]  # ②
>>> a_list_of_lists
[['user', 'pilgrim'], ['database', 'master'], ['password', 'PapayaWhip']]
>>> a_dict = dict(a_list_of_lists)                                   # ③
>>> a_dict
{'password': 'PapayaWhip', 'user': 'pilgrim', 'database': 'master'}

①split()メソッドは引数としてデリミタが1つ必要です。このメソッドは、文字列をデリミタで分けて文字列のリストに変換します。ここでのデリミタは & ですが、他のものも使えます。

②これで文字列のリストができました。各要素は「キー=値」の形になっています。リスト内包表記を使って、リストの要素すべてを「=」が出てきたときに分けることができます。

split()メソッドにオプションの第2引数を入れて、何回分割したいかを表すことができます。1が入っていれば"1回分割"の意味なので、split()メソッドは2つの要素を返します(厳密には、値は=記号を含んでも構いません。'key=value=foo'.split('=')の場合は、3要素のリスト['key', 'value', 'foo']が返ることになります。

③最終的にはその「リストのリスト」をdict()関数を使って辞書にします。

👉 この例はurl中のクエリパラメータを渡しているように見えますが、現実世界でのurlパースはこれよりもっと複雑です。もしurlクエリパラメータを扱いたければ、わかりにくい特殊な例も対処できるurlib.parse.parse_qu()関数を使ったほうが役に立ちます。

4.5.1 文字列のスライス


定義した文字列から、任意の部分を新しい文字列として取り出すことができます。文字列のスライスはリストと同様に扱えますが、これは文字列が文字のシーケンスであることを考えれば当然のことです。

>>> a_string = 'My alphabet starts where your alphabet ends.'
>>> a_string[3:11]           # ①
'alphabet'
>>> a_string[3:-3]           # ②
'alphabet starts where your alphabet en'
>>> a_string[0:2]            # ③
'My'
>>> a_string[:18]            # ④
'My alphabet starts'
>>> a_string[18:]            # ⑤
' where your alphabet ends.'

①2つのインデックスを指定し、文字列の一部分を取り出す"スライス"と呼ばれる操作です。返り値は新しい文字列で、最初のスライス・インデックスから始まるすべての文字を順番を変えずに持っています。

②リストのスライスのように、負のインデックスを使って文字列をスライスすることもできます。

③文字列は0始まりなので、a_string[0:2]は文字列の最初の2つの要素を返します。a_string[0]から始まり、a_string[2]は含まれません。

④左側のスライス・インデックスが0であるならば、省略しても自動で0と解釈されます。

⑤右のスライス・インデックスが文字列の長さと同じであれば、同じように省略できます。この文字列は44文字なので、a_string[18:]は、a_string[18:44]と全く同じです。

この44文字の文字列でa_string[:18]は最初の18文字を返し、a_string[18:]は最初の18文字以外のすべてを返すという嬉しい対称性があります。一般化すると、文字列の長さに関係なくa_string[:n]は最初のn文字を返し、a_string[n:]は残りを返します。

4.6 文字列 vs バイト


バイトは情報で、文字は抽象概念です。イミュータブルなUnicode文字の連続は文字列と呼ばれます。イミュータブルな0-255の間の番号のシーケンスは、bytesオブジェクトと呼ばれます。

>>> by = b'abcd\x65'  # ①
>>> by
b'abcde'
>>> type(by)          # ②
<class 'bytes'>
>>> len(by)           # ③
5
>>> by += b'\xff'     # ④
>>> by
b'abcde\xff'
>>> len(by)           # ⑤
6
>>> by[0]             # ⑥
97
>>> by[0] = 102       # ⑦
Traceback (most recent call last):
  File "", line 1, in 
TypeError: 'bytes' object does not support item assignment

①bytesオブジェクトを定義するには、b' 「バイトリテラル」記号を使います。バイトリテラルの間のバイトはascii文字やエンコードした \x00から \xff (0から255)の16進数文字になります。

②bytesオブジェクトのタイプはbytesです。

③リストや文字列のように、bytesオブジェクトの長さはビルトイン関数len()でわかります。

④リストや文字列のように、+演算子を使ってbytesオブジェクトを結合できます。結果は新しいbytesオブジェクトになります。

⑤5バイトのbytesオブジェクトに、1バイトのbytesオブジェクトを結合したので、合計6バイトになります。

⑥リストや文字列のように、インデックス表記でbytesオブジェクト中の個々のバイトを取り出すことができます。文字列の場合は文字列が要素ですが、bytesオブジェクトの要素は整数です。具体的には0-255の整数です。

⑦bytesオブジェクトはイミュータブルです。つまり、個々のバイトに代入することはできません。個々のバイトを変更する必要があるときは、文字列スライスか結合演算子を使います(文字列と同じ動作です)。あるいは、bytesオブジェクトをbytearrayオブジェクトに変換します。

>>> by = b'abcd\x65'
>>> barr = bytearray(by)  # ①
>>> barr
bytearray(b'abcde')
>>> len(barr)             # ②
5
>>> barr[0] = 102         # ③
>>> barr
bytearray(b'fbcde')

①ビルトイン関数のbytearray()を使うと、bytesオブジェクトをミュータブルなbytearrayオブジェクトに変換することができます。

②bytesオブジェクトで使えるメソッドや操作は、すべてbytearrayオブジェクトでも使うことができます。

③1点違うのは、bytearrayオブジェクトではインデックス表記によって個々の値を代入できるということです。0-255の整数を代入する必要があります。


>>> by = b'd'
>>> s = 'abcde'
>>> by + s                       # ①
Traceback (most recent call last):
  File "", line 1, in 
TypeError: can't concat bytes to str
>>> s.count(by)                  # ②
Traceback (most recent call last):
  File "", line 1, in 
TypeError: Can't convert 'bytes' object to str implicitly
>>> s.count(by.decode('ascii'))  # ③
1

①データ型が違うため、バイトと文字列を結合することはできません。

②文字列内でのバイトはカウントできません。なぜなら、文字列はバイトではないからです。文字列は文字のシーケンスなのです。「いずれかの文字エンコーディングでデコードしたあと、バイトのシーケンスでカウントする」と考えたかもしれませんが、その際は明示的に記述する必要があります。Python3はバイトと文字列を自動的に変換したりはしません。

③驚くべき偶然ですが、このコード行は「ascii文字エンコーディングを使ってバイト列をデコードし、文字列の中での出現回数を数える」という意味です。

文字列とバイトの共通点:bytesオブジェクトはdecode()メソッドを持ち、文字エンコーディングを使って文字列を返します。文字列はencode()メソッドを持ち、文字エンコーディングを使ってbytesオブジェクトを返します。

前の例では、デコードはどちらかというと直接的に行われていました。asciiでエンコードされたバイト列を文字列に変換していたのです。しかし、文字列をサポートしているエンコーディングであれば、Unicodeでないレガシーエンコードでも同じようにデコードできます。

>>> a_string = '深入 Python'         # ①
>>> len(a_string)
9
>>> by = a_string.encode('utf-8')    # ②
>>> by
b'\xe6\xb7\xb1\xe5\x85\xa5 Python'
>>> len(by)
13
>>> by = a_string.encode('gb18030')  # ③
>>> by
b'\xc9\xee\xc8\xeb Python'
>>> len(by)
11
>>> by = a_string.encode('big5')     # ④
>>> by
b'\xb2`\xa4J Python'
>>> len(by)
11
>>> roundtrip = by.decode('big5')    # ⑤
>>> roundtrip
'深入 Python'
>>> a_string == roundtrip
True

①9文字の文字列です。

②13バイトのbytesオブジェクトです。a_stringをUTF-8でエンコードしたときに得られるバイト列です。

③11バイトのbytesオブジェクトです。a_stringをGB18030でエンコードして得られるバイト列です。

④11バイトのbytesオブジェクトです。a_stringをBig5でエンコードして得られるバイト列です。

⑤9文字の文字列です。byをBig5でデコードして得られる文字列で、エンコード前の文字列と同一です。

4.7 追記:Pythonソースコードの文字エンコーディング


Python3ではソースコード、つまり.pyファイルはUTF-8でエンコードされていることが前提になっています。

👉 Python2では、.pyのデフォルトエンコーディングはasciiでした。Python3ではUTF-8がデフォルトです。

自分のPythonコードに別のエンコーディングを使いたい場合は、各ファイルの1行目にエンコード宣言を入れます。以下の宣言は.pyファイルがwindows-1252でエンコードされていると定義します

# -*- coding: windows-1252 -*-

厳密にいうと、1行目にunix式のhash-bangコマンドが入るのであれば、文字エンコードの再定義は2行目に来ることになります。

#!/usr/bin/python3
# -*- coding: windows-1252 -*-
詳しい情報は、PEP263: Defining Python Source Code Encodingsにありますので、そちらを参照してください。

0 件のコメント:

コメントを投稿