52
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MYJLabAdvent Calendar 2021

Day 7

完全理解を目指す、Pythonデコレータはむずくない!

Last updated at Posted at 2021-12-07

はじめに

おはようございます!

MYJLab Advent Calendar 2021の7日目担当セイです!

もう7日目ですね、昨日は@ren_180「今更だけどもテスト駆動開発」でした。

ネタは悩みどころですが、今日はPythonのデコレータについて少しお話しましょう!

プロ基礎をとった人をベースにしているので、うるさく書いています。

Python Decorators(デコレータ)とは?

デコレータ(decorators)とは修飾という意味です。簡単に言うと、ある関数を修飾する、つまり関数の機能を変更したり、追加したりするための関数です。

ある関数を呼び出すとき、その関数の前後に何かぶちかましたい(追加の処理をする)時にによく使われる。

デコレータを使うことによって、簡潔かつ綺麗なコードを描けるようになる。

簡単な例

デコレータは難しい概念なので、ステップを踏んで少しずつ紹介しますが、まずはその基本な文法を見て雰囲気を感じてみましょう!

def decorator(func):
    def wrapper(*args, **kwargs):
        print('Do some bullshit BEFORE executing %s' % func.__name__)
        func(*args, **kwargs)
        print('Do some bullshit AFTER executing %s' % func.__name__)

    return wrapper

@decorator
def a_function_requiring_decoration():
    print('I am a bullshit function which needs decoration')

a_function_requiring_decoration()

この @decorator は関数 a_function_requiring_decoration のデコレータです。これがデコレータの文法です。実行結果は次のようになる。

Do some bullshit BEFORE executing a_function_requiring_decoration
I am a bullshit function which needs decoration
Do some bullshit AFTER executing a_function_requiring_decoration

さて、順を追って説明していきましょう!

知っておくべきこと

知っている人もいるかもしれないが、デコレータを理解するためにいくつかの前提とする概念を紹介する!

Tips:
もし、関数を値や引数として渡して、返り値として返すことができるなどのことをすでに知っているならここに飛んでください。

1. Pythonにおいてすべてがオブジェクトである

タイトルの通り、Pythonにおいて全てがオブジェクトです。

変数、関数、クラス、モジュール、パッケージなどなど、全ての要素がオブジェクトです。

大事なことは3回いう、関数はオブジェクトです。

オブジェクトであれば、値のように渡したり返したりすることができる。

2. 関数を値として渡す

関数を値として渡す例を見てみよう!

def hello(name: str = 'sei'):
    return 'hello, ' + name

greet = hello

print('print hello:', hello)
print('print hello():', hello())

print('print greet', greet)
print('print greet()', greet())

実行結果はこうなる:

print hello: <function hello at 0x10c563ee0>
print hello(): hello, sei
print greet <function hello at 0x10c563ee0>
print greet() hello, sei

greet = hello をすることで、関数 hello を変数 greet に渡した。それによって、変数 greet はメモリの’0x10c563ee0’というところにある <function hello> をポイントしていることがわかった。だから、 hello()greet() の実行結果は同じになる。

ちなみに、関数の後ろに () をつけたらその関数を実行して結果を返すが、つけないと関数自身を返す。

3. 関数を返す関数

関数を関数の返り値として返すことができる。

次の例を見てみよう!

def hello(name: str = 'sei'):
    def greet():
        return 'now you are in the greet() function'

    def welcome():
        return 'now you are in the welcome() function'

    if name == 'sei':
        return greet
    else:
        return welcome

hello_returned = hello()

print('print hello_returned:', hello_returned)
print('print hello_returned():', hello_returned())

その実行結果は:

print hello_returned: <function hello.<locals>.greet at 0x10b08c3a0>
print hello_returned(): now you are in the greet() function

今変数 hello_returned は関数 greet をポイントしていることがわかった。

ここで注目、関数の後ろに () をつけたら関数が実行してしまう。関数自身を返したい場合は () をつけてはいけませんよ!

ちなみに、 hello()() っていう書き方もできる、実行すると now you are in the greet() function が出力される。

4. 関数を引数をとして渡す

もちろん、関数は関数の引数として渡すこともできる。

次の例を見てみよう!

def hi():
    return "hi sei!"
 
def do_something_before_hi(func):
    print("I am doing some bullshit before executing %s()" % func.__name__)
    print(func())
 
do_something_before_hi(hi)

関数 do_something_before_hi はprintした後、渡された関数を返り値をprintする関数

関数 hi を引数として do_something_before_hi を実行してみると以下になる:

I am doing some bullshit before executing hi()
hi sei!

ちなみに、関数には __name__ という関数の名前を格納する特別な属性があって、詳しくは公式ドキュメント

デコレータを作りましょう

必要な知識は揃えたので、いよいよデコレータとはについて説明しよう!

@ を使わずデコレータを作る

実は、「4. 関数を引数をとして渡す」の章ですでにデコレータを作っている。もう少しちゃんとしたやつを作ってみよう!

