「参照渡し」に関するまとめ 【Python】

programming Python

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

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

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

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

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

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

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

サンプルコードの説明

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

・ 下記のサンプルコードで、まず、関数の定義 def … は読み飛ばして、”test 1″ としたところに進んでください。
テスト1(”test 1″)は、Python で「参照」を渡す事例です。
リスト list1 = [‘a’, ‘b’, ‘c’] を定義し、function1( ) にリストを渡し、戻り値 list2 を受け取っています。
途中で print() を入れているのは内容を確認するためです。
・ print( id( list1 ) ) 等としているところで、list1 の id を出力しています。id は、オブジェクトごとに固有の値です。オブジェクトを識別するために表示しています。(C言語のアドレス、ポインタに対応するもの程度の理解でよいと思います。)
なお、以下のサンプルコードと実行結果の事例で、id の数値自体は例示としています。実行毎に、また、環境によって、id の値自体は変わります。
・ function1 について、冒頭の def function1 としたところで内容を定義しています。
受け取ったリストについて、リストの実体はそのまま維持して、リストの最初の要素のみを書き換え、return で返す処理としています。
したがって、実行結果は、戻り値 list2 の最初の要素が ‘aa’ となっています。
・ ところが、引数として渡した list1 を確認すると、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 は実体が異なるので、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 は、関数に値を渡す際、型に応じて(ミュータブルかイミュータブルかに応じて)注意すべき事項や影響範囲が変動することになります。
関数の処理内容に依存して、関数に渡したリスト等が書き換わる場合/書き変わらない場合の2パターンが発生するということです。
このため、たとえば、関数を完全にブラックボックスとみなしてしまうと、引数が書き換わるか否かは特定ができないことになり、意図しない動作が生じうる言語仕様となっています。

ここで、当然の疑問として、「最初から deepcopy を渡すようにすればよいではないか」と考える人がいると思います。
関数で引数を渡すとき、すべてコピー渡し(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をコピーしました