「参照」に関するポイントまとめ 【Python】

programming Python

Python の「参照」に関して、サンプルコードに基づく形でポイントをまとめておきます。
また、Python の「参照渡し」を回避するスクリプト(より正確には「参照」の値渡しを解消するスクリプト)についても、まとめておきます。

以下の環境で動作確認をしています。
環境: Windows 10、Python 3.x

背景:Python の引数では「参照」の値が渡される!

他のプログラミング言語と比べたとき、Python の注意点として、関数の引数では「参照」(「参照」の値)が渡されるという点があります。

参照渡しなのか、値渡しなのか、参照の値渡しなのか、…と観念的な話を始めてしまうとそれだけで終わってしまいます。

実際に動かしてみるのが最も話が早いと思いますので、特徴的なところをサンプルコードの形でまとめておくことにします。
末尾にサンプルコードをまとめていますので、まずは動かしてみてください。
行数が多いため、サンプルコードと実行結果は末尾に貼っておきます。

注記
なお、ネット検索をすると、「Python の参照渡し」と書かれたサイトが多数出てきます。しかし、末尾の「外部リンク」を確認してみると、「Pythonの『参照(reference)』が『値渡し(call by value)』で関数に渡される」という記載になっています。
こちらのほうが正しい表現のようです(修正したほうがよさそうなサイトが多数ある)ので、注記をしておきます。

サンプルコードの説明

テスト1:リストを渡して要素を書き換えるとどうなるか

・ 下記のサンプルコードで、まず、関数の定義 def … は読み飛ばしてください。
“test 1” としたところで「参照」を渡す事例です。
リスト list1 = [‘a’, ‘b’, ‘c’] を定義し、function1( ) にリストを渡し、戻り値 list2 を受け取っています。
途中で print() を入れているのは単なる確認のためです。
・ また、print( id( list1 ) ) 等としているところで、list1 の id を出力しています。(id は、オブジェクトごとに固有の値であって、C言語のアドレス、ポインタに対応するもの程度の理解でよいと思います。)
なお、サンプルコードと実行結果の記載で、id の数値自体は例示としています。実行毎に、また、環境によって、id の値自体は変わります。
・ function1 について、冒頭の def function1 としたところで内容を定義しています。
受け取ったリストについて、リストの実体はそのまま維持して、リストの最初の要素のみを書き換え、return で返す処理としています。
したがって、実行結果は、戻り値 list2 の最初の要素が ‘aa’ に書き換わっています。
・ ところが、引数として渡した list1 の側も内容が書き換わっています。結果として、list2 と list1 は、内容が同一となっています。
・ この動きから、Python の関数の引数でリストを渡したとき、引数は、リストの「値」ではなく「参照」が渡されており、list1 と list2 は、実体が同一(id が同一)となっていることがわかります。list1 の内容を書き換えて list2 に返すと、list1 の内容も書き換わる動きとなります。
・ その後、list2[1] = ‘bb’ として、list2 側だけを修正します。
すると、list1 と list2 は参照が同じなので同一の実体を指しており、明示的には書き換えた覚えのない list1 側も内容が書き換わります。一か所しか書き換えていないのに、二か所が書き換わる(二か所が書き換わったかのように動く)ということです。

テスト2: 関数内でリストを新たに定義しなおすとどうなるか

・ つぎに test 2 に進みます。function2 についても同様に、def function2 としたところで定義しています。
やっていることはほとんど同じなのですが、function2 では、list0 = … として、戻り値とするリスト list0 を新たに定義しなおしています。
・ function1 では、受け取ったリスト list0 を維持したまま、最初の要素だけを書き換えています。したがって、全体としての list0 の id は維持したままです。これに対し、function2 では、list0 を受け取ってはいるものの、関数の中で、list0 = … として、新たに list0 を定義しなおしているため、以後、別の id となっています。
function2 の実行後、戻り値 list2 は、list1 とは異なる id となっています。
・ その後、取得した list2 について、内容を変更します。すると、list2 のみが更新( ‘bb’ )されます。list1 と list2 は実体が異なるので、引数とした list1 には影響しません。list1 と list2 は、別々の内容になっています。

