Python 中的函数装饰器和闭包详解

函数装饰器可以被用于增强方法的某些行为,如果想自己实现装饰器,则必须了解闭包的概念。

装饰器的基本概念

装饰器是一个可调用对象,它的参数是另一个函数,称为被装饰函数。装饰器可以修改这个函数再将其返回,也可以将其替换为另一个函数或者可调用对象。

例如:有个名为 decorate 的装饰器:

@decorate
def target():
 print('running target()')

上述代码的写法和以下写法的效果是一样的:

def target():
 print('running target()')

target = decorate(target)

但是,它们返回的 target 不一定是原来的那个 target 函数,例如下面这个例子:

>>> def deco(func):
...  def inner():
...   print('running inner()')
...  return inner
...
>>> @deco
... def target():
...  print('running target()')
...
>>> target()
running inner()
>>> target
<function deco.<locals>.inner at 0x0000013D88563040>

可以看到,调用 target 函数执行的是 inner 函数,这里的 target 实际上是 inner 的引用。

何时执行装饰器

装饰器的另一个关键特性是,它们在被装饰函数定义时立即执行,这通常是发生在导入模块的时候。

例如下面的这个模块:registration.py

# 存储被装饰器 @register 装饰的函数
registry = []

# 装饰器
def register(func):
 print(f"注册函数 -> {func}")
 # 记录被装饰的函数
 registry.append(func)
 return func

@register
def f1():
 print("执行 f1()")

@register
def f2():
 print("执行 f2()")

def f3():
 print("执行 f3()")

if __name__ == "__main__":
 print("执行主函数")
 print("registry -> ", registry)
 f1()
 f2()
 f3()

现在我们在命令行执行这个脚本:

$ python registration.py
注册函数 -> <function f1 at 0x000001F6FC8320D0>
注册函数 -> <function f2 at 0x000001F6FC832160>
执行主函数
registry -> [<function f1 at 0x000001F6FC8320D0>, <function f2 at 0x000001F6FC832160>]
执行 f1()
执行 f2()
执行 f3()

这里我们可以看到,在主函数执行之前,register 已经执行了两次。加载模块后,registry 中已经有两个被装饰函数的引用:f1f2。不过这两个函数以及 f3 都是在脚本中明确调用后才开始执行的。

如果只是单纯的导入 registration.py 模块而不运行:

>>> import registration
注册函数 -> <function f1 at 0x0000022670012280>
注册函数 -> <function f2 at 0x0000022670012310>

查看 registry 中的值:

>>> registration.registry
[<function f1 at 0x0000022670012280>, <function f2 at 0x0000022670012310>]

这个例子主要说明:装饰器在导入模块时立即执行,而被装饰的函数只有在明确调用时才运行。这也突出了 Python 中导入时和运行时这个两个概念的区别。

在装饰器的实际使用中,有两点和示例是不同的:

  • 示例中装饰器和被装饰函数在同一个模块中。实际使用中,装饰器通常在一个单独的模块中定义,然后再应用到其它模块的函数上。
  • 示例中 register 装饰器返回的函数和传入的参数相同。实际使用中,装饰器会在内部定义一个新函数,然后将其返回。

装饰器内部定义并返回新函数的做法需要靠闭包才能正常运作。为了理解闭包,则必须先了解 Python 中的变量作用域。

变量作用域的规则

我们来看下面这个例子,一个函数读取一个局部变量 a,一个全局变量 b

>>> def f1(a):
...  print(a)
...  print(b)
...
>>> f1(3)
3
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "<stdin>", line 3, in f1
NameError: name 'b' is not defined

出现错误并不奇怪。如果我们先给 b 赋值,再调用 f1,那就不会出错了:

>>> b = 1
>>> f1(3)
3
1

现在,我们来看一个不寻常的例子:

>>> b = 1
>>> def f2(a):
...  print(a)
...  print(b)
...  b = 2
...
>>> f2(3)
3
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "<stdin>", line 3, in f2
UnboundLocalError: local variable 'b' referenced before assignment

这里,f2 函数的前两行和 f1 相同,然后再给 b 赋值。可是,在赋值之前,第二个 print 失败了。这是因为Python 在编译函数的定义体时,发现在函数中有给 b 赋值的语句,因此判断它是局部变量。而在上述示例中,当我们打印局部变量 b 时,它并没有被绑定值,故而报错。

Python 不要求声明变量,但是会把在函数定义体中赋值的变量当成局部变量。

如果想把上述示例中的 b 看成全局变量,则需要使用 global 声明:

