Python装饰器以及高级用法

介绍

首先我要认可,装饰器很是难!你在本教程中看到的一些代码将会有一些复杂。大多数人在学习Python时都跟装饰器作过斗争,因此若是这对你来讲很奇怪,不要感到沮丧,由于一样的大多数人均可以克服这种苦难。在本教程中,我将逐步介绍了解装饰器的过程。首先我假设你已经能够编写基本函数和基本类。若是你不能作这些事,那么我建议你在回到这里以前先学习如何去作到编写基本函数和基本类(除非你迷路了,在这种状况下你能够原谅)。python

用例:计时函数执行

假设咱们正在执行一段代码,执行时间比咱们想的还要长一些。这段代码由一堆函数调用组成,咱们确信这些调用中至少有一个调用构成了咱们代码中的瓶颈。咱们如何找到瓶颈?如今有一个解决方案,就是咱们如今要关注的解决方案,就是对函数执行进行计时。数据库

让咱们从一个简单的例子开始。咱们只有一个函数须要计时,func_a设计模式

def func_a(stuff):
    do_important_things_1()
    do_important_things_2()
    do_important_things_3()

一种方法是将时钟代码放在每一个函数调用周围。因此就像这样:框架

func_a(current_stuff)

看起来会更像这样:ide

before = datetime.datetime.now()
func_a(current_stuff)
after = datetime.datetime.now()
print ("Elapsed Time = {0}".format(after-before))

这样就能够了。可是若是咱们有屡次调用func_a而且咱们想要为全部这些计时会发生什么呢?咱们能够用计时代码包围func_a的每一个调用,可是这样作也有很差的效果。它只准备编写一次计时代码。所以,咱们将其放在函数定义中,而不是将其放在函数以外。函数

def func_a(stuff):
    before = datetime.datetime.now()
    do_important_things_1()
    do_important_things_2()
    do_important_things_3()
    after = datetime.datetime.now()
    print("Elapsed Time = {0}".format(after-before))

这种方法的好处是:学习

  1. 咱们将代码放在一个地方,因此若是咱们想要更改它(例如,若是咱们想将通过的时间存储在数据库或日志中)那么咱们只须要在一个地方而不是每个函数调用中更改它
  2. 咱们不须要记住每次调用func_a都要写四行代码而不是一行,这是很是好的

好的,可是只须要计算一个函数的时间是不现实的。若是你须要对一件事进行计时,你颇有可能须要至少对两件事进行计时。因此咱们会选择三个。测试

def func_a(stuff):
    before = datetime.datetime.now()
    do_important_things_1()
    do_important_things_2()
    do_important_things_3()
    after = datetime.datetime.now()
    print("Elapsed Time = {0}".format(after-before))

def func_b(stuff):
    before = datetime.datetime.now()
    do_important_things_4()
    do_important_things_5()
    do_important_things_6()
    after = datetime.datetime.now()
    print("Elapsed Time = {0}".format(after-before))

def func_c(stuff):
    before = datetime.datetime.now()
    do_important_things_7()
    do_important_things_8()
    do_important_things_9()
    after = datetime.datetime.now()
    print("Elapsed Time = {0}".format(after-before))

这看起来很糟糕。若是咱们想要对8个函数进行计时的时候怎么办?而后咱们决定将计时的信息存储在日志文件中。而后咱们决定创建一个更好的数据库。咱们这里须要的是将一种相同的代码合并到func_afunc_bfunc_c中的方法,这种方法不会让咱们处处复制粘贴代码。ui

一个简单的绕道:返回函数的函数

Python是一种很是特殊的语言,由于函数是第一类对象。这意味着一旦函数在做用域中被定义,它就能够传递给函数,赋值给变量,甚至从函数返回。这个简单的事实是使python装饰器成为可能的缘由。查看下面的代码,看看你是否能够猜出标记为A,B,C和D的行会发生什么。this

def get_function():
    print ("inside get_function")                 
    def returned_function():                    
        print("inside returned_function")        
        return 1
    print("outside returned_function")
    return returned_function

returned_function()     # A                         
x = get_function()      # B                         
x                       # C                        
x()                     # D

A

这一行给出了一个NameError并声明returned_function不存在。但咱们只是定义了它,对吧?你在这里须要知道的是,它是在get_function的范围内定义的。也就是说,在get_function里面定义了它。它不是在get_function以外。若是这让你感到困惑,那么你能够尝试使用该locals()函数,并阅读Python的范围。

B

这行代码打印出如下内容:

