• 締切済み

python:関数が複数globalを読むロジック

pythonの次のコードがあります #------------------------------------ #code default_name = "Ichiro" def kyodai1(name=default_name): return f"Hello {name}," def kyodai2(name=None): if name is None: name = default_name return f"Hello {name}." default_name = "Jiro" print(kyodai1(), kyodai2()) # 出力 # Hello Ichiro, Hello Jiro. #----------------------------------- kyodai2()関数では、 name = default_name とあり、default_nameは最初の行の"Ichiro"を呼ぶかと思ったら、関数の下にある"Jiro"を読みに行っています default_nameはglobal変数として default_name = "Ichiro" があるのに、def kyodai2()関数だけ default_name = "Jiro" を読みに行った理由がわからずにいます このロジックを3つのAIにコードを読ませて説明させたところ、うち1つが 「(kyodai2スコープ内で)default_nameが定義されていません。この場合、default_name = "Ichiro"ではなく、default_name = "Jiro"を参照します」 と答えました 質問: この説明が正しいかどうかもわからないのですが、なぜkyodai2()関数は、最初のIchiroではなくJiroを読みにいくのでしょうか? Tutolialで該当箇所がわからずにいます 【回答上のご注意】 回答は、解答(答え)を求めています わたしはプログラマーではないので、昭和的な「自分で考えろ」的なものは求めていません わからなければ答えない自由もあなたにはあります 不明点があれば説明いたします

みんなの回答

回答No.5

う〜む、これはだな・・・・・・。 最初に言っておく。実は直感的な話を言うと # 出力 # Hello Jiro, Hello Jiro. になるのが「正しい」。 繰り返すが、「マトモなレキシカルスコープを持つ」プログラミング言語だと全部同様の結果になる。 // JavaScriptの例 default_name = "Ichiro"; function kyodai1(name = default_name) { return `Hello ${name},`; } function kyodai2(name = null) { if (name === null) { name = default_name; } return `Hello ${name}.`; } default_name = "Jiro"; console.log(`${kyodai1()} ${kyodai2()}`); これも出力はJiro Jiroだ。 いくらファイルの冒頭でdefault_name = "Ichiro";と定義されていても実行直前でdefault_nameが"Jiro"と書き換えられている以上、実行時に参照するdefault_nameは"Jiro"になる。 他のレキシカルスコープ採用の(しかもキーワード引数アリの)言語で試しても同じ結果になるだろう。 レキシカルスコープは別名「字句的スコープ」と言う。「書いた通りにスコープが決定する」と言う意味だ。 つまり、他のスコーピングのシステムに比べると「悩む必要がない」設計になっている。直感的に判断が可能だ。 一方、Pythonは「一応」レキシカルスコープ採用、って言って良い言語だが、実はその実装は極めて不完全なんだ。 んで、だ。 どこからこの問題を持ってきた? この問題は、普遍的なレキシカルスコープの挙動を問うてる問題じゃなくって、実の事を言うと「Pythonのバグ地味た挙動」を問うてる問題なんだ。 問題は、Pythonのスコープの問題ではなくって、「Pythonの関数定義に於いてのキーワード引数の挙動の不審さ」の問題なんだ。 従って、 > Tutolialで該当箇所がわからずにいます はこれ、だ。 4.8.1. デフォルトの引数値: https://docs.python.org/ja/3/tutorial/controlflow.html#default-argument-values つまり、kyodai1の定義時にdefault_nameが"Ichiro"だった為、一旦それを評価した以上「そこから変わらない」と言うバグ地味た挙動が他の「マトモなレキシカルスコープを持った言語」と違う挙動をする原因になってる(これは全然何も得がない、んだ)。 そして、「こういうスタイルでPythonでプログラムしたらダメ」なんだよ。 ダメなプログラムを出して、つまり、重箱の隅をつつくような出題をする、ってのがPythonの「資格試験」の特徴なんだけど、まさしくそのテの問題だと思う。 Pythonにキーワード引数に大域変数で定義したデフォルト値を与えちゃいけない、んだ。この例のように「思わぬ挙動を引き起こす」場合がある、ってこった。 (もっとも「Pythonの危険性を知る」にはいい問題かもしれんが・・・・・・苦笑) プログラミング言語に於いては、この、Pythonのように「実装者のポカ」が影響を与える場合がある。 従って、 > このロジックを3つのAIにコードを読ませて説明させた 極論、「ロジック」じゃねぇんだよ。「実装者がそういう実装をしちまった」以上に理由がない場合がしばしばある。 従って、「資格試験対策」を考えるなら、「ロジックを考える」んじゃなくって「暗記対策」しかあり得ないわけだ。 「重箱の隅をつつく」問題が出る以上、そこには「普遍的な(レキシカルスコープ採用言語に於いての)プログラミングに於いてのお約束」を問うてるわけじゃないんで、当然「暗記での対策」になっちまうわけだ。 Pythonは比較的、「どこを取っても論理的で整然としてる」言語じゃないんだ(もっとも言っちゃうと、かなりの確率でどの言語も「理論的にはどっか破綻してはいるが・・・「人が設計してる以上しょうがない」が)。

ketae
質問者

お礼

実際AIにコードを読ませたあと、実行させると Hello Jiro, Hello Jiro. と出したのがあったので、pythonではない言語の振る舞いではそうなるかなと昨日思っていました 同時にAIはglobal変数の多用は勧めないともいうので、これは問題集上の問題だなと思います ありがとうございます

回答No.4

