見出し画像

Python チュートリアル 第4版 4.6 関数の定義 を読んで仮引数の正体を探る!

Python チュートリアル 第4版 4.6 関数の定義 を読んで仮引数の正体を探る!

引用>>関数をコールするときに渡される実引数は、コールの時点でその関数にローカルなシンボル表に加えられる。つまり、引数は call by value で渡される (この value とは常にオブジェクトの参照のことであり、オブジェクトの値そのものではない※2)。関数が他の関数をコールしたときは、新しいローカルシンボル表が作られる。

引用>>※2 原注:これは実のところ、call by object reference (オブジェクトの参照渡し) と呼ぶべきかもしれない。ここで可変オブジェクトが渡されると、関数側でオブジェクトに加えた変更 (リストに挿入されたアイテムなど) が、呼び出し側から見えるからである。

えっ?、実引数の値(のコピー)を仮引数に渡すのが call by value で、実引数の値の参照(のコピー)を仮引数に渡すのが call by reference だよね。

だとするなら、『仮引数へ’オブジェクトの参照’ を渡してるから call by reference 。そして、’常にオブジェクトの参照’ を渡してるから call by object reference と呼ぶべきかもしれない。』が正しいと思う。間違ってたらゴメンね‥😶

実は前にも Python が call by value なのか call by reference なのか考察した事があって、その記事が note のマガジンのどこかに残っているを思うけど、まあいいや、この話はこれまでにして、次!

要するに、関数をコールするときに渡される実引数は、そのコールされた関数のローカルなシンボル表に加えられて、その関数の仮引数名への束縛 (name binding) が行われる事になる。

であるなら、次に示すサンプルプログラムの f関数の仮引数 ap と f関数内のローカル変数 ar 、f関数の仮引数 bp と f関数内のローカル変数 br は同じオブジェクトと言う事になる。(同じ関数のローカルシンボル表に加えられる点とオブジェクトの '識別値' が同じ。) なお、関数内の変数の参照については、4.6 関数の定義 を読んでね!

def f(ap, bp):
    print("    f(ap, bp)\n")

    # ar, br は関数内のローカル変数
    print("    ar = a 実行!")
    ar = a
    print("    br = b 実行!\n")
    br = b

    print("    ap", ap, id(ap), "(実引数 a と '識別値' が同じ)")
    print("    ar", ar, id(ar), "(仮引数 ap と '識別値' が同じ。つまり a, ap, ar の '識別値' は同じ)\n")

    print("    bp", bp, id(bp), "(実引数 b と '識別値' が同じ)")
    print("    br", br, id(br), "(仮引数 bp と '識別値' が同じ。つまり b, bp, br の '識別値' は同じ)\n")
    
    print("    br[0] = 99 実行! (bp[0] = 99 と同じでコール元の実引数 b に影響する)\n")
    br[0] = 99

    print("    br", br, id(br), "\n")

    print("    return\n")
    return


a = 1
b = [10, 20]

print("a", a, id(a))
print("b", b, id(b), "\n")

print("f(a, b) 実行!\n")
f(a, b)

print("a", a, id(a))
print("b", b, id(b), "(b[0]が 99 に変更されている)")

実行例


a 1 134107714468144
b [10, 20] 134107713915584 

f(a, b) 実行!

    f(ap, bp)

    ar = a 実行!
    br = b 実行!

    ap 1 134107714468144 (実引数 a と '識別値' が同じ)
    ar 1 134107714468144 (仮引数 ap と '識別値' が同じ。つまり a, ap, ar の '識別値' は同じ)

    bp [10, 20] 134107713915584 (実引数 b と '識別値' が同じ)
    br [10, 20] 134107713915584 (仮引数 bp と '識別値' が同じ。つまり b, bp, br の '識別値' は同じ)

    br[0] = 99 実行! (bp[0] = 99 と同じでコール元の実引数 b に影響する)

    br [99, 20] 134107713915584 

    return

a 1 134107714468144
b [99, 20] 134107713915584 (b[0]が 99 に変更されている)


実行結果を見ると、やっぱり f関数の仮引数 ap と f関数内のローカル変数 ar 、f関数の仮引数 bp と f関数内のローカル変数 br は同じオブジェクトだね!

つまり『関数の仮引数は、コール元の実引数を代入文で代入(束縛)した関数内のローカル変数に等しい。』と言える。

これで仮引数の正体は分かったけど、ついでに次は仮引数への代入を考察してみる。

まずは、代入文の説明をまとめてみた。

代入文は、名前を値に束縛 (name binding) または再束縛 (rebind) したり、変更可能なオブジェクトの属性や要素を変更したりするために使われる。ターゲットが識別子(名前)の場合は、束縛がおこなわれる。そして、ターゲットが識別子 (名前) で識別子がすでに束縛済みの場合は、再束縛がおこなわれる。

