1
2

More than 1 year has passed since last update.

Pythonのdecoratorについて

Last updated at Posted at 2022-02-12

著者紹介など

初めまして!
sai-haiと申します.よろしくお願いします!

この記事では,Python の decorator について調べたことをまとめました.はじめての記事で至らない点が多いかと思います.間違っている点はコメントなどで指摘していただけると嬉しいです.

実行環境

Python3.11.2

decorator のお気持ち理解

decorator とは @ を用いた表現です。後ろに続いて定義した関数・メソッド・クラスを装飾することができます。

@a # decorator
@b # decorator
def c:
    pass

# c = a(b(c))

@D # decorator
class E:

# E = D(E)

この時、a, b, A, B はどれも呼び出し可能 (callable) なオブジェクトであるとします。decorator は上のように任意の個数で続けて記述することが可能です。その場合、入れ子になります。( decorator 同士の間には必ず改行が必要です。)

python における decorator の位置付け・動機

Python 2.4 におけるPEP318 - Decorators for Functions and Methodsを参照します。以下 Abstract の和訳です。

関数やメソッドを変換する現在の方法(例えば、クラスや静的メソッドとして宣言する)は厄介で、理解しにくいコードにつながる可能性があります。理想的には、これらの変換は、宣言自体が行われるコード内の同じ箇所で行われるべきです。このPEPでは、関数またはメソッド宣言の変換のための新しい構文を導入します。

具体的なdecoratorの使用意図は、

def foo(cls):
    pass
foo = synchronized(lock)(foo)
foo = classmethod(foo)

のように、関数 foo が3回宣言されている pythonic でないコードを、

@classmethod
@synchronized(lock)
def foo(cls):
    pass

のように簡潔に表現することです。(出典:Motivation)

Python 2.4 では、decorator は関数とメソッドにのみ適用可能でしたが、PEP3129 – Class Decorators にある通り、Python 3.0 において、クラスについても適用可能になりました。

文法について

Python 3.11.2 Documentation における、Function definitions, Class definitions を参照すると、関数・クラスの定義はそれぞれ

funcdef ::= [decorators] "def" funcname "(" [parameter_list] ")" ["->" expression] ":" suite
classdef ::= [decorators] "class" classname [inheritance] ":" suite

です。このうち decorators は decorator が一つ以上続いたものです。

decorators ::= decorator+
decorator ::= "@" assignment_expression NEWLINE

decorator の使用法

関数・メソッドの装飾

基本の例

def decorate(func):
    def wrapper(*arg,**kwarg):
        print("===1")
        func(*arg,**kwarg)
        print("===2")
    return wrapper

@decorate # 直後にdefされたものを引数にとって装飾
def greet():
    print("Hello Qiita!")
# greet = decorate(greet)

@decorate # 直後にdefされたものを引数にとって装飾. 再利用可能!!
def greet2():
    print("Hi Qiita!")
# greet2 = decorate(greet2)

greet()
# 出力
# ===1
# Hello Qiita!
# ===2

greet2()
# 出力
# ===1
# Hi Qiita!
# ===2

decorator をネストした場合

ネストした場合は、次のように入れ子になります。

def deco1(func):
    def wrapper1(*arg,**kwarg):
        print("===1")
        func(*arg,**kwarg)
        print("===2")
    return wrapper1

def deco2(func):
    def wrapper2(*arg,**kwarg):
        print("===3")
        func(*arg,**kwarg)
        print("===4")
    return wrapper2

@deco1
@deco2
def greet():
    print("Hello Qiita!")
# greet = deco1(deco2(greet))

greet() 
# 出力
# ===1
# ===3
# Hello Qiita!
# ===4
# ===2

decorator によって装飾された関数が引数を持つ場合

続いて,decorator によって装飾された関数が引数を持つ場合は次のようになります。

def decorate1(func):
    def wrapper1(*arg,**kwarg):
        print("===1")
        func(*arg,**kwarg)
        print("===2")
    return wrapper1

@decorate1
def greet(value):
    print(value)
# greet = decorate1(greet)

greet("Hello World!") # wrapper1("Hello World!")
# 出力
# ===1
# Hello World!
# ===2

クラスによって関数・メソッドを装飾する場合

装飾した定義の時点で、装飾元のクラスの __init__ 関数が呼ばれていることに留意してください。

