【问题标题】:Why Python decorators rather than closures?为什么使用 Python 装饰器而不是闭包?
【发布时间】:2010-09-17 23:00:22
【问题描述】:

我仍然对 Python 中的装饰器一无所知。

我已经开始使用很多闭包来做一些事情,比如在我的编码中自定义函数和类。

例如。

class Node :
    def __init__(self,val,children) :
        self.val = val
        self.children = children

def makeRunner(f) :
    def run(node) :
        f(node)
        for x in node.children :
            run(x)
    return run

tree=Node(1,[Node(2,[]),Node(3,[Node(4,[]),Node(5,[])])])

def pp(n) : print "%s," % n.val
printTree = makeRunner(pp)
printTree(tree)

据我所知,装饰器只是做类似事情的不同语法。

代替

def pp(n) : print "%s," % n.val
printTree = makeRunner(pp)

我会写:

@makeRunner
def printTree(n) : print "%s," % n.val

这就是装饰器的全部内容吗?还是我错过了根本的区别?

【问题讨论】:

    标签: python decorator


    【解决方案1】:

    虽然从语法上讲,装饰器只是“糖”,但这并不是考虑它们的最佳方式。

    装饰器允许您将功能编织到现有代码中,而无需实际修改它。它们允许您以声明性的方式进行操作。

    这允许您使用装饰器进行面向方面的编程 (AOP)。因此,当您想要将横切关注点封装在一个地方时,您需要使用装饰器。

    典型的例子可能是日志记录,你想记录一个函数的进入或退出,或两者兼而有之。使用装饰器相当于将建议(记录这个!)应用于连接点(在方法进入或退出期间)。

    方法修饰是一个类似于 OOP 或列表推导的概念。正如您所指出的,它并不总是合适的,并且 可能 被过度使用。但在正确的地方,它对于使代码更加模块化和解耦很有用。

    【讨论】:

      【解决方案2】:

      你的例子是真实的代码,还是只是例子?

      如果它们是真正的代码,我认为你过度使用装饰器,可能是因为你的背景(即你习惯于其他编程语言)

      第 1 阶段:避免使用装饰器

      def run(rootnode, func):
          def _run(node): # recursive internal function
              func(node)
              for x in node.children:
                  _run(x) # recurse
          _run(rootnode) # initial run
      

      此运行方法已淘汰 makeRunner。你的例子变成了:

      def pp(n): print "%s," % n.val
      run(tree, pp)
      

      但是,这完全忽略了生成器,所以……

      第 2 阶段:使用生成器

      class Node :
          def __init__(self,val,children) :
              self.val = val
              self.children = children
      
          def __iter__(self): # recursive
              yield self
              for child in self.children:
                  for item in child: # recurse
                      yield item
      
      def run(rootnode, func):
          for node in rootnode:
              func(node)
      

      你的例子仍然存在

      def pp(n): print "%s," % n.val
      run(tree, pp)
      

      请注意,特殊方法 __iter__ 允许我们使用 for node in rootnode: 构造。如果您不喜欢它,只需将 __iter__ 方法重命名为例如walker,并将 run 循环更改为:for node in rootnode.walker():
      显然,run 函数可以是class Node 的方法。

      如您所见,我建议您直接使用run(tree, func),而不是将它们绑定到名称printTree,但您可以在装饰器中使用它们,也可以使用functools.partial 函数:

      printTree= functools.partial(run, func=pp)
      

      从那时起,你就会

      printTree(tree)
      

      【讨论】:

      • 虽然使用迭代器通常很好,但我发现你的重写更糟糕。 非常不明显“for item in child: # recurse”在做什么,即使有评论。仅使用函数的原始代码更明显,更小。
      • ΤZΩΤZΙΟΥ,这是一个人为的例子来说明我的问题;虽然基于我过去做过的类似事情。感谢您的替代方案。有趣的。我之前肯定使用过第 1 阶段的表单,但我认为我从未使用 iter 进行过树运行。
      • 实际上,看看你的最后一点,为什么你更喜欢 functools.partial 而不是装饰器版本? (即。每种方法的优缺点是什么?)
      • @interstar:我修正了最后一点的措辞;我只是提供了一种装饰替代品。
      • @James Antill:迭代器的另一个名称是否会使其更明显,如for item in child.walk_subtree()?我建议使用迭代器,因为它使其余代码比传递函数对象更容易,尤其是。如果你想在走树时保持上下文。
      【解决方案3】:

      装饰器,在一般意义上,是环绕另一个对象、扩展或装饰该对象的函数或类。装饰器支持与包装的函数或对象相同的接口,因此接收者甚至不知道对象已被装饰。

      闭包是一个匿名函数,它引用它的参数或范围之外的其他变量。

      所以基本上,decorators 使用 closures,而不是替换它们。

      def increment(x):
          return x + 1
      
      def double_increment(func):
          def wrapper(x):
              print 'decorator executed'
              r = func(x)   # --> func is saved in __closure__
              y = r * 2
              return r, y
          return wrapper
      
      @double_increment
      def increment(x):
          return x + 1
      
      >>> increment(2)
      decorator executed
      (3, 6)
      
      >>> increment.__closure__
      (<cell at 0x02C7DC50: function object at 0x02C85DB0>,)
      
      >>> increment.__closure__[0].cell_contents 
      <function increment at 0x02C85DB0>
      

      所以 decoratorclosure 保存原始函数。

      【讨论】:

      • 链接无处可去;虽然解释很好而且简洁
      【解决方案4】:

      跟进 Dutch Master 的 AOP 参考,您会发现当您开始添加参数 来修改装饰函数/方法的行为并阅读函数定义上方的内容时,使用装饰器变得特别有用容易多了。

      我记得在一个项目中,我们需要监督大量的 celery 任务,因此我们想出了使用装饰器根据需要进行插件和调整的想法,类似于:

      class tracked_with(object):
          """
          Method decorator used to track the results of celery tasks.
          """
          def __init__(self, model, unique=False, id_attr='results_id',
                       log_error=False, raise_error=False):
              self.model = model
              self.unique = unique
              self.id_attr = id_attr
              self.log_error = log_error
              self.raise_error = raise_error
      
          def __call__(self, fn):
      
              def wrapped(*args, **kwargs):
                  # Unique passed by parameter has priority above the decorator def
                  unique = kwargs.get('unique', None)
                  if unique is not None:
                      self.unique = unique
      
                  if self.unique:
                      caller = args[0]
                      pending = self.model.objects.filter(
                          state=self.model.Running,
                          task_type=caller.__class__.__name__
                      )
                      if pending.exists():
                          raise AssertionError('Another {} task is already running'
                                               ''.format(caller.__class__.__name__))
      
                  results_id = kwargs.get(self.id_attr)
                  try:
                      result = fn(*args, **kwargs)
      
                  except Retry:
                      # Retry must always be raised to retry a task
                      raise
      
                  except Exception as e:
                      # Error, update stats, log/raise/return depending on values
                      if results_id:
                          self.model.update_stats(results_id, error=e)
                      if self.log_error:
                          logger.error(e)
                      if self.raise_error:
                          raise
                      else:
                          return e
      
                  else:
                      # No error, save results in refresh object and return
                      if results_id:
                          self.model.update_stats(results_id, **result)
                      return result
      
              return wrapped
      

      然后我们简单地使用每个案例所需的参数修饰任务上的run 方法,例如:

      class SomeTask(Task):
      
          @tracked_with(RefreshResults, unique=True, log_error=False)
          def run(self, *args, **kwargs)...
      

      然后更改任务的行为(或完全删除跟踪)意味着调整一个参数,或注释掉修饰的行。超级容易实现,但更重要的是,超级容易理解检查。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2022-11-12
        • 2021-11-29
        • 2011-08-11
        • 2013-02-25
        • 2014-04-09
        • 1970-01-01
        • 2010-10-03
        • 2013-08-13
        相关资源
        最近更新 更多