LoginSignup
3
2

More than 5 years have passed since last update.

Pythonの「デコレータ」に関するメモ

Last updated at Posted at 2017-10-13

はじめに

デコレータは分かりづらい。本質がもっと分かりづらいものは他にもある(例えばジェネレータ)けど、やはりデコレータは分かりづらい。何故なのだろうと考えていた。

デコレータという特別な関数はない

関数宣言の前に@をつけるあれは「デコレータ式」(Decorator Expression)と言う。この機能はある。つまり、立派に文法に組み込まれ、Pythonの中で特別な振る舞いをする。

この概念、考え方を全体として「デコレータ」と呼ぶことがある、という説明は問題ないと思う。

しかし「デコレータ式に与えることのできる専用の関数、関数のサブセット」みたいな括り、フラグ、実装はない。関数は関数であり、「デコレータ専用関数」などというものもない。

ジェネレータ(ジェネレータ関数、ジェネレータ・イテレータ)とはこの点では大きく違う。ジェネレータ関数とはdef文内にyieldが混じった瞬間に生まれるお化けな(surprising)仕組みのことだ。これは関数の一種だが、呼び出し後の戻り値がreturnの結果ではなく問答無用にジェネレータになるという意味で、従来の関数とは異質の存在だ。このあたりはJavaScriptoon3に色々書いたのでここでは省く。

「ジェネレータ」「ジェネレータ関数」という機能があるとは主張するのは、私は問題ないと思うしそうするべきだと思う。つまり、関数と独立した存在としてジェネレータ関数が確かにPythonのランタイム内で認識され、別々の管理をされる。

一方、「デコレータという存在(関数とか)」をPythonの処理系内で独立に規定できるものはない。仮に入門書などでそういう異質の存在があるかのような説明があって、入門者がそう理解しようとすると、逆に混乱する。現に「デコレータ関数というのがありましてぇ、」と説明されると私はめちゃくちゃ混乱する。更に言うと、デコレータの力を過小評価する。

「デコレータ」と呼ばれるものは、あくまで関数を受け取って(通常)関数を返す関数であって、「デコレータ文」というシンタックスシュガーとセットで使いやすいタイプの関数をおおよそ指す「ふんわりしたカテゴリ」に過ぎない。デコレータである関数はそうでない関数と何が本質的に違うのか?「関数」と「ジェネレータ関数」とは異なり、特に明快な区別はつけられない。

「関数を受け取って関数を返す」のがデコレータということではない。それは「典型的なパターン」ではあるけど全てではなく、重要な一部が欠損してすらいる。

  • 「関数を受け取って」 → 引き数付きデコレータ文を見よ。その「デコレータ」はまず関数外の引数を受取り、関数を受け取る関数を返す関数を返す。自分自身が直接引数に関数を受け取る必要はない(ま、ちょっと揚げ足取りだね)
  • 「関数を返す」→ 返ってきたオブジェクトが関数オブジェクトであることをデコレータ文は強制しない。

特に後者はエキサイティングなデコレータ文の使い方を示唆する。本稿末尾に示す記事の例を参考にした、少し変わったデコレータ式の使い方を示す。

def process_list(list_):
    def decorator(function):
        return function(list_)
    return decorator

unprocessed_list = [0, 1, 2, 3]

@process_list(unprocessed_list)
def processed_list(items):
    return [item for item in items if item > 1]

print(processed_list)
[2, 3]

processed_listは関数オブジェクト(を保持した変数)にみせかけてリスト(を保持した変数)である。確かに引数付きデコレータ式に与える関数としては process_list() は、パット見でもネストが1段浅く見える。

引数付きを想定したデコレータ式用の関数なら、愚直に買えば以下のようにもうちょっと関数のネストが深くなるはずだ。

def tag(name):
    def deco(func):
        def wrap(*args, **kwargs):
            return ('<{}>{}</{}>'
                    .format(name, func(*args, **kwargs), name))
        return wrap
    return deco

@tag('italic')
def get_hello():
    return 'Hello'
print(get_hello())  # <strong>Hello</strong>

「デコレータ式」に与えられるモノは、結果としてエラーが発生しない限り、関数風のものであればなんでも良い。__call__()を実装したCallableクラスのオブジェクトでも良い。クラスでも良い。

大事なのは「デコレータ文」が特定の操作を元の関数オブジェクトに適用するということだけだ。強いて言えば、それこそが「デコレータ」の考え方の真髄となる。

ただ、「デコレータ」という特別な関数があるものだと思うと、どうしてもstaticmethod()とかclassmethod()とか個別の関数に目が行きがちになってしまうように感じる。そして、それらが「デコレータ文とセットでしか使えない」と思ってしまう傾向を大きく強める結果になる。実際には、例えば「デバッグ対象の関数呼び出しの前後にログを残す」みたいな用途でも気軽に「デコレータを想定した関数」は使えるので、「デコレータはデコレータ式でしか使えない」などと考えようものなら、結構大きな損失になりかねない。

