rcmdnk's blog

パーフェクトPython

Pythonでのリストのコピーとか、 関数に引数を渡す時に 未だにハマる事になるのでその辺のメモ。

Sponsored Links

Pythonは参照渡し、だが。。。

Pythonでは関数に値を渡す時にその参照が渡されます。

ですが、渡された関数内で変更されるとき、 渡した値自体が変更されるかどうかは 渡されたオブジェクトのタイプによります。

オブジェクトのタイプはImmutable(変更不可)とMutable(変更可能)に分けられます。

  • Immutable: int, float, str, tuple 等
  • Mutable: list, set, dict 等

このうち、Mutableな値が関数に渡された場合、 中で変更すると値の変更が外にも反映されますが、 Immutableなものに関しては変更されません。

ただ、これもちょっと注意する必要があって、 listでもlist内の項目を変更したり値を追加したりするのではなく、 新たなlist自体を与える様な事をする場合は外では書き換えられません。

まず、intについて次の様なテストをしてみます。

int_test.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python


def f(i):
    print 'Input variables in the function'
    print 'i:', i,  id(i)
    print ''
    i = 2
    print 'After change'
    print 'i:', i,  id(i)
    print ''

print 'id of 1: %s' % id(1)
print 'id of 2: %s' % id(2)

test_i = 1
print 'Input variables'
print 'test_i:', test_i,  id(test_i)
print ''
f(test_i)
print 'After the function'
print 'test_i:', test_i,  id(test_i)

結果は

id of 1: 140469208133032
id of 2: 140469208133008
Input variables
test_i: 1 140469208133032

Input variables in the function
i: 1 140469208133032

After change
i: 2 140469208133008

After the function
test_i: 1 140469208133032

こんな感じ。

まず、Pythonではオブジェクトでなくても、 1とか値が呼び出されるときにもそのためのメモリが確保さあれています。

なので、最初に1140469208133032になっていて、 a=1によってaがこの場所を示す様になっています。

これを関数の中に入れると中でも同じIDを持ってる事が分かります。

その後、2を入れると2のIDを持つ様になりますが、 関数の外に行くとaは1を指したまま。

次に、リストについて。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env python


def f(l):
    print 'Input variables in the function'
    print 'l:', l,  id(l)
    print ''
    l[0] = 4
    l.append(5)
    print 'After change'
    print 'l:', l,  id(l)
    print ''

print 'id of [1, 2, 3]: %s' % id([1, 2, 3])
print 'id of [4, 5, 6]: %s' % id([4, 5, 6])

test_l = [1, 2, 3]
print 'Input variables'
print 'test_l:', test_l,  id(test_l)
print ''
f(test_l)
print 'After the function'
print 'test_l:', test_l,  id(test_l)

print ''
print 'id of [1, 2, 3]: %s' % id([1, 2, 3])

こんな感じ。関数内では[]appendを使ってリストを変更しています。

これを実行すると

id of [1, 2, 3]: 4340072032
id of [4, 5, 6]: 4340072032
Input variables
test_l: [1, 2, 3] 4340072032

Input variables in the function
l: [1, 2, 3] 4340072032

After change
l: [4, 2, 3, 5] 4340072032

After the function
test_l: [4, 2, 3, 5] 4340072032

id of [1, 2, 3]: 4340305288

こんな感じ。 まず、最初にリストそのもののIDを見ていますが、 中身を変えても同じ物を返します。

これはこの時点でlocalな値として作られているリストがあって、 同じ物を使っている、と言う事。

それをtest_lに代入するとそのリストの場所はtest_lのものになります。

関数に入れると同じIDの物が入っていて、中で変更すると 関数の外にも反映されます。

さらに、最後で再び[1, 2, 3]とオブジェクトに結びついてないものを見ると 新たなIDが割り振られています。

この様にint型だと中での変更は外に反映されないが、 list型だとされる、という感じなのですが、次の様な状況があるので 注意が必要です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python


def f(l):
    print 'Input variables in the function'
    print 'l:', l,  id(l)
    print ''
    l = [4, 5, 6]
    print 'After change'
    print 'l:', l,  id(l)
    print ''

test_l = [1, 2, 3]
print 'Input variables'
print 'test_l:', test_l,  id(test_l)
print ''
f(test_l)
print 'After the function'
print 'test_l:', test_l,  id(test_l)

この様にlistに新たなリストを代入すると

Input variables
test_l: [1, 2, 3] 4471934560

Input variables in the function
l: [1, 2, 3] 4471934560

After change
l: [4, 5, 6] 4472167816

After the function
test_l: [1, 2, 3] 4471934560

となって関数内での変更は関数外には反映されません。

つまり、MutableであろうとImmutableであろうと 新しい値を入れた場合には関数の外側には反映されませんが、 Mutableの場合には中身を変更できるのでその中身の変更は 外でも反映される、ということ。