inside get_function
outside returned_function

此时Python不执行returned_function的任何内容。

C

这一行输出:

<function returned_function at 0x7fdc4463f5f0>

也就是说,get_function()返回的值x自己就是一个函数。

尝试再次运行B和C行。请注意,每次重复此过程时,返回的returned_function地址都是不一样。每次调用get_function都会生成新的returned function

d

由于x是函数,因此就能够调用它。调用x就是调用returned_function的一个实例。这里输出的是:

inside returned_function
1

也就是说,它打印字符串,并返回值1

回到时间问题

你如今仍然在看么?如此咱们有了新的知识,那么咱们如何解决咱们的老问题?我建议咱们建立一个函数,让咱们调用它并称为time_this,它将接收另外一个函数做为参数,并将参数函数封装在某些计时代码中。有点像:

def time_this(original_function):                            # 1
    def new_function(*args,**kwargs):                        # 2
        before = datetime.datetime.now()                     # 3
        x = original_function(*args,**kwargs)                # 4
        after = datetime.datetime.now()                      # 5
        print("Elapsed Time = {0}".format(after-before))     # 6
        return x                                             # 7
    return new_function()                                    # 8

我认可它有点疯狂,因此让咱们一行一行的看下去:

1这只是time_this的原型。time_this是一个函数就像任何其余函数同样,而且只有一个参数。 2咱们在内部定义一个函数time_this。每当time_this执行时它都会建立一个新函数。 3计时代码,就像以前同样。 4咱们调用原始函数并保留结果以供往后使用。 5,6剩余的计时代码。 7new_function必须像原始函数同样运行,所以返回存储的结果。 8返回在time_this中建立的函数。

如今咱们要确保咱们的函数是计时的:

def func_a(stuff):
    do_important_things_1()
    do_important_things_2()
    do_important_things_3()
func_a = time_this(func_a)        # <---------

def func_b(stuff):
    do_important_things_4()
    do_important_things_5()
    do_important_things_6()
func_b = time_this(func_b)        # <---------

def func_c(stuff):
    do_important_things_7()
    do_important_things_8()
    do_important_things_9()
func_c = time_this(func_c)        # <---------

看看func_a,当咱们执行时func_a = time_this(func_a)咱们用time_this返回的函数替换func_a。因此咱们用一个函数替换func_A该函数执行一些计时操做(上面的第3行),将func a的结果存储在一个名为x的变量中(第4行),执行更多的计时操做(第5行和第6行),而后返回func_a返回的内容。换句话说func_a,仍然以相同的方式调用并返回相同的东西,它也只是被计时了。是否是感受很整洁?

介绍装饰器

咱们所作的工做很好,并且很是棒,可是很难看,很是难读懂。因此Python可爱的做者给了咱们一种不一样的,更漂亮的写做方式:

@time_this
def func_a(stuff):
    do_important_things_1()
    do_important_things_2()
    do_important_things_3()

彻底等同于:

def func_a(stuff):
    do_important_things_1()
    do_important_things_2()
    do_important_things_3()
func_a = time_this(func_a)

这一般被称为语法糖。@没有什么神奇的。这只是一个已达成一致的惯例。沿着这条路上的某个地方决定了。

总结

装饰器只是一个返回函数的函数。若是这些东西看起来很是的 - 那么请确保如下主题对你有意义而后再回到本教程:

  • Python函数
  • 范围
  • Python做为第一类对象(甚至能够查找lambda函数,它可能使它更容易理解)。

另外一方面,若是你对更多的话题感兴趣的话,你可能会发现:

  • 如装饰类:

python @add_class_functionality class MyClass: ...

  • 具备更多参数的装饰器, 例如:

python @requires_permission(name="edit") def save_changes(stuff): ...

下面就是我要介绍的高级装饰器的主题。

装饰器的高级用法

介绍

下面这些旨在介绍装饰器的一些更有趣的用法。具体来讲,如何在类上使用装饰器,以及如何将额外的参数传递给装饰器函数。

装饰者与装饰者模式

装饰器模式是一种面向对象的设计模式,其容许动态地将行为添加到现有的对象当中。当你装饰对象时,你将以独立于同类的其余实例方式扩展它的功能。

Python装饰器不是装饰器模式的实现。Python装饰器在定义时向函数和方法添加功能,它们不用于在运行时添加功能。装饰器模式自己能够在Python中实现,但因为Python是Duck-teped的,所以这是一件很是简单的事情。

一个基本的装饰