def a_new_decorator(func):
    def wrapTheFunction():
        print("I am doing some bullshit before executing %s()" % func.__name__)

        func()

        print("I am doing some bullshit after executing %s()" % func.__name__)

    return wrapTheFunction

def a_function_requiring_decoration():
    print("I am the bullshit function which needs some decoration")

print('デコレータをつける前:')
a_function_requiring_decoration()

# デコレータをつけている
a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)

print('\nデコレータをつけた後:')
a_function_requiring_decoration()

以上のコードを実行と:

デコレータをつける前:
I am the bullshit function which needs some decoration

デコレータをつけた後:
I am doing some bullshit before executing a_function_requiring_decoration()
I am the bullshit function which needs some decoration
I am doing some bullshit after executing a_function_requiring_decoration()

注目すべきなのはここ

a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)

これがすべてです。これがpythonにおいてデコレータの原理です。デコレータの本質は「元関数を受け取って新関数を返す関数」です。元関数を受け取って、新しい関数を作って、いろいろな方式で元関数の行為を変えることができる。

ここの場合、新しい関数は元関数が実行される前と後ろにprint文を入れている。

デコレータは @ を使うんしゃ.....と思うかもしれないが、それはデコレートされた関数を生成するための糖衣構文に過ぎない。

@ を使ってコードを整理する

では、 @ を使うとこうなる

def a_new_decorator(func):
    def wrapTheFunction():
        print("I am doing some bullshit before executing %s()" % func.__name__)

        func()

        print("I am doing some bullshit after executing %s()" % func.__name__)

    return wrapTheFunction

@a_new_decorator
def a_function_requiring_decoration():
    print("I am the bullshit function which needs some decoration")

a_function_requiring_decoration()

この部分は

@a_new_decorator
def a_function_requiring_decoration():
    ...

ここと同じことをやっている

a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)

ビルドインデコレータ wraps

ここで、デコレータの原理について大まかな理解はあったと思うが、上のサンプルコーデではひとつ問題がある。以下のコードを実行すると:

print(a_function_requiring_decoration.__name__)

出力は:

wrapTheFunction

おかしいですね、関数の名前は a_function_requiring_decoration であって欲しい。デコレータは元関数と違う新しい関数を返すのが基本だから、元関数のdocstringなどの情報は書き換えされる、それは望ましくない。幸いに、Pythonには簡単にこれを解決できる関数がある、それは functools.wraps 。これを使って上の例を直してみよう:

from functools import wraps

def a_new_decorator(func):
    @wraps(func)
    def wrapTheFunction():
        print("I am doing some bullshit before executing %s()" % func.__name__)

        func()

        print("I am doing some bullshit after executing %s()" % func.__name__)

    return wrapTheFunction

@a_new_decorator
def a_function_requiring_decoration():
    print("I am the bullshit function which needs some decoration")

a_function_requiring_decoration()

print("\n" + a_function_requiring_decoration.__name__)

出力は:

I am doing some bullshit before executing a_function_requiring_decoration()
I am the bullshit function which needs some decoration
I am doing some bullshit after executing a_function_requiring_decoration()

a_function_requiring_decoration

完璧ですね!

パラメーターの受け渡しも考えて、デコレータはこのように書くことが望ましいでしょう

from functools import wraps

def decorator_name(func):
    @wraps(func)
    def decorated(*args, **kwargs):
        print('Do some bullshit BEFORE executing %s' % func.__name__)
        res = func(*args, **kwargs)
        print('Do some bullshit AFTER executing %s' % func.__name__)
        return res

    return decorated

@decorator_name
def func(para="No parameter"):
    print("Function is running with parameter:{}".format(para))
    return "Function returned"

print(func(), end="\n\n")

print(func("MyjLab"))

出力は:

Do some bullshit BEFORE executing func
Function is running with parameter:No parameter
Do some bullshit AFTER executing func
Function returned

Do some bullshit BEFORE executing func
Function is running with parameter:MyjLab
Do some bullshit AFTER executing func
Function returned

*args, **kwargs は可変個の引数を受けるの書き方です。詳しくは公式ドキュメント

使用例

ここでまずひとつ、関数の実行の所要時間、performanceをログに出力する使用例を紹介します。

from functools import wraps
import time


def log_performance(func):
    @wraps(func)
    def with_logging_performance(*args, **kwargs):
        start_time = time.time()
        # 関数を実行
        res = func(*args, **kwargs)
        # 実行時間を計算して、プリントする
        run_time = time.time() - start_time
        print("%s was called in run time: %fs" % (func.__name__, run_time))
        return res

    return with_logging_performance


@log_performance
def Fib(n):
    """
    N番目のフィボナッチ数列を再帰法で求めている
    """
    def _fib(n):
        if n == 1:
            return 0
        elif n == 2:
            return 1
        else:
            return _fib(n - 1) + _fib(n - 2)

    return _fib(n)


print(Fib(35))

