- HOME
- 本当に誰でも読める!! Pythonソースコードの読み方講座(第10回)Python コードを俯瞰する 3:関数内の関数について
2023年4月21日 16:17
本記事は当社が発行しているシミュレーションメールマガジンVol.10の記事です。 シミュレーションメールマガジンの詳細・購読申込はこちら のサポートページから
- Pythonソースコードの読み方講座(第1回) Pythonコードの構成要素
- Pythonソースコードの読み方講座(第2回) classにまつわるコードの読み方
- Pythonソースコードの読み方講座(第3回) キーワード編 1: 構文に関するキーワード
- Pythonソースコードの読み方講座(第4回) キーワード編 2: 続・構文に関するキーワード
- Pythonソースコードの読み方講座(第5回) 関数にまつわるコードの読み方
- Pythonソースコードの読み方講座(第6回) ソースコードに現れる「単語」を読む
- Pythonソースコードの読み方講座(第7回) キーワード編 3:Pythonの生命線?import文について
- Pythonソースコードの読み方講座(第8回)Python コードを俯瞰する 1:コードの領域分割と実行順序
- Pythonソースコードの読み方講座(第9回)Python コードを俯瞰する 2:コードの領域間の「見え方」について
- Pythonソースコードの読み方講座(第10回)Python コードを俯瞰する 3:関数内の関数について
- Pythonソースコードの読み方講座(第11回)yield 文とジェネレータ
- Pythonソースコードの読み方講座(第12回)yield 文とジェネレータ 2
プログラマにもノン・プログラマにも読んでいただきたい「Pythonコードの読み方」講座の第10回です。前回に引き続き 「Python コードを俯瞰する」と題して、Python コードの全体像について解説します。コードを書く方にとっても有用な視点になると思いますのでぜひご一読ください。
前回は領域同士の関係、「コードのある部分から、別の領域がどのように見えるか」について、以下の2つの原則を説明しました。
2つの原則
- 原則1. 自分より外の領域は見えるが、自分の内側の領域は見えない
- 原則2. 変数の値を決めるときは自分の領域から始めて、外の領域を順番に探していく
今回はより複雑な例を用いて、複数の領域にまたがって存在している変数の挙動について観察していきます。
ネストした関数の例
前回の復習も兼ねて、次のような例を見てみましょう:
コード1
a = 1 def func(): a = 2 def inner_func(): print("inner_func:", a) inner_func() print("func:", a) func() print("global:", a)
出力1
inner_func: 2 func: 2 global: 1
コード1. で関数 func
が定義されており、その内側で inner_func
が定義されています。前々回の言い方だと「グローバル領域」「関数内ローカル領域」「ネストした関数内ローカル領域」の3つの領域が存在することになります。
これらの各領域で変数 a
が定義されていますが、それぞれ次のように動作して a
の値を取得しています:
- グローバル領域
原則1. より、自分より内側の領域は見えないので自身で定義したa
の値(=1)を取得します - 関数内ローカル領域
同様に、原則1. より、自身で定義したa
の値(=2)を取得します - ネストした関数内ローカル領域
自身の領域にa
が無いため 原則2. に従って外の領域を探しに行きます。一つ外側の 関数内ローカル領域で見つけたa
の値(=2)を取得します
global
文の利用
ネストした関数内ローカル領域で グローバル領域の値を取得したい場合はどのようにすればよいでしょうか?
前回扱った global
文を利用すれば良いです。次のように inner_func
の先頭で global a
と書くことで変数の値をグローバル領域から取得するようになります。
コード2
a = 1 def func(): a = 2 def inner_func(): global a print("inner_func:", a) inner_func() print("func:", a) func() print("global:", a)
出力2
inner_func: 1 func: 2 global: 1
nonlocal
文の利用
コード1において、inner_func
内で 関数内ローカル領域の a
の値を変更しようとすると何が起こるでしょうか:
コード3
a = 1 def func(): a = 2 def inner_func(): a = a + 1 print("inner_func:", a) inner_func() print("func:", a) func() print("global:", a)
出力3
UnboundLocalError Traceback (most recent call last) (中略) in inner_func() 3 a = 2 4 def inner_func(): ----> 5 a = a + 1 6 print("inner_func:", a) 7 inner_func() UnboundLocalError: local variable 'a' referenced before assignment
a
の値が準備される前に取得している、という旨のエラーが起きてしまいました。これは次のような処理が行われた結果です:
- l.5 で
a
への代入があるため、a
は ネストした関数内ローカル領域 の変数として解釈される - l.5 の右辺を評価するために ネストした関数内ローカル領域 の変数である
a
の値を取得したいが、まだ準備されていないためエラーとなる
今回の場合、次のように nonlocal
文を用いることで inner_func
から a
の値を変更することができます:
コード4
a = 1 def func(): a = 2 def inner_func(): nonlocal a a = a + 1 print("inner_func:", a) inner_func() print("func:", a) func() print("global:", a)
出力4
inner_func: 3 func: 3 global: 1
nonlocal
文は指定した変数が現在の領域よりも外側で、かつグローバル領域ではないことを示します。今回の例では次のような処理が起きています:
- (l.5)
nonlocal
文によりa
の属する領域を決定する。ここではひとつ外側の 関数内ローカル領域 にa
が存在するため、 関数内ローカル領域 が選ばれる - (l.6) 関数内ローカル領域 の
a
の値を1
増やす - (l.7, l.9) ネストした関数内ローカル領域, 関数内ローカル領域 は同じ
a
の値を見ているため、どちらも値が3
になっている
関数閉包
(この節の内容はやや発展的です)
じつは関数そのものと関数が参照している外側の変数は結びついて保持されています。
コード5
def func(): a = 0 def inner_func(): nonlocal a a = a + 1 print("increment a:", a) def inner_func2(): print("value of a:", a) return inner_func, inner_func2 f,g = func() f() f() f() g()
出力5
increment a: 1 increment a: 2 increment a: 3 value of a: 3
このコードでは関数 func
の中で2つのネストした関数 inner_func
, inner_func2
を定義して返しています。
グローバル領域でこれらを呼び出すことで、以下の処理が行われます:
f
(=func
内で定義したinner_func
) の呼び出しが3回行われる
→nonlocal
文によりf
はfunc
で定義したa
と結びついている。呼び出すたびにこれが1
増える
→ 結果、a
の値が3
まで増えるg
(=func
内で定義したinner_func2
) が呼ばれる
→inner_func2
にnonlocal
文は無いが、最初の 原則2. に従ってfunc
で定義したa
と結びついている
→ このa
はf
と結びついているa
と同一のものである
→ 結果、a
の値として3
が出力される
このように、関数とそれに結びついている変数から成るペアのことを関数閉包と呼びます。Python だとデコレータを実装する際などに活用されます。
関数閉包自体は便利な機能ではありますが、今回の a
のように変数の状態を保持・更新する目的であればクラス+インスタンスを使って保持対象を明示するのが良いでしょう:
コード6
class Counter: def __init__(self): self.a = 0 def increment(self): self.a += 1 def show(self): print("value of a:", self.a) c = Counter() c.increment() c.increment() c.increment() c.show()
出力6
value of a: 3
まとめ
2つの原則
- 原則1. 自分より外の領域は見えるが、自分の内側の領域は見えない
- 原則2. 変数の値を決めるときは自分の領域から始めて、外の領域を順番に探していく
変数の参照先の指定方法
- グローバル領域の変数を書き換えたい場合は
global
文を使う (前回) - 自身よりも外側、グローバル領域よりも内側の変数を書き換えたい場合は
nonlocal
文を使う (今回)
3回にわたって Python の領域の分かれ方と領域間の関係について解説してきました。Python コードを読む際に「この変数はどこからきたものなのか」考えることで理解が深まるのではないでしょうか。
なお、「領域間の見え方」は専門的には「名前解決」という仕組みのことになります。より詳しく知りたい公式ドキュメントを参照ください。本記事を読んだ後であれば理解しやすいかもしれません。
S4 Simulation System の開発のほか、機械学習・シミュレーション・数理最適化を幅広く扱い、分野を横断した問題解決に取り組んでいる。
入社後に競技プログラミングをはじめ、PGBATTLE2023にて企業の部団体3位入賞。
著書: 「XAI(説明可能なAI)──そのとき人工知能はどう考えたのか?」リックテレコム
趣味: 合唱