def deco_debug(func):
    def wrap(*args, **kwargs):
        print(f'DEBUG: {func.__name__} started')
        ret = func(*args, **kwargs)
        print(f'DEBUG: {func.__name__} ended')
        return ret
    return wrap

def say_hello():
    print('Hello')

...

# デバッグしたいタイミング
deco_debug(say_hello)()

...

# 特にしなくて良いタイミング
say_hello()

時折呼び出した関数の軌跡を全て追うためにtracebackやpdbとは別にloggerでトレースログをはさみたい衝動に駆られることがある。この際、本体の関数をいちいち書き換えると思わぬ副作用が起こることもあるし手作業としても面白くない。そういう場合に、デコレータを意識した上記のような関数を用意しておき、

  • 呼び出された全てのケースを記録したい -> デコレータ文で使う
  • 個別のケースを記録したい -> 単に関数をラップする形で使う

と使い分けるのは、状況によってではあるけれども、けっこう有効に思う

#蛇足だがデバッグに素のprint()はお薦めしない。これは例だ。

ドキュメントでは専用のマークアップが使われている

もう一点、私が混乱したのはドキュメントだった。functoolsモジュール等で @functool.lru_order のような記法が使われていたことだ。

スクリーンショット 2017-10-13 14.56.24.png

繰り返すが、関数とデコレータで(関数とジェネレータのような意味での)区別はない。デコレータ式とセットで使える関数を「デコレータとも呼ぶ」くらいなものだ。なのにドキュメントでは異なる書き方がされる「ことがある」のだった。

一方、私が見たときにはもっと代表的なデコレータ文向け関数staticmethod(), classmethod() 等はただの関数のマークアップが使われていた。

スクリーンショット 2017-10-13 14.57.53.png

聞くところ、歴史的には途中から decorator というマークアップが導入されたらしい。だから、歴史的な関数だとデコレータが中心的な用途でもマークアップとしては関数のものが使われていることがあるとのこと。なるほど

個人的には(前述の通り)このマークアップ自体良くないと思うけれど、それがコミュニティの方針であれば、staticmethod()classmethod()@staticmethod, @classmethod と書かれるべきだ。というわけで今(3.6の2017-10-13時点)ではそうなった。

スクリーンショット 2017-10-13 15.00.08.png

私が拙いPRを作った後、丁寧な人が「これ、普通の関数としても使えるってコメントした方が良いよな?」と追加でPRを作ってくれていた (staticmethodの説明が充実した)。なるほどそのとおり(だし的確な指摘)だが、順序が反転している感はある。デコレータを普通の関数としても使えるのではない、関数をデコレータとしても使えるのだ。

この場合デコレータは関数でしかないから「組み込みデコレータ」とは表現しない。そもそも「組み込み関数」と言いつつクラスが混じっているのだから、おおらかに見よう。

おわりに

この記事を書く際には気をつけたが、実際には……

結局「デコレータ式に与えることを目的とした関数」を指す良い用語としては「デコレータ」ということになると思う。本文ではその表現は避けたが、日々のディスカッションではきっと普通に「デコレータ」「デコレータ関数」と呼称されるケースを見るはずだ。私は、それは止めないし咎めようなどとも思わない。そもそも私が思う限り、Pythonはそういう厳密さに頓着するべき言語ではない。

ただもう一度。原理的には「デコレータ関数」などというものは(ジェネレータ関数のような形では)存在しない。ただの関数をデコレータ式に適用した際に便宜的にそう呼ぶだけだ。クラスに適用するデコレータ式に「クラスデコレータ」という表現もあるが、これもまた同じことだ。

def enclose_by_debug(class_):
    def _debug(func):
        def wrap(*args, **kwargs):
            print(f'DEBUG: {func.__name__} started')
            ret = func(*args, **kwargs)
            print(f'DEBUG: {func.__name__} ended')
            return ret
        return wrap
    setattr(class_, '__init__',
            _debug(getattr(class_, '__init__')))
    setattr(class_, '__str__',
            _debug(getattr(class_, '__str__')))
    return class_

@enclose_by_debug
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __str__(self):
        return f'({self.x}, {self.y})'

p = Point(1, 2)
print(p)
DEBUG: __init__ started
DEBUG: __init__ ended
DEBUG: __str__ started
DEBUG: __str__ ended
(1, 2)

補足

最近以下のような記事を見て面白かったのが本稿を書く動機の一つだ。興味があればこちらもどうぞ。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2