70

私が理解したように、Pythonデコレータを実行するに__call__は、クラスのを使用する方法と、デコレータとして関数を定義して呼び出す方法の2つがあります。これらの方法の長所/短所は何ですか?推奨される方法は1つありますか?

例1

class dec1(object):
    def __init__(self, f):
        self.f = f
    def __call__(self):
        print "Decorating", self.f.__name__
        self.f()

@dec1
def func1():
    print "inside func1()"

func1()

# Decorating func1
# inside func1()

例2

def dec2(f):
    def new_f():
        print "Decorating", f.__name__
        f()
    return new_f

@dec2
def func2():
    print "inside func2()"

func2()

# Decorating func2
# inside func2()
4

4 に答える 4

88

それぞれの方法に「利点」があるかどうかを言うのはかなり主観的です。

ただし、内部で何が行われるかをよく理解していると、それぞれの機会に最適な選択肢を選択するのが自然になります。

デコレータ(関数デコレータについて説明します)は、入力パラメータとして関数を受け取る呼び出し可能なオブジェクトです。Pythonにはかなり興味深い設計があり、関数以外に他の種類の呼び出し可能なオブジェクトを作成できます。これを使用して、より保守しやすいコードや短いコードを作成することもできます。

デコレータは、Python2.3で「構文ショートカット」として追加されました。

def a(x):
   ...

a = my_decorator(a)

それに加えて、私たちは通常、デコレータを「デコレータファクトリ」になりたいいくつかの「callables」と呼びます-この種類を使用する場合:

@my_decorator(param1, param2)
def my_func(...):
   ...

param1とparam2を使用して「my_decorator」が呼び出されます。次に、「my_func」をパラメータとして、再度呼び出されるオブジェクトが返されます。したがって、この場合、技術的には「デコレータ」は「my_decorator」によって返されるものであり、「デコレータファクトリ」になります。

現在、説明されているデコレータまたは「デコレータファクトリ」は、通常、内部状態を維持する必要があります。最初のケースでは、それが保持するのは元の関数(f例で呼び出される変数)への参照だけです。「デコレータファクトリ」は、追加の状態変数(上記の例では「param1」と「param2」)を登録したい場合があります。

関数として記述されたデコレータの場合、この余分な状態は、囲んでいる関数内の変数に保持され、実際のラッパー関数によって「非ローカル」変数としてアクセスされます。適切なクラスを作成すると、デコレータ関数(「関数」ではなく「呼び出し可能なオブジェクト」と見なされます)にインスタンス変数として保持でき、それらへのアクセスがより明示的で読みやすくなります。

したがって、ほとんどの場合、どちらのアプローチを好むかは読みやすさの問題です。短くて単純なデコレータの場合、機能的なアプローチは、クラスとして記述されたアプローチよりも読みやすいことがよくありますが、より複雑なアプローチ、特に1つのアプローチもあります。 「デコレータファクトリ」は、Pythonコーディングの前に「フラットはネストよりも優れている」というアドバイスを最大限に活用します。

検討:

def my_dec_factory(param1, param2):
   ...
   ...
   def real_decorator(func):
       ...
       def wraper_func(*args, **kwargs):
           ...
           #use param1
           result = func(*args, **kwargs)
           #use param2
           return result
       return wraper_func
   return real_decorator

この「ハイブリッド」ソリューションに対して:

class MyDecorator(object):
    """Decorator example mixing class and function definitions."""
    def __init__(self, func, param1, param2):
        self.func = func
        self.param1, self.param2 = param1, param2

    def __call__(self, *args, **kwargs):
        ...
        #use self.param1
        result = self.func(*args, **kwargs)
        #use self.param2
        return result

def my_dec_factory(param1, param2):
    def decorator(func):
         return MyDecorator(func, param1, param2)
    return decorator

更新:デコレータの「純粋なクラス」形式がありません

ここで、「ハイブリッド」メソッドは、最短で読みやすいコードを維持しようとする「両方の長所」を採用していることに注意してください。クラスのみで定義された完全な「デコレータファクトリ」には、2つのクラス、または装飾された関数を登録するために呼び出されたか、実際に最終関数を呼び出すために呼び出されたかを知るための「mode」属性が必要です。

class MyDecorator(object):
   """Decorator example defined entirely as class."""
   def __init__(self, p1, p2):
        self.p1 = p1
        ...
        self.mode = "decorating"

   def __call__(self, *args, **kw):
        if self.mode == "decorating":
             self.func = args[0]
             self.mode = "calling"
             return self
         # code to run prior to function call
         result = self.func(*args, **kw)
         # code to run after function call
         return result

@MyDecorator(p1, ...)
def myfunc():
    ...

