Подтвердить что ты не робот

Превосходная практика декоратора 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()
4b9b3361

Ответ 1

Довольно субъективно сказать, есть ли "преимущества" для каждого метода.

Тем не менее, хорошее понимание того, что происходит под капотом, сделало бы это естественным чтобы выбрать лучший выбор для каждого случая.

Декоратор (говоря о декораторах функций) - это просто вызываемый объект, который принимает функцию в качестве входного параметра. Python имеет довольно интересный дизайн, который позволяет один для создания других видов вызываемых объектов, помимо функций - и можно использовать это для использования для создания более удобного или короткого кода при необходимости.

Декораторы были добавлены обратно в Python 2.3 как "синтаксический ярлык" для

def a(x):
   ...

a = my_decorator(a)

Кроме того, мы обычно называем декораторов некоторыми "вызываемыми объектами", которые скорее будут "фабриками декораторов" - когда мы используем этот тип:

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

вызывается my_decorator с param1 и param2 - он возвращает объект, который будет вызван снова, на этот раз с 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

update: отсутствуют декораторы "чистого класса"

Теперь обратите внимание, что "гибридный" метод берет "лучшее из обоих миров", пытаясь сохранить самый короткий и более читаемый код. Для полной "фабрики декораторов", определенной исключительно с помощью классов, потребуется либо два класса, либо атрибут "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():
    ...

И, наконец, чистый декоратор "белого цвета", заданный двумя классами - возможно, для разделения вещей, но увеличение избыточности до такой степени, что нельзя сказать, что она более удобна в обслуживании:

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

Ответ 2

Я в основном согласен с 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.

Это делает decorated похожим на check_email, например. изменив атрибут func_name.

Во всяком случае, это обычно то, что я делаю, и то, что я вижу, окружающие меня делают, если я не хочу декоратора factory. В этом случае я просто добавляю еще один уровень 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

Кстати, я также буду настороженно относиться к чрезмерному использованию декораторов, потому что они могут очень тяжело следить за трассировкой стека.

Один подход для управления отвратительными трассировками стека заключается в том, чтобы иметь политику, существенно не изменяющую поведение декоратора. Например.

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)

Ответ 3

Существуют две различные реализации декораторов. Один из них использует класс как декоратор, а другой использует функцию в качестве декоратора. Вы должны выбрать предпочтительную реализацию для своих нужд.

Например, если ваш декоратор выполняет большую работу, вы можете использовать класс в качестве декоратора, например:

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

Ответ 4

Я позволю себе предложить другой подход к проблеме почти через семь лет после того, как вопрос был изначально задан. Эта версия не описана ни в одном из предыдущих (очень хороших!) Ответов.

Самые большие различия между использованием классов и функций в качестве декораторов уже очень хорошо описаны здесь. Для полноты картины я снова кратко расскажу об этом, но чтобы быть более практичным, я собираюсь использовать конкретный пример.

Допустим, вы хотите написать декоратор для кэширования результата "чистых" функций (без побочных эффектов, поэтому возвращаемое значение является детерминированным с учетом аргументов) в некоторой службе кэширования.

Вот два эквивалентных и очень простых декоратора для этого в обоих вариантах (функциональных и объектно-ориентированных):

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

Я думаю, это довольно легко понять. Это просто глупый пример! Я пропускаю все обработки ошибок и крайние случаи для простоты. Вы не должны в любом случае нажимать ctrl + c/ctrl + v код из Qaru, верно? ;)

Как можно заметить, обе версии по сути одинаковы. Объектно-ориентированная версия немного длиннее и более многословна, чем функциональная, потому что мы должны определить методы и использовать переменную 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

Как и все идеально, у этого последнего подхода есть два небольших недостатка:

  1. Невозможно украсить, используя @CacheClassWithOptions напрямую. Мы должны использовать круглые скобки @CacheClassWithOptions(), даже если мы не хотим передавать какой-либо параметр. Это потому, что нам нужно сначала создать экземпляр, прежде чем пытаться декорировать, поэтому метод __call__ получит функцию, которую нужно декорировать, а не в __init__. Можно обойти это ограничение, но оно очень хакерское. Лучше просто принять, что эти скобки необходимы.

  2. Нет очевидного места, где можно применить декоратор functools.wraps к возвращенной функции-обёртке, что было бы легко в функциональной версии. Это легко сделать, создав посредническую функцию внутри __call__ перед возвратом. Это выглядит не очень хорошо, и лучше не обращать на это внимания, если вам не нужны приятные вещи, которые делает functools.wraps.