2014年9月25日木曜日

[Python] どうでもいいClosure

最近Closureについての記事を見かけることが多いので、まとめる。
ref(えいご) ref(にほん語)

  1. closureの基本
  2. Closure = function + environment
    これをOOPの文脈で置き換えると
    Class = Method + member(attributes)
    Closureのほうはあくまで関数が主役。
    次のPythonのコードは典型的なClosureの応用
    def fibonacci_factory():
      memo = {}
      def fibonacci(n):
        if n in memo: return memo[n]
        if n < 1: return 1
        memo[n] = fibonacci(n-1) + fibonacci(n-2)
        return memo[n]
      return fibonacci
    
    ファクトリの呼び出しにより、fibonacci関数が作成され関数は内部的に memo というローカル変数への参照を保持し続ける。
    pythonでは、__closure__ attribute を用いて内包している環境を見る事ができる。
    f = fibonacci_factory()
    f(100) # -> 927372692193078999176
    print(f.__closure__) # -> tuple of cell object
    print(f.__closure__[0].cell_contents) # -> function oject of f itself
    print(f.__closure__[1].cell_contents) # -> might be a dictionary
    
    環境が、cell object のタプルとして保存されているのをみることができる。
    もう一度ファクトリを呼び出すと異なる環境を保持するfibonacci関数が作成される。
    g = fibonacci_factory()
    id(g.__closure__[0]) == id(f.__closure__[0]) # -> false
    
    f と g では各々別々のメモ辞書を持ってるのが分かる。
    別の例として、呼び出す毎に内部変数が1ずつ増えていくカウンターを考える

    def counter_factory():
      x = 0
      def _counter():
        nonlocal x  
        x += 1
        print(x)
      return _counter
    
    上の nonlocal せんっげんはpython3から新しく追加されたもので、これを付けないと、下の様なエラーになる。
    counter_factory()()
    ---------------------------------------------------------------------------
    UnboundLocalError                         Traceback (most recent call last)
     in ()
    ----> 1 counter_factory()()
    
     in _counter()
          2   x = 0
          3   def _counter():
    ----> 4     x += 1
          5     print(x)
          6   return _counter
    
    UnboundLocalError: local variable 'x' referenced before assignment
    
    ファクトリ内部関数での x へのアサインメントは上手くいかない。というのはスコープ外の変数への代入だからである。
    従って、x は一つ上のスコープの変数なんですよ、という宣言が必要になる。
    ちなみに、attribute を用いる事でnonlocal宣言を回避して、クロージャっぽいものを作る事もできる。
    def counter_factory():
      def _counter():
        _counter.x += 1
        print(_counter.x)
      _counter.x = 0
      return _counter
    
  3. Closureとアクセサ
  4. 次に、クラスオブジェクト的な操作を模倣することを考える
    例えば、オブジェクトであればアクセサやらプロパティを用いて、内部で保持している変数(attributes)を変更できる。
    同じ事をクロージャでやってみたいが、__closure__ attribute は tuple なので直接変更できない。
    上で用いた nonlocalせんっげん を使って、アクセサ関数を作って一緒に返してやればよい

    def counter_factory():
      x = 0
      def _counter():
        nonlocal x  
        x += 1
        print(x)
      def set_count(new_x):
        nonlocal x
        x = new_x
      def get_count():
        return x
      return (_counter, set_count, get_count)
    
    get_countには nonlocal は必要ない。単に参照するだけだからである
    正直こういった使い方は邪道な気がする

  5. クロージャとクラス変数

  6. 一部の環境を他のクロージャと共有したい場合があるでありそう
    クラスであれば、単にクラス変数を用いれば実現できるが、クロージャだとどうなるだろう?
    そのときはファクトリのファクトリを作って、共有したい変数を第一レベルに置いて各々の関数で保持したい変数を第二レベルにおけばよい。
    大まかな作りは次のようになるはず
    def factory_factory(x):
      share_var = ... # some collections
      def _factory(y):
        each_vars = ... # some other collections
        def __process(z):
          someprocesses
        return __process
      return _factory
    
    こうしてやると、x, share_var は全体で共有することになって、y, each_vars は各々で保持する
    応用として、二つのカウント値を持ったカウンタを作る。
    一つは全体で共有するカウント値で、もう一つは各々別々に持つカウント値である
    def counter_fact_fact():
      whole_count = 0
      def _counter_fact():
        _count = 0
        def __counter():
          nonlocal _count, whole_count
          _count += 1
          whole_count += 1
          return (_count, whole_count)
        return __counter
      return _counter_fact
    
    アンダーバーが多くて醜い(^q^)
    実際に呼び出して使ってみると、ちゃんと動いくうぃ(^q^)
    cf = counter_fact_fact()
    f = cf()
    g = cf()
    f() # -> (1, 1)
    g() # -> (1, 2)
    f() # -> (2, 3)
    g() # -> (2, 4)
    
  7. まとめ
  • 単なるメモ化程度ならばクロージャは便利だし、デコレータと組み合わせると色々な操作がシンプルに書ける
  • 継承をclosureで実装するにはどうすればいいだろうか?
  • 基本的にpythonはOOPLなので、無理せずクラスを作った方が良い場合が殆どだろう
  • どのようにしてクラスオブジェクトとクロージャを使い分けてやればいいのか明確な判断基準が欲しいところ
  • ブログかくのめんどい(^q^)

0 件のコメント:

コメントを投稿