Python Tips: Python でクロージャを使いたい

Python でクロージャを使う方法をご紹介します。

クロージャとは

一言でいうと、クロージャとは「関数内の変数の名前解決がその関数が 宣言されたときのスコープ で行われるもの」です。

もう少し丁寧にいうなら、クロージャとは

  • 関数内のローカル変数以外の変数の名前解決が
  • 呼び出し時のスコープではなく
  • 宣言時のスコープによって行われるもの。
  • またそのような機能を持った関数のこと。

を指します。

例をあげます。

x = 1
def get_x():
    return x

class MyClass(object):
    x = 10
    print get_x()  # 10 ではなく 1 が返される

MyClass 内の print get_x() は何を出力するでしょうか?答えは「 1 」です。 10 ではないというところがポイントです。

この挙動になる理由は get_x() の中の x は「呼び出し時のスコープではなく関数宣言時のスコープ」から取得され、また、 Python の名前解決は「LEGB」のルールに従って行われる からです。

この仕組みがクロージャです。

ちなみに Wikipedia には次のような説明が載っています。

In programming languages, a closure (also lexical closure or function closure) is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.[1] A closure—unlike a plain function pointer—allows a function to access those non-local variables even when invoked outside its immediate lexical scope.

意訳:

クロージャ(またはレキシカルクロージャ、関数クロージャ)とは、参照環境を伴ったような関数、あるいはその関数への参照のことを指します。参照環境というのは、特定の関数が持つ、ローカル環境にはない変数(自由変数あるいは upvalue )への参照を保持したテーブルのことです。通常の関数ポインタとは異なり、クロージャによって、関数が別のスコープで呼び出されたときにおいてもそこのローカル変数以外の変数にアクセスすることが可能となります。

クロージャの使い方

クロージャの使い方を見てみましょう。

Python では「関数内関数が定義できる」という特徴と「関数がスコープを分ける」という特徴を利用して、次のようなことができます。

# coding: utf-8

def gen_circle_area_func(pi):
    """円の面積を求める関数を返す"""
    def circle_area(radius):
        # ここの pi は circle_area_func に渡された pi が使われる
        return pi * radius ** 2

    return circle_area

# 円周率を 3.14 として円の面積を計算する関数を作成
circle_area1 = gen_circle_area_func(3.14)
print circle_area1(1)  # => 3.14
print circle_area1(2)  # => 12.56

# 次は、円周率の精度をぐっと上げて
# 円周率 3.141592 で円の面積を計算する関数を作成
circle_area2 = gen_circle_area_func(3.141592)
print circle_area2(1)  # => 3.141592
print circle_area2(2)  # => 12.566370614

circle_area1() circle_area2() はいずれも「円の半径を引数に受け取りその面積を返す関数」です。

ロジックは同じですが、それぞれ参照する pi の値が異なるため、計算の精度が異なっています。

このようにクロージャの特徴を使えば、関数を生成して返す関数をひとつ作っておくだけで「ロジックは同じだけどその内部で使用するパラメータが異なる関数」を動的に作成することができます。この仕組みにより、「変数だけでなく処理(=関数)を部品化し使いまわせる」という特徴(関数型言語的特徴)が言語に加わります。

また別の例を見てみましょう。

def fibonacci_func():
    """フィボナッチ数列を返す関数を返す メモイズ機能つき"""
    table = {}  # 計算済みのフィボナッチ数列を格納するテーブル

    def fibonacci(n):
        # 計算したことのある数値についてはテーブルを参照して返す
        if n in table:
            return table[n]

        # 計算したことのない数値についてはフィボナッチ数列の定義どおり計算
        if n < 2:
            return 1
        table[n] = fibonacci(n - 1) + fibonacci(n - 2)
        return table[n]

    return fibonacci

# 関数を生成してから 50 番までのフィボナッチ数列を計算して表示する
f = fibonacci_func()
for i in range(50):
    print f(i)

こちらはフィボナッチ数列を計算して返す関数です。

クロージャのおかげで、グローバル変数を作ることもクラスを作ることもなく、関数だけでメモイズ機能が実現できています。 フィボナッチ数列をメモ化なしで 50 番目くらいまで求めると結構な時間がかかると思うのですが、このようなメモ化を行うと一瞬で出てきます。

ちなみに、クロージャとなった関数には __closure__ というプロパティがあり、その中身をのぞくと、その関数が持つ参照が「セルオブジェクト」として収められていることが確認できます。

print f.__closure__  # => セルオブジェクトのリスト
print f.__closure__[0].cell_contents  # => fibonacci 関数そのものを表すセルオブジェクト
print f.__closure__[1].cell_contents  # => table を表すセルオブジェクト

以上です。

参考