Python decorator лучшая практика, используя класс против функции



Как я понял, есть два способа сделать Python decorator: либо использовать __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()
664   3  

3 ответов:

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

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

Обновление : отсутствуют формы декораторов "чистого класса"

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

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

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

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)

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

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

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

Comments

    Ничего не найдено.