一文读懂Python装饰器,这是一个会打扮的装饰器

点击上方 "程序员小乐"关注公众号, 星标或置顶一起成长

每天早上8点20分, 第一时间与你相约

每日英文

No matter how painful an experience you might have had, eventually everything will be forgotten. There's nothing that can beat time.

不管经历多痛的事情,到最后都会渐渐遗忘。没有什么能敌得过时光。


每日掏心话

信任是把刀,你给了别人,他就有两个选择,捅你或者保护你。看清一个人何必去揭穿;讨厌一个人又何必去翻脸。


来自:机器之心 | 责编:乐乐

640?wx_fmt=jpeg

程序员小乐(ID:study_tech)第 649 次推文   图片来自网络


   正文   


Python 是一种对新手很友好的语言。但是,它也有很多较难掌握的高级功能,比如装饰器(decorator)。很多初学者一直不理解装饰器及其工作原理,在这篇文章中,我们将介绍装饰器的来龙去脉。


在 Python 中,函数是一种非常灵活的结构,我们可以把它赋值给变量、当作参数传递给另一个函数,或者当成某个函数的输出。装饰器本质上也是一种函数,它可以让其它函数在不经过修改的情况下增加一些功能。


这也就是「装饰」的意义,这种「装饰」本身代表着一种功能,如果用它修饰不同的函数,那么也就是为这些函数增加这种功能。


一般而言,我们可以使用装饰器提供的 @ 语法糖(Syntactic Sugar)来修饰其它函数或对象。如下所示我们用 @dec 装饰器修饰函数 func ():


 
                                                   

@dec
def func():
  pass


理解装饰器的最好方式是了解装饰器解决什么问题,本文将从具体问题出发一步步引出装饰器,并展示它的优雅与强大。


设置问题


为了解装饰器的目的,接下来我们来看一个简单的示例。假如你有一个简单的加法函数 dec.py,第二个参数的默认值为 10:


 
                                                   

# dec.py

def add(x, y=10):
  return x + y

我们来更认真地看一下这个加法函数:

 
                                                   

>>> add(10, 20)
30
>>> add
<function add at 0x7fce0da2fe18>
>>> add.__name__
'add'
>>> add.__module__
'__main__'
>>> add.__defaults__ # default value of the `add` function
(10,)
>>> add.__code__.co_varnames # the variable names of the `add` function
('x', 'y')

我们无需理解这些都是什么,只需要记住 Python 中的每个函数都是对象,它们有各种属性和方法。你还可以通过 inspect 模块查看 add() 函数的源代码:

 
                                                   

>>> from inspect import getsource
>>> print(getsource(add))

def add(x, y=10):
  return x + y

现在你以某种方式使用该加法函数,比如你使用一些操作来测试该函数:

 
                                                   

# dec.py
from time import time

def add(x, y=10):
  return x + y

print('add(10)',         add(10))
print('add(20, 30)',     add(20, 30))
print('add("a", "b")',   add("a", "b"))
Output: i

add(10) 20
add(20, 30) 50
add("a", "b") ab

假如你想了解每个操作的时间,可以调用 time 模块:

 
                                                   

# dec.py
from time import time


def add(x, y=10):
  return x + y

before = time()
print('add(10)',         add(10))
after = time()
print('time taken: ', after - before)
before = time()
print('add(20, 30)',     add(20, 30))
after = time()
print('time taken: ', after - before)
before = time()
print('add("a", "b")',   add("a", "b"))
after = time()
print('time taken: ', after - before)
Output:

add(10) 20
time taken:  6.699562072753906e-05
add(20, 30) 50
time taken:  6.9141387939453125e-06
add("a", "b") ab
time taken:  6.9141387939453125e-06


现在,你作为一个编程人员是不是有些手痒,毕竟我们不喜欢总是复制粘贴相同的代码。现在的代码可读性不强,如果你想改变什么,你就得修改所有出现的地方,Python 肯定有更好的方式。


我们可以按照如下做法,直接在 add 函数中捕捉运行时间:


 
                                                  

# dec.py
from time import time

def add(x, y=10):
  before = time()
  rv = x + y
  after = time()
  print('time taken: ', after - before)
  return rv

print('add(10)',         add(10))
print('add(20, 30)',     add(20, 30))
print('add("a", "b")',   add("a", "b"))

这种方法肯定比前一种要好。但是如果你还有另一个函数,那么这似乎就不方便了。当我们有多个函数时:

 
                                                  

# dec.py
from time import time

def add(x, y=10):
  before = time()
  rv = x + y
  after = time()
  print('time taken: ', after - before)
  return rv

def sub(x, y=10):
  return x - y

print('add(10)', add(10))
print('add(20, 30)', add(20, 30))
print('add("a", "b")', add("a", "b"))
print('sub(10)', sub(10))
print('sub(20, 30)', sub(20, 30))

因为 add 和 sub 都是函数,我们可以利用这一点写一个 timer 函数。我们希望 timer 能计算一个函数的运算时间:

 
                                                  

def timer(func, x, y=10):
  before = time()
  rv = func(x, y)
  after = time()
  print('time taken: ', after - before)
  return rv

这很不错,不过我们必须使用 timer 函数包装不同的函数,如下所示:

 
                                                  

