web-dev-qa-db-ja.com

Pythonデコレータのベストプラクティス、クラスと関数を使用

私が理解したように、Pythonデコレーター、__call__またはクラスをデコレータとして定義して呼び出すため。これらの方法の長所/短所は何ですか?好ましい方法はありますか?

例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()
48
olofom

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

しかし、ボンネットの下にあるものをよく理解することは、各機会に最適な選択を選ぶのが自然になります。

デコレータ(関数デコレータについて話す)は、入力パラメータとして関数を受け取る呼び出し可能なオブジェクトです。 Pythonは、関数に加えて、他の種類の呼び出し可能なオブジェクトを作成できる、かなり興味深いデザインを持っています-そして、それを使用して、より保守しやすい、または短いコードを作成することができます。

デコレータはPython 2.3の「構文のショートカット」として追加されました

def a(x):
   ...

a = my_decorator(a)

それに加えて、私たちは通常、デコレータを「デコレータファクトリ」と呼ばれる「呼び出し可能オブジェクト」と呼びます。

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

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

現在、通常、デコレータまたは「デコレータファクトリ」のいずれかは、内部状態を保持する必要があります。最初の場合、保持するのは元の関数(例ではfという変数)への参照のみです。 「デコレータファクトリ」は、追加の状態変数(上記の例では「param1」と「param2」)を登録する場合があります。

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

したがって、ほとんどの場合、どちらのアプローチを好むかは読みやすさの問題です:短くシンプルなデコレーターの場合、機能的なアプローチはクラスとして書かれたものよりも読みやすいことがあります-時にはより複雑なもの-特に「デコレータファクトリ」は、「フラットはネストよりも優れています」というアドバイスを最大限に活用します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つのクラス、または装飾された関数を登録するために呼び出されたか、実際に最終関数を呼び出すために呼び出されたかを知るための「モード」属性が必要です:

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, ...):
52
jsbueno

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

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

Tangent:クラスと関数のどちらを使用するかに関係なく、 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.

これにより、decoratedcheck_emailのようになります。 func_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)
9
allyourcode

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()
1
Denis

質問が最初に作成されてからほぼ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

これはかなり理解しやすいと思います。それは単なるばかげた例です!簡単にするために、すべてのエラー処理とEdgeのケースをスキップしています。とにかく、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が必要とするニースのことを必要としないのであれば、それを省いた方が良いでしょう。

0