ホームページ  >  記事  >  バックエンド開発  >  Pythonのデコレータについて詳しく解説

Pythonのデコレータについて詳しく解説

高洛峰
高洛峰オリジナル
2016-11-01 11:24:361252ブラウズ

Python のデコレーターは、Python に入るハードルです。越えるかどうかに関係なく、ハードルは存在します。

なぜデコレーターが必要なのですか

プログラムでは、say_hello() とsay_goodbye() という 2 つの関数が実装されていると仮定します。

def say_hello(): 
    print "hello!" 
     
def say_goodbye(): 
    print "hello!"  # bug here 
 
if __name__ == '__main__': 
    say_hello() 
    say_goodbye()

しかし、実際の呼び出しでは、プログラムが間違っていることがわかり、上記のコードは 2 つの hello を出力しました。デバッグ後、say_goodbye() にエラーがあることがわかります。上司は、次のように、各メソッドを呼び出す前に、入力する関数の名前を記録することを要求します。

[DEBUG]: Enter say_hello() 
Hello! 
[DEBUG]: Enter say_goodbye() 
Goodbye!

さて、小さな A は卒業生で、彼はそれを次のように実装しました。

rreee

とても低いですよね?小さなBはしばらく仕事をしており、小さなAに「このように書くことができる」と言いました。

def say_hello(): 
    print "[DEBUG]: enter say_hello()" 
    print "hello!" 
 
def say_goodbye(): 
    print "[DEBUG]: enter say_goodbye()" 
    print "hello!" 
 
if __name__ == '__main__': 
    say_hello() 
    say_goodbye()

もちろん、その方が良いでしょうか? しかし、すべてのビジネス関数で debug() 関数を呼び出すのは不快です。上司が、デバッグは、say 関連の関数には必要なく、do 関連の関数にのみ必要であると言ったらどうしますか?

それでは、この時点でデコレーターが現れるはずです。

デコレーターは本質的に Python 関数であり、これにより、コードを変更せずに他の関数が関数を追加できます。デコレーターの戻り値も関数オブジェクトです。これは、ログ挿入、パフォーマンス テスト、トランザクション処理、キャッシュ、権限の検証など、横断的な要件を持つシナリオでよく使用されます。デコレータは、この種の問題を解決するための優れた設計です。デコレータを使用すると、関数自体とは関係のない大量の類似コードを抽出し、それを再利用し続けることができます。

一言で言えば、デコレーターの役割は、既存の関数またはオブジェクトに機能を追加することです。

デコレーターの書き方

初期の頃 (Python バージョン 2.4 未満、2004 年以前)、関数に追加の機能を追加する方法は次のようなものでした。

def debug(): 
    import inspect 
    caller_name = inspect.stack()[1][3] 
    print "[DEBUG]: enter {}()".format(caller_name)    
 
def say_hello(): 
    debug() 
    print "hello!" 
 
def say_goodbye(): 
    debug() 
    print "goodbye!" 
 
if __name__ == '__main__': 
    say_hello() 
    say_goodbye()

上記のデバッグ関数は、実際には、元の関数をラップして別の関数を返し、いくつかの追加関数を追加します。この方法での記述はあまり洗練されていないため、Python の新しいバージョンでは @ 構文シュガーがサポートされています。次のコードは以前の記述方法と同等です。

def debug(func): 
    def wrapper(): 
        print "[DEBUG]: enter {}()".format(func.__name__) 
        return func() 
    return wrapper 
 
def say_hello(): 
    print "hello!" 
 
say_hello = debug(say_hello)  # 添加功能并保持原函数名不变

これは最も単純なデコレータですが、デコレータ関数がパラメータを渡す必要がある場合、このデコレータは壊れます。返された関数はパラメータを受け入れることができないため、次のように、デコレータ関数ラッパーが元の関数と同じパラメータを受け入れるように指定できます。

def debug(func): 
    def wrapper(): 
        print "[DEBUG]: enter {}()".format(func.__name__) 
        return func() 
    return wrapper 
 
@debug 
def say_hello(): 
    print "hello!"

この方法で、1 つの問題を解決できますが、さらに N 個の問題が発生します。何千もの関数があるため、他の人の関数パラメータがどのようなものであるかは誰にもわかりません。幸いなことに、Python には変数パラメータ *args とキーワード パラメータ **kwargs が用意されており、これら 2 つのパラメータを使用してデコレータを使用できます。任意のターゲット関数に対して