・ ここで見方を変え、function1 と function2 の内容(def 文)はブラックボックスだったとみなします。そして、一連の処理の外観だけを見比べてみます。
すると、サンプルコードの外観、見かけとしては、function1 も function2 もまったく同じ処理になっています。ところが、list2 を書き換えたとき、list1 が書き換わってしまうか、書き換わらないかで、実際のプログラムの動き、処理結果が異なることがわかります。
・ つまり、関数を実行し、得られた結果を後の処理で使おうと思ったとき(list2 を書き換えようとしたとき)、もとの引数に影響するか否か(引数 list1 が書き換わるのかどうか)は、関数の内部の処理に依存する(def 文の内容を追わないことには引数の動きがどうなるのか特定できない)ことになります。
いいかえると、呼び出される側の関数の定義に依存して、呼び出す側の処理内容が影響を受けてしまう(処理を関数化してブラックボックス化できない。ブラックボックス化しようとすると、バグの要因となる。)ことになります。理由は、「参照」が渡されているからです。
・ function2 は、新たにリスト list0 を定義した例としました。ところが、リストの各要素を決めるとき、引数で受け取った要素を使ってしまっています。するとリストの各要素については、要素ごとに再び、上記の「参照」の問題が生じうることになります。例えば、リストの各要素にさらにリストが入っていると、同じことが生じえます。(処理結果のリストを書き換えると、元の渡した側のリストが書き換わってしまい、ごちゃごちゃになってしまう。)

テスト3: deepcopy を使って Python の「参照」を渡す問題を回避する

・ つぎに test 3 に進みます。test 3 の function3 では、上記の問題をなくすため、「参照」の影響が出ないようにする事例です。つまり、上記の「参照」の問題を回避するためにコピー (deepcopy) を行い、別の実体を作ります。これにより、参照を渡してしまう問題を回避するスクリプトの事例です。
・ サンプルコードでは、function3 で受け取った引数を deepcopy しています。これにより、引数と新たに作ったオブジェクトでは別の id となっています。
まったく別の実体(id)となったので、function3 の実行後、元の引数や戻り値をそれぞれ変更しても、互いに影響することがなくなります。
・ いいかえると、「参照」を渡す際、渡された関数内で deepcopy をして使うようにすれば、リスト間で値が影響しあうことがなくなるという点で、プログラムの複雑度を下げることができることになります。
複雑度が下がるということは、その後のコーディング、レビュー、ソフトウェアテストのコスト低減につながります。

テスト4: int型、str型等を渡すとどうなるか

・ test 4 の function4 は、リスト(ミュータブル)ではなく、整数(イミュータブル)を渡した例です。
・ function4 は、引数で受け取った値に1を足すだけの関数です。足したところで別の id が振られていることを確認できます。したがって、数値や文字列を渡すだけの場合は、もとの引数に影響するのかどうかは気にする必要はないということになります。

ミュータブルとイミュータブルで挙動が違う!

・ リスト型や int 型等のオブジェクトがあったとき、内容(要素)の書き換えが起こりうるものをミュータブル、内容の書き換えが起こらないものをイミュータブルといいます。
・ ミュータブルなオブジェクトには、list型、dict型などがあります。
・ イミュータブルなオブジェクトには、int型、str型、bool型、tuple型などがあります。

・ 内容の書き換えが起こらない(イミュータブルの)場合、オブジェクトを関数等に渡すとき、上記の「参照」に起因する書き換えは注意する必要がないことになります。要素の書き換えが生じないからです。
すると、内容の書き換えに注意を払う必要があるのは、引数として、list 型、dict 型、自由に定義ができる class 型を渡すとき、ということになります。
Python の引数は、「参照」が渡されるからです。