这是装饰器能够作的一个很是基本的例子。我只是把它做为一个参考点。在继续以前,请确保你彻底理解这段代码。

def time_this(original_function):      
    def new_function(*args,**kwargs):
        import datetime                 
        before = datetime.datetime.now()                     
        x = original_function(*args,**kwargs)                
        after = datetime.datetime.now()                      
        print ("Elapsed Time = {0}".format(after-before))    
        return x                                             
    return new_function                                   

@time_this
def func_a(stuff):
    import time
    time.sleep(3)

func_a(1)

接受参数的装饰器

有时,除了装饰的函数以外,装饰器还可使用参数。这种技术常常用于函数注册等事情。一个著名的例子是Pyramid Web应用程序框架中的视图配置。例如:

@view_config(route_name='home', renderer='templates/mytemplate.pt')
def my_view(request):
    return {'project': 'hello decorators'}

假设咱们有一个应用程序,用户能够登陆并与一个漂亮的gui(图形用户界面)进行交互。用户与gui的交互触发事件,而这些事件致使Python函数被执行。让咱们假设有不少用户使用这个应用程序,而且他们有许多不一样的权限级别。执行不一样的功能须要不一样的权限类型。例如,考虑如下功能:

#这些功能是存在的
def current_user_id():
    """
    此函数返回当前登陆的用户ID,若是没有通过身份验证,则返回None 
    """

def get_permissions(iUserId):
    """
    返回给定用户的权限字符串列表,例如 ['logged_in','administrator','premium_member']
    """

#咱们须要对这些函数进行权限检查

def delete_user(iUserId):
   """
   删除具备给定ID的用户,只有管理员权限才能访问此函数
   """

def new_game():
    """
    任何已登陆的用户均可以启动一个新游戏
    """

def premium_checkpoint():
   """
   保存游戏进程,只容许高级成员访问
   """

实现这些权限的一种方法是建立多个装饰器,例如:

def requires_admin(fn):
    def ret_fn(*args,**kwargs):
        lPermissions = get_permissions(current_user_id())
        if 'administrator' in lPermissions:
            return fn(*args,**kwargs)
        else:
            raise Exception("Not allowed")
    return ret_fn

def requires_logged_in(fn):
    def ret_fn(*args,**kwargs):
        lPermissions = get_permissions(current_user_id())
        if 'logged_in' in lPermissions:
            return fn(*args,**kwargs)
        else:
            raise Exception("Not allowed")
    return ret_fn

def requires_premium_member(fn):
    def ret_fn(*args,**kwargs):
        lPermissions = get_permissions(current_user_id())
        if 'premium_member' in lPermissions:
            return fn(*args,**kwargs)
        else:
            raise Exception("Not allowed")
    return ret_fn

@requires_admin
def delete_user(iUserId):
   """
   删除具备给定Id的用户,只有具备管理员权限的用户才能访问此函数
   """

@requires_logged_in 
def new_game():
    """
    任何已登陆的用户均可以启动一个新游戏
    """

@requires_premium_member
def premium_checkpoint():
   """
   保存游戏进程,只容许高级成员访问
   """

但这太可怕了。它须要大量的复制粘贴,而且每一个装饰器须要不一样的名称,若是对权限的检查方式进行了任何更改,则必须更新每一个装饰器。有一个装饰器能够完成这三个工做不是很好吗?

为此,咱们须要一个返回装饰器的函数:

def requires_permission(sPermission):                            
    def decorator(fn):                                            
        def decorated(*args,**kwargs):                            
            lPermissions = get_permissions(current_user_id())     
            if sPermission in lPermissions:                       
                return fn(*args,**kwargs)                         
            raise Exception("permission denied")                  
        return decorated                                          
    return decorator       

def get_permissions(iUserId): #这样装饰器就不会抛出NameError
    return ['logged_in',]

def current_user_id():        #名称错误也是如此
    return 1

#如今咱们能够进行装饰了                                 

@requires_permission('administrator')
def delete_user(iUserId):
   """
   删除具备给定Id的用户,只有具备管理员权限的用户才能访问此函数
   """

@requires_permission('logged_in')
def new_game():
    """
    任何已登陆的用户均可以启动一个新游戏
    """

@requires_permission('premium_member')
def premium_checkpoint():
   """
   保存游戏进程,只容许高级成员访问
   """

尝试调用delete_usernew_gamepremium_checkpoint看看会发生什么。

premium_checkpointdelete_user都在消息“权限被拒绝”的状况下引起异常,new_game执行得很好(但没有太多的做用)。