import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.num_calls = 0

    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print(f"Call {self.num_calls} of {self.func.__name__!r}")
        return self.func(*args, **kwargs)

@CountCalls
def say_whee(*arg):
    print("Whee!",*args)
# say_whee = CountCalls(say_whee)
# この時点で、say_whee は クラス CountCalls のインスタンス
# すでに CountCalls.__init__ が呼ばれている

say_whee(1,2) # __call__ が呼ばれる
# 出力
# Call 1 of 'say_whee'
# Whee! 1 2

say_whee(3,4,5) # __call__ が呼ばれる
# 出力
# Call 2 of 'say_whee'
# Whee! 3 4 5

(コードの出典:Primer on Python Decoratorsから改変)

decorator 使用時の注意点!!

上記のコードたちは不親切になっている点がありました.2つ上のコードの場合について,

print(greet.__name__)
# 出力
# wrapper1

つまり,装飾された greet 関数の名前が decorator の関数の戻り値の関数の名前になってしまいます.そこでfunctoolsをインポートして,以下のようにすると,この点は解決されます。

import functools

def decorate1(func):
    @functools.wraps(func) # wrapper1 の名前を 引数の func の func.__name__ に変更
    def wrapper1(*arg,**kwarg):
        print("===1")
        func(*arg,**kwarg)
        print("===2")
    # wrapper1 = functools.wraps(func)(wrapper1)
    return wrapper1

@decorate1
def greet(value):
    print(value)

print(greet.__name__) # 出力 greet

ここでもまた decorator を使っています.

クラスの装飾

基本の使用例

関数・メソッドの装飾の場合と大きな違いはありません。いくつか具体例を挙げます。

dataclass の利用

dataclasses の dataclass を利用すると、クラスに使いやすい __init__ , __repr__ , __eq__ が自動生成されます。

from dataclasses import dataclass

@dataclass
class Student:
    name: str
    age: int
# Student = dataclass(Student)

student1 = Student("Alice", 19)
student2 = Student("Bob", 21)

print(student1)
print(student2)
print(student1 == student2)

# Student(name='Alice', age=19)
# Student(name='Bob', age=21)
# False

そのほかにも、classmethod, staticmethod, property などが主に利用されます。

シングルトンの作成(実用的ではない)

シングルトンとは,全く同じインスタンスしか生成できないクラスのことです.

import functools

def singleton(cls):
    """Make a class a Singleton class (only one instance)"""
    @functools.wraps(cls)  # singletonで返すwrapper_singletonを.引数cls
    def wrapper_singleton(*args, **kwargs):
        if not wrapper_singleton.instance:
            wrapper_singleton.instance = cls(*args, **kwargs)
        return wrapper_singleton.instance
    wrapper_singleton.instance = None
    return wrapper_singleton

@singleton
class TheOne:
    pass
# TheOne = singleton(TheOne)

first_one = TheOne()
another_one = TheOne()

print(id(first_one))
# 出力
# 140094218762280

print(id(another_one))
# 出力
# 140094218762280

print(first_one is another_one)
# 出力
# True

(コードの出典:Primer on Python Decoratorsから一部改変)

このように,適当なクラス (TheOne) を,その状態を持つためのクラス (singleton) でデコレートすることで,簡単にシングルトンにすることができました.もちろんこの decorator は再利用可能です.

Django の実装の一部

admin.py などにおいて、decorator が利用されています。

'''
code from:https://github.com/django/django/blob/5fcfe5361e5b8c9738b1ee4c1e9a6f293a7dda40/django/contrib/auth/admin.py#L25
'''
@admin.register(Group)
class GroupAdmin(admin.ModelAdmin):
    # 中略
    def formfield_for_manytomany(self, db_field, request=None, **kwargs):
        # 中略
        return super().formfield_for_manytomany(db_field, request=request, **kwargs)

decorator のまとめ

  • decorator とは @ を用いた表現で、後ろに続いて定義した関数・メソッド・クラスを装飾するもの
  • @a NEWLINE def b: は b = a(b) 
  • クラスで装飾する場合は、装飾されるものの定義のすぐ後ろで __init__ が呼ばれることに注意
  • クラスの装飾には、dataclass などが用いられる
1
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
1
2