そして最後に、2つのクラスで定義された純粋な「ホワイトコラー」デコレータ-おそらく物事をより分離したままにしますが、冗長性をより維持しやすいとは言えないほどに増やします。

class Stage2Decorator(object):
    def __init__(self, func, p1, p2, ...):
         self.func = func
         self.p1 = p1
         ...
    def __call__(self, *args, **kw):
         # code to run prior to function call
         ...
         result = self.func(*args, **kw)
         # code to run after function call
         ...
         return result

class Stage1Decorator(object):
   """Decorator example defined as two classes.
   
   No "hacks" on the object model, most bureacratic.
   """
   def __init__(self, p1, p2):
        self.p1 = p1
        ...
        self.mode = "decorating"

   def __call__(self, func):
       return Stage2Decorator(func, self.p1, self.p2, ...)


@Stage1Decorator(p1, p2, ...)
def myfunc():
    ...

2018年の更新

私は数年前に上記のテキストを書きました。私は最近、「よりフラットな」コードを作成するために好むパターンを思いつきました。

基本的な考え方は関数を使用するpartialことですが、デコレータとして使用される前にパラメータを使用して呼び出された場合は、それ自体のオブジェクトを返します。

from functools import wraps, partial

def decorator(func=None, parameter1=None, parameter2=None, ...):

   if not func:
        # The only drawback is that for functions there is no thing
        # like "self" - we have to rely on the decorator 
        # function name on the module namespace
        return partial(decorator, parameter1=parameter1, parameter2=parameter2)
   @wraps(func)
   def wrapper(*args, **kwargs):
        # Decorator code-  parameter1, etc... can be used 
        # freely here
        return func(*args, **kwargs)
   return wrapper

そして、それだけです。このパターンを使用して記述されたデコレータは、最初に「呼び出される」ことなく、関数をすぐにデコレートできます。

@decorator
def my_func():
    pass

またはパラメータでカスタマイズ:

@decorator(parameter1="example.com", ...):
def my_func():
    pass
        
        

2019 -Python 3.8と位置のみのパラメーターを使用すると、func引数を位置のみとして宣言でき、パラメーターに名前を付ける必要があるため、この最後のパターンはさらに良くなります。

def decorator(func=None, *, parameter1=None, parameter2=None, ...):
于 2012-04-24T15:23:22.520 に答える
11

私はjsbuenoにほとんど同意します:正しい方法はありません。状況によります。しかし、ほとんどの場合、defの方が優れていると思います。クラスに参加する場合、「実際の」作業のほとんどは__call__とにかく行われるためです。また、関数ではない呼び出し可能オブジェクトは非常にまれであり(クラスのインスタンス化を除いて)、一般的に人々はそれを期待していません。また、ローカル変数は通常、スコープが限定されているため、インスタンス変数と比較して追跡しやすくなりますが、この場合、インスタンス変数はおそらく__call____init__引数からコピーするだけで)でのみ使用されます。

しかし、私は彼のハイブリッドアプローチに反対しなければなりません。それは面白いデザインですが、おそらくあなたや数ヶ月後にそれを見る他の誰かのがらくたを混乱させるだろうと思います。

接線:クラスと関数のどちらを使用するかに関係なく、を使用する必要があります。これは、次functools.wrapsのようにデコレータとして使用することを目的としています(さらに深くする必要があります!)。

import functools

def require_authorization(f):
    @functools.wraps(f)
    def decorated(user, *args, **kwargs):
        if not is_authorized(user):
            raise UserIsNotAuthorized
        return f(user, *args, **kwargs)
    return decorated

@require_authorization
def check_email(user, etc):
    # etc.

これにより、たとえば属性を変更することで、次のdecoratedようになります。check_emailfunc_name

とにかく、デコレータファクトリが必要な場合を除いて、これは通常、私が行っていることであり、周囲の他の人が行っていることです。その場合、私はdefの別のレベルを追加するだけです:

def require_authorization(action):
    def decorate(f):
        @functools.wraps(f):
        def decorated(user, *args, **kwargs):
            if not is_allowed_to(user, action):
                raise UserIsNotAuthorized(action, user)
            return f(user, *args, **kwargs)
        return decorated
    return decorate

ちなみに、デコレータを過度に使用すると、スタックトレースを追跡するのが非常に難しくなる可能性があるため、注意が必要です。

恐ろしいスタックトレースを管理するための1つのアプローチは、装飾対象者の動作を実質的に変更しないというポリシーを持つことです。例えば

def log_call(f):
    @functools.wraps(f)
    def decorated(*args, **kwargs):
        logging.debug('call being made: %s(*%r, **%r)',
                      f.func_name, args, kwargs)
        return f(*args, **kwargs)
    return decorated

スタックトレースを正常に保つためのより極端なアプローチは、次のように、デコレータがデコレータを変更せずに返すことです。