def debug(func): 
    def wrapper(something):  # 指定一毛一样的参数 
        print "[DEBUG]: enter {}()".format(func.__name__) 
        return func(something) 
    return wrapper  # 返回包装过函数 
 
@debug 
def say(something): 
    print "hello {}!".format(something)

この時点で、デコレータの基本的な作成方法を完全にマスターしました。

高度なデコレータ

パラメータを備えたデコレータとクラスデコレータは高度なコンテンツです。これらのデコレータを理解する前に、関数クロージャとデコレータ インターフェイスの規約についてある程度理解しておくことが最善です。 (http://betacat.online/posts/p... を参照してください...

パラメータ付きデコレータ

前のデコレータが完了する必要がある機能は、特定の関数を入力した後にログ情報を出力するだけでなく、ログ レベルも指定する必要があります。そうすると、デコレータは次のようになります

def debug(func): 
    def wrapper(*args, **kwargs):  # 指定宇宙无敌参数 
        print "[DEBUG]: enter {}()".format(func.__name__) 
        print 'Prepare and say...', 
        return func(*args, **kwargs) 
    return wrapper  # 返回 
 
@debug 
def say(something): 
    print "hello {}!".format(something)

@logging( などのパラメータを持つデコレータが配置される場合、このように理解できますか? level) ='DEBUG')、これは実際には関数であり、返される結果がデコレータである限り、問題はありません。クラスベースのデコレータを詳しく見てみましょう

。デコレータ関数は実際には、呼び出し可能なオブジェクトをパラメータとして受け入れてから呼び出し可能なオブジェクトを返す必要があるインターフェイス制約です。Python では、呼び出し可能なオブジェクトは通常関数ですが、オブジェクトが __call__() メソッドをオーバーロードする場合には例外があります。 __call__ の前後にアンダースコアが付いたメソッドは、Python では組み込みメソッドと呼ばれ、これらのマジック メソッドをオーバーロードすると、通常、クラス オブジェクトに呼び出されるオブジェクトが変更されます。

デコレーターの概念に戻ると、デコレーターは呼び出し可能なオブジェクトを必要とし、呼び出し可能なオブジェクトを返します (あまり厳密ではありません。後述を参照)。クラスのコンストラクターを使用して実装することもできます。 __init__() は関数を受け入れ、__call__() をオーバーロードして関数を返します。これにより、

def logging(level): 
    def wrapper(func): 
        def inner_wrapper(*args, **kwargs): 
            print "[{level}]: enter function {func}()".format( 
                level=level, 
                func=func.__name__) 
            return func(*args, **kwargs) 
        return inner_wrapper 
    return wrapper 
 
@logging(level='INFO') 
def say(something): 
    print "say {}!".format(something) 
 
# 如果没有使用@语法,等同于 
# say = logging(level='INFO')(say) 
 
@logging(level='DEBUG') 
def do(something): 
    print "do {}...".format(something) 
 
if __name__ == '__main__': 
    say('hello') 
    do("my work")

パラメーターを使用したクラス デコレーター

クラスにパラメーターを使用してデコレーターを実装する必要がある場合も、その効果を実現できます。フォームでは、前の例よりも少し複雑になります。コンストラクターで受け入れられるのは関数ではなく、パラメーターです。その後、__call__ メソッドをオーバーロードするときに、これらのパラメーターを受け入れる必要があります。

class Test(): 
    def __call__(self): 
        print 'call me!' 
 
t = Test() 
t()  # call me

組み込みデコレータは通常のデコレータと同じですが、関数ではなくクラスオブジェクトを返すため、

@property

。このデコレータを理解する前に、デコレータを使用せずにプロパティを記述する方法を知る必要があります。

def getx(self): 
    return self._x 
 
def setx(self, value): 
    self._x = value 
     
def delx(self): 
   del self._x 
 
# create a property 
x = property(getx, setx, delx, "I am doc for x property")

以上就是一个Python属性的标准写法,其实和Java挺像的,但是太罗嗦。有了@语法糖,能达到一样的效果但看起来更简单。

@property 
def x(self): ... 
 
# 等同于 
 
def x(self): ... 
x = property(x)

属性有三个装饰器:setter, getter, deleter ,都是在property()的基础上做了一些封装,因为setter和deleter是property()的第二和第三个参数,不能直接套用@语法。getter装饰器和不带getter的属性装饰器效果是一样的,估计只是为了凑数,本身没有任何存在的意义。经过@property装饰过的函数返回的不再是一个函数,而是一个property对象。

>>> property() 
<property object at 0x10ff07940>

@staticmethod,@classmethod

