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
も含めて渡されている。
デコレーターによって変換された関数がインスタンスやクラスに束縛されたメソッドの場合、デコレータ実装中は気をつけないといけない。
self
やcls
を意識して実装することがないため。
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_a
はDebugDecorator
に置き換わっている。
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'
クラス -> メソッドの解決策
原因をまとめると
- デコレーターによる関数の書き換えは宣言時に行われる。つまり、
self
やcls
のような暗黙的な変数は宣言時に関数内部に渡すことができない。 - クラスで実装したデコレーターの場合、対象の関数はCallableなオブジェクトに置き換わる。暗黙的に
self
やcls
が与えられなくなるらしい。
確かにインスタンスからメソッドを呼んだ場合はself
が与えられているが、そのものの関数を読んだ場合は与えられていない。インスタンスに束縛された関数がメソッドとして呼ばれた場合、自身のインスタンスが暗黙的に第1引数に渡される処理がありそう。
多分デコレータとしてとてもお馴染みの@property
の実装を調べてみて見つけた。
__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
クラスへのデコレーターは気が向いたらやりたい