>>> b = 1
>>> def f3(a):
...  global b
...  print(a)
...  print(b)
...  b = 2
...
>>> f3(3)
3
1
>>> b
2
>>> f3(3)
3
2

闭包

闭包是指延伸了作用域的函数,其中包含了函数定义体中的引用,以及不在定义体中定义的非全局变量。

我们通过以下示例来理解这句话。

假设我们有这种需求,计算某个商品在整个历史中的平均收盘价格(商品每天的价格会变化)。例如:

>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

那么如何获取 avg 函数?历史收盘价格又是如何保存的?

我们可以用一个类来实现:

class Averager:
 def __init__(self):
  self.serial = []

 def __call__(self, price):
  self.serial.append(price)
  return sum(self.serial) / len(self.serial)

Averager 的实例是一个可调用对象。

>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

也可以使用一个函数来实现:

>>> def make_averager():
...  serial = []
...  def averager(price):
...   serial.append(price)
...   return sum(serial) / len(serial)
...  return averager
...
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

第一种写法很明显的可以看到,所有历史收盘价均保存在实例变量 self.serial 中。

第二种写法我们要好好的分析一下:serialmake_averager 的局部变量,但是当我们调用 avg(10) 时,make_averager 函数已经返回了,它的作用域不是应该消失了吗?

实际上,在 averager 函数中,serial 是自由变量(未在本地作用域中绑定的变量)。如下图所示:

我们可以在 averager 返回对象的 __code__ 属性中查看它的局部变量和自由变量的名字。

>>> avg.__code__.co_varnames
('price',)
>>> avg.__code__.co_freevars
('serial',)

自由变量 serial 绑定的值存放在 avg 对象的 __closure__ 属性中,它是一个元组,里面的元素是 cell 对象,它的 cell_contents 属性保存实际的值:

>>> avg.__closure__
(<cell at 0x000002266FF99430: list object at 0x00000226702841C0>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]

综上所述,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定值,这样在我们调用这个函数时,即使作用域不在了,仍然可以使用这些绑定的值。

注意:

只有嵌套在其它函数中的函数才可能需要处理不在全局作用域中的外部变量。

nonlocal 声明

前面的 make_averager 方法的效率并不高,我们可以只保存当前的总值和元素个数,再使用它们计算平均值。下面是我们更改后的函数体:

>>> def make_averager():
...  count = total = 0
...  def averager(price):
...   count += 1
...   total += price
...   return total / count
...  return averager

但是这个写法实际上是有问题的,我们先运行再分析:

>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "<stdin>", line 4, in averager
UnboundLocalError: local variable 'count' referenced before assignment

这里 count 被当成 averager 的局部变量,而不是我们期望的自由变量。这是因为 count += 1 相当于 count = count + 1。因此,我们在 averager 函数体中实际包含了给 count 赋值的操作,这就把 count 变成局部变量。total 也有这个问题。

为了解决这个问题,Python3 引入了 nonlocal 关键字,用于声明自由变量。使用 nonlocal 修改上述的例子:

>>> def make_averager():
...  count = total = 0
...  def averager(price):
...   nonlocal count, total
...   count += 1
...   total += price
...   return total / count
...  return averager
...
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

叠放装饰器

如果我们把 @d1@d2 两个装饰器应用到同一个函数 f() 上,实际相当于 f = d1(d2(f))

也就是说,下属代码:

@d1
@d2
def f():
	pass

等同于:

def f():
	pass
f = d1(d2(f))

参数化装饰器

Python 会把被装饰的参数作为第一个参数传递给装饰器函数,那么如何让装饰器接受其它的参数呢?这里我们需要定义一个装饰器工厂函数,返回真正的装饰器函数。

以本文开头的 register 装饰器为例,我们为它添加一个 active 参数,如果置为 False,那就不注册这个函数。

registry = []

def register(active=True):
 def decorate(func):
  if active:
   print(f"注册函数 -> {func}")
   # 记录被装饰的函数
   registry.append(func)
  return func

 return decorate

@register()
def f1():
 print("执行 f1")

@register(active=False)
def f2():
 print("执行 f2")

现在我们导入这个模块:

>>> import registration
注册函数 -> <function f1 at 0x0000016D80402280>

可以看到只注册了 f1 函数。

实现一个简单的装饰器

这里我们使用嵌套函数实现一个简单的装饰器:计算被装饰函数执行的耗时,并将函数名、参数和执行的结果打印出来。

