Closed14

Pythonのデコレーターについて

みはなだみはなだ

きっかけ

実装として良いか悪いかはおいておいて、クラスで定義した引数なしのデコレーターをインスタンスメソッドに利用したかった。
思いの外ハマったしまだ解決していないけど、とりあえず調べたことを書き連ねる

そもそもデコレーターって

pythonのデコレーターは関数とクラスで実装できる。また、デコレーターが引数を持つか持たないかで微妙に実装方法が異なる。
さらに、インスタンスやクラスに束縛されたメソッドと単独で定義した関数を対象にする場合でさらに実装方法が異なる。
なので、結局分けると全部で8パターン考える必要がありそう。

対象 デコレーターの実装 引数有無
関数 関数 なし
関数 関数 あり
関数 クラス なし
関数 クラス あり
メソッド 関数 なし
メソッド 関数 あり
メソッド クラス なし
メソッド クラス あり
みはなだみはなだ

関数 -> 関数

デコレーターの対象になる関数の実装

def target_function():
    print("Target function is called.")
    return 1

print(target_function())
# Target function is called.
# 1

まずは引数なしから

def decorator(func):
    """関数の戻り値を2に変更するdecorator"""
    def wraps(*args, **kwargs):
        print("=== Function is decorated.")
        result = func(*args, **kwargs)
        print("=== decorator changes return value to 2.")
        return 2
    
    return wraps

@decorator
def target_function():
    print("Target function is called.")
    return 1

print(target_function())
# === Function is decorated.
# Target function is called.
# === decorator changes return value to 2.
# 2

次に引数のあるデコレーター

def decorator_with_args(return_value):
    """関数の戻り値をデコレーターの引数として与えた値に変更するデコレーター"""
    
    def decoration(func):
        def wraps(*args, **kwargs):
            print("=== Function is decorated.")
            result = func(*args, **kwargs)
            print(f"=== decorator changes return value to {return_value}.")
            return return_value

        return wraps
    
    return decoration

@decorator_with_args(-1)
def target_function():
    print("Target function is called.")
    return 1

print(target_function())
# === Function is decorated.
# Target function is called.
# === decorator changes return value to -1.
# -1
  • 引数がない場合:@に続けて記載したデコレーターがCallableである必要がある
  • 引数がある場合:@に続けて記載したデコレーターの引数を与えた実行結果がCallableである必要がある。
みはなだみはなだ

関数 -> メソッド

まずは、対象のクラスから

class TargetClass:
    
    def __init__(self):
        self.a = 1
        self.b = "b"
        
    def print_a(self):
        print("print_a() is called.")
        return self.a
        
    def print_b(self):
        print("print_b() is called.")
        return self.b
    
target = TargetClass()
print("return value: ", target.print_a())
print("return value: ", target.print_b())
# print_a() is called.
# return value:  1
# print_b() is called.
# return value:  b

関数 -> 関数で実装したデコレーターを利用する

class TargetClass:
    
    def __init__(self):
        self.a = 1
        self.b = "b"
        
    @decorator
    def print_a(self):
        print("print_a() is called.")
        return self.a
        
    @decorator_with_args("c")
    def print_b(self):
        print("print_b() is called.")
        return self.b
    
    
target = TargetClass()
print("return value: ", target.print_a())
print("return value: ", target.print_b())
# === Function is decorated.
# print_a() is called.
# === decorator changes return value to 2.
# return value:  2
# === Function is decorated.
# print_b() is called.
# === decorator changes return value to c.
# return value:  c

関数 -> 関数と同じ時のようにちゃんとデコレーターが働いていることがわかる。

ところで、、、
クラスのメソッドには暗黙的にselfを一番最初の引数に置きます。
これがどうなっているのか気になります。
このままではわからないので、以下のような引数を表示するデコレーターを用意し確認します。

def decorator_print_inputs(func):
    """decoratorに引数と元の関数の出力を出力する処理を追加したデコレーター"""
    def wraps(*args, **kwargs):
        print("=== Function is decorated.")
        print("input values: ", *args, **kwargs)
        result = func(*args, **kwargs)
        print(f"return value of function is {result}")
        print(f"=== decorator changes return value to -1.")
        return -1
    return wraps

適当にクラスを作成し、実行結果を確認します。

class TargetClass2:
    
    def __init__(self):
        self.a = 1
    
    @decorator_print_inputs
    def add_a(self, other: int):
        print("add_a() is called.")
        return self.a + other
    
target2 = TargetClass2()
print("return value: ", target2.add_a(3))
# === Function is decorated.
# input values:  <__main__.TargetClass2 object at 0x11a25fbd0> 3 
# add_a() is called.
# return value of function is 4
# === decorator changes return value to -1.
# return value:  -1
  • 元の戻り値は4だけどデコレーターによって-1に変換されている。
  • デコレーターには selfも含めて渡されている。
みはなだみはなだ

デコレーターによって変換された関数がインスタンスやクラスに束縛されたメソッドの場合、デコレータ実装中は気をつけないといけない。
selfclsを意識して実装することがないため。

