本当に誰でも読める!! Pythonソースコードの読み方講座(第10回)Python コードを俯瞰する 3:関数内の関数について

  • HOME
  • 本当に誰でも読める!! Pythonソースコードの読み方講座(第10回)Python コードを俯瞰する 3:関数内の関数について

本記事は当社が発行しているシミュレーションメールマガジンVol.10の記事です。 シミュレーションメールマガジンの詳細・購読申込はこちら のサポートページから

プログラマにもノン・プログラマにも読んでいただきたい「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 文により ffunc で定義した a と結びついている。呼び出すたびにこれが 1 増える
    → 結果、 a の値が 3 まで増える
  • g (= func 内で定義した inner_func2) が呼ばれる
    inner_func2nonlocal 文は無いが、最初の 原則2. に従って func で定義した a と結びついている
    → この af と結びついている 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 コードを読む際に「この変数はどこからきたものなのか」考えることで理解が深まるのではないでしょうか。

なお、「領域間の見え方」は専門的には「名前解決」という仕組みのことになります。より詳しく知りたい公式ドキュメントを参照ください。本記事を読んだ後であれば理解しやすいかもしれません。

豊岡 祥 株式会社 NTTデータ数理システム シミュレーション&マイニング部所属。
S4 Simulation System の開発のほか、機械学習・シミュレーション・数理最適化を幅広く扱い、分野を横断した問題解決に取り組んでいる。
入社後に競技プログラミングをはじめ、PGBATTLE2023にて企業の部団体3位入賞。

著書: 「XAI(説明可能なAI)──そのとき人工知能はどう考えたのか?」リックテレコム
趣味: 合唱
「数理科学の基礎知識」e-book無料ダウンロードはこちら

関連記事