import time
def clock(func):
 def clocked(*args):
  start_time = time.perf_counter()
  result = func(*args)
  cost = time.perf_counter() - start_time
  print(
   "[%.2f] %s(%s) -> %r" % (cost, func.__name__, list(map(repr, args)), result)
  )
  return result

 return clocked

下面我们来试试这个装饰器:

>>> @clock
... def factorial(n):
...  # 计算 n 的阶乘
...  return 1 if n < 2 else n * factorial(n - 1)
>>>
>>> factorial(6)
[0.00] factorial(['1']) -> 1
[0.00] factorial(['2']) -> 2
[0.00] factorial(['3']) -> 6
[0.00] factorial(['4']) -> 24
[0.00] factorial(['5']) -> 120
[0.00] factorial(['6']) -> 720
720

具体来分析一下,这里 factorial 作为 func 参数传递给 clock 函数,然后 clock 函数返回 clocked 函数,Python 解释器会把 clocked 赋值给 factorial。所以,如果我们查看 factorial__name__ 属性,会发现它的值是 clocked 而不是 factorial

>>> factorial.__name__
'clocked'

所以,factorial 保存的是 clocked 的引用,每次调用 factorial 实际上都是在调用 clocked 函数。

我们也可以使用 functools.wraps 装饰器把 func 的一些属性复制到 clocked 函数上,例如:__name____doc__

def clock(func):
 @functools.wraps(func)
 def clocked(*args):
  start_time = time.perf_counter()
  result = func(*args)
  cost = time.perf_counter() - start_time
  print(
   "[%.2f] %s(%s) -> %r" % (cost, func.__name__, list(map(repr, args)), result)
  )
  return result

 return clocked
>>>
>>> @clock
... def factorial(n):
...  return 1 if n < 2 else n * factorial(n - 1)
>>>
>>> factorial.__name__
'factorial'

标准库中的装饰器

使用 functools.lru_cache 做备忘

functools.lru_cache 会把耗时的函数的结果保存起来,避免传入相同的参数时的重复计算。lru 的意思是 Least Recently Used,表示缓存不会无限增长,一段时间不用的缓存条目会被丢弃。

lru_cache 非常适合计算第 n 个斐波那契数这样的慢速递归函数。

我们来看看不使用 lru_cache 时的情况:

>>> @clock
... def fibonacci(n):
...  return n if n < 2 else fibonacci(n - 2) + fibonacci(n - 1)
...
>>> fibonacci(6)
[0.00000040] fibonacci(['0']) -> 0
[0.00000060] fibonacci(['1']) -> 1
[0.00030500] fibonacci(['2']) -> 1
[0.00000030] fibonacci(['1']) -> 1
[0.00000040] fibonacci(['0']) -> 0
[0.00000060] fibonacci(['1']) -> 1
[0.00042110] fibonacci(['2']) -> 1
[0.00074440] fibonacci(['3']) -> 2
[0.00128530] fibonacci(['4']) -> 3
[0.00000020] fibonacci(['1']) -> 1
[0.00000030] fibonacci(['0']) -> 0
[0.00000050] fibonacci(['1']) -> 1
[0.00035500] fibonacci(['2']) -> 1
[0.00055270] fibonacci(['3']) -> 2
[0.00000030] fibonacci(['0']) -> 0
[0.00000060] fibonacci(['1']) -> 1
[0.00041220] fibonacci(['2']) -> 1
[0.00000040] fibonacci(['1']) -> 1
[0.00000040] fibonacci(['0']) -> 0
[0.00000050] fibonacci(['1']) -> 1
[0.00032410] fibonacci(['2']) -> 1
[0.00061420] fibonacci(['3']) -> 2
[0.00122760] fibonacci(['4']) -> 3
[0.00206850] fibonacci(['5']) -> 5
[0.00352630] fibonacci(['6']) -> 8
8

这种方式有很多重复的计算,例如 fibonacci(['1']) 执行了 8 次,fibonacci(['2']) 执行了 5 次等等。

现在我们使用 functools.lru_cache 优化一下:

>>> @functools.lru_cache
... @clock
... def fibonacci(n):
...  return n if n < 2 else fibonacci(n - 2) + fibonacci(n - 1)
...
>>> fibonacci(6)
[0.00000060] fibonacci(['0']) -> 0
[0.00000070] fibonacci(['1']) -> 1
[0.00106320] fibonacci(['2']) -> 1
[0.00000080] fibonacci(['3']) -> 2
[0.00132790] fibonacci(['4']) -> 3
[0.00000060] fibonacci(['5']) -> 5
[0.00159670] fibonacci(['6']) -> 8
8

可以看到节省了一般的执行时间,并且 n 的每个值只调用了一次函数。

