Python のメソッドをクロージャとして使う

Python においてメソッドはユニークな性質を持っており、bound されているか unbound か、明確な違いがある。bound / unbound とは、そのメソッドが特定のインスタンスに属しているか、いないかという言い方が出来ると思う。

class Person(object):
    def __init__(self, name, age):
        self.__name = name
        self.__age = age

    def get_name(self):
        return self.__name

    def get_age(self):
        return self.__age

単純なクラスを定義してみた。このクラスのメソッドを調べてみると、

>>> Person.get_age
<unbound method Person.get_age>

こちらは unbound となっているが、

>>> p = Person("A", 10)
>>> p.get_age
<bound method Person.get_age of <test.Person object at 0x56eb0>>

インスタンスの場合は bound となっている。

unbound メソッドを使うためには、"self" に相当するインスタンスを与える必要がある。

>>> f = Person.get_age
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: unbound method get_age() must be called with Person instance as first argument (got nothing instead)
>>> p = Person("A", 10)
>>> f(p)
10

一方で、bound メソッドは関数として使うことができる。

>>> p = Person("B", 20)
>>> g = p.get_age
>>> g()
20

上記の関数 g() は、実態はbound メソッドだが、外部から見た場合は単なる関数だ。しかし g() の内部では p.__age を参照することができるから、クロージャとして見ることができる。

さて、複数の Person のインスタンスの age の平均値を求める関数があるとする。

def average_age(persons):
    return sum([p.get_age() for p in persons]) / len(persons)

この関数をもう少し抽象化して、とにかく関数リストが与えられたらそれらの返り値の平均を求めるようにしてみる。

def average_func(funcs):
    return sum([f() for f in funcs]) / len(funcs)

Person の get_age() を bound メソッドとしてリストにして使えば良い。

>>> persons = [Person("A", 10), Person("B", 20), Person("C", 50)]
>>> funcs = [p.get_age for p in persons]
>>> average_func(funcs)
26

average_func() 自体は Person とは関係のない汎用的な関数であり、色々な場面で使えるはず。こうしたクロージャ高階関数を使ったパターンは Scheme ではよく目にするのだが、Python でも同様の概念を実現することが出来る。しかもクラス、メソッドなどの Pythonオブジェクト指向言語としての要素をそのまま組み合わせれば良いので、全体の中に自然にとけ込ませることが出来るのではないだろうか。やはり Python は良くできた言語だ。

もっとも今回のようなケースはわざわざ average_age() を導入するような場面ではなかったと思う。もう少し面白い例があればよかったのだが、思い浮かばず。