Skip to content

Instantly share code, notes, and snippets.

@masakos
Last active February 27, 2022 02:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save masakos/fdeaecf8e5617b3e430ce4e2d00265fa to your computer and use it in GitHub Desktop.
Save masakos/fdeaecf8e5617b3e430ce4e2d00265fa to your computer and use it in GitHub Desktop.
[資料] Pyladies Tokyo Meetup デコレーターを理解しよう

Pythonのデコレーターを完全に理解しよう!

sugita@

このセッションの内容

  • 関数・クラスデコレーターの使い方を理解する
  • 関数デコレーターを理解して自作できるようになる

デコレーターとは

  • クラスデコレーターと関数デコレーターがある
  • 関数やメソッド、クラスをデコレート(装飾)する機能
  • デコレーターを使用すると、関数やメソッド、クラスそのものの中身を変えずに共通のロジックを適用することができる

使用されるケース

  • ログ、トランザクション、セキュリティ、例外処理、キャッシュ、リトライなどでよく使用される

  • (例)Django

from django.db import transaction

@transaction.atomic
def viewfunc(request):
    # This code executes inside a transaction.
    do_stuff()

https://docs.djangoproject.com/en/4.0/topics/db/transactions/

  • コードのブロックが正常に完了すると、変更はデータベースにコミットされる。例外が発生すると変更はロールバックされる。

デコレーターの使い方

デコレーターを適用する関数やメソッド、クラス定義の前に @デコレーター名 とつけるだけ!!。

@デコレーター名
def デコレート対象の関数:
    pass

クラス、クラスのメソッドも同様

# クラスメソッドにデコレーターを適用 
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    @property
    def start_name(self):
        return self.name[0]
    
user = User('panda-senpai', 17)
print(user.name)
print(user.start_name)
# クラスにデコレーターを適用
from dataclasses import dataclass

@dataclass  # dataclassデコレーターをUserクラスに適用
class User:
    name: str
    age: int

user = User('panda', 29)
print(user.name)
print(user.age)
  • dataclassデコレータを使用することで、コンストラクタのメソッドである__init__などが自動的に定義される。

関数デコレーターを使用する


サードパーティライブラリの retrying

  • デコレート対象の関数内で例外が発生した際に、再度関数の実行を行ってくれるデコレーターretryを提供している。
$ pip install retrying

以下で動作確認してみよう

  • @retryなし
  • @retryあり
    • stop_max_attempt_numberあり(最大リトライ回数を指定する引数)
from retrying import retry

@retry(stop_max_attempt_number=2)
def my_func():
  for i in range(10):
      if i == 3:
        print('==============> 3だ!!!')
        raise ValueError('3はエラー')
      else:
        print(f'{i=}です')

my_func()

2つ以上のデコレーターを適用する

  • 1つの関数やメソッド、クラスに複数のデコレーターを適用できる。
  • 一行ごとに1つのデコレーターを指定します。

以下の例で、複数のデコレーターを適用する構文と、代入文は等価です。

@my_decorator2
@my_decorator1
def func():
    pass

デコレーターは内側のmy_decorator1から外側に向かって順に適用される。

関数デコレーターを自作する

デコレーターは下記の代入文と等価です。

@my_decorator
def func():
    pass
func = my_decorator(func)
  • (参考)https://www.python.org/dev/peps/pep-0318/
  • デコレーターは対象のオブジェクトを置き換えるための機能です。
  • my_decorator() 関数(デコレーター関数)は関数 func を引数にとり、その返された結果を func に代入することで func を置き換えている。

Python の関数の特徴をまずは知ろう!

  1. Pythonでは関数内に関数を定義することができる。
    • 外側の関数の戻り値として内側の関数を返すことができる。
def func_greeting(name):
    def print_greeting():
        print(f'Hello! {name} さん')
    return print_greeting

func = func_greeting('panda-senpai')
func() # () をつけてよびだすことができる
  1. Pythonでは、関数を別の関数の引数として与えることができます。
def greeting(name):
    print(f'Hello {name} さん')

def after_greeting(func, name):
    func(name)
    print('今日はいいお天気ですね')

