Pythonのデコレータを理解するときに残したメモ

Pythonのデコレータ(decorator)を理解する 3 - -を1,2,3全て読んで、以前僕がデコレータを理解するために残したメモもこんな感じだったなというのを思い出した。ずっと記事にしようと思いつつ忘れてたんだけどせっかくなので加筆修正しつつ公開してみる。

引数無しのデコレータ

デコレータとは、その名の通り元の関数を装飾するもの。別の言い方をすれば、元の関数そのものを弄ること無く挙動を変更するもの。

まずは単純なデコレータから。

def hello(function):
    def _hello(*args, **kw):
        result = function(*args, **kw)
        return "Hello, {0}".format(result)
    return _hello

@hello
def name(arg):
    """my docstring"""
    return arg

# これは以下と同義。実際デコレータ式が導入される前はこうしていた。
def name(arg):
    """my docstring"""
    return arg

name = hello(name)

# 実行
print name('John Doe')
  • デコレータ付ける前:name('John') # 'John'
  • デコレータ付けた後:name('John') # 'Hello, John'

元の関数nameは単純に与えられた引数をそのまま返すだけ。そこにデコレータhelloを付けることによって、"Hello, xxxx"のような文字列を返すように挙動が変更される。デコレータ式じゃない方を見てみるとわかるように、関数を返す関数helloの引数として関数nameを渡し、返ってきた関数をnameに再代入する感じ。

ところで、これだと元の関数名がデコレータの内側の関数名で上書きされてしまう。

name.func_name # '_hello'
name.func_doc  # None

これを解決するためには、functoolモジュールのwrapsを使う。

from functools import wraps

def hello(function):
    @wraps(function)
    def _hello(*args, **kw):
        return function(*args, **kw)
    return _hello

@hello
def name(arg):
    """my docstring"""
    return arg

これで以下のように関数名と関数のdocstringが正しく取得できるようになる

name.func_name # 'name'
name.func_doc  # 'my docstring'

デコレータ自体に引数を与える

flaskの簡単なview関数

@app.route('/')
def hello():
    return "Hello World!"

こんな風にデコレータ自体に引数を渡したい場合はどうするか。

この場合、

  • デコレート対象の関数の引数を受ける関数
  • デコレート対象の関数自身を引数として受け取る関数
  • デコレータ自身の引数を受け取る関数

が必要になるので、少なくとも3重にネストした関数になる。

from functools import wraps


def hello(friend1, friend2):
    def _hello(function):
        @wraps(function)
        def __hello(*args, **kw):
            result = function(*args, **kw)
            return "Hello, {0} & {1}! I'm {2}.".format(friend1, friend2, *args, **kw)
        return __hello
    return _hello


@hello("Jack", "Mary")
def name(arg):
    return arg


# デコレータ式を使わない場合
# 式の左側の変数名はイメージしやすいように_helloとかnameを使ったけれど別になんでもいい。
_hello = hello("Jack", "Mary")
name = _hello(name)


print name("John")  # 'Hello, Jack & Mary! I'm John.'

集合演算の実行速度:メソッドと演算子

集合演算をメソッドでやる場合と演算子でやる場合、どちらが速いのか気になったので試してみた。

なんとなくの予想だと、対象を全てset型に揃えないといけない演算子による計算よりも、メソッドの引数がイテラブルであればなんでもよいメソッド演算のほうが速いのかなー?

import timeit

setup = """
from random import choice
from string import (
    ascii_letters,
    uppercase,
    lowercase,
)

hoge = [choice(ascii_letters) for i in xrange(30)]
fuga = [choice(uppercase) for i in xrange(30)]
piyo = [choice(lowercase) for i in xrange(30)]
"""

oper = "set(hoge) - set(fuga) - set(piyo)"
meth = "set(hoge).difference(fuga, piyo)"

ot = timeit.Timer(oper, setup)
mt = timeit.Timer(meth, setup)

print "Operator: {0}\nMethod: {1}".format(ot.timeit(), mt.timeit())
結果
Operator: 9.30117201805
Method: 6.49405097961

メソッドのほうが速かった

DjangoでreStructuredText

探し方が悪かったのか、見つけるのに手間取った。でもその割に設定はすごく簡単だったというオチ。まあそもそも素のdjangoで出来るという考えが全くなかったのがいけない。

まず、docutilsがインストールされてないと元も子もないので、無ければインストールする。

$ pip install docutils

続いて、settings.pyのINSTALLED_APPSにdjango.contrib.markupを追加する。

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    ...
    'django.contrib.markup', # これ追加
    ...
)

あとはテンプレート中に {% load markup %} を追加して、ReSTなテキストに対して restructuredtext というテンプレートフィルターをかませば、htmlに変換されて表示される。

{% load markup %}
...
  <div id="article-body">
    {{ article.body|restructuredtext }}
  </div>
...

ちなみに、細かい挙動のカスタマイズに関しては、settings.pyのなかに

RESTRUCTUREDTEXT_FILTER_SETTINGS = {'initial_header_level': 3}

みたいにすることで制御することができるらしい。
参考:DjangoでreStructuredTextではまった点のメモ - 偏った言語信者の垂れ流し