少し考察してみる: なぜこのような仕様にしたのか/なぜ「参照」を渡すのか

ここで、推測も含めた考察をしてみます。
上記のように Python は、関数に値を渡す際、型に応じて(ミュータブルかイミュータブルかに応じて)注意すべき事項や影響範囲が変動することになります。
このため、関数をブラックボックスとみなしてしまうと、意図しない不具合が生じうる言語仕様となっていることがわかります。

ここで、当然の疑問として、初学者が Python 言語を学ぶとき、「最初から deepcopy を渡すようにすればよいではないか」と考えると思います。
関数で引数を渡すとき、すべてコピー渡し(deepcopy)をする仕様にしておけば、上記の「勝手に書き換わる」問題を気にせずに済みます。また、「ミュータブル」、「イミュータブル」も気にせずに済みます。
素人が簡単に使えるということを第一に置くのであれば、素人が何も考えずにプログラムを作成したとき、意図せぬ書き換えが起こらないように仕様を作成すべき、という設計思想もあろうかと思われます。
しかし、Python は、オブジェクト指向言語でもあり、あえて「参照」を渡す言語仕様にしたものと考えられます。
関数で値を渡すとき、デフォルトでコピー渡し(deepcopy)をすることにすると、大きな配列を渡すとき、配列のサイズに比例したすべてのコピーを実行することになります。処理速度は遅くなるものと考えられます。
そこで、素人でも簡単に使えることよりは、処理速度を重視したもの、と考えることができます。

上記のように考えると、「参照」に関する現状もよく納得ができます。このあたりは、ネット検索をしても、なぜこのような仕様になっているのか考察したサイトがあまり見つかりませんでした。そこで、推測の域を出ませんが、あえて公開しておくことにします。

まとめ

Python において、関数の引数が「参照」で渡される点について実際のプログラムの動作に基づく形でまとめました。

ネットで探すと、高度な解説、概念的な解説は見つかります。
ところが、スクリプトが実際にどう動くかわかる形で平易に解説したものがあまり見つかりませんでした。
そこで、ポイントを整理して公開しておくことにします。

とくに、複数のプログラミング言語を知っている人からすると、他の言語と同じと思ってプログラムを作ってしまうと意図しない動きとなりうるため、注意が必要です。

関連リンク
・ メソッドと関数の違いがわかるサンプルコード 【Python】
・ クラスのポイントまとめ 【Python】

外部リンク
・ https://docs.python.org/ja/3.8/tutorial/controlflow.html#defining-functions
抜粋すると、「そうすることで、実引数は値渡し(call by value)で関数に渡されることになります(ここでの値(value)とは常にオブジェクトへの参照(reference)をいい、オブジェクトの値そのものではありません)。」となっています。
Python の関数で渡される「実引数では、オブジェクトへの参照(reference)が、値渡し(call by value)で渡される」と読めます。
ソフトウェアの分野では日本は遅れているので、英語ベースで理解したほうが正確に理解できる印象があります。(日本語に訳そうとすると用語の議論になってしまう。) C言語、C++、Python …等の主要言語を一通り理解されている方に主要言語で端的なサンプルコードを入れた解説をお願いしたいです。各言語でどう動くか差異がわかればそれで終わると思うためです。

サンプルコード

import copy as cp1 

def function1( list0 ): 
    print( "\nfunction1" ) 
    print( id ( list0 ) ) 
    list0[0] = 'aa' 
    print( id ( list0 ) ) 
    print( "" ) 
    return list0 

def function2( list0 ): 
    print( "\nfunction2" ) 
    print( id( list0 ) ) 
    list0 = ['aa', list0[1], list0[2]] 
    print( id( list0 ) ) 
    print( "" ) 
    return list0 

