発端

見たことない構文が目に留まった。

@property
def foo(self):
    なんちゃらかんちゃら

なんだこの@は? 調べたところによるとデコレータっていうものらしい。ひとつ覚えてみるかというわけで「1日1python」のお時間です。

どうもデコレータは機能ってよりプログラムの書き方の技術のひとつみたいだな。ある汎用的な関数に何か機能を追加したいときに用いる技術だ。上書きじゃなくて、追加。上書きしたいんだったら単に定義しなおせばいいわけだもんね。関数をまるごと上書きするんじゃもったいないから少しずつデコレーションして使おうってことな。ほんで、汎用的関数をデコレーションするために使う関数のことをデコレータっていうわけだ。そういうことなら話が早い。実際に書いてみるぜ。

 

クラスとインスタンスを使ったデコレータを書いてみる

まず汎用的なものを作る。

class Foo:
    def __init__(self, name):
        self.name = name

    def call(self):
        return self.name

名前を与えてインスタンスを作ってcall()を呼べば、その名前を返してくれるクラスね。以下のように使う。

name = 'William Forsyth'
instance = Foo(name)
print(instance.call())
# William Forsyth と表示される。

使ってるうちに、名前だけじゃちょっとさみしいから枠線をつけたくなった。枠線でデコレーションをする、Deco_Wakusenデコレータを作る。上のクラスの続きに記述する。

class Deco_Wakusen:
    def __init__(self, instance):
        self.instance = instance

    def call(self):
        ret = ''
        ret += '--------------------\n'
        ret += '  ' + self.instance.call() + '\n'
        ret += '--------------------'
        return ret

これは上で作ったFooのインスタンスを与えてインスタンスを作る。イメージとしてはチョコレートを糖衣で包む感じか。糖衣であるDeco_Wakusenにもcall()メソッドを準備してある。このcall()では元のFooインスタンスの結果を使いつつ、枠線をつけて返すようにしてある。別にクラスを継承してるわけじゃないから、上書きにもならない。デコレーションしてるだけ。以下のように使うと、上のとおんなじようにcallしてるはずなのにデコレーションされたものが飛び出してくるぜ。

instance_wakusen = Deco_Wakusen(instance)
print(instance_wakusen.call())
# 以下のように表示される
--------------------
  William Forsyth
--------------------

今度は枠線じゃなくて、名前を大文字にして表示したくなった。やることは同じだ。

class Deco_Oomoji:
    def __init__(self, instance):
        self.instance = instance

    def call(self):
        return self.instance.call().upper()

使い方は枠線のときと同じ。

instance_oomoji = Deco_Oomoji(instance)
print(instance_oomoji.call())
# WILLIAM FORSYTHと表示される。

それでデコレータの楽しいトコなんだけど、メソッドを上書きしてるわけじゃないから、続けざまにデコレーションをすると単純に機能が追加されていくのだよ。まず汎用物を作って、枠線デコレータでデコレーションして、さらに大文字デコレータでデコレーションしてみる。

instance_wakusen = Deco_Wakusen(instance)
instance_wakusen_oomoji = Deco_Oomoji(instance_wakusen)
print(instance_wakusen_oomoji.call())
# 以下のように表示される。
--------------------
  WILLIAM FORSYTH
--------------------

ビューリホー。新しい機能を使ってるわけでも、なんでもない。ただのロジックの組み方で面白い機構ができた。

上述の例ではわかりやすさのためいちいちインスタンス名を変えてたけれど、同じインスタンス名を使ってもOK。使用感がまったく変わらないままデコレートした結果が飛び出してくる。テンプレートとしてまとめるとこんな感じか?

class 汎用:
    def デコレーションされるメソッド(self):
        なんちゃらかんちゃら

class デコレータ:
    def __init__(self, instance):
        self.instance = instance

    def デコレーションするメソッドと同名のメソッド(self):
        self.instance.メソッド()を使用しつつ
        前後に機能を書き加えて結果をreturn

instance = 汎用()
instance = デコレータ(instance)
instance.デコレーションされたメソッド()

 

クラスはちと大袈裟だからふつーの関数で書いてみる

いやお前いちいちクラス使うことはねーだろということで関数でやってみる。

def call(name):
    return name

シンプルどころの話じゃない。

name = 'William Forsyth'
print(call(name))
# William Forsyth