print('add(10)', timer(add,10)))

现在默认值还是 10 吗?未必。那么如何做得更好呢?


这里有一个主意:创建一个新的 timer 函数,并包装其他函数,然后返回包装后的函数:


 
                                                  

def timer(func):
  def f(x, y=10):
    before = time()
    rv = func(x, y)
    after = time()
    print('time taken: ', after - before)
    return rv
  return f

现在,你只需用 timer 包装一下 add 和 sub 函数 :

 
                                                  

add = timer(add)

这样就可以了!以下是完整代码:

 
                                                  

# dec.py
from time import time
def timer(func):
  def f(x, y=10):
    before = time()
    rv = func(x, y)
    after = time()
    print('time taken: ', after - before)
    return rv
  return f

def add(x, y=10):
  return x + y
add = timer(add)


def sub(x, y=10):
  return x - y
sub = timer(sub)

print('add(10)',         add(10))
print('add(20, 30)',     add(20, 30))
print('add("a", "b")',   add("a", "b"))
print('sub(10)',         sub(10))
print('sub(20, 30)',     sub(20, 30))
Output:

time taken:  0.0
add(10) 20
time taken:  9.5367431640625e-07
add(20, 30) 50
time taken:  0.0
add("a", "b") ab
time taken:  9.5367431640625e-07
sub(10) 0
time taken:  9.5367431640625e-07
sub(20, 30) -10


我们来总结一下这个过程:我们有一个函数(比如 add 函数),然后用一个动作(比如计时)包装该函数。包装的结果是一个新函数,能实现某些新功能。


当然了,默认值还有点问题,稍后我们会解决它。

装饰器

现在,上面的解决方案以及非常接近装饰器的思想了,使用常见行为包装某个具体的函数,这种模式就是装饰器在做的事。使用装饰器后的代码是:

 
                                                  

def add(x, y=10):
  return x + y
add = timer(add)
You write:

@timer
def add(x, y=10):
  return x + y

它们的作用是一样的,这就是 Python 装饰器的作用。它实现的作用类似于 add = timer(add),只不过装饰器把句法放在函数上面,且句法更加简单:@timer。

 
                                                  

# dec.py
from time import time
def timer(func):
  def f(x, y=10):
    before = time()
    rv = func(x, y)
    after = time()
    print('time taken: ', after - before)
    return rv
  return f

@timer
def add(x, y=10):
  return x + y

@timer
def sub(x, y=10):
  return x - y

print('add(10)',         add(10))
print('add(20, 30)',     add(20, 30))
print('add("a", "b")',   add("a", "b"))
print('sub(10)',         sub(10))
print('sub(20, 30)',     sub(20, 30))

参数和关键字参数

现在,还有一个小问题没有解决。在 timer 函数中,我们将参数 x 和 y 写死了,即指定 y 的默认值为 10。有一种方法可以传输该函数的参数和关键字参数,即 *args 和 **kwargs。参数是函数的标准参数(在本例中 x 为参数),关键字参数是已具备默认值的参数(本例中是 y=10)。代码如下:

 
                                                  

# dec.py
from time import time
def timer(func):
  def f(*args, **kwargs):
    before = time()
    rv = func(*args, **kwargs)
    after = time()
    print('time taken: ', after - before)
    return rv
  return f

@timer
def add(x, y=10):
  return x + y

@timer
def sub(x, y=10):
  return x - y

print('add(10)',         add(10))
print('add(20, 30)',     add(20, 30))
print('add("a", "b")',   add("a", "b"))
print('sub(10)',         sub(10))
print('sub(20, 30)',     sub(20, 30))

现在,该 timer 函数可以处理任意函数、任意参数和任意默认值设置了,因为它仅仅将这些参数传输到函数中。

高阶装饰器

你们可能会疑惑:如果我们可以用一个函数包装另一个函数来添加有用的行为,那么我们可以再进一步吗?我们用一个函数包装另一个函数,再被另一个函数包装吗?

可以!事实上,函数的深度可以随你的意。例如,你想写一个装饰器来执行某个函数 n 次。如下所示:

 
                                                  

def ntimes(n):
  def inner(f):
    def wrapper(*args, **kwargs):
      for _ in range(n):
        rv = f(*args, **kwargs)
      return rv
    return wrapper
  return inner

然后你可以使用上述函数包装另一个函数,例如前文中的 add 函数:

 
                                                  

@ntimes(3)
def add(x, y):
  print(x + y)
  return x + y

输出的语句表明该代码确实执行了 3 次。640?wx_fmt=png

原文地址:https://pouannes.github.io/blog/decorators/


640?wx_fmt=png

欢迎在留言区留下你的观点,一起讨论提高。如果今天的文章让你有新的启发,学习能力的提升上有新的认识,欢迎转发分享给更多人。

欢迎各位读者加入程序员小乐技术群,在公众号后台回复“加群”或者“学习”即可。

猜你还想看


Redis 面试最常被问到21个知识点总结

Java 开发中如何正确的踩坑

如何写一个更好的Python函数?


640?wx_fmt=png

关注微信公众号「程序员小乐」,收看更多精彩内容
嘿,你在看吗640?
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值