クロージャってどんなときに使うの? ~ 利用場面を 3つ 挙げてみる

  • 507
    Like
  • 0
    Comment

結論を先にまとめると、以下の3つです。


1. グローバル変数の宣言をなるべく減らしたい場合

2. ユーザが引数を与えてカスタマイズ可能な自由度の高い「関数」を生成したい場合

3. 前回、呼び出されて実行されたときの演算結果(値)を内部で保存して、次に呼び出されたときに、前回の結果(値)に対して、さらに同じ処理(演算)を行う関数を生成したい場合


以下、「クロージャ」の定義から、頭の整理まで、分かりやすい参考ウェブサイトへのリンクを張りつつ、見ていきます。

【 定義 】クロージャ

「自分を囲むスコープにある変数を参照できる関数」

引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする。関数とそれを評価する環境のペアであるともいえる。この概念は少なくとも1960年代のSECDマシンまで遡ることができる。


【 参考 】

関数終了後もローカル変数を参照できることのメリット1、グローバル変数の節減

・・・関数内部の変数を永続的に保持することができる。これが答え。
(通常、関数は呼び出されるたびに関数内部の変数はリセットされる。例えば「関数自身が何回呼び出されたか」といった情報を記憶することはできない(関数外部の変数を利用する必要がある)。)

例えるならクラスベースのオブジェクト指向言語のインスタンス(プライベート)変数みたいなものですね。

プログラム初心者の方は「クロージャーなんて使わなくてもグローバル変数使えば解決じゃない?」と思うかも知れません。

確かにグローバル変数は便利です。クロージャーなんて使わなくても大概の物は作れます。
しかし、グローバル変数の数が100、200となったらどうでしょう?
知らぬ間に変数名が重複して意図しない動作をするなんて事態になりかねません。
数が多くなれば変数名も複雑なものになるでしょう。名前を考えるのも一苦労です。

クロージャーを使えば変数名の重複や命名に悩まされることはありません。
簡潔にすることでバグに悩まされる可能性が大幅に減ります。

また、クロージャーを使うことで関数外の変数を参照する必要性が減る為、他のコードと切り離すことが容易(疎結合)になります。そうすることで再利用しやすくなったりメンテナンス性が向上したりと様々な恩恵が得られます。


【 利用場面 1 】

( クロージャの利点1 )

グローバル変数の個数を減らすことができる。

コード内の任意の場所から参照可能であることが求められる変数だけをグローバル変数として定義して、参照元が特定の関数(メソッド)に限定される変数は、利用する言語の「クロージャ」構文をつかって(「クロージャ」が実装された言語の場合)、「それら特定関数」を、別の「関数」のスコープ内で定義して、その外側の関数のスコープ内(且つ 「それら特定関数」のスコープ外)に、変数を宣言すればよい。

  • 「変数」は、「それら特定関数」の上位の名前空間で定義されているので、「それら特定関数」が参照することができる。 さらに、
  • 「変数」は、「それら特定関数」を自身の(スコープの)内側に含む「関数」の内部で定義されているので、その「関数」の外側(上位)の名前空間からは、参照されない。

これにより、

  • コード全体の「グローバル変数」と名前が衝突するリスクが生じない。

ことが保証される。


【 利用場面 2 】

( クロージャの利点2 )

ユーザが引数を与えてカスタマイズ可能な自由度の高い「関数」を生成する「高階関数」として使用することができる。


【 参考ウェブサイト 】

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


( 以下、2件の利用ケースはともに、上記ウェブサイトより転載 )

(ケース1)「円周率の桁数」と「(円の)半径」の2つの変数 自由度をもつ「円の面積を計算する関数」を返す「高階関数」として

内側の関数 circle_area_func() にとっては、この関数がその中で宣言された circle_area_func(pi) で定義された(=関数の引数で受け取った)変数 pi は(自分より)上位の階層の名前空間内に定義されているため、アクセスすることができる。

Python2.7
def circle_area_func(pi):
    """円の面積を求める関数を返す"""
    def circle_area(radius):
        return pi * radius ** 2 #このpiは、circle_area_func()の引数に指定された値

    return circle_area #関数を返り値として返す

#円周率を 3.14 に設定した場合の面積を計算する関数を生成
ca1 = circle_area_func(3.14)

#次に、円周率を3.141592に設定した場合の関数を生成
ca2 = circle_area_func(3.141592)

#上記で作成した2つの関数に、半径=1 を引数に与えて、演算結果を取得
ca1(1)
ca2(1)

#上記で作成した2つの関数に、半径=2 を引数に与えて、演算結果を取得
ca1(2)
ca2(2)

p16.png

(ケース2)フィボナッチ数列を返す関数として

フィボナッチ数列とは?

最初の2つの数は、0, 1から始まり、3つ目以降の数字は、直前の2つの数字(ペア)の足し算の結果になる数列。無限に続く。

Python2.7
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)

3ament.png

4.PNG


【 利用場面 3 】

( クロージャの利点3 )

前回、呼び出されて実行されたときの演算結果(値)を内部で保存して、次に呼び出されたときに、前回の結果(値)に対して、さらに同じ処理(演算)を行う関数を生成することができる。

(ケース)

ある関数が前回、外部から呼び出された実行されたときの結果(値)を内部で保存して、次に呼び出されたときに、前回の結果(値)に対して、さらに同じ処理(演算)を行う関数を返す高階関数として

( 以下、上記ウェブサイトより転載 )

1.PNG

関数 Counter()が返す 関数 function() の実行結果が、function() 内の変数 n に保存されている。

return function () {
        n = n + x;
        return n;
    };

【 参考 】

( 実装ロジックは異なるが、同じ振る舞いを示す関数は、以下でも実装できる )

python 2.7

func = (x+1 for x in range(4))

func.next()
func.next()
func.next()

new_1.PNG

python 2.7
def func(n):
    for i in range(n):
        yield (i+1)

G = func(4)
# list(G)

G.next()
G.next()
G.next()
G.next()

new_2.PNG

new_3.PNG

new_4.PNG

【 クロージャ 使い道 参考 】

【 用途 と 利用場面 】

クロージャには多くの用途がある。

  • ライブラリの設計者はクロージャを関数への引数として渡すことで利用者が挙動をカスタマイズ可能なようにできる。例えばソートを行う関数は比較のコードをクロージャとして引数にとることで利用者が定義した基準でソートできるようになる。

  • クロージャは遅延評価される(呼び出されるまで何も実行しない)ので、制御構造の定義に用いることができる。
    例として、Smalltalk の分岐 (if-then-else) や繰り返し (while、for) を含むすべての標準制御構造は、クロージャを引数にとるメソッドを持つオブジェクトを利用することで定義されている。同様な方法で利用者は自作の制御構造を簡単に定義できる。

  • 遅延評価される引数のように、その値を求めるためのものは揃っているが、まだ値自体は計算されていない、というものを記憶しておくために、追加の引数を持たないクロージャのようなデータ構造を使う。これをサンク(thinkの過去形)という。ALGOL 60の名前渡しの実装において考案された。


【 参考ウェブサイト 】


【 関連ウェブサイト 】