在执行 fibonacci(30) 时,如果使用未优化的版本需要 141 秒,使用优化后的版本只需要 0.002 秒。

除了优化递归算法之外,lru_cache 在从 WEB 获取信息的应用中也能发挥巨大作用。

lru_cache 还有两个可选参数:

def lru_cache(maxsize=128, typed=False):
  • maxsize:最多可存储的调用结果的个数。缓存满了之后,旧的结果被丢弃。为了获取最佳的性能,maxsize 应该设置为 2 的幂。
  • typed:如果置为 True,会把不同参数类型得到的结果分开保存。例如:f(3.0)f(3) 会被当成不同的调用。

单分派泛函数

假设我们现在开发一个调试 WEB 应用的工具:生成 HTML,显示不同类型的 Python 对象。

我们可以这样编写一个函数:

import html
def htmlize(obj):
 content = html.escape(repr(obj))
 return f"<pre>{content}</pre>"

现在我们需要做一些拓展,让它使用特别的方式显示某些特定类型:

  • str:把字符串内部的 \n 替换为 <br>\n,并且使用 <p> 替换 <pre>
  • int:以十进制和十六进制显示数字;
  • list:显示一个 HTML 列表,根据各个元素的类型格式化;

最常用的方式就是写 if...elif..else 判断:

import numbers
from collections.abc import MutableSequence

def htmlize(obj):
 if isinstance(obj, str):
  content = obj.replace("\n", "<br>\n")
  return f"<p>{content}</p>"
 elif isinstance(obj, numbers.Integral):
  content = f"{obj} ({hex(obj)})"
  return f"<pre>{content}</pre>"
 elif isinstance(obj, MutableSequence):
  content = "</li>\n<li>".join(htmlize(item) for item in obj)
  return "<ul>\n<li>" + content + "</li>\n</ul>"
 else:
  content = f"<pre>{obj}</pre>"
  return content

如果想添加新的类型判断,只会将函数越写越长,并且各个类型之间耦合度较高,不利于维护。

Python 3.4 新增的 functools.singledispatch 装饰器可以将整个方案拆分成多个模块。

import numbers
from collections.abc import MutableSequence
from functools import singledispatch

@singledispatch
def htmlize(obj):
 content = f"<pre>{obj}</pre>"
 return content

@htmlize.register(str)
def _(text):
 content = text.replace("\n", "<br>\n")
 return f"<p>{content}</p>"
@htmlize.register(numbers.Integral)
def _(num):
 content = f"{num} ({hex(num)})"
 return f"<pre>{content}</pre>"

@htmlize.register(MutableSequence)
def _(seq):
 content = "</li>\n<li>".join(htmlize(item) for item in seq)
 return "<ul>\n<li>" + content + "</li>\n</ul>"

这里我们为每一个需要特殊处理的类型都定义另一个专门的函数。

functools.singledispatch 的更详细的文档参考:https://www.python.org/dev/peps/pep-0443/