通常のコピー時

これらは何も関数の引数に限るものではなくて、通常の値のコピーでも同じことです。

int型とlist型のコピーを見てみると

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> a = 1
>>> b = a
>>> b = 2
>>> print a
1
>>> a = [1, 2, 3]
>>> b = a
>>> b[0] = 4
>>> print a
[4, 2, 3]
>>> b = a
>>> b = [4, 5, 6]
>>> print a
[4, 2, 3]
>>> print b
[4, 5, 6]

こんな感じでlistでコピーした先で中の値を変更すると 元の値も変更されます。

リストで元の値を変更しないようにするには

b = list(b)

の様に新たなリストとして作って上げればOK。

1
2
3
4
5
6
7
>>> a = [1, 2, 3]
>>> b = list(a)
>>> b[0] = 4
>>> print a
[1, 2, 3]
>>> print b
[4, 2, 3]

ただ、これも多階層になってくると単純には行きません。

1
2
3
4
5
6
7
8
>>> a = [[1, 2, 3]]
>>> b = list(a)
>>> b[0][0] = 4
>>> print a
[[4, 2, 3]]
>>> print b
[[4, 2, 3]]
>>>

と、2重になってる内側のリストを変更しようとすると元の値が変更されてしまいました。

もし、元の値を変更したくない場合には深いコピー(deepcopy)を使います。

1
2
3
4
5
6
7
8
>>> import copy
>>> a = [[1, 2, 3]]
>>> b = copy.deepcopy(a)
>>> b[0][0] = 4
>>> print a
[[1, 2, 3]]
>>> print b
[[4, 2, 3]]

と変更が元に反映されません。

ちょっとIDを見てみると

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> import copy
>>> a = [[1, 2, 3]]
>>> b = a
>>> c = list(a)
>>> d = copy.deepcopy(a)
>>> print id(a), id(a[0]), id(a[0][0])
4328305728 4328306232 140496177522408
>>> print id(b), id(b[0]), id(b[0][0])
4328305728 4328306232 140496177522408
>>> print id(c), id(c[0]), id(c[0][0])
4328308104 4328306232 140496177522408
>>> print id(d), id(d[0]), id(d[0][0])
4328307672 4328305872 140496177522408

な感じで普通のコポーでは全てが同じID、 listを使ったものでは中のリストは同じID、 最後のdeepcopyは中のリストも別のIDで別物になっていることが分かります。

デフォルト引数の罠

関数の引数に関して、もう一つ、 デフォルト引数を与える時に結構見逃しがちな罠があります。

Pythonのデフォルト引数は

1
2
def f(a=1):
    ...

みたいにするとデフォルト引数を指定できて、 この関数には引数として値を与えなくてもデフォルトの値が使われる様になりますが、 このデフォルトの値は関数が初めて使われた時に初期化されて以降、 値が保存されます。

ちょっと分かりづらいですが、 Immutableなオブジェクトをデフォルト引数に使う場合は特に注意する必要はありません。

変更不可なので最初にa=1となったらその後このaは常に1になります。

ですが、Mutableなlist等にデフォルト引数を使うと、 その値を変更してしまうとデフォルトの値が変わります。

1
2
3
4
5
6
7
8
9
10
11
>>> def f(a=[1, 2, 3]):
...     return a
...
>>> x = f()
>>> print x
[1, 2, 3]
>>> x[0] = 4
>>> y = f()
>>> print y
[4, 2, 3]
>>>

の様に、関数のデフォルト引数の値を外に返すだけの関数ですが、 そのリストを変更すると、次に関数を呼ぶ時にそのデフォルト引数も変更されてる事が分かります。

勿論、関数の中でa.append(4)などして変更した場合にもその次の時から 変更されたaがデフォルトの値になります。

これを避けるためには、Mutableなものをデフォルト引数として使わないか、 使う場合には 中で1回list(a)なり必要ならdeepcopyを使って 元に影響しない物を用意してから使うか、

1
2
3
4
def f(a=None):
    if a is None:
        a = [1, 2, 3]
    ...

の様にデフォルト引数としてはNoneとかを指定しておいて、 関数の中でNoneのままなら決まった値にする、みたいな事をするか。

通常そのまま渡されたリストを変更しようとは余り思わないかもしれませんが、 コピーしたつもりでも普通にa = bとしただけだったり 多階層のlistやdictの場合にdeepcopyを使わずにコピーすると 思わずデフォルト引数の値を変更してしまうかもしれないので注意が必要です。

Sponsored Links
Sponsored Links

« Google DevelopersのPageSpeed Insightsを参考にしてサイトを高速化 Pythonの'=='と'is'、ついでにJavaの'=='と'equals'について »