有了@property装饰器的了解,这两个装饰器的原理是差不多的。@staticmethod返回的是一个staticmethod类对象,而@classmethod返回的是一个classmethod类对象。他们都是调用的是各自的__init__()构造函数。

class classmethod(object): 
    """ 
    classmethod(function) -> method 
    """     
    def __init__(self, function): # for @classmethod decorator 
        pass 
    # ... 
class staticmethod(object): 
    """ 
    staticmethod(function) -> method 
    """ 
    def __init__(self, function): # for @staticmethod decorator 
        pass 
    # ...

装饰器的@语法就等同调用了这两个类的构造函数。

class Foo(object): 
 
    @staticmethod 
    def bar(): 
        pass 
     
    # 等同于 bar = staticmethod(bar)

至此,我们上文提到的装饰器接口定义可以更加明确一些,装饰器必须接受一个callable对象,其实它并不关心你返回什么,可以是另外一个callable对象(大部分情况),也可以是其他类对象,比如property。

装饰器里的那些坑

装饰器可以让你代码更加优雅,减少重复,但也不全是优点,也会带来一些问题。

位置错误的代码

让我们直接看示例代码。

def html_tags(tag_name): 
    print &#39;begin outer function.&#39; 
    def wrapper_(func): 
        print "begin of inner wrapper function." 
        def wrapper(*args, **kwargs): 
            content = func(*args, **kwargs) 
            print "<{tag}>{content}</{tag}>".format(tag=tag_name, content=content) 
        print &#39;end of inner wrapper function.&#39; 
        return wrapper 
    print &#39;end of outer function&#39; 
    return wrapper_ 
 
@html_tags(&#39;b&#39;) 
def hello(name=&#39;Toby&#39;): 
    return &#39;Hello {}!&#39;.format(name) 
 
hello() 
hello()

在装饰器中我在各个可能的位置都加上了print语句,用于记录被调用的情况。你知道他们最后打印出来的顺序吗?如果你心里没底,那么最好不要在装饰器函数之外添加逻辑功能,否则这个装饰器就不受你控制了。以下是输出结果:

begin outer function. 
end of outer function 
begin of inner wrapper function. 
end of inner wrapper function. 
<b>Hello Toby!</b> 
<b>Hello Toby!</b>

错误的函数签名和文档

装饰器装饰过的函数看上去名字没变,其实已经变了。

def logging(func): 
    def wrapper(*args, **kwargs): 
        """print log before a function.""" 
        print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__) 
        return func(*args, **kwargs) 
    return wrapper 
 
@logging 
def say(something): 
    """say something""" 
    print "say {}!".format(something) 
 
print say.__name__  # wrapper

为什么会这样呢?只要你想想装饰器的语法糖@代替的东西就明白了。@等同于这样的写法。

say = logging(say)

logging其实返回的函数名字刚好是wrapper,那么上面的这个语句刚好就是把这个结果赋值给say,say的__name__自然也就是wrapper了,不仅仅是name,其他属性也都是来自wrapper,比如doc,source等等。

使用标准库里的functools.wraps,可以基本解决这个问题。

from functools import wraps 
 
def logging(func): 
    @wraps(func) 
    def wrapper(*args, **kwargs): 
        """print log before a function.""" 
        print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__) 
        return func(*args, **kwargs) 
    return wrapper 
 
@logging 
def say(something): 
    """say something""" 
    print "say {}!".format(something) 
 
print say.__name__  # say 
print say.__doc__ # say something

看上去不错!主要问题解决了,但其实还不太完美。因为函数的签名和源码还是拿不到的。

import inspect 
print inspect.getargspec(say)  # failed 
print inspect.getsource(say)  # failed

如果要彻底解决这个问题可以借用第三方包,比如wrapt。后文有介绍。

不能装饰@staticmethod 或者 @classmethod

当你想把装饰器用在一个静态方法或者类方法时,不好意思,报错了。

class Car(object): 
    def __init__(self, model): 
        self.model = model 
 
    @logging  # 装饰实例方法,OK 
    def run(self): 
        print "{} is running!".format(self.model) 
 
    @logging  # 装饰静态方法,Failed 
    @staticmethod 
    def check_model_for(obj): 
        if isinstance(obj, Car): 
            print "The model of your car is {}".format(obj.model) 
        else: 
            print "{} is not a car!".format(obj) 
 