到此这篇关于Python 中的函数装饰器和闭包详解的文章就介绍到这了,更多相关Python函数装饰器和闭包内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Python closure闭包解释及其注意点详解

    一.闭包 1.定义:当一个函数在内部定义函数,并且内部的函数应用外部函数的参数或者局部变量,当内部函数被当做返回值的时候,相关参数和变量保存在返回的函数之中,这种结果,叫做闭包. 2.例子:连载17中的myF4就是一个典型的例子​. 3.闭包常见的坑 def count(): #定义列表 fs = [] for i in range(1, 4): def f(): return i * i fs.append(f) # 按照我们的预期, 应该是fs = [f, f, f].其中f中一次包含1,

  • Python中利用函数装饰器实现备忘功能

    "备忘"的定义 "memoization"(备忘)这个词是由Donald Michie在1968年提出的,它基于拉丁语单词"memorandum"(备忘录),意思是"被记住".虽然它和单词"memorization"在某种程度上有些相似,但它并不是该单词的错误拼写.实际上,Memoisation是一种用于通过计算来加速程序的技术,它通过记住输入量的计算结果,例如函数调用结果,来实现其加速目的.如果遇到相同的

  • python函数装饰器之带参数的函数和带参数的装饰器用法示例

    本文实例讲述了python函数装饰器之带参数的函数和带参数的装饰器用法.分享给大家供大家参考,具体如下: 1. 函数带多个参数 # 普通的装饰器, 打印函数的运行时间 def decrator(func): def wrap(*args, **kwargs): start_time = time.time() res = func(*args, **kwargs) end_time = time.time() print('运行时间为', end_time-start_time) return

  • 详解python函数的闭包问题(内部函数与外部函数详述)

    python函数的闭包问题(内嵌函数) >>> def func1(): ... print ('func1 running...') ... def func2(): ... print ('func2 running...') ... func2() ... >>> func1() func1 running... func2 running... 内部函数func2作用域都在外部函数func1作用域之内 如果试图在外部函数的外部调用内部函数将会报错 >>

  • python函数装饰器用法实例详解

    本文实例讲述了python函数装饰器用法.分享给大家供大家参考.具体如下: 装饰器经常被用于有切面需求的场景,较为经典的有插入日志.性能测试.事务处理等.装饰器是解决这类问题的绝佳设计, 有了装饰器,我们就可以抽离出大量函数中与函数功能本身无关的雷同代码并继续重用.概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能. #! coding=utf-8 import time def timeit(func): def wrapper(a): start = time.clock() func

  • Python 中的函数装饰器和闭包详解

    函数装饰器可以被用于增强方法的某些行为,如果想自己实现装饰器,则必须了解闭包的概念. 装饰器的基本概念 装饰器是一个可调用对象,它的参数是另一个函数,称为被装饰函数.装饰器可以修改这个函数再将其返回,也可以将其替换为另一个函数或者可调用对象. 例如:有个名为 decorate 的装饰器: @decorate def target(): print('running target()') 上述代码的写法和以下写法的效果是一样的: def target(): print('running targe

  • Python函数装饰器的使用详解

    目录 装饰器 装饰器的定义 装饰器的意义 装饰器的使用 无参装饰器 有参装饰器 实例练习 总结 装饰器 装饰器的定义 关于装饰器的定义,我们先来看一段github上大佬的定义: Function decorators are simply wrappers to existing functions.In the context of design patterns,decorators dynamically alter the functionality of a function, met

  • Python 带有参数的装饰器实例代码详解

    demo.py(装饰器,带参数的装饰器): def set_level(level_num): def set_func(func): def call_func(*args, **kwargs): if level_num == 1: print("----权限级别1,验证----") elif level_num == 2: print("----权限级别2,验证----") return func() return call_func return set_f

  • Python函数装饰器实现方法详解

    本文实例讲述了Python函数装饰器实现方法.分享给大家供大家参考,具体如下: 编写函数装饰器 这里主要介绍编写函数装饰器的相关内容. 跟踪调用 如下代码定义并应用一个函数装饰器,来统计对装饰的函数的调用次数,并且针对每一次调用打印跟踪信息. class tracer: def __init__(self,func): self.calls = 0 self.func = func def __call__(self,*args): self.calls += 1 print('call %s

  • Python中的上下文管理器相关知识详解

    前言 with 这个关键字,对于每一学习Python的人,都不会陌生. 操作文本对象的时候,几乎所有的人都会让我们要用 with open ,这就是一个上下文管理的例子.你一定已经相当熟悉了,我就不再废话了. with open('test.txt') as f: print f.readlines() 什么是上下文管理器? 基本语法 with EXPR as VAR: BLOCK 先理清几个概念 1. 上下文表达式:with open('test.txt') as f: 2. 上下文管理器:o

  • react-redux中connect的装饰器用法@connect详解

    最近在琢磨react中的一些小技巧,这篇文章记录一下在redux中用装饰器来写connect. 通常我们需要一个reducer和一个action,然后使用connect来包裹你的Component.假设你已经有一个key为main的reducer和一个action.js. 我们的App.js一般都这么写: import React from 'react' import {render} from 'react-dom' import {connect} from 'react-redux' i

  • python装饰器实例大详解

    一.作用域 在python中,作用域分为两种:全局作用域和局部作用域. 全局作用域是定义在文件级别的变量,函数名.而局部作用域,则是定义函数内部. 关于作用域,我们要理解两点: a.在全局不能访问到局部定义的变量 b.在局部能够访问到全局定义的变量,但是不能修改全局定义的变量(当然有方法可以修改) 下面我们来看看下面实例: x = 1 def funx(): x = 10 print(x) # 打印出10 funx() print(x) # 打印出1 如果局部没有定义变量x,那么函数内部会从内往

  • python装饰器使用实例详解

    这篇文章主要介绍了python装饰器使用实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 python装饰器的作用就是在不想改变原函数代码的情况下,增加新的功能.主要应用了python闭包的概念,现在用1个小例子说明 import time def foo(): time.sleep(1) def bar(): time.sleep(2) def show_time(f): def inner(): start_time = time.t

随机推荐