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()
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.Это делает
Во всяком случае, это обычно то, что я делаю, и то, что я вижу, делают другие люди вокруг меня, если я не хочу завод декоратора. В этом случае я просто добавляю еще один уровень def:decoratedпохожим наcheck_email, например, изменяя его атрибутfunc_name.Кстати, я бы также остерегался чрезмерного использования декораторов, потому что они могут сделать очень трудным отслеживание следов стека.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