はじめに
JavaScriptのそれについて書かれた記事のコメント欄が大変興味深かったので、現在業務でメインに使用しているPythonについても改めて認識を整理しようと思いこの記事を書きました。
結論:Pythonは値渡し?参照渡し?
オブジェクトへの参照渡しです。
公式ドキュメントにもそう書いてあります。
https://docs.python.org/ja/3/tutorial/controlflow.html#defining-functions
関数を呼び出す際の実際の引数 (実引数) は、関数が呼び出されるときに関数のローカルなシンボルテーブル内に取り込まれます。そうすることで、引数は 値渡し (call by value) で関数に渡されることになります (ここでの 値 (value) とは常にオブジェクトへの 参照(reference) をいい、オブジェクトの値そのものではありません) [1]。
・・・
脚注[1] 実際には、オブジェクトへの参照渡し (call by object reference) と書けばよいのかもしれません。というのは、変更可能なオブジェクトが渡されると、関数の呼び出し側は、呼び出された側の関数がオブジェクトに行ったどんな変更 (例えばリストに挿入された要素) にも出くわすことになるからです。
なるほどな~。うんうん
極度説明(しなさい)
そもそもPythonは変数がオブジェクトへの参照なので、変数である仮引数は当然オブジェクトへの参照を持つでしょっていうシンプルな話なのですが、さすがにこの公式ドキュメントを読んで納得できる人間はいないと思うので順を追って説明します。
Pythonの変数ってなんだ
下の関数を見てください。
def Hoge():
x = 'hoge'
x = x + 'fuga'
まず、関数Hoge(抽象化して言えばfunctionクラスのインスタンス)は内部に名前とその名前が指すオブジェクトへの参照とのkey-valueペアを持つシンボルテーブルと呼ばれる辞書を持っています。
2行目では、リテラルが値hogeを持ったstr型のオブジェクトを作成し、作成されたオブジェクトへの参照が関数Hogeのシンボルテーブルにxという名前で登録されます。これがPythonにおける変数と呼ばれるものの正体です。
最後に3行目ですが、まず右辺の式では+演算子によってx('hoge')と'fuga'を結合した新しいstr型オブジェクトが作成されます。そして、シンボルテーブルのkey: xのvalueがこの新しいオブジェクトへの参照で上書きされます。
これが正しいことを確認するために、2行目の後と3行目の後でそれぞれxのidを調べてみましょう。
def Hoge():
x = 'hoge'
print(id(x))
x = x + 'hoge'
print(id(x))
Hoge()
43412704
30974448
というわけで、xという名前の示すオブジェクトが再代入(再束縛)によって変わっていることが確認できました。
仮引数 is 変数
仮引数とは、関数が呼び出される際に引き渡された値(実引数)を受け入れるための変数です。変数名だけが先に用意されていると思ってください。
下のプログラムを実行してみましょう。
def Hoge():
x = 'hoge'
print(id(x))
Fuga(x)
def Fuga(y):
print(id(y))
Hoge()
43281408
43281408
ここで大事なのは、関数Hogeのシンボルテーブルに存在するxのvalue: 'hoge'(id:43281408)への参照を、関数Fugaのシンボルテーブルにyという名前でコピーしているということです。
何回か推敲したんですがうまいこと日本語にできなかったので実際のシンボルテーブルを見てください。
def Hoge():
x = 'hoge'
print(f'{locals()} / x id: {id(x)}')
Fuga(x)
def Fuga(y):
print(f'{locals()} / y id: {id(y)}')
Hoge()
{'x': 'hoge'} / x id: 43281408
{'y': 'hoge'} / y id: 43281408
id: 43281408への参照が関数Hogeと関数Fugaのシンボルテーブルそれぞれに存在することが確認できました。
話はどこまでも変数について
def Hoge():
x = ['hoge']
print(f'{locals()} / x id: {id(x)}')
Fuga(x)
print(f'{locals()} / x id: {id(x)}')
def Fuga(y):
print(f'{locals()} / y id: {id(y)}')
y[0] = 'fuga'
print(f'{locals()} / y id: {id(y)}')
y = ['piyo']
print(f'{locals()} / y id: {id(y)}')
Hoge()
{'x': ['hoge']} / x id: 30237384
{'y': ['hoge']} / y id: 30237384
{'y': ['fuga']} / y id: 30237384
{'y': ['piyo']} / y id: 41879688
{'x': ['fuga']} / x id: 30237384
ここまで読めばこの結果についてもすんなり受け入れられると思います。
id: 30237384はy[0] = 'fuga'
で値を変更されていますが、y = ['piyo']
は新しいオブジェクト(id: 41879688)で関数Fugaのシンボルテーブルを書き換えるため、id: 30237384に影響を及ぼさないのです。
で、この挙動なんですが。どこかで見たことありませんか?
def Hoge():
x = ['hoge']
y = x
print(f'{locals()} / x id: {id(x)} y id: {id(y)}')
y[0] = 'fuga'
print(f'{locals()} / x id: {id(x)} y id: {id(y)}')
y = ['piyo']
print(f'{locals()} / x id: {id(x)} y id: {id(y)}')
Hoge()
{'x': ['hoge'], 'y': ['hoge']} / x id: 5989064 y id: 5989064
{'x': ['fuga'], 'y': ['fuga']} / x id: 5989064 y id: 5989064
{'x': ['fuga'], 'y': ['piyo']} / x id: 5989064 y id: 35981384
そうです。別に引数じゃなくても同じなのです。なので冒頭の
- Pythonはオブジェクトへの参照渡し
という覚え方にはあまり意味がなく、
- Pythonの変数はオブジェクトへの参照に名前をバインドしたもの
- バインドの一覧であるシンボルテーブルは関数それぞれが持つ
- 仮引数は変数
という3段階を理解していることが大事なんじゃないかなあと個人的には思っています。というか突然「オブジェクトへの参照渡し」とか言われても意味分からんって。
演習問題
最後に1つクイズを出して終わろうと思います。以下のプログラムの出力はどうなるでしょうか?
def Hoge():
x = ['hoge']
Fuga(x)
print(x)
def Fuga(y):
y = y.append('fuga')
y = ['piyo']
Hoge()
答え
['hoge', 'fuga']