静かなる名辞

pythonとプログラミングのこと


Pythonの文字列は同じ長さでもメモリ消費量が違うときがある

概要

 Pythonの文字列は、内容によって一文字の幅が違います。

 なお、Python3のstrを前提にさせてください。

実験

 sys.getsizeofで測ってみます。これを使うのはちょっと議論の余地がありますが、

object のサイズをバイト数で返します。object は任意の型のオブジェクトです。すべての組み込みオブジェクトは正しい値を返します。サードパーティー製の型については実装依存になります。
オブジェクトに直接起因するメモリ消費のみを表し、参照するオブジェクトは含みません。
sys --- システムパラメータと関数 — Python 3.8.2 ドキュメント

 とりあえず信じていいと信じます。

>>> import sys
>>> sys.getsizeof("hogefuga")
57
>>> sys.getsizeof("ほげふが")
82

 なんかすでにヘンなことになっています。

>>> len("hogefuga")
8
>>> len("ほげふが")
4

 あ、lenが違いました。

>>> sys.getsizeof("ほげふが"*2)
90

 ひらがなで4文字増えて、さっきの82バイトから8バイト増えたので、1文字2バイトで持っている訳です。

 ASCIIのhogefugaでも同じ要領で1文字のメモリ消費量を測ってみましょう。

>>> sys.getsizeof("hogefuga"*2)
65

 (65-57)/8で、1文字1バイトです。

理由

 まあ、こういう仕様が合理的というのは誰でも理解はできるのではないかと思います。単バイト文字なのかマルチバイト文字なのかによって扱いが変わる訳です。

 リファレンスにも記述があります。「Python/C API リファレンスマニュアル」という、普段馴染みのない方に書いてありますが。

Python3.3 の PEP 393 実装から、メモリ効率を維持しながらUnicode文字の完全な範囲を扱えるように、Unicodeオブジェクトは内部的に多様な表現形式を用いています。

Unicode オブジェクトと codec — Python 3.8.2 ドキュメント

 内部表現は1バイトか2バイトか4バイトのどれかです! ASCIIなら1バイト、日本語はだいたい2バイトでしょうね。他の言語とか絵文字記号の類はきっと4バイトもあり得るのでしょう。

 内部でうまく処理するには1文字の長さが固定長でないとめちゃくちゃ都合が悪く、かといって32bitに揃えるのもメモリ効率が悪いので(というかASCII主義のアルファベット話者から見たら無駄なので)、こういう策を取っているのだと思います。詳しくは調査していませんが。

 Pythonの文字列はimmutableなので、これで困らない訳です。操作するときに適切に扱う処理を、組み込みで実装してくれているから。

こうなっているから起こること

 表現できる文字種に応じて内部表現が決まるので、ASCIIの長い文にマルチバイト文字1つ付け足すと内部表現の大きさがほぼ二倍になったりします。

>>> sys.getsizeof("hogefuga"*1000)
8049
>>> sys.getsizeof("hogefuga"*1000 + "あ")
16076

 異なる内部表現の文字列同士を結合すると、当然大きい方に揃えられます。処理速度に差が出るかもしれません。

>>> import timeit
>>> a = "hoge" * 1000
>>> b = "fuga" * 1000
>>> timeit.timeit(lambda : a + b)
0.4121553519998997
>>> c = "ぴよ" * 1000
>>> timeit.timeit(lambda : a + c)
1.6905145230000471

 出ました。

結論

 普通にPythonを使う分には気にする必要はまったくない事柄なのですが、知っておくと文字列がちょっと思っていたのと違うものに見えてくるときがあるかもしれないと思いました。