一文看懂Python系列之装饰器(decorator)(工作面试必读)

Python的装饰器(decorator)可以说是Python的一个神器,它可以在不改变一个函数代码和调用方式的情况下给函数添加新的功能。Python的装饰器同时也是Python学习从入门到精通过程中必需要熟练掌握的知识。小编我当初学习Python时差点被装饰器搞晕掉,今天尝试用浅显的语言解释下Python装饰器的工作原理及如何编写自己的装饰器吧。

Python装饰器的本质

Python的装饰器本质上是一个嵌套函数,它接受被装饰的函数(func)作为参数,并返回一个包装过的函数。这样我们可以在不改变被装饰函数的代码的情况下给被装饰函数或程序添加新的功能。Python的装饰器广泛应用于缓存、权限校验(如django中的@login_required和@permission_required装饰器)、性能测试(比如统计一段程序的运行时间)和插入日志等应用场景。有了装饰器,我们就可以抽离出大量与函数功能本身无关的代码,增加一个函数的重用性。

试想你写了很多程序,一直运行也没啥问题。有一天老板突然让你统计每个程序都运行了多长时间并比较下运行效率。此时如果你去手动修改每个程序的代码一定会让你抓狂,而且还破坏了那些程序的重用性。聪明的程序员是绝不能干这种蠢事的。此时你可以编写一个@time_it的装饰器(代码如下所示)。如果你想打印出某个函数或程序运行时间,只需在函数前面@一下,是不是很帅?

import time

def time_it(func):
   def inner():
       start = time.time()
       func()
       end = time.time()
       print('用时:{}秒'.format(end-start))
   return inner

@time_it
def func1():
   time.sleep(2)
   print('Func1 is running.')

if __name__ == '__main__':
   func1()

运行结果如下:

Func1 is running.

用时:2.0056326389312744

由于Python装饰器的工作原理主要依赖于嵌套函数和闭包,所以我们必须先对嵌套函数和闭包有深入的了解。嵌套函数和闭包几乎是Python工作面试必考题哦。

嵌套函数

如果在一个函数的内部还定义了另一个函数(注意: 是定义,不是引用!),这个函数就叫嵌套函数。外部的我们叫它外函数,内部的我们叫他内函数。

我们先来看一个最简单的嵌套函数的例子。我们在outer函数里又定义了一个inner函数,并调用了它。你注意到了吗? 内函数在自己作用域内查找局部变量失败后,会进一步向上一层作用域里查找。

def outer():    x = 1    def inner():        y = x + 1        print(y)    inner()

outer() #输出结果 2

如果我们在外函数里不直接调用内函数,而是通过return inner返回一个内函数的引用 这时会发生什么呢? 你将会得到一个内函数对象,而不是运行结果。

def outer():
   x = 1
   def inner():
       y = x + 1
       print(y)
   return inner

outer() # 输出<function outer.<locals>.inner at 0x039248E8>
f1 = outer()
f1() # 输出2

上述这个案例比较简单,因为outer和inner函数都是没有参数的。我们现在对上述代码做点改动,加入参数。你可以看到外函数的参数或变量可以很容易传递到内函数。

def outer(x):    a = x

    def inner(y):        b = y        print(a+b)

    return inner

f1 = outer(1) # 返回inner函数对象f1(10) # 相当于inner(10)。输出11

如果上例中外函数的变量x换成被装饰函数对象(func),内函数的变量y换成被装饰函数的参数,我们就可以得到一个通用的装饰器啦(如下所示)。你注意到了吗? 我们在没对func本身做任何修改的情况下,添加了其它功能, 从而实现了对函数的装饰。

def decorator(func):
   def inner(*args, **kwargs):
       add_other_actions()
       return func(*args, **kwargs)
   return inner

请你仔细再读读上面这段代码,我们的decorator返回的仅仅是inner函数吗? 答案是不。它返回的其实是个闭包(Closure)。整个装饰器的工作都依赖于Python的闭包原理。

闭包(Closure)

闭包是Python编程一个非常重要的概念。如果一个外函数中定义了一个内函数,且内函数体内引用到了体外的变量,这时外函数通过return返回内函数的引用时,会把定义时涉及到的外部引用变量和内函数打包成一个整体(闭包)返回。我们在看下之间案例。我们的outer方法返回的只是内函数对象吗? 错。我们的outer函数返回的实际上是一个由inner函数和外部引用变量(a)组成的闭包!

def outer(x):    a = x

    def inner(y):        b = y        print(a+b)

    return inner

f1 = outer(1) # 返回inner函数对象+局部变量1(闭包)f1(10) # 相当于inner(10)。输出11