出力は:

Fib was called in run time: 2.021877s
5702887

log_performance が生成した新しい関数は

  1. 時間を記録する
  2. 元関数の実行結果を res に保存する
  3. 実行時間を計算する
  4. 実行時間プリントする
  5. 関数の結果 res を返す

のことをやっている。

元関数がなんであれ、デコレータをつけるだけで元関数を影響与えずに簡単に実行時間を出力することができた。

注意!
再帰関数にデコレータをつけるとき、そのままつけると、自身を呼ぶ出すたびにデコレータ内の処理も実行されるので、要注意!それを避けたいときは、例のように再帰ではない関数の内部に入れればいい

パラメータを持つデコレータ

前に出た @wraps(func) についてもう少し考えよう、 @wraps はデコレータなのに普通の関数のようにパラメータを受けている。

@my_decorator の構文を使っているときは「元関数を受け取って新関数を返す関数」を実行している。その関数のことをデコレータという。

だから、デコレータを返す関数を @ の後ろにつければ、デコレータとしてパラメーターを受けることができる。

引数を受けることでデコレータの挙動を指定できるので便利です。

上のログの例を少し直して出力ファイルを指定できるようしてみると:

from functools import wraps
import time


def log_performance(logfile="out.log"):
    def logging_decorator(func):
        @wraps(func)
        def with_logging_performance(*args, **kwargs):
            start_time = time.time()
            # 関数を実行
            res = func(*args, **kwargs)
            # 実行時間を計算して、プリントする
            run_time = time.time() - start_time
            log_string = "%s was called in run time: %fs" % (func.__name__, run_time)
            print(log_string)

            # 指定のlogfileにログを出力
            with open(logfile, 'a') as opened_file:
                opened_file.write(log_string + '\n')
            return res

        return with_logging_performance

    return logging_decorator


@log_performance()
def Fib(n):
    """
    N番目のフィボナッチ数列を再帰法で求めている
    """
    def _fib(n):
        if n == 1:
            return 0
        elif n == 2:
            return 1
        else:
            return _fib(n - 1) + _fib(n - 2)

    return _fib(n)


@log_performance(logfile="Fib_for.log")
def Fib_for(n):
    """
    N番目のフィボナッチ数列をfor循環で求めている
    """
    a, b = 0, 1
    if n == 1:
        return a
    elif n == 2:
        return b
    else:
        for i in range(n - 2):
            a, b = b, a + b
        return b

print(Fib(35))
print(Fib_for(35))

まずは log_performance はネストをひとつ増やして、 logfile という引数を受け取ってデコレータを返す関数に変更した。

内部の17行目から20行目は logfile で指定したファイルにログを書き込む処理を追加した。

さらに、新しいfor循環でフィボナッチ数列を求む Fib_for を作ってデフォルトと違う logfile を指定した。

実行すると、アウトプットは:

Fib was called in run time: 10.855503s
5702887
Fib_for was called in run time: 0.000048s
5702887

フォルダーに out.logFib_for.log の二つのファイルが作成されるはずです。

引数を受け取るデコレータの挙動を説明すると、この部分は

    ...
@log_performance()
def Fib(n):
    ...

次に書き方と同じことやっている

logging_decorator = log_performance()  # デフォルトでlogfile="out.log"
Fib = logging_decorator(Fib)

前にも言ったが、関数の後ろに () を書くと関数が実行される。なので、デコレータの場合も同じなので実行の順番はこんな感じ:

  1. log_performance() が実行されて、デコレータの logging_decorator を返す
  2. logging_decorator は関数 Fib を受けて、新しい関数を返す

P.S. 詳細仕様

正直ちょっと難しい概念なので飛ばしてもいいと思う。

上で言った通り、デコレータは関数を返す関数。だけど、その関数はデコレータのパラメータによって挙動が変わる状態をもつ関数。ではそのパラメータはどこに格納されてるの?

実はここではクロージャという技術が使われている。個人的な理解だと、クロージャとは、その関数の外のスコープのこと。状態(変数)をもつ関数という理解でもいい。

なので、引数を受けるデコレータは正確にいうと関数を返しているではなく、クロージャを返している。

関数(クロージャ)の __closure__ 属性を使ってクロージャ内部の変数(オブジェクト)をアクセスすることができる。__closure__はcellオブジェクトのタプルで、その値は cell_contents に格納されている。また、変数(オブジェクト)の名前はまた別で関数(クロージャ)の __code__.co_freevars というタプルに格納されている。

上の例で言うと

# 上のつづき
for i in range(len(Fib.__closure__)):
    print(Fib.__code__.co_freevars[i], ":", Fib.__closure__[i].cell_contents)

出力は:

func : <function Fib at 0x108959790>
logfile : out.log

ちなみに、推奨はできませんがこんなことも書ける

Fib.__closure__[1].cell_contents = "out2.log"

参考文献

一応載せときますが中国語のサイトもあります。

52
32
3

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
52
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?