たまに、実引数がミュータブルかイミュータブルかで、仮引数への代入文の振る舞いが違うと書かれている説明があるけどそんな事は無い。あくまでもターゲットが識別子かどうかで代入文の振る舞いが異なるだけ。ミュータブルなオブジェクトであってもターゲットが識別子なら再束縛が行われる。

それで、仮引数への代入をテストしたサンプルプログラムがこれ!

def f2(x, y):
    print("    f2(x, y)\n")
    
    print("    x", x, id(x))
    print("    y", y, id(y), "\n")

    print("    x = 9 実行! (再束縛がおこなわれる。コール元の実引数 a には影響しない)")
    x = 9

    print("    y[1] = 99 実行! (リストの要素を変更。コール元の実引数 b に影響する)\n")
    y[1] = 99

    print("    x", x, id(x), '("識別値" が変化している!)')
    print("    y", y, id(y), '("識別値" が変化していない!)\n') 

    print("    y = [100, 200] 実行! (リストはミュータブルだが再束縛がおこなわれる。コール元の実引数 b には影響しない)\n")
    y = [100, 200]
    
    print("    y", y, id(y), '("識別値" が変化してる!) \n')

    print("    return\n")
    return


a = 1
b = [10, 20]

print("a", a, id(a))
print("b", b, id(b), "\n")

print("f2(a, b) 実行!\n")
f2(a, b)

print("a", a, id(a))
print("b", b, id(b), "(b[1]が 99 に変更されている)")

実行例


a 1 138771015797040
b [10, 20] 138771015244544 

f2(a, b) 実行!

    f2(x, y)

    x 1 138771015797040
    y [10, 20] 138771015244544 

    x = 9 実行! (再束縛がおこなわれる。コール元の実引数 a には影響しない)
    y[1] = 99 実行! (リストの要素を変更。コール元の実引数 b に影響する)

    x 9 138771015797296 ("識別値" が変化している!)
    y [10, 99] 138771015244544 ("識別値" が変化していない!)

    y = [100, 200] 実行! (リストはミュータブルだが再束縛がおこなわれる。コール元の実引数 b には影響しない)

    y [100, 200] 138771015245312 ("識別値" が変化してる!) 

    return

a 1 138771015797040
b [10, 99] 138771015244544 (b[1]が 99 に変更されている)


実行結果から、仮引数への代入は、ターゲットが識別子ならコール元の実引数に影響を与えず、そうでなければコール元の実引数に影響を与える事が分かるよね。

ここまで分かれば仮引数の取り扱いに困ることは無いと思うので、これで考察終わり〜やっと関数再帰呼び出し (リカーシブコール) がガンガン書けるようになった気がする〜チャンチャン!


━━おまけ━━
コール元の実引数がリスト。そしてコールされた関数で仮引数のリストの項目を変更。それでもコール元の実引数のリストが変更されないサンプルプログラムを示しておくね! それでは ВУё ВУё (o・・o)/~ またね~♪

def f3(p):
    print("    f3(p)\n")

    print("    p", p, id(p), "\n")
    
    print("    p = p[:] 実行! (自らの shallow コピーを再束縛! 完全な call by value 仮引数にしたい時は deepcopy 関数を使ってね!)\n")
    p = p[:]

    print("    p", p, id(p), '("識別値" が変化してる!)\n')

    print("    p[2] = 99 実行! (コール元の実引数 a に影響しない)\n")
    p[2] = 99

    print("    p", p, id(p), "(p[2] が 99 に変化してる事を確認!)\n")

    print("    return\n")
    return


a = [10, 20, 30]

print("a", a, id(a), "\n")

print("f3(a) 実行!\n")
f3(a)

print("a", a, id(a), "(a[2] は 30 のままで変更されていない)")

実行例


a [10, 20, 30] 140369840851712 

f3(a) 実行!

    f3(p)

    p [10, 20, 30] 140369840851712 

    p = p[:] 実行! (自らの shallow コピーを再束縛! 完全な call by value 仮引数にしたい時は deepcopy 関数を使ってね!)

    p [10, 20, 30] 140369840852480 ("識別値" が変化してる!)

    p[2] = 99 実行! (コール元の実引数 a に影響しない)

    p [10, 20, 99] 140369840852480 (p[2] が 99 に変化してる事を確認!)

    return

a [10, 20, 30] 140369840851712 (a[2] は 30 のままで変更されていない)




#Pythonチュートリアル第4版
#Pythonチュートリアル
#Python #Python3
#関数の定義 #関数定義
#実引数
#仮引数
#callbyvalue
#callbyreference
#callbyobjectreference
#代入文
#代入
#束縛 #namebinding
#再束縛 #rebind