""" 
Traceback (most recent call last): 
... 
  File "example_4.py", line 10, in logging 
    @wraps(func) 
  File "C:\Python27\lib\functools.py", line 33, in update_wrapper 
    setattr(wrapper, attr, getattr(wrapped, attr)) 
AttributeError: &#39;staticmethod&#39; object has no attribute &#39;__module__&#39; 
"""

前面已经解释了@staticmethod这个装饰器,其实它返回的并不是一个callable对象,而是一个staticmethod对象,那么它是不符合装饰器要求的(比如传入一个callable对象),你自然不能在它之上再加别的装饰器。要解决这个问题很简单,只要把你的装饰器放在@staticmethod之前就好了,因为你的装饰器返回的还是一个正常的函数,然后再加上一个@staticmethod是不会出问题的。

class Car(object): 
    def __init__(self, model): 
        self.model = model 
 
    @staticmethod 
    @logging  # 在@staticmethod之前装饰,OK 
    def check_model_for(obj): 
        pass

如何优化你的装饰器

嵌套的装饰函数不太直观,我们可以使用第三方包类改进这样的情况,让装饰器函数可读性更好。

decorator.py

decorator.py 是一个非常简单的装饰器加强包。你可以很直观的先定义包装函数wrapper(),再使用decorate(func, wrapper)方法就可以完成一个装饰器。

from decorator import decorate 
 
def wrapper(func, *args, **kwargs): 
    """print log before a function.""" 
    print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__) 
    return func(*args, **kwargs) 
 
def logging(func): 
    return decorate(func, wrapper)  # 用wrapper装饰func

你也可以使用它自带的@decorator装饰器来完成你的装饰器。

from decorator import decorator 
 
@decorator 
def logging(func, *args, **kwargs): 
    print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__) 
    return func(*args, **kwargs)

decorator.py实现的装饰器能完整保留原函数的name,doc和args,唯一有问题的就是inspect.getsource(func)返回的还是装饰器的源代码,你需要改成inspect.getsource(func.__wrapped__)。

wrapt

wrapt是一个功能非常完善的包,用于实现各种你想到或者你没想到的装饰器。使用wrapt实现的装饰器你不需要担心之前inspect中遇到的所有问题,因为它都帮你处理了,甚至inspect.getsource(func)也准确无误。

import wrapt 
 
# without argument in decorator 
@wrapt.decorator 
def logging(wrapped, instance, args, kwargs):  # instance is must 
    print "[DEBUG]: enter {}()".format(wrapped.__name__) 
    return wrapped(*args, **kwargs) 
 
@logging 
def say(something): pass

使用wrapt你只需要定义一个装饰器函数,但是函数签名是固定的,必须是(wrapped, instance, args, kwargs),注意第二个参数instance是必须的,就算你不用它。当装饰器装饰在不同位置时它将得到不同的值,比如装饰在类实例方法时你可以拿到这个类实例。根据instance的值你能够更加灵活的调整你的装饰器。另外,args和kwargs也是固定的,注意前面没有星号。在装饰器内部调用原函数时才带星号。

如果你需要使用wrapt写一个带参数的装饰器,可以这样写。

def logging(level): 
    @wrapt.decorator 
    def wrapper(wrapped, instance, args, kwargs): 
        print "[{}]: enter {}()".format(level, wrapped.__name__) 
        return wrapped(*args, **kwargs) 
    return wrapper 
 
@logging(level="INFO") 
def do(work): pass

关于wrapt的使用,建议查阅官方文档,在此不在赘述。

http://wrapt.readthedocs.io/e...

小结

Python のデコレータは Java のアノテーションと同じものではなく、C# の属性とも同じではありません。これらは 2 つの完全に異なる概念です。

デコレータの概念は元の関数やオブジェクトを強化することであり、これは再カプセル化に相当するので、一般的にデコレータ関数はパッケージ化を意味するwrapper()という名前が付けられます。関数は呼び出されたときにのみその機能を実行します。たとえば、@logging デコレータは関数の実行時に追加のログを出力でき、@cache でデコレートされた関数は計算結果などをキャッシュできます。

アノテーションと機能は、ターゲット関数またはオブジェクトにいくつかの属性を追加します。これは、それを分類することと同じです。これらのプロパティはリフレクションを通じて取得でき、プログラムの実行中にさまざまな特徴的な関数やオブジェクトに介入できます。たとえば、Setup を含む関数が準備ステップとして実行されるか、TestMethod を含むすべての関数が検索されて順番に実行されるなどです。

ここまでで私が知っているデコレーターについて語り終わりましたが、装飾デコレーターなどまだ触れていないものもいくつかあります。機会があればさらに追加します。見てくれてありがとう。


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。