def function3( list0 ): 
    print( "\nfunction3" ) 
    print( id( list0 ) ) 
    list1 = cp1.deepcopy( list0 ) 
    print( id( list1 ) ) 
    print( "" ) 
    return list1 

def function4( n1 ): 
    print( "\nfunction4" ) 
    print( id ( n1 ) ) 
    n1 = n1 + 1 
    print( id ( n1 ) ) 
    print( "" ) 
    return n1 

print( '\n---- test 1 ----\n' ) 

list1 = ['a', 'b', 'c']     # list 

print( list1 )              # ['a', 'b', 'c'] 
print( id( list1 ) )        # 2000385646408

list2 = function1( list1 )  

print( list1 )              # ['aa', 'b', 'c'] 
print( id( list1 ) )        # 2000385646408
print( list2 )              # ['aa', 'b', 'c'] 
print( id( list2 ) )        # 2000385646408

list2[1] = 'bb' 

print( list1 )              # ['aa', 'bb', 'c'] 
print( id( list1 ) )        # 2000385646408
print( list2 )              # ['aa', 'bb', 'c'] 
print( id( list2 ) )        # 2000385646408

print( '\n---- test 2 ----\n' ) 

list1 = ['a', 'b', 'c']     # list 

print( list1 )              # ['a', 'b', 'c'] 
print( id( list1 ) )        # 2000385646344

list2 = function2( list1 ) 

print( list1 )              # ['a', 'b', 'c'] 
print( id( list1 ) )        # 2000385646344
print( list2 )              # ['aa', 'b', 'c']
print( id( list2 ) )        # 2000385646280

list2[1] = 'bb' 

print( list1 )              # ['a', 'b', 'c'] 
print( id( list1 ) )        # 2000385646344
print( list2 )              # ['aa', 'bb', 'c']
print( id( list2 ) )        # 2000385646280

print( '\n---- test 3 ----\n' ) 

list1 = ['a', 'b', 'c']     # list 

print( list1 )              # ['a', 'b', 'c'] 
print( id( list1 ) )        # 2000385646408

list2 = function3( list1 ) 

print( list1 )              # ['a', 'b', 'c'] 
print( id( list1 ) )        # 2000385646408
print( list2 )              # ['a', 'b', 'c'] 
print( id( list2 ) )        # 2000385646344

list2[1] = 'bb' 

print( list1 )              # ['a', 'b', 'c'] 
print( id( list1 ) )        # 2000385646408
print( list2 )              # ['a', 'bb', 'c'] 
print( id( list2 ) )        # 2000385646344

print( '\n---- test 4 ----\n' ) 

n1 = 10 
print( n1 )                 # 10 
print( id( n1 ) )           # 140710299542192

n2 = function4( n1 ) 

print( n1 )                 # 10 
print( id( n1 ) )           # 140710299542192
print( n2 )                 # 11 
print( id( n2 ) )           # 140710299542224

実行結果の例

---- test 1 ----

['a', 'b', 'c']
2000385646408

function1
2000385646408
2000385646408

['aa', 'b', 'c']
2000385646408
['aa', 'b', 'c']
2000385646408
['aa', 'bb', 'c']
2000385646408
['aa', 'bb', 'c']
2000385646408

---- test 2 ----

['a', 'b', 'c']
2000385646344

function2
2000385646344
2000385646280

['a', 'b', 'c']
2000385646344
['aa', 'b', 'c']
2000385646280
['a', 'b', 'c']
2000385646344
['aa', 'bb', 'c']
2000385646280

---- test 3 ----

['a', 'b', 'c']
2000385646408

function3
2000385646408
2000385646344

['a', 'b', 'c']
2000385646408
['a', 'b', 'c']
2000385646344
['a', 'b', 'c']
2000385646408
['a', 'bb', 'c']
2000385646344

---- test 4 ----

10
140710299542192

function4
140710299542192
140710299542224

10
140710299542192
11
140710299542224

 

タイトルとURLをコピーしました