下面是装饰器的通常形式,带有参数和使用说明:

def outer_decorator(*outer_args,**outer_kwargs):                            
    def decorator(fn):                                            
        def decorated(*args,**kwargs):                            
            do_something(*outer_args,**outer_kwargs)                      
            return fn(*args,**kwargs)                         
        return decorated                                          
    return decorator       

@outer_decorator(1,2,3)
def foo(a,b,c):
    print (a)
    print (b)
    print (c)

foo()

这至关于:

def decorator(fn):                                            
    def decorated(*args,**kwargs):                            
        do_something(1,2,3)                      
        return fn(*args,**kwargs)                         
    return decorated                                          
return decorator       

@decorator
def foo(a,b,c):
    print (a)
    print (b)
    print (c)

foo()

装饰课程

装饰器不只限于对函数进行操做,它们也能够对类进行操做。比方说,咱们有一个类能够作不少很是重要的事情,咱们想要把它所作的一切都进行计时。而后咱们可使用time_this像之前同样使用装饰器:

class ImportantStuff(object):
    @time_this
    def do_stuff_1(self):
        ...
    @time_this
    def do_stuff_2(self):
        ...
    @time_this
    def do_stuff_3(self):
        ...

这样就能够了。可是这个类中还有一些额外的代码行。若是咱们写一些更多的类方法并忘记装饰它们中的一个呢?若是咱们决定再也不为进行计时怎么办?这里确定存在人为错误的空间。这样编写它会好得多:

@time_all_class_methods
class ImportantStuff:
    def do_stuff_1(self):
        ...
    def do_stuff_2(self):
        ...
    def do_stuff_3(self):
        ...

如你所知,该代码至关于:

class ImportantStuff:
    def do_stuff_1(self):
        ...
    def do_stuff_2(self):
        ...
    def do_stuff_3(self):
        ...

ImportantStuff = time_all_class_methods(ImportantStuff)

那么time_all_class_methods是如何工做的? 首先,咱们知道它须要将一个类做为参数,并返回一个类。咱们也知道返回类的函数应该与原始ImportantStuff类的函数相同。也就是说,咱们仍然但愿想要完成重要的事情,咱们须要进行计时。如下是咱们将如何作到这一点:

def time_this(original_function):      
    print ("decorating")                      
    def new_function(*args,**kwargs):
        print ("starting timer")      
        import datetime                 
        before = datetime.datetime.now()                     
        x = original_function(*args,**kwargs)                
        after = datetime.datetime.now()                      
        print ("Elapsed Time = {0}".format(after-before))      
        return x                                             
    return new_function  

def time_all_class_methods(Cls):
    class NewCls(object):
        def __init__(self,*args,**kwargs):
            self.oInstance = Cls(*args,**kwargs)
        def __getattribute__(self,s):
            """
            每当访问NewCls对象的任何属性时,都会调用这个函数。这个函数首先尝试
            从NewCls获取属性。若是失败,则尝试从self获取属性。oInstance(一个
            修饰类的实例)。若是它设法从self获取属性。oInstance,
            属性是一个实例方法,而后应用' time_this '。
            """
            try:    
                x = super(NewCls,self).__getattribute__(s)
            except AttributeError:      
                pass
            else:
                return x
            x = self.oInstance.__getattribute__(s)
            if type(x) == type(self.__init__): # 这是一个实例方法
                return time_this(x)                 # 这等价于用time_this修饰方法
            else:
                return x
    return NewCls

#如今让咱们作一个虚拟类来测试它:

@time_all_class_methods
class Foo(object):
    def a(self):
        print ("entering a")
        import time
        time.sleep(3)
        print ("exiting a")

oF = Foo()
oF.a()

结论

在装饰器的高级用法中,我向你展现了使用Python装饰器的一些技巧 - 我已经向你展现了如何将参数传递给装饰器,以及如何装饰类。但这仍然只是冰山的一角。在各类奇怪的状况下,有大量的方法用于装饰器。你甚至能够装饰你的装饰器(但若是你到达那一点,那么作一个全面的检查多是个好主意)。Python同时内置了一些值得了解的装饰器,例如装饰器staticmethodclassmethod

接下来要怎么作?除了我在这篇文章中向你展现的内容外,一般不须要对装饰器执行任何更复杂的操做。若是你对更改类功能的更多方法感兴趣,那么我建议阅读有关继承和通常OO设计原则的数据。或者,若是你真的想学会他们,那么请阅读元类(但一样,处理这些东西几乎不须要)。

相关文章
相关标签/搜索