after_greeting(greeting, 'panda')

これらの機能を使用して作成するのがデコレーター!!

デコレーターは以下のように実装します。

  • デコレーター関数を定義する
  • デコレーター関数はデコレート対象の関数を引数として受け取るようにする
  • デコレーター関数の中にデコレート対象の関数の変わりに呼び出されるラッパー関数を定義し、その中で引数で受け取った関数を呼びだす。関数呼び出しの前後に追加、変更の処理を加える
  • デコレーター関数の戻り値として呼び出し可能オブジェクト(関数)を返す
def my_decorator(func):  # デコレーター関数
     def wrap_function():  # デコレート対象の関数の変わりに呼び出されるwrapper関数
         print('デコレート対象の関数を呼ぶ前にしたい処理')
         func()
         print('デコレート対象の関数を呼んだ後にしたい処理')
     return wrap_function
  • 代入文を使用する書き方
 def greeting():
     print(f'こんにちは')
 
 greeting = my_decorator(greeting) 
 greeting()
  • @デコレーター名構文を使用して置き換える
@my_decorator
def greeting():
    print(f'こんにちは')

greeting()

デコレートされる関数に引数がある場合

def my_decorator(func): 
    def wrap_function(*args, **kwargs):
        print(f'{args=}:{kwargs=}')
        return func(*args, **kwargs)
    return wrap_function

@my_decorator
def my_sum(a, b, initial=0):
    return initial + a + b ;

total = my_sum(10, 20, initial=100)
print(f'合計は {total} です')

functools.wrapsを使用する

  • デコレーターを使用すると、関数が置き換わるため元の関数名やdocstringが失われ、ログを表示したい場合やエラーが発生した場合など正しく表示されないという問題がある。
  • この問題を回避するために、functools.wrapsが提供されている。functools.wrapsを設定すると、名前やdocstringを元のデコレート対象の関数のものに設定してくれる。
  • wrapsはデコレーターなので、wrapper関数にwrapsデコレーターを適用し、引数に元のデコレート対象の関数を設定。
  • 通常、デコレーターを作成する際には、functools.wrapsを使用するとよいです!
# docstringや関数名が失われる例

def my_decorator(func): 
     
    def wrap_function():
        '''wrap用の関数です'''
        func()
        print(f'function: {func.__name__} called')
    return wrap_function

@my_decorator
def greeting():
    '''あいさつを返す関数です'''
    print(f'こんにちは')

greeting()
print(greeting.__name__)
print(greeting.__doc__)
# functools.wrapsを使用した例

from functools import wraps
def my_decorator(func): 
     @wraps(func)  # この1文を足すのみ
     def wrap_function(): 
         '''wrap用の関数です'''
         func()
         print(f'function: {func.__name__} called')
     return wrap_function

@my_decorator
def greeting():
    '''あいさつを返す関数です'''
    print(f'こんにちは')

retryデコレーターを自作してみよう

引数を受け取るデコレーター

  • 引数を受け取ってデコレータ関数を生成する関数を作成する
def my_decorator(引数):  # 引数を受け取ってデコレーター関数を生成する関数
    def _my_decorator(func):  # デコレーター関数
        def wrap_function():  # デコレート対象の関数の変わりに呼び出されるwrapper関数
            print('デコレート対象の関数を呼ぶ前にしたい処理')
            func()
            print('デコレート対象の関数を呼んだ後にしたい処理')
        return wrap_function
    return _my_decorator
  • retryデコレーター
from functools import wraps
from time import sleep

def my_retry(stop_max_attempt_number=1):
    def _my_retry(func):
        @wraps(func)
        def wrap_function():
            for i in range(stop_max_attempt_number):
                try:
                    return func()
                except Exception as e:
                    if i < stop_max_attempt_number-1:
                        sleep(1)
                    else:
                        raise e
        return wrap_function
    return _my_retry
        
@my_retry(stop_max_attempt_number=2)
def my_func():
    for i in range(10):
        if i == 3:
            print('==============> 3だ!!!')
            raise ValueError('3はエラー')
        else:
            print(f'{i=}です')


my_func()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment