Python

Pythonの引数の渡し方について解説(値渡し・参照渡し)

By 2021年4月4日No Comments

プログラミング言語によって、関数に引数を渡したときの動作は異なり、主に「値渡し」と「参照渡し」という2種類の動作があります。

それぞれの詳細な動作について解説し、Pythonはどちらの動作をするか、またはその具体例を説明します。

メモリやアドレスの基礎知識

「メモリ」とは、コンピュータ上で数値などを記憶しておくための領域のことです。また、メモリのどの位置にデータを記憶しておくかを示す番地のことを、「メモリアドレス」といいます。

上記の図のように、メモリには、変数と対応した番地(アドレス)が振り分けられており、メモリの各区画には、数値や文字列といった実データが記憶されています。

「値渡し」と「参照渡し」

関数に引数を与えて呼び出すことを考えます。コードで示すと、以下のような例になります。

def func(b):
    # 処理

a = 10
func(a)

関数funcを呼び出すと、aの「データ」がbに渡されるわけですが、具体的にどんな「データ」が渡されるかで、「値渡し」と「参照渡し」の2種類に分類されます。

なお、「値渡し」や「参照渡し」の定義は、文脈やプログラミング言語によって異なった説明がなされることがあります。本記事では、一般的に用いられる(PythonやC#などの言語仕様に沿った)定義で説明しているつもりですが、文脈によって定義が変わりうることはご留意ください。

「値渡し」とは

値渡しでは、呼び出し元の引数の「実データ」を、呼び出し先の引数にコピーします。

この例では、変数aに格納された「10」を、変数bに対応したメモリ上にコピーします。

この場合、aとbは独立な値として存在しているため、bの変更がaに、もしくは、aの変更がbに影響を与えることはありません。

コードで説明すると、以下のようになります。(下記コードはPythonに似た擬似言語であると解釈してください。Pythonの正確な動作は後ほど紹介します。)

### ※ 疑似言語 ###
def func(b): # b には 「10」 が渡される。
    b = 20   # bの変更はaに影響を及ぼさない。(a=10のまま)
a = 10
func(a)

なお、Pythonは「値渡し」が実装された言語です。また、その他のプログラミング言語(CやJavaなど)の多くは「値渡し」が実装されています。

「参照渡し」とは

参照渡しでは、呼び出し元の引数の「アドレス」を、呼び出し先の引数にコピーします。

この場合、bの値を変更しようとすると、aにも影響を及ぼします。それはプログラムが、bに格納された、aのアドレスを元にメモリを参照し、aのデータを変更しようとするためです。同様に、aの値を変更すると、bはaのアドレスを参照しているため、bの値も変更されたかのように見えます。

コードで説明すると、以下のようになります。(下記コードは疑似言語です。)

### ※ 疑似言語 ###
def func(b): # b には aのアドレス「00001473」 が渡される。
    b = 20   # bの変更はaに影響を及ぼす。(a=20になる)

a = 10 # アドレス 00001473
func(a)

Pythonでは、参照渡しは実装されていません。参照渡しが実装されている主要なプログラミング言語は、C#やPHPなどです。

Pythonの変数は、必ず「参照値」を持つ

Pythonの変数は、必ず「参照値」と呼ばれる、別領域に記憶された実データを参照するための値を持っています。

実際のメモリ上では、下図のようにデータが管理されています。

変数aに10を代入すると、変数aが指すメモリ上に直接10が記憶されるわけではなく、別のアドレス上に10が記憶されます。その別のアドレスのことを参照値と言い、変数aが指すメモリ上に参照値が記憶されます。ここでは、「00002771」が参照値と呼ばれます。

変数の持つ参照値は、id()メソッドで確認できます。

a = 10
print(id(a))

実行結果

4488788016

参照値は自動で振り分けられるため、実行結果は環境によって異なります。

Pythonは「参照値」の「値渡し」

Pythonは、「参照値」を「値渡し」する言語です。呼び出し元の引数が持つ「参照値」を、呼び出し先の引数にコピーします。

変数aに対応した「アドレス」ではなく、参照値という「実データ」を渡しているので、この動作は「値渡し」であることに注意してください。

コードで説明すると、以下のようになります。

def func(b): # b には aの持つ参照値「00002771」 が渡される。
    # 処理

a = 10 # 参照値 00002771
func(a)

この「参照値」の「値渡し」のことを、「参照の値渡し」や「共有渡し」と呼ぶこともあります。

数値を渡して、別の数値を代入するときの動作

今までの解説で、Pythonが「参照値」の「値渡し」をする言語であることがわかりました。次は、実際にいくつかコードを書いてみて動作を確かめます。

初めに、関数の引数に数値を渡して、関数呼び出し側の引数に、別の数値を代入するケースを確かめます。

a = 10

print('----- a = 10 -----')
print(f'変数:a, データ:{a}, 参照値:{id(a)}')

def func(b):

    print('----- 関数呼び出し -----')
    print(f'変数:a, データ:{a}, 参照値:{id(a)}')
    print(f'変数:b, データ:{b}, 参照値:{id(b)}')

    b = 20

    print('----- b = 20 -----')
    print(f'変数:a, データ:{a}, 参照値:{id(a)}')
    print(f'変数:b, データ:{b}, 参照値:{id(b)}')

func(a)

実行結果

----- a = 10 -----
変数:a, データ:10, 参照値:00002771
----- 関数呼び出し -----
変数:a, データ:10, 参照値:00002771
変数:b, データ:10, 参照値:00002771
----- b = 20 -----
変数:a, データ:10, 参照値:00002771
変数:b, データ:20, 参照値:00004996

この実行結果から、

・関数を呼び出した直後は、aとbの参照値が同じである。(参照値が値渡しされている)

・b = 20 とすると、bの参照値が上書きされ、20という数値を指すようになる。

・b = 20 としたとき、aのデータや参照値には影響がない。

ことがわかります。

更に、具体的にメモリ上の動きを見ていきます。

1. a = 10とすると、aとは別領域に10が記憶される。

2. aには、10が記憶されたメモリへの参照値が記憶される。

3. 関数呼び出しによって、aの参照値が、bに値渡しされる。

4. b = 20とすると、bとは別領域に20が記憶される。

5. bには、20が記憶されたメモリへの参照値が記憶される。

メモリ上では、1〜5のような動作になっています。

関数内で引数に別の値を代入しても、呼び出し元の変数の中身は変わらないことを覚えておくとよいでしょう。

リストを渡して、別のリストを代入するときの動作

続いて、関数の引数にリストを渡して、関数呼び出し側の引数に、別のリストを代入するケースを確かめます。

a = [1, 2]

print('----- a = [1, 2] -----')
print(f'変数:a, データ:{a}, 参照値:{id(a)}')

def func(b):

    print('----- 関数呼び出し -----')
    print(f'変数:a, データ:{a}, 参照値:{id(a)}')
    print(f'変数:b, データ:{b}, 参照値:{id(b)}')

    b = [3, 4]

    print('----- b = [3, 4] -----')
    print(f'変数:a, データ:{a}, 参照値:{id(a)}')
    print(f'変数:b, データ:{b}, 参照値:{id(b)}')

func(a)

実行結果

----- a = [1, 2] -----
変数:a, データ:[1, 2], 参照値:00002771
----- 関数呼び出し -----
変数:a, データ:[1, 2], 参照値:00002771
変数:b, データ:[1, 2], 参照値:00002771
----- b = [3, 4] -----
変数:a, データ:[1, 2], 参照値:00002771
変数:b, データ:[3, 4], 参照値:00004996

リストを代入したときも、数値を代入したときの動作と同じように、

・関数を呼び出した直後は、aとbの参照値が同じである。(参照値が値渡しされている)

・b = [3, 4] とすると、bの参照値が上書きされ、[3, 4]というリストを指すようになる。

・b = [3, 4] としたとき、aのデータや参照値には影響がない。

ことがわかります。

代入時におけるこの一連の動作は、数値やリストに限らず、文字列などの他の型全てで同じ動作をします。

リストを渡して、リストに要素を追加するときの動作

続いて、関数の引数にリストを渡して、関数呼び出し側の引数に、appendメソッドで要素を追加するときの動作を確かめます。

a = [1, 2]

print('----- a = [1, 2] -----')
print(f'変数:a, データ:{a}, 参照値:{id(a)}')

def func(b):

    print('----- 関数呼び出し -----')
    print(f'変数:a, データ:{a}, 参照値:{id(a)}')
    print(f'変数:b, データ:{b}, 参照値:{id(b)}')

    b.append(3)

    print('----- b.append(3) -----')
    print(f'変数:a, データ:{a}, 参照値:{id(a)}')
    print(f'変数:b, データ:{b}, 参照値:{id(b)}')

func(a)

実行結果

----- a = [1, 2] -----
変数:a, データ:[1, 2], 参照値:00002771
----- 関数呼び出し -----
変数:a, データ:[1, 2], 参照値:00002771
変数:b, データ:[1, 2], 参照値:00002771
----- b.append(3) -----
変数:a, データ:[1, 2, 3], 参照値:00002771
変数:b, データ:[1, 2, 3], 参照値:00002771

この実行結果から、

・関数を呼び出した直後は、aとbの参照値が同じである(参照値が値渡しされている)

という点に関しては、先ほどまでと同様です。

ただし、

・b.append(3)とすると、bの要素に3が追加される。bの参照値は変わらずaと同じ。

・b.append(3)とすると、aの要素に3が追加される。aの参照値は変わらない。

という点が違います。

更に、具体的にメモリ上の動きを見ていきます。

1. a = [1,2]とすると、[1,2]の値を持ったリストオブジェクトが、aとは別領域に記憶され、aにはその領域への参照値が記憶される。

2. 関数呼び出しによって、aの参照値が、bに値渡しされる。

3. b.append(3)とすると、bの参照値を元にメモリを参照し、参照された領域のデータを書き換えることで、リストに要素3を追加する。

メモリ上では、1〜3のような動作になっています。

代入操作では、別の領域に新たな値が作成されていたのに対し、append操作では、参照値の指すメモリのデータを直接書き換えます。そのため、同じ参照値を持つ変数aとbは、両方とも要素3が追加されたかのように見えるのです。

なお、このように参照値を介して直接データを書き換えることのできるオブジェクトの性質を、「ミュータブル」といいます。リストの他に、辞書や集合などはミュータブルなオブジェクトです。

一方、数値は、直接データを書き換えることのできない「イミュータブル」なオブジェクトです。イミュータブルなオブジェクトを変更操作した場合、メモリ上の新しい領域に変更結果が記憶され、変数の持つ参照値も更新されます。数値の他に、文字列やタプルなどはイミュータブルなオブジェクトです。

まとめ

・「値渡し」とは、関数呼び出し元の引数の「実データ」を、呼び出し先の引数にコピーする動作である。

・「参照渡し」とは、関数呼び出し元の引数の「アドレス」を、呼び出し先の引数にコピーする動作である。

・Pythonの変数は、「参照値」と呼ばれる、別領域に記憶された実際のデータを参照するための値を持っている。

・Pythonは、「参照値」を「値渡し」する言語である。

・参照値を介して直接データを書き換えることのできるオブジェクトの性質を、「ミュータブル」と言う。リスト、辞書、集合などはミュータブルである。

・直接データを書き換えることのできないオブジェクトの性質を、「イミュータブル」と言う。数値、文字列、タプルなどはイミュータブルなオブジェクトである。

Udemyの動画学習でもPythonを勉強しよう!

「平日の夜の勉強会には時間が間に合わなくて参加できない」「通勤時間のわずかな隙間時間を勉強時間にあてたい」「本ではよく分からないところを動画で理解を深めたい」そんなあなたはUdemyの動画学習がお勧めです!

UdemyのPythonおすすめ33講座レビューリスト
HOSL

Author HOSL

More posts by HOSL