プログラミングの基本を学ぶ中で最初に出会うものの1つが文字列の概念です。
様々なプログラミング言語と同様に、Pythonの文字列はUnicode文字を表すバイトの配列、つまり文字の配列またはシーケンスです。
Pythonは多くのプログラミング言語と異なり、明確な文字のデータ型を持っておらず、文字は長さ1の文字列とみなされます。
文字列はシングルクォーテーションやダブルクォーテーションを使って定義することができ、例えば a = "Hello World"
や a = 'Hello World'
のようになります。
文字列の特定の要素にアクセスするには、角括弧 ([]
) とアクセスしたい文字のインデックス (インデックスは 0 から始まります) を使用します。
例えば a[0]
を呼び出すと、H
が返されます。
ということで、このコード例を見てみましょう。
a = 'Hello World'
b = 'Hello World'
c = 'Hello Worl'
print(a is b)
print(a == b)
print(a is c+'d')
print(a == c+'d')
比較する文字列はすべて Hello World
という値を持っています (a
, b
, そして c +'d'
). 直感的に、これらの文の出力はすべて True
になると思うかもしれません。
しかし、このコードを実行すると、結果は次のようになります。
True
True
False
True
この出力で直感的でないのは、a is c + 'd'
が False
を返し、a is b
が True
を返していることです。
これによって、同じ値を保持していても、 a
と b
は同じオブジェクトで、 c
は別のオブジェクトであると結論づけることができます。
もし、 ==
と is
の違いをよく知らないなら、 is
は変数がメモリ上で同じオブジェクトを参照しているかどうかをチェックし、 ==
は変数が同じ値を持っているかどうかをチェックする。
この a
, b
, c
の区別は、String Interning の産物である。
注意: コードを実行する環境は、文字列インターニングの動作に影響を与えます。
これまでの例は、現在の最新バージョンのPython(バージョン3.8.5)を使って、非インタラクティブ環境でスクリプトとしてコードを実行した結果です。
コンソール/Jupyterを使用した場合、コードの最適化方法が異なるため、あるいはPythonのバージョン間でも動作が異なります。
これは、環境によって最適化レベルが異なるためです。
文字列のインターナル
Pythonでは、文字列は不変のオブジェクトです。
これは、文字列が一度作成されると、変更や更新ができないことを意味します。
文字列が変更されたように見えても、裏側では変更された値を持つコピーが作成され、変数に代入されており、元の文字列は同じままなのです。
それでは、文字列を変更してみましょう。
name = 'Wtack Abuse!'
name[0] = 'S'
文字列 name
は不変なので、このコードは最後の行で失敗します。
name[0] = 'S'
TypeError: 'str' object does not support item assignment
注意: もし本当に文字列の特定の文字を変更したいのであれば、文字列を list
のような変更可能なオブジェクトに変換し、必要な要素を変更することができます。
name = 'Wtack Abuse!'
name = list(name)
name[0] = 'S'
# Converting back to string
name = "".join(name)
print(name)
これで目的の出力が得られます。
Stack Abuse!
文字列ではなく)リストの中の文字を変更できる理由は、リストがミュータブルだからです – つまり、その要素を変更できるのです。
文字列のインターニングは、各文字列の値のコピーを1つだけメモリに保存する処理です。
つまり、同じ値を持つ2つの文字列を作成する場合、両方の文字列に対してメモリを確保するのではなく、実際にメモリに確保されるのは1つの文字列だけということになります。
もう一方の文字列は、その同じメモリ位置を指すだけです。
この情報をもとに、最初の Hello World
の例に戻ってみましょう。
a = 'Hello World'
b = 'Hello World'
c = 'Hello Worl'
文字列 a
が作られるとき、コンパイラは Hello World
が内部メモリに存在するかどうかをチェックします。
この文字列の値は最初に出現するので、Pythonはオブジェクトを作成してこの文字列をメモリにキャッシュし、 a
をこの参照に向けます。
bが作成されたとき、コンパイラは内部メモリに
Hello Worldを見つけたので、別の文字列を作成する代わりに、
b` は単に以前に割り当てられたメモリを指します。
この場合、a is b
と a == b
となります。
最後に、文字列 c = 'Hello Worl'
を作成するとき、コンパイラはインターネ ットメモリに別のオブジェクトをインスタンス化します。
aと
c+’d’を比較すると、後者は
Hello Worldと評価されます。
しかし、Python は実行時にインターン処理を行わないので、代わりに新しいオブジェクトが生成されます。
したがって、この2つは同じオブジェクトではないので、isは
False` を返します。
is演算子とは対照的に、
==演算子は実行時の式を計算した後に文字列の値を比較します。
Hello World == Hello World` といった具合です。
このとき、 a
と c+'d'
は値としては同じなので、これは True
を返します。
検証
作成した文字列オブジェクトの ID を見てみましょう。
Pythonの id(object)
関数は object
のIDを返す。
このIDは、そのオブジェクトが生きている間、一意であることが保証されている。
これは、そのオブジェクトが生きている間は一意であることが保証されています。
2つの変数が同じオブジェクトを指している場合、 id
を呼び出すと同じ番号が返されます。
letter_d = 'd'
a = 'Hello World'
b = 'Hello World'
c = 'Hello Worl' + letter_d
d = 'Hello Worl' + 'd'
print(f"The ID of a: {id(a)}")
print(f"The ID of b: {id(b)}")
print(f"The ID of c: {id(c)}")
print(f"The ID of d: {id(d)}")
という結果になります。
The ID of a: 16785960
The ID of b: 16785960
The ID of c: 17152424
The ID of d: 16785960
cだけは異なる id を持っています。
すべての参照は同じHello Worldの値を持つオブジェクトを指すようになりました。
しかし、cはコンパイル時に計算されたものではなく、実行時に計算されたものです。
d’文字を追加して生成した d
も、今では a
と b
が指すオブジェクトと同じものを指しています。
文字列の内部処理方法
Pythonでは、プログラマとのインタラクションによって、文字列をインターンする方法が2つあります。
- 暗黙のインターン
- 明示的な内部処理
暗黙のインターリング
Python は文字列が生成された時点で、いくつかの文字列を自動的にインターンします。
文字列がインターンされるかどうかは、いくつかの要因に依存します。
- 空文字列と長さ1の文字列はすべてインターンされます。
- バージョン3.7まで、Pythonはpeephole最適化を使っていて、20文字より長い文字列はインターンされませんでし た。しかし、現在ではASTオプティマイザが使用され、4096文字までの文字列はインターネ ットされます(ほとんどの場合)。
- 関数名、クラス名、変数名、引数名などは暗黙のうちに内部化されます。
- モジュール,クラス,インスタンスの属性を保持するための辞書のキーはインターネ ットされます.
-
文字列はコンパイル時にのみインターンされます。
これは、コンパイル時に値が計算できない場合はインターンされないことを意味します。
-
例えば,以下のような文字列がインターンされます。
a = 'why'
b = 'why' * 5
- 次の式は実行時に計算されるため、この文字列はインターンされません。
b = "".join(['w','h','y'])
- ASCII以外の文字を持つ文字列は、インターンされない可能性が高いです。
思い起こせば、'Hello Worl' + letter_d
は実行時に計算されたので、インターンされないと言いました。
文字列のインターンに関して一貫した基準はないので、コンパイル時/実行時という考え方があります。
これは、コンパイル時に計算できる文字列はインターンされると仮定するものです。
明示的なインターフェイス
Python の暗黙のインターンの条件に当てはまらない文字列によく出くわしますが、任意の文字列をインターンする方法があります。
sysモジュールに
intern(immutable_object)という関数があり、この関数は
immutable_object` (この例では string) をインターン先のメモリテーブルに格納するよう Python に指示します。
以下のように、どのような文字列でもインターンすることができます。
import sys
c = sys.intern('Hello World'+'!')
これは前の例でうまくいくことがわかります。
import sys
letter_d = 'd'
a = sys.intern('Hello World')
b = sys.intern('Hello Worl' + letter_d)
print(f"The ID of a: {id(a)}")
print(f"The ID of b: {id(b)}")
print(f"a is b? {a is b}")
という出力が得られます。
The ID of a: 26878464
The ID of b: 26878464
a is b? True
さて、Pythonで文字列がどのように、そしてどのようにインターンされるかがわかりました。
文字列のインターナショナルの利点
文字列のインターリングには、いくつかの利点がある。
- メモリの節約。メモリの節約: 2つの文字列オブジェクトが同じであれば、別々にメモリに保存する 必要はありません。同じ内容の新しい変数はすべて、インターンされたテーブルリテラル の参照を指すだけです。もし、何らかの理由で、Jane Austenの「高慢と偏見」に登場するすべての単語を含むリストを持ちたい場合、明示的なインターンなしだと4.006.559バイト、それぞれの単語を明示的にインターンすると、785.509バイトのメモリしか必要ありません。
- 比較が速い。比較の高速化:インターンされた文字列の比較は、インターンされていない文字列の比較よりはるかに高速で、プログラムが多くの比較を行う場合に有効である。これは、内部文字列を比較する際に、内容を比較するのではなく、 メモリアドレスが同じかどうかだけを比較すればよいからです。
- 高速な辞書検索。ルックアップのキーがインターンされている場合、文字列比較の代わりにポインタ比較を行うことができます。
文字列のインターナショナルのデメリット
しかし、文字列のインターンにはいくつかの欠点があり、使用する前に考慮すべき点があります。
- メモリコスト: プログラムが、異なる値を持つ多数の文字列を持ち、全体として 比較が比較的少ない場合、インターンしたテーブル自体がメモリを消費するため。つまり、文字列の数が比較的少なく、文字列間の比較の回数が多い場合に、 文字列をインターンすることをお勧めします。
- 時間コスト。時間コスト:
intern()
関数の呼び出しは、インターン先のテーブルを管理しなければならないので、コストがかかります。 - マルチスレッド環境。マルチスレッド環境: インターンされたメモリ(テーブル)はマルチスレッド環境ではグローバルリソースであり、その同期を変更する必要がある。このチェックは、インターン先のテーブルにアクセスするとき、つまり新しい文字列を作成するときだけ必要かもしれませんが、高価になる可能性があります。
結論
文字列のインターリングを使用すると、同じ内容の文字列を複数定義しても、オブジェクトは1つしか生成されないことが保証されます。
ただし、文字列間引きのメリットとデメリットのバランスに留意し、自分のプログラムが恩恵を受けると思う場合にのみ使用する必要があります。
文字列間通信を使用する場合は、必ずコメントやドキュメントを追加し、他のチームメンバーがプログラム内で文字列をどのように扱うか分かるようにすることを忘れないようにしてください。
Python インタープリタの実装や、コードを実行する環境によって結果は異なるかもしれませんが、 intern()
関数を使いこなすために、ぜひ遊んでみてください。
この概念は、あなたのコードの設計とパフォーマンスを向上させるのに役立ちます。
次の就職の面接でも役に立つかもしれません。