16
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

PythonAdvent Calendar 2022

Day 13

[Python]変数を扱う際の注意点

Last updated at Posted at 2022-12-12

はじめに

  • 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

変数を「値を格納する箱」とたとえることがよくありますが、参照に関してはこのたとえは適切ではないと言えるでしょう。なぜなら、複数の箱は同じオブジェクトを格納できないからです。
「箱」の代わりに、「ラベル」とたとえることができます。
python_var.drawio.png

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']

参考

16
5
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?