关闭

Python装饰器----类型转换

标签: pythonpython装饰器可调用对象任务类
152人阅读 评论(0) 收藏 举报
分类:

类型转换

装饰器的一个更高级的使用情景,一个独特的很有价值的用法是,它装饰一个函数但返回一个类。在有大量的样板代码增加情况下,或者允许开发者在简单情况使用一个简单的函数,在复杂情况下才去子类化在一个应用中的API的类。
装饰器在这方面的一个应用的例子是在Python生态系统的中的一个流行的任务队列:celery。celery包提供了一个@celery.task装饰器,用来装饰一个函数。这个装饰器实际做的是返回celery内部类Task的子类,让被装饰函数在子类的run方法中被使用。
考虑下面的类似方式的简单例子:

class Task(object):
  """
    一个简单的任务类. Task类有一个run方法,用来启动任务 
  """
  def run(self, *args, **kwargs):
    raise NotImplementedError('Subclasses must implement   
    `run`.')
  def identify(self):
    return 'I am a task.'
def task(decorated):
  """
    返回一个类,如果这个类的run被调用,就会运行给定函数
  """
  class TaskSubclass(Task):
    def run(self, *args, **kwargs):
      return decorated(*args, **kwargs)
  return TaskSubclass

发生了什么? 装饰器创建了Task的子类并返回这个子类。这个子类是可调用的, 调用一个类创建那个类的实例并运行它的_init_方法。
这么做的价值是,它可以为很多扩展功能提供一个钩子。
基类Task可以定义很多方法,远不止是run.例如一个start方法可能会异步运行任务。基类也可能提供保存任务状态信息的方法。使用换出( swaps out)一个函数为一个类的装饰器可以允许开发者只考虑他的任务的实际功能部分,装饰器来做其余工作。

你可以使用这个类的实例,运行一下它的identify方法,实际看一下:

>>> @task
>>> def foo():
>>> return 2 + 2
>>>
>>> f = foo()
>>> f.run()
4>
>> f.identify()
'I am a task.'

一个坑(pitfall)
这个恰当的方法也携带来一些问题。This exact approach carries with it some problems. 尤其是,一旦一个任务函数被@task_class装饰,它就变成了一个类。考虑下面的以这种方式装饰的简单函数:

@task
def foo():
  return 2 + 2

现在,试着在解释器直接运行它:

>>> foo()
<__main__.TaskSubclass object at 0x10c3612d0>

这很糟糕!如果开发者运行它,装饰器以这样的方式修改了函数,它没有像人们期望的那样做。期待一个函数被声明为foo,然后绕着弯的使用foo().run()运行(目前的情况需要这样),实际上这难以接受。
满足这个需求应该放些心思在装饰器和Task类是如何构造的上面。看下面被修改的版本:

class Task(object):
  """
    一个简单的任务类. Task类有一个run方法,用来启动任务 
  """
  def __call__(self, *args, **kwargs):
    return self.run(*args, **kwargs)
  def run(self, *args, **kwargs):
    raise NotImplementedError('Subclasses must implement 'run`.')
  def identify(self):
    return 'I am a task.'
def task(decorated):
  """
   返回一个类,如果这个类的run被调用,就会运行给定函数
  """
  class TaskSubclass(Task):
    def run(self, *args, **kwargs):
    return decorated(*args, **kwargs)
  return TaskSubclass()

这里存在几处主要的不同. 首先是Task基类中的__call__. 第二处不同是@task_class装饰器返回了TaskSubclass类的实例而不是类本身。这是可接受的,因为对装饰器的唯一需求是它返回一个可调用对象,而Task中额外添加的call方法意味着它的实例现在是可调用对象。
为什么这种模式很有价值?再次强调,Task类很简单,但很容易发现更多功能如何能够被添加,这些功能对管理和运行任务很有用。

无论如何,这种方式保持了原始函数精神面貌,如果它被直接请求。
再次看下被装饰函数:

@task
def foo():
  return 2 + 2

现在,在解释器中运行:

>>> foo()
4 

这才是你想要的, 在背后装饰器返回了TaskSubclass实例 。
当这个实例在解释器中被调用,它的 __call__ method 被请求,
这会调用 run, 这会调用原始函数。
你可以看懂啊你仍然能获取你的实例,使用identify方法

>>> foo.identify()
'I am a task.'

现在你拥有一个实例,当直接调用时,就像在调用原始函数。然而,它能包括为其它功能提供的方法和属性。
这非常强大。它允许开发者写一个函数,这个函数容易,明确地移植到一个类,这个类提供了可选的方式给这个函数来被请求或者其它功能。

总结

装饰器是非常有价值的工具,你可以用它写出好管理,可读性强的Python代码。装饰器的价值在于这样的事实,它很明显而且可重用。它提供了出色的方式来使用样板代码,写一次,然后在多个地方使用。
是因为Python的数据模型提供了作为第一类对象(first-class objects)的函数和类,它能够像这个语言中任何其它对象一样被传递,被增强。

另一方面,这个模型也有缺点,尤其是,装饰器语法,在干净易于阅读的同时,也隐蔽了一个事实:一个函数被包装在另一个函数中,这带来了调试上的挑战。 写的差的装饰器可能制造错误,由于忽略了它包装的可调用对象的本质。(例如,忽视了绑定方法未绑定函数的区别).
除此以外,要牢记,像任何函数一样,解释器必须实际地在装饰器中运行代码,这有性能上的影响。对于这装饰器是不会不受影响的;
要多加注意你正在让你的装饰器做什么,也包括任何你写的其它代码。
考虑使用装饰器吧,当你采用前导(leading)和跟踪(trailing)功能,然后让它包装没有关联的函数。同样地,对于函数注册,信号通知,类增强的某种情况和许多其它事情,装饰器是非常有用的工具。

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:6750次
    • 积分:330
    • 等级:
    • 排名:千里之外
    • 原创:13篇
    • 转载:5篇
    • 译文:14篇
    • 评论:3条