魂の生命の領域

AWS とか Python とか本読んだ感想とか哲学とか書きます

デコレータを自分で作れるようにする

背景

Python のデコレータってカッコ良くないですか? 何も見ずにササっと書けたら絶対かっこいいと思うんですよね。 なので基本的な戦略を学んでみたいと思います。

Fluent Python第7章 関数デコレータとクロージャ を参考にしながら進めていきます。

基本のイメージ

冒頭で書いた本のほぼパクリなんですが、例えば deco という名前のデコレータを予め作っておいたとすれば

@deco
def target():
  print('running target()')

というような使い方をすることになります。 これは以下のように書いた場合と同じで、つまりデコレータを頭に付けた関数を呼び出したときはそのデコレータの戻り値 deco(target) を参照しています。

def target():
  print('running target()')

target = deco(target)

デコレータはその関数を置き換えていることの露骨な例として以下のものを紹介します。

>>> def deco(func):
>>>     def inner():
>>>         print('running inner()')
>>>     return inner

def の中に def があって混乱しますが、deco という関数は内部で inner という関数を定義していて、それを返しています。よく見ると引数の func に対しては何もしていませんね。

これを冒頭の例で呼び出すとこうなります。

>>> @deco
>>> def target():
>>>     print('running target()')


>>> target()
running inner()

target の挙動が inner に置き換わっていますね。 といっても実際に使用される場合は、引数として渡された関数と同じ関数を返すように定義するこが多いです。

また、二つ目の例でこっそり書いていますが、このデコレータ deco はモジュールが読み込まれた時点で即時実行されています。

def target():
  print('running target()')

target = deco(target)  # 即時実行

デコレータ自体はモジュールのインポート時に実行されますが、中身の関数、ここでは target ですが、こいつは当然ながら呼び出されたときだけ実行されます。

クロージャ

JavaScript やってるとよく見る言葉ですよね。 無名関数とよく混同されているが別物だよ、という風に書いてあります。

私はクロージャに慣れてない中で無名関数をよく使っていたので別に混同はしていなかったので特に目から鱗という感じはないのですが、そうらしいです。

本書での例で言えばこのようなものが説明されています。

def make_averager():
    series = []

    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)

    return averager

これは次のように使います。

>>> avg = make_averager()

まずこの関数を呼び出すと return 文からわかるように averager という関数オブジェクトを返します。 そこに int 型の変数を渡すと、 new_value として代入され、それが series という list に格納され、series の中身を全て足し上げたものをその個数で除したもの(つまり平均)が返ってきます。

>>> avg(10)  # 10/1
10.0
>>> avg(11)  # (10 + 11)/2 = 21/2
10.5
>>> avg(12)  # (10 + 11 + 12)/3 = 33/3
11.0

この series という変数は make_averager 関数の直下に定義されているので、avg(10) のように averager 関数を呼び出しても毎回初期化されることなく、呼び出されるたびに更新されるわけです。

クラスを使えばこのように書けます。

class Averager():
    def __init__(self):
        self.series = []

    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total/len(self.series)

これをこう使います。

>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

__call__ メソッドはインスタンスを関数のように呼び出せる特殊メソッドです。 つまり avg(10)avg.__call__(10)と同じです 。

話を戻して、もう一度これを見ます。

def make_averager():
    series = []  # ここから

    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)  # ここまで

    return averager

averager 関数をよく見ると series という変数はその外部である make_averager の中で定義されています。 この series自由変数 と呼ばれるものです。そして、この make_averager の中身で「ここから」「ここまで」と書いた範囲がクロージャと呼ばれるものです。 クロージャはスコープ外の変数 series にアクセスできています。

ちなみにここで挙げた関数は averager 関数が呼び出される旅に毎回 list の中身を足し上げて平均を計算しているため非効率的です。 make_averager 関数の内部に合計と個数を持たせておけば、呼び出されるたびに増えた分だけ足せば良いので計算量は一定です。 実際のコードはこうなります。

def make_averager():
    count = 0
    total = 0

    def averager(new_value):
        nonlocal count, total
        count += 1
        total += new_value
        return total/count

    return averager

急に nonlocal count, total という風に書いたのには意味があります。 実際これを飛ばすと動きません。 なぜなら count += 1count = count + 1 のことで、新しく変数を定義しようとしていることになるからです。 一つ前の例では series.append(new_value) と書いているので series という自由変数を直接書き換えていますが、ここでは count という変数( averager 関数内のローカル変数)に代入しようとします。 total も同じことをしています。

なので、これはローカルな変数ではなくて自由変数のことだよ、と明示的に宣言してやる必要があります。 それが nonlocal です。

デコレータ

という訳で簡単なデコレータを書いてみます。 書籍では結構ややこしいデコレータを書いてますので、ここでは思い切ってハイパー簡単な例を勝手に作ります。

def deco(func):
    def inner(*args, **kwargs):
        print('Start')
        result = func(*args, **kwargs)
        print('End')
    return inner


@deco
def target():
    print('running target()')

こんな感じだとしてみましょう。 decoinner の関数オブジェクトを返します。 inner のに渡される引数は *args, **kwargs で、これで関数へ代入されうる引数の最も一般的な形式を網羅(可変長引数)しています。

args はキーワードなしの引数を束ねたもので、それを *args とすることでタプルとしてひとまとめに代入します。可変長の引数を押し込めています。

さらに **kwargs はキーワード引数です。 kwargs は辞書型の引数を想定しています。 {'name': 'tanaka', 'age': 100}func(name='tanaka', age=100) と代入される感じです。

なので、このデコレータ deco はどんな形式の関数であっても受け入れることができます。 定義を見ると、 result = func(*args, **kwargs) のように呼び出し元の関数が実行されていて、その前後で print 文が二つあります。

実行結果は案の定こうなります。

>>> target()
Start
running target()
End

もう少し工夫できそうですが、死ぬほど眠いので今日はこの辺にしておきます。

次は logging モジュールの良さげな使い方を見て、最終的には使い回しの利く logger モジュールを作ろうと思います。