>のような流れをイメージしました。間違っていたらご指摘ください。 間違っています。 >print(kyodai2()) #最終行で > ↓ 関数kyodai2に飛ぶ >def kyodai2(name=None): > if name is None: > name = default_name > #default_nameがはどこだ?上にglobalの"Ichiro"あるけど、他も探してみよう default_nameは探しません。 default_nameはもう既に「定義済み」で「値が代入済み」なので探す必要はありません。 メモリ内の「グローバル変数一覧」に存在していて「このグローバル変数の現在の値はJiro」と、一覧に記載されています。 default_nameには「一番最後に代入されていた値」が読みだされます。 代入文が「どこにあるか?」は関係ありません。上にあろうが、下にあろうが、関係ありません。重要なのは「一番最後に行われた代入が、どれなのか?」です。 そして、一番最後に行われた代入で何が代入されたかは「グローバル変数一覧」の「このグローバル変数の現在の値」に書かれています。 > return f"Hello {name}." > ↓ >default_name = "Jiro" #あっ、下に"Jiro"があった 繰り返しますが、上にあろうが、下にあろうが、関係ありません。「どれが一番最近に行われた代入なのか」が重要です。 で、今回は「一番最近に行われたのはJiroの代入」だった訳です。

ketae
質問者

お礼

はい。 「一番最近に行われた代入」を理解するため、 default_name = "Jiro" の行を撤去すると、"Ichiro, Ichiro" なのでそこは理解できていると思います。 最初のコードにもどると、この質問の主旨はなぜ def kyodai2(): のスコープの中で、default_name への代入がない しかしglobalで default_name = "Jiro" があった では、なぜdef kyodai1()で「一番最近に行われた代入」であるはずの、Jiroが用いられないのかというのが疑問でした def kyodai1()では、関数内で def kyodai1(name=default_name): と代入が起こってしまっている(既済) def kyodai2(): のスコープ内ではdefault_nameはglobalで「一番最近に行われた代入」であるはずのJiroを出力する global変数に大して「一番最近に行われた代入」=Jiroであるが、def kyodai1()関数の中で、先行してname=default_nameの代入が起こっており、name = "Ichiro"となった (先ほどまでdefault_nameを出力していると思い込んでいた) nameを出力している そう理解しました ありがとうございます

回答No.3

> それぞれ異なるグローバル変数の値を利用するロジックがいまいち? 利用しているのはもちろん同じグローバル変数です。 kyodai1()は関数定義時にdefault_nameという名前の箱に入った値である"Ichiro"を利用し、kyodai2()は関数実行時、つまりprint()される直前の行で箱の中身が"Jiro"に入れ替えられたdefault_nameの値を利用するという違いだけです。

ketae
質問者

補足

default_nameを出力していると思いこんでいましたが、 name を出力していると気づきました ありがとうございます

回答No.2

関数を def kyodai1(name=default_name): と定義した場合、nameのデフォルト値は「関数定義時点に決定」します。 default_nameは、この関数が定義された時点では「ichiroと定義されている」(「ichiroが代入されている、のではない」ことに注意)ので、この関数定義は def kyodai1(name="ichiro"): と同義です。 変数と関数の定義が終わると、実行に移ります。 実行段階に入ると、 default_name = "Ichiro" が実行された後に、関数定義部分は読み飛ばされ、何も実行されずに、 default_name = "Jiro" が実行され、default_nameにJiroが「代入」されます(定義ではなく「代入」である事に注意) そして、 def kyodai1(name=default_name): def kyodai2(name=None): と定義された関数が連続で呼ばれ kyodai2関数の中で if name is None: name = default_name が実行され、結果として Hello Ichiro, Hello Jiro. が出力されます。 最近の高級言語では、定義段階でソースコードを上から下まで走査(スキャン)して、実行段階で再度ソースコードを上から下まで走査(実行)して、のように、1つのソースコードを、何度も繰り返して読み込みます。一回で上から順にいきなり実行している訳ではないので「書いてある順に実行されるとは限らない」ので注意しましょう。

ketae
質問者

お礼

いつも詳しい説明をありがとうございます def がたぶん定義のdefinitionかなとおもっています 今回謎なのは、最終行で print(kyodai1(), kyodai2()) と出力する際、kyodai1()と kyodai2()の2つの関数が実行されているのはわかるのですが、 kyodai1()が"Ichiro"を呼んでいるのはわかります kyodai2()の関数が実行された場合、関数が参照する流れは結局 name = default_name のdefault_nameを探すとき print(kyodai2()) #最終行で  ↓ 関数kyodai2に飛ぶ def kyodai2(name=None): if name is None: name = default_name #default_nameがはどこだ?上にglobalの"Ichiro"あるけど、他も探してみよう return f"Hello {name}."  ↓ default_name = "Jiro" #あっ、下に"Jiro"があった  ↓ #出力 Hello Jiro. のような流れをイメージしました。間違っていたらご指摘ください。

回答No.1

> なぜkyodai2()関数は、最初のIchiroではなくJiroを読みにいくのでしょうか? kyodai2()関数が呼ばれる直前に default_name = "Jiro" が実行され、kyodai2()関数内の name = default_name のdefault_nameは上記で書き換えられたグローバル変数を参照するからです。

ketae
質問者

お礼

ありがとうございます kyodai2()関数が呼ばれるとき、kyodai1()も呼ばれているのに、それぞれ異なるグローバル変数の値を利用するロジックがいまいち?です笑

関連するQ&A