一般一个函数运行结束的时候,临时变量会被销毁。但是闭包是一个特别的情况。当外函数发现,自己的临时变量会在将来的内函数中用到,自己在结束的时候,返回内函数的同时,会把外函数的临时变量同内函数绑定在一起。这样即使外函数已经结束了,内函数仍然能够使用外函数的临时变量。这就是闭包的强大之处。

如何编写一个通用的装饰器

我们现在可以开始动手写个名为hint的装饰器了,其作用是在某个函数运行前给我们提示。这里外函数以hint命名,内函数以常用的wrapper(包裹函数)命名。

def hint(func):
   def wrapper(*args, **kwargs):
       print('{} is running'.format(func.__name__))
       return func(*args, **kwargs)
   return wrapper

@hint
def hello():
   print('Hello!')

我们现在对hello已经进行了装饰,当我们调用hello()时,我们可以看到如下结果。

>>> hello()hello is running.Hello!

值得一提的是被装饰器装饰过的函数看上去名字没变,其实已经变了。当你运行hello()后,你会发现它的名字已经悄悄变成了wrapper,这显然不是我们想要的(如下图所示)。这一点也不奇怪,因为外函数返回的是由wrapper函数和其外部引用变量组成的闭包。

>>> hello.__name__
'wrapper'

为了解决这个问题保证装饰过的函数__name__属性不变,我们可以使用functools模块里的wraps方法,先对func变量进行wraps。下面这段代码可以作为编写一个通用装饰器的示范代码,注意收藏哦。

from functools import wraps

def hint(func):    @wraps(func)    def wrapper(*args, **kwargs):        print('{} is running'.format(func.__name__))        return func(*args, **kwargs)    return wrapper

@hintdef hello():    print('Hello!')

恭喜你,你已经学会写一个比较通用的装饰器啦,并保证装饰过的函数__name__属性不变啦。当然使用嵌套函数也有缺点,比如不直观。这时你可以借助Python的decorator模块(需事先安装)可以简化装饰器的编写和使用。如下所示。

from decorator import decorator

@decorator
def hint(func, *args, **kwargs):
   print('{} is running'.format(func.__name__))
   return func(*args, **kwargs)

编写带参数的高级装饰器

前面几个装饰器一般是内外两层嵌套函数。如果我们需要编写的装饰器本身是带参数的,我们需要编写三层的嵌套函数,其中最外一层用来传递装饰器的参数。现在我们要对@hint装饰器做点改进,使其能通过@hint(coder='John')传递参数。该装饰器在函数运行前给出提示的时候还显示函数编写人员的名字。完整代码如下所示:

from functools import wraps

def hint(coder):    def wrapper(func):        @wraps(func)        def inner_wrapper(*args, **kwargs):            print('{} is running'.format(func.__name__))            print('Coder: {}'.format(coder))            return func(*args, **kwargs)        return inner_wrapper    return wrapper

@hint(coder='John')def hello():    print('Hello!')

下面这段代码是一段经典的Python装饰器代码,显示了@cache这个装饰器怎么编写和工作的。它需要使用缓存实例做为一个参数,所以也是三层嵌套函数。

import time
from functools import wraps

# 装饰器增加缓存功能
def cache(instance):
   def wrapper(func):
       @wraps(func)
       def inner_wrapper(*args, **kwargs):
           # 构建key: key => func_name::args::kwargs
           joint_args = ','.join((str(x) for x in args))
           joint_kwargs = ','.join('{}={}'.format(k, v) for k, v in sorted(kwargs.items()))
           key = '{}::{}::{}'.format(func.__name__,joint_args, joint_kwargs)
           # 根据key获取结果。如果key已存在直接返回结果,不用重复计算。
        result = instance.get(key)
           if result is not None:
               return result
           # 如果结果不存在,重新计算,缓存。
        result = func(*args, **kwargs)
           instance.set(key, result)
           return result
       return inner_wrapper
   return wrapper

# 创建字典构造函数,用户缓存K/V键值对
class DictCache:
   def __init__(self):
       self.cache = dict()

def get(self, key):
       return self.cache.get(key)

def set(self, key, value):
       self.cache[key] = value

def __str__(self):
       return str(self.cache)

def __repr__(self):
       return repr(self.cache)

# 创建缓存对象
cache_instance = DictCache()

# Python语法糖调用装饰器
@cache(cache_instance)
def long_time_func(x):
   time.sleep(x)
   return x

# 调用装饰过函数
long_time_func(3)

基于类实现的装饰器

Python的装饰器不仅可以用嵌套函数来编写,还可以使用类来编写。其调用__init__方法创建实例,传递参数,并调用__call__方法实现对被装饰函数功能的添加。

from functools import wraps

#类的装饰器写法, 不带参数class Hint(object):    def __init__(self, func):        self.func = func

    def __call__(self, *args, **kwargs):        print('{} is running'.format(self.func.__name__))        return self.func(*args, **kwargs)

