デコレータの使いどころ

  • 35
    Like
  • 0
    Comment
More than 1 year has passed since last update.

デコレータの書き方はわかった。

デコレータとはその名の通りデコレートするものだ。
何をデコレートするのかといえばそれはもう関数、関数である。
あとメソッドである。

俺はわかった。たいへんだ。デコレータをわかってしまった。

タケシはそう呟くと弾かれたように走りだした。
だが信号が点滅し始めたので止まった。
そう、彼は交通ルールを守るのだ。

そして暇を持て余すように考える。

書けるようになったら使っていきたいのが人情というものだ。
だが、実際使ってみたいと思うと、いまいち使いどころがわからない。
有効な使い道が思い浮かばないのだ。

そんな人。

ハイ!俺もわかりません!

いや、でも、ちょっとわかってきたかも。

なぜデコレータを使うのか

なぜデコレータを使うのか?

  • 好ましくない冗長性の排除やDRYのためにデコレータを使う

  • 読みやすさの向上のためにデコレータを使う

思いつくものとしてこういうのがあります。
使う利点としても、よくこういうのがあげられています。

これはなんにも間違ってない!圧倒的に正しい!
あぁきっと正しいのだろうさ!

でも、ぶっちゃけこれだけじゃ使いどころがわかんないっていうね。

なぜなら、上で書いたような理由のためであれば、他にもいっぱいやりかたあるじゃん?
たとえばぁ、継承とかあるじゃん?
てか、そのまま関数にしてもいいじゃん?
そっちの方が慣れてるじゃん?
わざわざデコレータみたいなの書きたくねーんだけど。
そう思うんだけど、文句あんの?

と思っちゃいますよね?思わないですか?
俺は思っちゃう。

ではなぜあえてデコレータを使うのか?

それを考えるためには、コードの処理の 意味 に注目する必要があります。
ここでもう結論からいっちゃうと、 ビジネスロジックの分離 のためにデコレータを使います。

ビジネスロジックの分離

関数の呼び出しをトレースするデコレータ

デコレータの第一歩として、こういう例を見たことがあると思います。

from functools import wraps

def trace_func_call(func):
    @wraps(func)
    def wrapper(*arg):
        print(func.__name__ + ' is called.')
        return func(*arg)
    return wrapper

@trace_func_call
def spam(s):
    print((s + '!') * 5)

@trace_func_call
def ham(s):
    print((s + '?') * 5)

spam('egg')
ham('sausage')

実行結果

spam is called.
egg!egg!egg!egg!egg!
ham is called.
sausage?sausage?sausage?sausage?sausage?

関数が呼び出されるたびに、それと表示するデコレータです。

このとき、デコレータが担っている役割というのは、ビジネスロジックではないですよね。
例は超適当ですが、あくまでビジネスロジックとしてやりたい処理というのは、spamham関数の処理ですよね。
引数に!とか?つけて5回繰り返したり、っていうクソくだらねぇやつさ。

そのとき、関数呼び出しを表示するというのはビジネスロジックではなく、管理上必要だから実装するロジックなわけです。
それらのロジックというのは意味的に異なっています。

デコレータを使うことで、管理ロジックとビジネスロジックを分離することができます。
やりたいことを基準に言い直すと、コードの主目的であるビジネスロジックを管理ロジックから分離したい、という意図をスッキリとした書き方で実現するものがデコレータです。
もちろん、デコレータの役割がこれだけと言うつもりは全然ないんですが、デコレータの使いどころについて大きな示唆を与えてくれます。

キャッシュデコレータ

次はもう少し実用的なもの。
PyCon US 2013のRaymond Hettinger氏の講演 「Transforming Code into Beautiful, Idiomatic Python」 からのデコレータのサンプルです。

まずは、オリジナルの「あまり好ましくない」コードから。

def web_lookup(url, saved={}):
    if url in saved:
        return saved[url]
    page = urllib.urlopen(url).read()
    saved[url] = page
    return page

上記のコードをデコレータを使ってbetterにしたものはこちら。

def cache(func):
    saved = {}
    @wraps(func)
    def newfunc(*args):
        if args in saved:
            return saved[args]
        result = func(*args)
        saved[args] = result
        return result
    return newfunc

@cache
def web_lookup(url):
    return urllib.urlopen(url).read()

web_lookup関数は、指定したURLのWebページを取得する関数です。
そのとき、取得済みのページはキャッシュしておくという機能を持っています。

ごく短いコードですが、このコードの機能が意味的に以下のように分けられることがわかると思います。

  • ビジネスロジック (business logic)
    -> 指定したURLのWebページを取得
  • 管理上のロジック (administrative logic)
    -> 指定したURLのWebページを取得

オリジナルのコードでは、キャッシュ機能はweb_lookup関数に渡す引数のディクショナリによって実現されます。
つまり、関数それ自体に、ビジネスロジックと管理上のロジックの機能が組み込まれた実装になっています。

対してbetterなコードでは、関数はビジネスロジックのみを実装し、管理上のロジックは、クロージャになっているデコレータが実装しています。
このようにすることで、ビジネスロジックが明快に分離され、コードの可読性やメンテナンス性も向上することがわかると思います。

デコレータの実装でコード量はバリバリ増えていますが、それを「美しいコード」と呼ぶのはとてもpythonicな感覚だと感じます。

ちなみに、こういったキャッシュデコレータのパターンは、定番のパターンになりますので、難しくもないしぜひテクっておきましょう。

ちなみに、引用した講演のスライドのコードは間違ってると思う・・・
だから上で書いたコードは修正しています。
これのデコレータのとこ。間違ってるよね?
https://speakerdeck.com/pyconslides/transforming-code-into-beautiful-idiomatic-python-by-raymond-hettinger-1

もう1つ簡単な例

ついででもう1つ。

特に2.x使ってて、入力の文字コードが不明な場合、文字列の扱いでなんとかデコードなんとかとか、うにこーどなんとかなんとかとか、マジでうんざりされられるので、内部処理ではデコレータでchardet使って全部unicodeにしちゃって処理しよう、てやつ。

ちょろっとしたツール書くときとかに便利、かもしれません。

import chardet

def unicode_conv(func):
    def wrapper(*args):
        f = lambda x: x.decode(chardet.detect(x)['encoding'])
        args = [f(arg) if type(arg) != unicode else arg for arg in args]
        return func(*args)
    return wrapper

def funcA(*args):
    for arg in args:
        print(repr(arg))

@unicode_conv
def funcB(*args):
    for arg in args:
        print(repr(arg))

funcA('shrubbery')
funcB('shrubbery')
funcB(u'shrubbery')

実行結果

'shrubbery'
u'shrubbery'
u'shrubbery'