枠線デコレータとしてはこういう関数を用意。

def deco_wakusen(func):
    def call_kari(name):
        ret = ''
        ret += '--------------------\n'
        ret += '  ' + func(name) + '\n'
        ret += '--------------------'
        return ret
    return call_kari

関数バージョンはクラスよりシンプルになるかと思いきや、ネストのある関数が登場しちまった。けれどやってることはクラスバージョンと同じで、「もともとの関数を引数にして、内部でその関数をデコレーションした関数を作成し、その関数を返」してるだけ。関数を呼び出してる側からすれば、「関数投げたら機能追加されて返ってきたわ」みたいな感じ。

call_wakusen = deco_wakusen(call)
print(call_wakusen(name))
# こうなる。
--------------------
  William Forsyth
--------------------

大文字デコレータ。

# 元の関数を投げて、
def deco_oomoji(func):
    # 元の関数を利用しつつデコレーションする関数を作って、
    def call_kari(name):
        return func(name).upper()
    # デコレート済みの関数を返す。
    return call_kari

使い方。

call_oomoji = deco_oomoji(call)
print(call_oomoji(name))
# WILLIAM FORSYTH

重ねがけ。

call_wakusen = deco_wakusen(call)
call_wakusen_oomoji = deco_oomoji(call_wakusen)
print(call_wakusen_oomoji(name))
# こうなる。
--------------------
  WILLIAM FORSYTH
--------------------

まとめるとこう。

def デコレーションされる汎用関数():
    なんちゃらかんちゃら

def デコレータ関数(func):
    def ここの名前はどうでもいい():
        funcを使用しつつ
        前後に機能を書き加えて結果をreturn
    作った関数をreturn

汎用関数 = デコレータ関数(汎用関数)
汎用関数()

 

@を使って書いてみる

おいデコレータ実現できちゃったじゃねーか。どこにも@が出てこないぞ、というわけなんだが、これは上述のスクリプトを簡単に書くために使えるようだ。具体的には以下。

@deco_wakusen
def call(name):
    return name

こういう風に書くと、call()関数がdeco_wakusenデコレータでデコレーションされたことになるようだ。大文字のほうも重ねがけするならこう。

@deco_wakusen
@deco_oomoji
def call(name):
    return name

直後にこう使える。

print(call(name))
# 結果は以下。
--------------------
  WILLIAM FORSYTH
--------------------

というわけでな、冒頭で俺の目に留まった記述は、関数foo()の中身を、どっかにあるproperty()という関数で加工しているんだよーということを表していたわけだ。これの直後にfoo()を実行したら、fooに書いてあることそのままじゃなくて、propertyでデコレーションされた結果が返ってくるのだろう。いやー、調べる前に実行していたらえらい混乱が俺を襲っていたぜ、ハッハッハ。

@property
def foo(self):
    なんちゃらかんちゃら

# 実行したら、「なんちゃらかんちゃら」じゃなくて
# property()で加工されたなんちゃらかんちゃらが
# 起こる。(たぶん)

疑問

さて、みっつの章に分けてまとめてみたわけだが、最初のふたつと@使用バージョンでは趣が異なるのが気になる。

俺がデコレータという概念を調べて実際に試してみた「クラスバージョン」と「関数バージョン」は、「まず汎用的な関数があり、それをあとから目的に合わせて異なるパッチを作って当てていく」という状況想定で書いてある。

いっぽう@でデコレーションを簡易化しちゃうぜバージョンは、「まず汎用的なデコレータがあり、それを必要に応じて装着していく」という状況を想定しているように見える。たとえばデコレータが「関数実行結果のログをとっておく」みたいな内容だとしたら断然後者のほうが使いやすいだろう。でも前者の登場機会だっておんなじくらいあるんじゃねえ?

俺が何に引っかかってるかっていうとさ、「汎用関数が最初にある」型と「汎用デコレータが最初にある」型のうち、後者にだけ@を使った省略的書き方があるのはなんでなんだろう? ってとこなんだよ。俺がデコレータという概念を知って自然に発想した型が前者だったから、余計にね。これについては特に答えが見つかんなかった。まあ当初の目的は果たせたのでグーとする。1日1pythonどころか、このスクリプト完成にこぎつけるのには一週間くらいかかっちゃったけどこれにて閉幕。