example1 = Example(1)
example2 = Example(2)

# 普段はこっち
example1.add(example2)

# 一応これでも等しい
# デコレータの引数として与えられた関数はこの形式になってる。
Example.add(example1, example2)

確かに、クラス内に定義された関数へデコレータを定義した場合、クラス宣言時に実行されている。
つまり、確かにインスタンス変数としてselfが与えられてるとおかしい。

def simple_decorator(func):
    print("Called decorateor")
    def wraps(*args, **kwargs):
        result = func(*args, **kwargs)
        return result
    return wraps
    
class TargetClass3:
    
    def __init__(self):
        pass
    
    @simple_decorator
    def temp(self):
        print("This is temp function.")
# Called decorateor

TargetClass3を宣言した段階でデコレーターの処理は行われている。

みはなだみはなだ

Class -> 関数

Classでデコレータを実装

class Decorator:
    
    def __init__(self, func):
        self.func = func
        
    def __call__(self, *args, **kwargs):
        print("=== Function is decorated.")
        print("input values: ", *args, **kwargs)
        result = self.func(*args, **kwargs)
        print(f"return value of function is {result}")
        print(f"=== decorator changes return value to -1.")
        return -1
    
@Decorator
def target_function(v):
    print("Target function is called.")
    return v

print("return value: ", target_function(1))

対応さえ間違わなければそこまで違和感なくそのまま実装可能

みはなだみはなだ

Class -> メソッド

こいつがおかしいことになる。
今までの結果からいくと、*args, **kwargsさえ与えておけばなんとかなっていたはず。
ということで、とりあえずやってみる

class DecoratorWithArguments:
    """引数のあるデコレーター"""
    
    def __init__(self, return_value):
        self.return_value = return_value
        
    def __call__(self, func):
        def wraps(*args, **kwargs):
            print("=== Function is decorated.")
            result = self,func(*args, **kwargs)
            print(f"=== decorator changes return value to {self.return_value}.")
            return self.return_value

        return wraps

        

class TargetClass:
    
    def __init__(self):
        self.a = 1
        self.b = "b"
        
    @Decorator
    def print_a(self):
        print("print_a() is called.")
        return self.a
        
    @DecoratorWithArguments("c")
    def print_b(self):
        print("print_b() is called.")
        return self.b
    
    
target = TargetClass()
print("return value: ", target.print_a())
print("return value: ", target.print_b())

どうやらselfがないらしい。

みはなだみはなだ

デバッグ用のデコレーターを作成し、変数を確認してみる。

class DebugDecorator:

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

    def __call__(self, *args, **kwargs):
        print("self: ", self)
        print("args: ", args)
        print("kwargs: ", kwargs)
        result = self.func(*args, **kwargs)
        return result


class TargetClass:

    def __init__(self):
        self.a = 1
        self.b = "b"

    @DebugDecorator
    def print_a(self):
        print("print_a() is called.")
        return self.a
みはなだみはなだ
target = TargetClass()
try:
    print("return value: ", target.print_a()) 
except:
    pass
# -> self:  <__main__.DebugDecorator object at 0x11afefa50>
# -> args:  ()
# -> kwargs:  {}

args, kwargsには何も入ってない。関数実行時の引数がそのまま渡されていて、selfがここには渡されていない。
print_aについて確認してみる。

print(target.print_a)  # <__main__.DebugDecorator object at 0x11afefa50>
print(target.print_a.func) # <function TargetClass.print_a at 0x11a348fe0>

デコレーターによってprint_aDebugDecoratorに置き換わっている。
DebugDecorator__init__内ではインスタンス変数に代入されていて、実際にTargetClass.print_aが代入されている。

この時のprint_aはクラスに紐づく関数として渡されているので、実行時に暗黙的にselfは渡されない。
その結果、selfが与えられていないという話になりそう。

実際にクラスから直接print_aを呼ぶと同様にselfが存在しないというエラーが出る。

class TargetClass:

    def __init__(self):
        self.a = 1
        self.b = "b"

    def print_a(self):
        print("print_a() is called.")
        return self.a


print(TargetClass.print_a)  # <function TargetClass.print_a at 0x11b171d00>
TargetClass.print_a()  # TypeError: TargetClass.print_a() missing 1 required positional argument: 'self'
みはなだみはなだ

クラス -> メソッドの解決策

原因をまとめると

  1. デコレーターによる関数の書き換えは宣言時に行われる。つまり、selfclsのような暗黙的な変数は宣言時に関数内部に渡すことができない。
  2. クラスで実装したデコレーターの場合、対象の関数はCallableなオブジェクトに置き換わる。暗黙的にselfclsが与えられなくなるらしい。

確かにインスタンスからメソッドを呼んだ場合はselfが与えられているが、そのものの関数を読んだ場合は与えられていない。インスタンスに束縛された関数がメソッドとして呼ばれた場合、自身のインスタンスが暗黙的に第1引数に渡される処理がありそう。

