はじめに
- Pythonにおける変数とオブジェクト、不変なオブジェクトと可変なオブジェクト、および可変値を扱う際の注意点についてまとめてみました。
- 動作環境
- Python 3.11.0
オブジェクトについて
Pythonのすべてのオブジェクトは「値」、「データ型」、「ID」を持っています。オブジェクトの値を変更できないオブジェクトは不変な
(immutable)オブジェクトで、値を変更できるオブジェクトは可変な
(mutable)オブジェクトです。
以下、Pythonにおける不変なオブジェクトと可変なオブジェクトのデータ型の一例です。
不変 | 可変 |
---|---|
int | list |
float | dict |
bool | set |
str | byearray |
frozenset | array |
bytes | |
tuple |
type()関数でオブジェクトのデータ型を取得できます。
>>> type("hello")
<class 'str'>
>>> type(300)
<class 'int'>
>>> type([1, 2, 3])
<class 'list'>
変数について
変数は値を格納する箱ではない
Pythonでは、すべての変数がオブジェクトへの参照になります。オブジェクトは固有のIDを持っており、id()関数でオブジェクトのIDを確認することができます。IDとはオブジェクトの識別子で、オブジェクトがメモリ上で割り当てられたアドレス
を指しています。
# str1とstr2は異なるオブジェクトを参照するため異なるIDを持つ
>>> str1 = "hello"
>>> str2 = "hi"
>>> id(str1)
140270955849136
>>> id(str2)
140270955719728
また、複数の変数が同じオブジェクトを参照することができます。
# list1とlist2は同じオブジェクトを参照するため同じIDを持つ
>>> list1 = [1, 2, 3]
>>> list2 = list1
>>> id(list1)
139669259568960
>>> id(list2)
139669259568960
変数を「値を格納する箱」とたとえることがよくありますが、参照に関してはこのたとえは適切ではないと言えるでしょう。なぜなら、複数の箱は同じオブジェクトを格納できないからです。
「箱」の代わりに、「ラベル」
とたとえることができます。
is演算子と==演算子
「is」演算子を使ってオブジェクトの同一性を比較することができます。同じオブジェクト(同じIDを持つ)であればTrueを返します。
>>> str1 = "hello"
>>> str2 = "hi"
>>> list1 = [1, 2, 3]
>>> list2 = list1
>>> str1 is str2
False
>>> list1 is list2
True
一方、「==」演算子は同値性を比較します。同じ値を持つ異なるオブジェクトでもTrueを返します。
>>> num1 = 300
>>> num2 = 300
>>> id(num1)
140414463929584
>>> id(num2)
140414463929648
# 異なるオブジェクトなのでFalse
>>> num1 is num2
False
# 値が同じなのでTrue
>>> num1 == num2
True
代入演算子「=」は参照をコピーする
代入演算子「=」はオブジェクトではなくオブジェクトへの参照をコピーします。
# 値が300の2つの異なるオブジェクトが生成される
>>> num1 = 300
>>> num2 = 300
>>> num1 is num2
False
# num1の参照をnum2にコピーしているので、同じオブジェクトを参照することになる
>>> num2 = num1
>>> num1 is num2
True
不変なオブジェクトを参照する変数の場合、変数の値を更新すると、オブジェクトの値が更新されるのではなく、新しいオブジェクトが生成され、そのオブジェクトを参照する
ことになります。
>>> str1 = "hello"
>>> id(str1)
140213893921904
>>> str1 = "hi"
>>> id(str1)
140213893786608
代入演算子「=」がオブジェクトの複製を作っていると勘違いすると、バグが発生する可能性があります。たとえば可変なオブジェクトを参照する変数を扱う際
に、下記のように気を付ける必要があります。
>>> list1 = [1, 2, 3]
# list1の値をコピーしているつもりだが、参照をコピーしてしまう
>>> list2 = list1
# list型のオブジェクトは可変なオブジェクトなので、値は変更可能
# list2に新しい要素を追加すると、list1も更新されてしまう
>>> list2.append(4)
>>> list2
[1, 2, 3, 4]
>>> list1
[1, 2, 3, 4]
# 同じオブジェクトを参照していることがわかる
>>> list2 is list1
True
事前配置整数と文字列のインターニング
Python(CPython)ではちょっとした最適化として、プログラム開始時に-5~256
までの整数オブジェクトを作成します。参照ごとに重複したオブジェクトを使用するのでなく、1つの整数オブジェクトを参照することでメモリーを節約しています。
>>> num1 = 256
>>> num2 = 256
>>> num1 is num2
True
>>> num3 = 257
>>> num4 = 257
>>> num3 is num4
False
同様に同じ文字列リテラルを表す際にも、オブジェクトを再利用
します。ただし、この最適化はどんな文字列でも見つけられるわけでありません。
>>> str1 = "hello"
>>> str2 = "hello"
>>> str1 is str2
True
>>>
>>> str2 = "he"
>>> str2 += "llo"
>>> str2
'hello'
>>> str1 is str2
False
可変値を扱う際の注意点
ほかにも色々あると思いますが、可変値を扱う際の注意点を3つ取り上げます。
copy.copy()やcopy.deepcopy()で可変値をコピーする
たとえばlistをコピーする際に、以下のようにcopy()メソッドを使うことができます。
>>> list1 = [1, 2, 3]
>>> list2 = list1.copy()
>>> list2
[1, 2, 3]
>>> list2.append(4)
>>> list2
[1, 2, 3, 4]
>>> list1
[1, 2, 3]
>>> list2 is list1
False
また、copy.copy()関数を使うこともできます。
>>> import copy
>>> list1 = [1, 2, 3]
>>> list2 = copy.copy(list1)
>>> list2
[1, 2, 3]
>>> list2.append(4)
>>> list2
[1, 2, 3, 4]
>>> list1
[1, 2, 3]
>>> list2 is list1
False
しかし、上記の2つの方法では、可変値の中に含まれる可変値をコピーできません
。
>>> import copy
>>> list1 = [[1, 2], [3, 4]]
>>> list2 = copy.copy(list1)
>>> list2
[[1, 2], [3, 4]]
>>> list2[0][0] = "hello"
>>> list2
[['hello', 2], [3, 4]]
>>> list1
[['hello', 2], [3, 4]]
# list2とlist1は異なるオブジェクトだが、内部では同じオブジェクトを参照している
>>> list2 is list1
False
>>> list2[0] is list1[0]
True
可変値の中に含まれる可変値をコピーしたい場合はcopy.deepcopy()
を使うことができます。
>>> import copy
>>> list1 = [[1, 2], [3, 4]]
>>> list2 = copy.deepcopy(list1)
>>> list2
[[1, 2], [3, 4]]
>>> list2[0][0] = "hello"
>>> list2
[['hello', 2], [3, 4]]
>>> list1
[[1, 2], [3, 4]]
関数のデフォルト引数に可変値を使わない
関数のデフォルト引数にlistやdictのような可変なオブジェクトを使うと、バグが発生しやすいです。
def sample_func(num, l=[]):
l.append(num)
return l
x = sample_func(4)
print(x)
x = sample_func(5)
print(x)
[4]
[4, 5]
一回目の実行では空のリストが作成され、整数4がリストに追加されたのですが、二回目の実行では、以前作成されたリストを参照しているので、既存のリストをそのまま使うことになっています。
この場合、デフォルト引数をNone
に設定することができます。
def sample_func(num, l=None):
if l is None:
l = []
l.append(num)
return l
x = sample_func(4)
print(x)
x = sample_func(5)
print(x)
[4]
[5]
ループ時にリストの追加と削除をしない
ループ時にリストの追加と削除を行うと、バグが発生しやすいです。
リストの追加
例えば以下の処理では、リストに「"Python"」という文字列を見つけたら、同じリストに同じ文字列「"Python"」を追加しようとしていますが、これを実行すると、無限ループ
になってしまいます。
languages = ["Java", "Go", "Python"]
# 無限ループに陥る
for language in languages:
if "Python" in language:
languages.append(language)
print("要素を追加済み:", language)
...
...
...
要素を追加済み: Python
要素を追加済み: Python
要素を追加済み: Python
要素を追加済み: Python
要素を追加済み: Python
^C
Traceback (most recent call last):
File "/tmp/sample.py", line 7, in <module>
print("要素を追加済み:", language)
KeyboardInterrupt
解決策として、新しいリストを用意します。
languages = ["Java", "Go", "Python"]
new_languages = []
for language in languages:
if "Python" in language:
new_languages.append(language)
print("要素を追加済み:", language)
# new_languagesの要素をlanguagesに追加
languages.extend(new_languages)
print(languages)
要素を追加済み: Python
['Java', 'Go', 'Python', 'Python']
リストの削除
例えば以下の処理では、リストをforループで回し、「"Python"」以外の要素をすべて削除しようとしています。しかし、これを実行すると、要素が削除された途端に、次の要素のインデックスが1つ前に移動するので、「"Go"」がスキップされてしまいます。
languages = ["Python", "Python", "Java", "Go", "Python"]
# "Go"がスキップされる
for i, language in enumerate(languages):
if language != "Python":
del languages[i]
print(languages)
['Python', 'Python', 'Go', 'Python']
解決策として、削除したい要素を除く要素をコピーしたリストを作成し、元のリストを置き換えます。
languages = ["Python", "Python", "Java", "Go", "Python"]
new_languages = []
for language in languages:
if language == "Python":
new_languages.append(language)
languages = new_languages
print(languages)
['Python', 'Python', 'Python']