社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

一文读懂 Python 装饰器

技术最前线 • 1 年前 • 192 次点击  
推荐关注↓

作者:Pierre Ouannes

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

来源:机器之心

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(1020)
30
>>> add
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(2030))
print('add("a", "b")',   add("a""b"))
Output: i

add(1020
add(203050
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(2030))
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(1020
time taken:  6.699562072753906e-05
add(203050
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(2030))
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(2030))
print('add("a", "b")', add("a""b"))
print('sub(10)', sub(10))
print('sub(20, 30)', sub(2030))

因为 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(2030))
print('add("a", "b")',   add("a""b"))
print('sub(10)',         sub(10))
print('sub(20, 30)',     sub(2030))
Output:

time taken:  0.0
add(1020
time taken:  9.5367431640625e-07
add(203050
time taken:  0.0
add("a""b") ab
time taken:  9.5367431640625e-07
sub(100
time taken:  9.5367431640625e-07
sub(2030-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(2030))
print('add("a", "b")',   add("a""b"))
print('sub(10)',         sub(10))
print('sub(20, 30)',     sub(2030))

参数和关键字参数

现在,还有一个小问题没有解决。在 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(2030))
print('add("a", "b")',   add("a""b"))
print('sub(10)',         sub(10))
print('sub(20, 30)',     sub(2030))

现在,该 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 次。

- EOF -

推荐阅读  点击标题可跳转

1、Python 实现 8 个概率分布公式及可视化

2、涨知识!Python 的异常信息还能这样展现

3、For-else:Python 中一个奇怪但有用的特性


↓推荐关注↓

「Python开发者」日常分享 Python 相关的技术文章、实用案例、工具资源、精选课程、热点资讯等


点赞和在看就是最大的支持❤️

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/149745
 
192 次点击