多分デコレータとしてとてもお馴染みの@propertyの実装を調べてみて見つけた。
https://github.com/python/cpython/blob/bc07c8f096791d678ca5c1e3486cb9648f7a027b/Lib/enum.py#L184

__get____set____delete__というのがあるらしい。

みはなだみはなだ

触ってみる。

こんなクラスを実装した。

class Number:
    
    def __init__(self, v):
        self.v = v
            
    def __get__(self, instance, owner_class):
        """
        属性参照された時に実行される。
        
        Parameters
        -----
        instance: 
            Numberのインスタンスを管理しているインスタンス
        owner_class: type
            自身を管理しているクラスの型
        """
        print("__get__ is called.")
        print(instance, owner_class)
        return self.v
        
    def __set__(self, instance, input_value):
        """
        属性として設定された時に実行される。
        
        Parameters
        -----
        instance: 
            Numberのインスタンス
        input_value: 
            設定されようとした値
        """
        print("__set__ is called.")
        print(self, instance, input_value)
        self.v = input_value
        return self 
        
    def __delete__(self, instance):
        print("__delete__ is called.")
        
class Temp:
    d = Number(1)

役割

  • __get__: 属性を参照された時に呼び出される
  • __set__: 属性を設定された時に呼び出される
  • __delete__: 属性を削除された時に呼び出される
みはなだみはなだ
temp = Temp()

print("========== __get__")
x = temp.d
print(x)

print("========== __set__")
temp.d = 2
print(temp.d)

print("========== __delete__")
del temp.d
del temp.d

実行結果

========== __get__
__get__ is called.
<__main__.Temp object at 0x11e770f90> <class '__main__.Temp'>
1
========== __set__
__set__ is called.
<__main__.Number object at 0x11e773110> <__main__.Temp object at 0x11e770f90> 2
__get__ is called.
<__main__.Temp object at 0x11e770f90> <class '__main__.Temp'>
2
========== __delete__
__delete__ is called.
__delete__ is called.

ちゃんと、getやsetでNumberクラスのインスタンス変数を参照できていることがわかる。

deleteは呼ばれているけど、削除されていない。とっても謎。
多分削除しちゃダメな変数とかを削除されそうになった時にエラーにできる。

みはなだみはなだ

デコレーターを適応したメソッドは関数として呼ばれるために、呼び出し元のインスタンスを取得することができなかった。
しかし、__get__を使うと呼び出し元のインスタンスやクラスを利用することができる。

DebugDecorator__get__を追加する

class DebugDecorator:

    instance = None
    
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print("self: ", self)
        print("args: ", args)
        print("kwargs: ", kwargs)
        result = self.func(self.instance, *args, **kwargs)
        return result
    
    def __get__(self, instance, owner_class):
        """
        __get__で呼ばれた際に、自身のクラスのインスタンスとして、親クラスのインスタンスを覚える
        """
        self.instance = instance
        return self

これで、ひとまず呼び出し元のインスタンスを取得することができるはず。
確認してみる


class TargetClass:

    def __init__(self):
        self.a = 1
        self.b = "b"

    @DebugDecorator
    def print_a(self):
        print("print_a() is called.")
        return self.a


target = TargetClass()
print("print_a is ", target.print_a) 
print("return value: ", target.print_a()) 
print_a is  <__main__.DebugDecorator object at 0x11eb30e10>
self:  <__main__.DebugDecorator object at 0x11eb30e10>
args:  ()
kwargs:  {}
print_a() is called.
return value:  1

ちゃんとエラーが起きずに実行された。

みはなだみはなだ

ちょっと安直すぎて微妙な気がするけど、一旦instanceの値によって処理を変更するようにするとうまくいった。

class DebugDecorator:

    instance = None
    
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print("self: ", self)
        print("args: ", args)
        print("kwargs: ", kwargs)
        if self.instance:
            result = self.func(self.instance, *args, **kwargs)
        else:
            result = self.func(*args, **kwargs)
        return result
    
    def __get__(self, instance, owner_class):
        """__get__で呼ばれた際に、自身のクラスのインスタンスとして、親クラスのインスタンスを覚える"""
        self.instance = instance
        return self


class TargetClass:

    def __init__(self):
        self.a = 1
        self.b = "b"

    @DebugDecorator
    def print_a(self):
        print("print_a() is called.")
        return self.a

# クラス内のメソッドへのデコレーター
target = TargetClass()
print("print_a is ", target.print_a) 
print("return value: ", target.print_a()) 

# 関数へのデコレーター
@DebugDecorator
def target_function(v):
    print("Target function is called.")
    return v

print(target_function(1))
print_a is  <__main__.DebugDecorator object at 0x11eb5dbd0>
self:  <__main__.DebugDecorator object at 0x11eb5dbd0>
args:  ()
kwargs:  {}
print_a() is called.
return value:  1

self:  <__main__.DebugDecorator object at 0x11b0f4410>
args:  (1,)
kwargs:  {}
Target function is called.
1
このスクラップは2023/06/20にクローズされました