2014/09/23

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() は何を出力するでしょうか? 10 ? 1 ?それとも None でしょうか?答えは「 1 」です。

なぜなら、 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 を表すセルオブジェクト
以上です。 他にもよい例が出てこれば追加していければと思います。 参考 Pythonのクロージャ その1 Closures in Python - Stack Overflow a life of coding: Closures in Python Python Closures Explained | Shut Up and Ship Understanding Python's closures Closure (computer programming) - Wikipedia, the free encyclopedia

2 件のコメント:

c.c. さんのコメント...

分かり易く書かれていて良かったです

ゴトウハヤト さんのコメント...

c.c. さん、コメントいただきありがとうございます。