高階関数(クロージャ)とメソッド(クラス)

(書いていたら当たり前な感じのことになった。)

クロージャとオブジェクト

こんな話がある。

  • クロージャ(閉包)があればオブジェクトは作れる。
  • クロージャとオブジェクトの機能としての豊かさは等価だ。
  • オブジェクトは状態を持つ。

あまり厳密な話をする気はないけれど、例えばpythonなどのクラスとクロージャの両方をサポートする言語を使うとき、どちらを使ったら良いか迷うことがあった。

普通にコードを書いている時には一定のガイドラインが頭の中にあるかもしれない。

  • クロージャもクラスも状態を持つ
    • その状態を利用した機能が1つだけで十分な時にはクロージャを使う。
    • その状態を利用した機能が複数存在する時にはクラスにすることを考える。

具体的な話。

例えば以下のような感じ。

引数を受け取りその値を状態として保持する。次に適用する時には、保持した状態との和を返す。

そういう機能を持ったものを作りたい。機能はひとつしかないのでクロージャで書ける。

クロージャで書いたadder
def adder(x):
    def _adder(y):
        return x + y
    return _adder

#------------

adr = adder(10)

print adr(20) # => 30
print adr(30) # => 40
print adr(-10) # => 0

機能がひとつしかないときはクロージャで十分。ただし、機能が複数になるとクロージャだけでは対応しづらくなる。

例えば以下のような機能を追加したいとき

1. 保持した状態を異なる値に変更したい

2. 和だけではなく、差、積、商などを求めたい。

共通する状態を利用する機能が複数ある時クラス(オブジェクト)を作成した方が楽なことが多い。

クラスで書いたAdder
class Adder(object):
    def __init__(self, x):
        self.x = x

    def add(self, y):
        return self.x + y
    def sub(self, y):
        return self.x - y
    def mul(self, y):
        return self.x * y
    def div(self, y):
        return self.x / y
    def update(self, y):
        return self.x = y

# ------------

adr = Adder(10)
print adr.add(20) # => 30
adr.edit(20) #保持する状態変更
print adr.add(20) # => 40

## 名前が良くないけど
print adr.sub(20) # => 0
print adr.mul(100) # => 2000
print adr.div(2) # => 10

これは普通正しい。でも、クロージャでもやり辛いだけでやれないことはない。

クロージャで頑張ったadder
def adder(x):
    state = [x] # pythonのcellオブジェクトのせい
    def add(y):
        return state[0] + y
    def sub(y):
        return state[0] - y
    def mul(y):
        return state[0] * y
    def div(y):
        return state[0] / y
    def edit(y):
        state[0] = y

    return dict(add=add, sub=sub, 
                mul=mul, div=div, 
                edit=edit)

# ------------

adr = adder(10)
print adr["add"](20) # => 30
adr["edit"](20) #保持する状態変更
print adr["add"](20) # => 40

## 名前が良くないけど
print adr["sub"](20) # => 0
print adr["mul"](100) # => 2000
print adr["div"](2) # => 10

ちょっとアクセスの方法が違うけれど、オブジェクトを作ったのと同じことはできている。
アクセスの形式は以下のようなクラスを定義すればまったく同じになる。

アクセスの方法を揃えるためにdictの代わりにObjectLikeを使ったもの
class ObjectLike(dict):
    """これをdictの代わりに使う。"""
    __getattr__ = dict.__getitem__
    __setattr__ = dict.__setitem__

def adder(x):
    state = [x] # pythonのcellオブジェクトのせい
	## 省略
    return ObjectLike(add=add, sub=sub, 
                      mul=mul, div=div, 
                      edit=edit)

# ------------

adr = adder(10)
print adr.add(20) # => 30
adr.edit(20) #保持する状態変更
print adr.add(20) # => 40

## 名前が良くないけど
print adr.sub(20) # => 0
print adr.mul(100) # => 2000
print adr.div(2) # => 10

継承も難しくない。

クロージャを利用した方法でもクラスの継承をシミュレートできる。
(今回のは内部でクラスを使ってしまっているけれど)

例えば以下のようなクラス定義を真似する。

BarはFooを継承したクラス。メソッドにbar,fooを持つ。fooは親クラスのFooから継承したもの。

普通にクラスで書いた場合、

クラスで作ったBar
class Foo(object):
    def foo(self):
        return "foo"

class Bar(Foo):
    def __init__(self, prefix):
        self.prefix = prefix

    def bar(self, prefix=None):
        return (prefix or self.prefix) + "bar"

# -----------------------------

print Bar("bar:").foo() # => "foo"
print Bar("bar:").bar() # => "bar:bar"
print Bar("bar:").bar("obj:") # => "obj:bar"

クロージャを使ってクラスっぽいものを作る。

クロージャで頑張ったbar
class Body(object):
    def __init__(self, obj, *supers):
        list_of_obj = []
        for _super in supers:
            for x in _super.list_of_obj:
                list_of_obj.append(x)
        list_of_obj.append(obj)
        self.list_of_obj = list_of_obj
        
    def _find_method(self, name):
        for obj in self.list_of_obj:
            if obj.get(name):
                return obj[name]

    __getitem__ = _find_method

def foo():
    return Body({"foo": lambda : "foo"})

def bar(pfx):
    _pfx = pfx
    return Body({"bar": lambda pfx=None: (pfx or _pfx) + "bar"}, foo())


# -------------

print bar("bar:")["foo"]() # => "foo"
print bar("bar:")["bar"]() # => "bar:bar"
print bar("bar:")["bar"]("obj:") # => "obj:bar"