アルパカログ

カスタマーサポート (CS) とエンジニアリングを掛け算したい CRE (Customer Reliability Engineer) が気になる技術や思ったことなど。

Pythonのデコレータとは?使い方からテストの書き方まで解説

最近仕事で Python のデコレータをレビューしてもらう機会があり「デコレータなんぞや?」となっていたのを見かけたので、備忘録を兼ねて書き残しておきたいと思います。

デコレータとは?

デコレータとは、一言で言えば関数を受け取って関数を返す関数です。

言葉だと難しいですね。早速例を見てみましょう。

from functools import wraps

def to_john(f):
    @wraps(f)
    def wrapper():
        return f() + ' John!'
    return wrapper

@to_john
def say_hello():
    return 'Hello!'

Python では関数に対して @ を使って関数を修飾 (デコレート) します。上記の例では to_john がデコレータです。

デコレータ to_john を見てみると、内部で wrapper という関数を定義して返しています (return wrapper)。

結論から言うと、 @to_john によってデコレートされた say_hello()wrapper 関数の戻り値を返します。

デコレータはデコレートされる側の関数を乗っ取ると考えるとイメージしやすいかもしれません。

では、元の say_hello はどこに行ったのでしょうか?

デコレートされる側の元の関数は、デコレータ側に引数として渡されます。ここでは f にあたります。

先ほど、デコレートされた側の関数の戻り値は、実際には wrapper の戻り値になると言いました。

wrapper の中を見てみると f() + ' John!' を返しています。この f() が元の say_hello() になっているというわけです。

ところで、functools.wraps は何をしているのでしょうか?

@wraps デコレータは wrapper 関数を wrapped 関数に見えるようにしています。

詳しくは下記をご覧ください。

どんなときにデコレータを使うか?

実際デコレータはどんなときに使えば良いのでしょう?

Webアプリケーションの例を挙げると、ログインセッションのチェックや入力値のバリデーションなど、様々なところで利用されています。

ここでは少し毛色は違いますが、テキスト処理を例として見ていきましょう。

下記のような問い合わせのテキストがあるとします。

【お名前】
○○○○

【お問い合わせ内容】
〜〜〜〜
〜〜〜〜

このようなテキストのリストを入力として下記の操作を行うとします。

  • 名前と問い合わせ内容を抽出しディクショナリのリストにする
  • 問い合わせ内容が空のものを除外する

デコレータを使うと次のようなコードになります。

from functools import wraps

class Inquiries:

    def __init__(self, raw_texts):
        self.raw_texts = raw_texts


    def filter_by_body(f):
        @wraps(f)
        def wrapper(self):
            return list(filter(lambda item: item['body'] != '', f(self)))
        return wrapper


    def extract_name_and_body(f):
        @wraps(f)
        def wrapper(self):
            pattern_name = '【お名前】'
            pattern_body = '【お問い合わせ内容】'

            items = []
            for text in f(self):
                name, body = text.split(pattern_name, 1)[-1].split(pattern_body, 1)
                items.append({'name': name.strip(), 'body': body.strip()})
            return items
        return wrapper


    @filter_by_body
    @extract_name_and_body
    def items(self):
        return self.raw_texts

items メソッドに注目してください。@extract_name_and_body@filter_by_body の2つのデコレータが修飾しています。

直感的におわかりいただけるかと思いますが、デコレータは関数定義に近い方から実行されていきます。つまりこのケースでは、@extract_name_and_body@filter_by_body の順になります。

デコレータを使うと一連の処理がリーダブルになるのがおわかりいただけたでしょうか?

もしデコレータを使わないとなると filter_by_body(extract_name_and_body(raw_texts)) のようになってしまうことでしょう。

おまけ: Elixir のパイプライン演算子

Elixir にはパイプライン演算子という前の関数の戻り値を次の関数の第1引数として渡しながらチェインする仕組みがあります。

ちょっと難しいですね。実際に例を見てみましょう。

先ほどの見づらかった filter_by_body(extract_name_and_body(raw_texts)) は Elixir で書くと次のようになります。|> がパイプライン演算子です。

raw_texts
|> extract_name_and_body
|> filter_by_body

シンプルで美しいですね。

デコレータのテスト

リーダブルなデコレータですが、関数を返す関数になっていることからテストには少し工夫が必要です。

ちなみに引数や戻り値に関数をとる関数のことを高階関数と言います。

それでは先ほどの extract_name_and_body を例に見てみましょう。

def extract_name_and_body(f):
    @wraps(f)
    def wrapper(self):
        pattern_name = '【お名前】'
        pattern_body = '【お問い合わせ内容】'

        items = []
        for text in f(self):
            name, body = text.split(pattern_name, 1)[-1].split(pattern_body, 1)
            items.append({'name': name.strip(), 'body': body.strip()})
        return items
    return wrapper

今回テストしたいのはメインの処理となる内側の関数 wrapper です。ということは、テストでは内側の関数を抜き出してあげる必要があるわけです。

内側の関数を得るには、外側の関数を呼び出してあげれば良いです。しかし厄介なのは、このとき引数として修飾される側の関数を渡してあげなければならないということです。

そこで引数用にダミーの関数を用意します。unittest.mock.MagicMock を使うと戻り値を任意に設定したダミーの関数を作ることができます。

それではテストコードを見てみましょう。

import unittest
from unittest.mock import MagicMock

from inquiries import Inquiries

class TestInquiries(unittest.TestCase):

    def test_extract_name_and_body(self):
        inquiries = Inquiries('')

        texts = ['【お名前】\nあああ\n\n【お問い合わせ内容】\nこんにちは\nこんにちは']

        inquiries.items = MagicMock(return_value=texts)
        wrapper = Inquiries.extract_name_and_body(inquiries.items)

        results = wrapper(inquiries)
        self.assertEqual(results, [{'name': 'あああ', 'body': 'こんにちは\nこんにちは'}])


if __name__ == '__main__':
    unittest.main()

抜き出した wrapperinquiries.items メソッドの代替なので、第1引数に self となるインスタンス (ここでは inquiries) を渡してあげなければならない点に注意しましょう。


Python のデコレータについて、その使い方とテストの書き方を説明しました。

デコレータはリスト内包表記と並んで Python のコードをリーダブルに保つために欠かせないテクニックです。

また Python 製の Webアプリケーションフレームワークでは頻繁に目にすることになります。

ぜひこの機会に使えるようになっておきましょう。

宣伝

今回は Python の話でしたが、私の所属するCREチームでは1日のうちに Python の他に Ruby, Elixir を書き、レビューしています。時々 JavaScript や Go もあります。

そんないろんな言語入り混じるエキサイティングな環境に身を置いてみたいという方、ぜひ下記をご覧になってください。貴方様のご応募をお待ちしております。

career.xflag.com

career.xflag.com

こちらもあわせて読みたい

alpacat.hatenablog.com