2012-04-24 1 views
33

Как я понял, есть два способа сделать декоратор Python, либо использовать класс __call__ класса, либо определить и вызвать функцию в качестве декоратора. Каковы преимущества/недостатки этих методов? Есть ли один предпочтительный метод?Лучшая практика декоратора Python, используя класс vs a function

Пример 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() 
+5

Одна важная вещь: фактические функции обертки называют оригинальный 'f' функции, но не возвращает его возвращаемое значение вызываемым: это, скорее всего, приведет к некорректному поведению. – jsbueno

+0

Возможный дубликат [Разница между классами декораторов и функциями декоратора] (http://stackoverflow.com/questions/4650333/difference-between-decorator-classes-and-decorator-functions) –

ответ

29

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

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

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

обновления: Недостающий "чистого класса" форма декораторов

Теперь, обратите внимание на "гибрид" метод берет «лучшее из обоих миров», пытаясь сохранить самый короткий и читаемый код. Полный «декоратор фабрика» определяется исключительно с классами будет необходимо либо два класса, или атрибут «режим», чтобы знать, если он был призван, чтобы зарегистрировать украшенную функцию или на самом деле назвать конечную функцию:

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(): 
    ... 
+0

Извините, мне потребовалось столько времени, чтобы ответить, я был в отъезде. Во всяком случае, это был хороший ответ, спасибо :) – olofom

+0

@jiamo - извините, ваше редактирование было правильным - мне не удалось вернуться к нему, поскольку я нажал «отклонить» по ошибке. Исправлена ​​ошибка, которую вы заметили. – jsbueno

+0

Удивительный ответ, спасибо. Чтобы избежать «self.mode», вы также можете вернуть функцию внутри __call__. например def __call __ (self, func): def wrapper (* args, ** kwargs): return func (* args, ** kwargs) return wrapper – Josejulio

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

Да, но я все еще удивляюсь о преимуществах/недостатки для двух способов сделать это и когда использовать что. Хорошо, класс декоратора может быть немного более продвинутым, я думаю? Любые недостатки? – olofom

+0

ОК, декоратор его функции или класса, который принимает функцию или класс на входе. И теперь вы можете подумать, в каких случаях удобно использовать функцию или когда вы хотите использовать класс. – Denis

+1

Этот ответ концептуально неверен: это не «декоратор класса» - «декоратор класса» украшает класс, и он не коррелирует с тем, что его реализация является классом или функцией. – jsbueno

9

я в основном согласен с jsbueno: есть ни один правильный путь. Это зависит от ситуации. Но я думаю, что def, вероятно, лучше в большинстве случаев, потому что, если вы идете с классом, большая часть «реальной» работы будет проводиться в __call__ в любом случае. Кроме того, вызывающие вызовы, которые не являются функциями, довольно редки (за исключением исключения экземпляра класса), и люди обычно этого не ожидают. Кроме того, локальные переменные обычно легче отслеживать переменные vs. экземпляра, просто потому, что они имеют более ограниченную область действия, хотя в этом случае переменные экземпляра, вероятно, используются только в __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 eg путем изменения его атрибута 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 

Более экстремальный подход для поддержания вашего стека прослеживает здравомыслящим для декоратора, чтобы вернуть decoratee неизмененным, например, так:

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) 
+0

То, что я называю «гибридным подходом», просто заставляет метод '__call__' действовать как сам декоратор - получение функции-украшения в качестве параметра. Можете ли вы привести пример декоратора класса, который бы этого не сделал? (Для этого '__call__' должен был бы передать объект еще в другом объекте, возможно, другого класса, тесно связанном с первым или - удерживать состояние, чтобы« знать », если оно уже связано с функцией или нет . - Обе вещи, которые я нахожу более запутанными, чем «__call__», обертывают сама украшенную функцию. – jsbueno

+0

Отличный ответ. –