import threading

DEPRECATED_LOCK = threading.Lock()
DEPRECATED = set()

def deprecated(f):
    with DEPRECATED_LOCK:
        DEPRECATED.add(f)
    return f

@deprecated
def old_hack():
    # etc.

deprecatedこれは、デコレータを認識しているフレームワーク内で関数が呼び出される場合に役立ちます。例えば

class MyLamerFramework(object):
    def register_handler(self, maybe_deprecated):
        if not self.allow_deprecated and is_deprecated(f):
            raise ValueError(
                'Attempted to register deprecated function %s as a handler.'
                % f.func_name)
        self._handlers.add(maybe_deprecated)
于 2014-06-04T16:41:45.553 に答える
5

質問が最初に出されてからほぼ7年後に、私はあえてこの問題に対して別のアプローチを提供します。このバージョンは、以前の(非常に素晴らしい!)回答のいずれにも説明されていません。

クラスと関数をデコレータとして使用することの最大の違いは、ここですでに非常によく説明されています。完全を期すために、これをもう一度簡単に説明しますが、より実用的にするために、具体的な例を使用します。

あるキャッシュサービスで「純粋」関数(副作用がないため、引数が与えられた場合の戻り値は決定論的)の結果をキャッシュするデコレータを作成するとします。

これを行うための2つの同等で非常に単純なデコレータを、両方のフレーバー(機能指向とオブジェクト指向)で次に示します。

import json
import your_cache_service as cache

def cache_func(f):
    def wrapper(*args, **kwargs):
        key = json.dumps([f.__name__, args, kwargs])
        cached_value = cache.get(key)
        if cached_value is not None:
            print('cache HIT')
            return cached_value
        print('cache MISS')
        value = f(*args, **kwargs)
        cache.set(key, value)
        return value
    return wrapper

class CacheClass(object):
    def __init__(self, f):
        self.orig_func = f

    def __call__(self, *args, **kwargs):
        key = json.dumps([self.orig_func.__name__, args, kwargs])
        cached_value = cache.get(key)
        if cached_value is not None:
            print('cache HIT')
            return cached_value
        print('cache MISS')
        value = self.orig_func(*args, **kwargs)
        cache.set(key, value)
        return value

これはかなり理解しやすいと思います。それはばかげた例です!簡単にするために、すべてのエラー処理とエッジケースをスキップしています。とにかくStackOverflowからctrl+c / ctrl + vコードを使用するべきではありませんよね?;)

お気づきのように、両方のバージョンは本質的に同じです。オブジェクト指向バージョンは、メソッドを定義して変数を使用する必要があるため、機能バージョンよりも少し長く、冗長ですが、self少し読みやすいと思います。この要素は、より複雑なデコレータにとって非常に重要になります。すぐにわかります。

上記のデコレータは次のように使用されます。

@cache_func
def test_one(a, b=0, c=1):
    return (a + b)*c

# Behind the scenes:
#     test_one = cache_func(test_one)

print(test_one(3, 4, 6))
print(test_one(3, 4, 6))

# Prints:
#     cache MISS
#     42
#     cache HIT
#     42

@CacheClass
def test_two(x, y=0, z=1):
    return (x + y)*z

# Behind the scenes:
#     test_two = CacheClass(test_two)

print(test_two(1, 1, 569))
print(test_two(1, 1, 569))

# Prints:
#     cache MISS
#     1138
#     cache HIT
#     1138

ただし、キャッシュサービスが各キャッシュエントリのTTLの設定をサポートしているとしましょう。あなたは装飾時にそれを定義する必要があるでしょう。どうやってするの?

従来の機能的アプローチは、構成されたデコレーターを返す新しいラッパーレイヤーを追加することです(この質問に対する他の回答には、より良い提案があります)。

import json
import your_cache_service as cache

def cache_func_with_options(ttl=None):
    def configured_decorator(*args, **kwargs):
        def wrapper(*args, **kwargs):
            key = json.dumps([f.__name__, args, kwargs])
            cached_value = cache.get(key)
            if cached_value is not None:
                print('cache HIT')
                return cached_value
            print('cache MISS')
            value = f(*args, **kwargs)
            cache.set(key, value, ttl=ttl)
            return value
        return wrapper
    return configured_decorator

これは次のように使用されます。

from time import sleep

@cache_func_with_options(ttl=100)
def test_three(a, b=0, c=1):
    return hex((a + b)*c)

# Behind the scenes:
#     test_three = cache_func_with_options(ttl=100)(test_three)

print(test_three(8731))
print(test_three(8731))
sleep(0.2)
print(test_three(8731))

# Prints:
#     cache MISS
#     0x221b
#     cache HIT
#     0x221b
#     cache MISS
#     0x221b