#类的装饰器写法, 带参数class Hint(object):    def __init__(self, coder=None):        self.coder = coder

    def __call__(self, func):        @wraps(func)        def wrapper(*args, **kwargs):            print('{} is running'.format(func.__name__))            print('Coder: {}'.format(self.coder))            return func(*args, **kwargs)     # 正式调用主要处理函数        return wrapper

小结

本文总结了什么是Python的装饰器及其工作原理,并重点介绍了嵌套函数和闭包原理。最后详细展示了如何编写一个通用装饰器及带参数的高级装饰器, 包括使用类来编写装饰器。大家要熟练掌握哦。看不懂的可以先加入微信收藏以后再反复阅读。

大江狗

2018.11.29

参考资料

  • https://www.cnblogs.com/Lin-Yi/p/7305364.html

  • http://python.jobbole.com/81683/

  • http://lib.csdn.net/article/python/62942

  • http://lib.csdn.net/article/python/64769

  • http://www.cnblogs.com/cicaday/p/python-decorator.html

(0)

相关推荐

  • python笔记35-装饰器

    前言 python装饰器本质上就是一个函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外的功能,装饰器的返回值也是一个函数对象. 很多python初学者学到面向对象类和方法是一道大坎,那么p ...

  • Python学习—装饰器

    学习Python已经有一段时间了,陆续学了一些基础部分,但是理解的不是很深刻,每过一段时间就会忘记,所以不得不写一些博客进行记录,加深自己的理解.这两个星期一直在研究装饰器,开始觉得很简单,但是只知其 ...

  • python笔记36-装饰器之wraps

    前言 前面一篇对python装饰器有了初步的了解了,但是还不够完美,领导看了后又提出了新的需求,希望运行的日志能显示出具体运行的哪个函数. name和doc __name__用于获取函数的名称,__d ...

  • 一文看懂Python系列文章 - 入门与进阶必备

    本文收集整理了本公众号已发表的一文看懂Python系列精华文章,建议先收藏后阅读. 入门篇 一文看懂Python列表.元组和字符串操作 一文看懂Python字典类型数据常见操作及排序 一文看懂Pyth ...

  • 一文看懂Python的装饰器

    在 Python 中使用装饰器,可以在不修改代码的前提下,为已有的函数添加新功能,例如打印日志.缓存数据等. 为什么需要装饰器 假如你要为某个函数添加新功能.直接的办法是,在该函数中实现这个功能,或者 ...

  • 一文看懂Python异常处理(exception, try和raise语句)

    一个Python程序在运行时,如果解释器遇到到一个错误,会停止程序的执行,并且提示一些错误信息,这就是异常(Exception).即便Python程序的语法是正确的,还是会有各种各样意想不到的异常或错 ...

  • 一文看懂Python面向对象编程(Python学习与新手入门必看)-绝对原创

    尽管网上有许多关于Python面向对像的编程介绍,小编我看完后总觉得不是很满意,也不过瘾,所以决定自己亲自动手写篇文章,帮你理解Python面向对象的编程的基本概念和核心思想.本文内含很多实例代码,以 ...

  • 一文看懂Python中的集合运算&,|,

    关于集合的概念 Python 中常用的集合方法是执行标准的数学运算,例如:求并集.交集.差集以及对称差.下图显示了一些在集合 A 和集合 B 上进行的标准数学运算.每个韦恩(Venn)图中的红色部分是 ...

  • 技术干货 | 一文看懂汽车电路图的画法规则,汽车电子电气工程师必读!

    要修好汽车电气设备,必须读懂和掌握汽车电路图.而要学会如何看汽车电路图,就要先了解汽车电路图的画法规则. 由于目前各国汽车电路图的绘制方法.符号标注.文字标注.技术标准的不同,各汽车生产厂家,汽车电路 ...

  • 一文看懂北朝瓷器,造型、釉色、装饰技法都各有特色

    北朝时期,河南.河北与山东地区积极吸纳南方瓷器烧造工艺,同时又融汇当地汉晋釉陶技术传统,在变革与创新中不仅成功烧制出了具有当地艺术特色的青釉.黑釉与褐釉瓷器,同时还发明了白釉瓷器. 根据考古资料表明, ...

  • 【进阶】一文读懂Python装饰器,搞清来龙去脉!

    (给机器学习算法与Python学习加星标,提升AI技能) 选自pouannes.github.io 作者:Pierre Ouannes 本文由机器之心(nearhuman2014)翻译 原文:http ...

  • 读财报系列—— 一文看懂思进智能21年半年度利润表

    财报看似复杂,其实内含一定的基本逻辑,就利润表而言,从收入的产生到净利润的生成,其间包含了费用的调控.营业外收入的作用.往往投资者只关心营收和净利润的变化,而忽视了中间的生产过程. 从利润的产生过程, ...