これはまだ問題ありませんが、経験豊富な開発者であっても、このパターンに従うより複雑なデコレータを理解するのにかなりの時間がかかることがあることを認めなければなりません。ここで注意が必要なのは、内部関数には外部関数のスコープで定義された変数が必要なため、関数を「ネスト解除」することは実際には不可能なことです。

オブジェクト指向バージョンは役に立ちますか?私はそう思いますが、クラスベースの構造の前の構造に従うと、機能的な構造と同じネストされた構造になるか、さらに悪いことに、デコレータが実行していることの状態を保持するためにフラグを使用します(良い)。

したがって、メソッドでデコレートされる関数を受け取り、__init__メソッドでラッピングとデコレータのパラメータを処理する__call__(または複数のクラス/関数を使用して、私の好みには複雑すぎる)代わりに、デコレータを処理することをお勧めしますメソッド内のパラメーターは、__init__メソッド内の関数を受け取り、__call__最後に、の終わりまでに返される追加のメソッドでラッピングを処理し__call__ます。

次のようになります。

import json
import your_cache_service as cache

class CacheClassWithOptions(object):
    def __init__(self, ttl=None):
        self.ttl = ttl

    def __call__(self, f):
        self.orig_func = f
        return self.wrapper

    def wrapper(self, *args, **kwargs):
        key = json.dumps([self.orig_func.__name__, args, kwargs])
        cached_value = cache.get(key)
        if cached_value is not None:
            print('cache HIT')
            return cached_value
        print('cache MISS')
        value = self.orig_func(*args, **kwargs)
        cache.set(key, value, ttl=self.ttl)
        return value

使用法は予想通りです:

from time import sleep

@CacheClassWithOptions(ttl=100)
def test_four(x, y=0, z=1):
    return (x + y)*z

# Behind the scenes:
#     test_four = CacheClassWithOptions(ttl=100)(test_four)

print(test_four(21, 42, 27))
print(test_four(21, 42, 27))
sleep(0.2)
print(test_four(21, 42, 27))

# Prints:
#     cache MISS
#     1701
#     cache HIT
#     1701
#     cache MISS
#     1701

何でも完璧なので、この最後のアプローチには2つの小さな欠点があります。

  1. @CacheClassWithOptions直接使って飾ることはできません。@CacheClassWithOptions()パラメータを渡したくない場合でも、括弧を使用する必要があります。これは、デコレーションを試みる前に、最初にインスタンスを作成する必要があるためです。そのため、__call__メソッドは、ではなく、デコレーションされる関数を受け取ります__init__。この制限を回避することは可能ですが、それは非常にハッキーです。それらの括弧が必要であることを単に受け入れる方が良いです。

  2. functools.wraps返されたラップされた関数にデコレータを適用する明確な場所はありません。機能バージョンでは簡単です。__call__ただし、戻る前に内部に中間関数を作成することで、簡単に実行できます。見た目はそれほど良くないので、必要がない場合は省略したほうがよいでしょうfunctools.wraps

于 2019-03-27T11:02:45.673 に答える
2

2つの異なるデコレータの実装があります。これらの1つはデコレータとしてクラスを使用し、もう1つはデコレータとして関数を使用します。ニーズに合った実装を選択する必要があります。

たとえば、デコレータが多くの作業を行う場合は、次のようにクラスをデコレータとして使用できます。

import logging
import time
import pymongo
import hashlib
import random

DEBUG_MODE = True

class logger(object):

        def __new__(cls, *args, **kwargs):
                if DEBUG_MODE:
                        return object.__new__(cls, *args, **kwargs)
                else:
                        return args[0]

        def __init__(self, foo):
                self.foo = foo
                logging.basicConfig(filename='exceptions.log', format='%(levelname)s %   (asctime)s: %(message)s')
                self.log = logging.getLogger(__name__)

        def __call__(self, *args, **kwargs):
                def _log():
                        try:
                               t = time.time()
                               func_hash = self._make_hash(t)
                               col = self._make_db_connection()
                               log_record = {'func_name':self.foo.__name__, 'start_time':t, 'func_hash':func_hash}
                               col.insert(log_record)
                               res = self.foo(*args, **kwargs)
                               log_record = {'func_name':self.foo.__name__, 'exc_time':round(time.time() - t,4), 'end_time':time.time(),'func_hash':func_hash}
                               col.insert(log_record)
                               return res
                        except Exception as e:
                               self.log.error(e)
                return _log()

        def _make_db_connection(self):
                connection = pymongo.Connection()
                db = connection.logger
                collection = db.log
                return collection

        def _make_hash(self, t):
                m = hashlib.md5()
                m.update(str(t)+str(random.randrange(1,10)))
                return m.hexdigest()
于 2012-04-24T08:42:56.090 に答える