实例详解Python装饰器与闭包


Posted in Python onJuly 29, 2019

闭包是Python装饰器的基础。要理解闭包,先要了解Python中的变量作用域规则。

变量作用域规则

首先,在函数中是能访问全局变量的:

>>> a = 'global var'
>>> def foo():
 print(a)
>>> foo()
global var

然后,在一个嵌套函数中,内层函数能够访问在外层函数中定义的局部变量:

>>> def foo():
 a = 'free var'
 def bar():
  print(a)
 return bar

>>> foo()()
free var

闭包

上面的嵌套函数就是闭包。 闭包 是指延伸了作用域的函数,在其中能够访问未在函数定义体中定义的非全局变量。未在函数定义体中定义的非全局变量一般都是在嵌套函数中出现的。

上述示例中的变量a就是一个并未在函数bar中定义的非全局变量。对于bar来说,它有个专业名字,叫做 自由变量 。

自由变量的名称可以在字节码对象中查看:

>>> bar = foo()
>>> bar.__code__.co_freevars
('a',)

自由变量的值绑定在函数的__closure__属性中:

>>> bar.__closure__
(<cell at 0x000001CB2912DF48: str object at 0x000001CB291D3D70>,)

其中保存了对应自由变量的cell对象的序列,cell对象的cell_contents属性保存了变量的值:

>>> bar.__closure__[0].cell_contents
'free var'

这与JavaScript中闭包的行为是类似的,JavaScript中嵌套函数会将外层函数的活动对象添加到它的作用域链中。但与JavaScript不同的是,当Python函数中的全局变量或者自由变量是不可变对象(数字、字符串、元组等)时,是只能读取,无法更新的:

>>> a = 1
>>> def foo():
 print(a)
 a += 1

>>> foo()
UnboundLocalError: local variable 'a' referenced before assignment

>>> def foo():
 a = 1
 def bar():
  print(a)
  a += 1
 return bar

>>> foo()()
UnboundLocalError: local variable 'a' referenced before assignment

两种情况下,都会报错。这并不是缺陷,而是Python的设计选择。Python不要求声明变量,但是会假定在函数定义体中赋值的变量是局部变量,以避免在不知情的情况下修改全局变量。

a += 1 与 a = a + 1 相同,编译函数的定义体时,会将a当做局部变量,不会当做自由变量保存。然后尝试获取a的值时,发现a并没有绑定值,于是报错。

解决这个问题的办法,一是将变量置于一些可变对象,如列表、字典中:

def foo():
 ns = {}
 ns['a'] = 1
 def bar():
  ns['a'] += 1
  print (ns['a'])
 return bar

另外的方法就是使用 global 或者 nonlocal 将变量声明为全局变量或者自由变量:

>>> def foo():
 a = 1
 def bar():
  nonlocal a
  a += 1
  print(a)
 return bar

>>> foo()()
2

当自由变量本身是可变对象时,是可以直接进行操作的:

def make_avg():
 ls = []
 def avg(x):
  ls.append(x)
  print(sum(ls)/len(ls))
 return avg

装饰器

装饰器是可调用对象,参数一般是另一个函数。装饰器可以以某种方式增强被装饰函数的行为,然后返回被装饰的函数或者将其替换成一个新的函数。

一个最简单的不做任何额外行为的装饰器:

def decorate(func):
 return func

decorate 函数就是一个最简单的装饰器,使用方法:

def target():
 pass
target = decorate(target)

Python为装饰器的使用提供了语法糖,可以简便的写为:

@decorate
def target():
 pass

导入时运行

装饰器一个很重要的特性是它是导入时(加载模块时)运行的:

def decorate(func):
 print('running decorator when import')
 return func
@decorate
def foo():
 print('running foo')
 pass
if __name__ == '__main__':
 print('start foo')
 foo()

结果:

running decorator when import start foo running foo

可以看到,装饰器是导入时运行的,而被装饰的函数是明确调用时运行的。

装饰器可以返回被装饰的函数本身,和运行时导入的特性结合起来,可以实现简单的注册器功能:

view_registry = []
def register(func):
 view_registry.append(func)
 return func
@register
def view1():
 pass
@register
def view2():
 pass
def main():
 print(view_registry)
if __name__ == '__main__':
 main()

返回新函数

上述装饰器的例子都返回了被装饰的原函数,但装饰器的典型行为还是返回一个新函数:把被装饰的函数替换成新函数,新函数接受与原函数相同的参数,并且返回原函数本该返回的值。写法类似于:

def deco(func):
 def new_func(*args, **kwargs):
  return func(*args, **kwargs)
 return new_func

这种情况下装饰器就使用到了闭包。JavaScript中的防抖与节流函数就是这种典型的装饰器行为。新函数一般会使用外部装饰器函数中的变量当做自由变量,对函数作出某种增强行为。

举个例子,我们知道,当Python函数的参数是个可变对象时,会产生意料之外的行为:

def foo(x, y=[]):
  y.append(x)
  print(y)

foo(1)
foo(2)
foo(3)

输出:

[1] [1, 2] [1, 2, 3]

这是因为,函数的参数默认值保存在__defaults__属性中,指向了同一个列表:

>>> foo.__defaults__
([1, 2, 3],)

我们就可以用一个装饰器在函数执行前取出默认值做深复制,然后覆盖函数原先的参数默认值:

import copy
def fresh_defaults(func):
  defaults = func.__defaults__
  def deco(*args, **kwargs):
    func.__defaults__ = copy.deepcopy(defaults)
    return func(*args, **kwargs)
  return deco
@fresh_defaults
def foo(x, y=[]):
  y.append(x)
  print(y)
foo(1)
foo(2)
foo(3)

输出:

[1] [2] [3]

接收参数的装饰器

装饰器除了可以接受函数作为参数外,还可以接受其他参数。使用方法是:创建一个装饰器工厂,接受参数,返回一个装饰器,再把它应用到被装饰的函数上,语法如下:

def deco_factory(*args, **kwargs):
  def deco(func):
    print(args)
    return func
  return deco
@deco_factory('factory')
def foo():
  pass

在Web框架中,通常要将URL模式映射到生成响应的view函数,并将view函数注册到某些中央注册处。之前我们曾经实现过一个简单的注册装饰器,只是注册了view函数,却没有URL映射,是远远不够的。

在Flask中,注册view函数需要一个装饰器:

@app.route('/hello')
def hello():
  return 'Hello, World'

原理就是使用了装饰器工厂,可以简单的模拟一下实现:

class App:
  def __init__(self):
    self.view_functions = {}
  def route(self, rule):
    def deco(view_func):
      self.view_functions[rule] = view_func
      return view_func
    return deco
app = App()
@app.route('/')
def index():
  pass
@app.route('/hello')
def hello():
  pass
for rule, view in app.view_functions.items():
  print(rule, ':', view.__name__)

输出:

/ : index /hello : hello

还可以使用装饰器工厂来确定view函数可以允许哪些HTTP请求方法:

def action(methods):
  def deco(view):
    view.allow_methods = [method.lower() for method in methods]
    return view
  return deco
@action(['GET', 'POST'])
def view(request):
  if request.method.lower() in view.allow_methods:
    ...

重叠的装饰器

装饰器也是可以重叠使用的:

@d1
@d2
def foo():
  pass

等同于:

foo = d1(d2(foo))

类装饰器

装饰器的参数也可以是一个类,也就是说,装饰器可以装饰类:

import types
def deco(cls):
  for key, method in cls.__dict__.items():
    if isinstance(method, types.FunctionType):
      print(key, ':', method.__name__)
  return cls
@deco
class Test:
  def __init__(self):
    pass
  def foo(self):
    pass

总结

以上所述是小编给大家介绍的实例详解Python装饰器与闭包,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

Python 相关文章推荐
Python守护线程用法实例
Jun 23 Python
python http接口自动化脚本详解
Jan 02 Python
numpy.random模块用法总结
May 27 Python
用Python实现二叉树、二叉树非递归遍历及绘制的例子
Aug 09 Python
Python通过cv2读取多个USB摄像头
Aug 28 Python
Python中的单下划线和双下划线使用场景详解
Sep 09 Python
Python 利用邮件系统完成远程控制电脑的实现(关机、重启等)
Nov 19 Python
Python实现在Windows平台修改文件属性
Mar 05 Python
利用Python自动化操作AutoCAD的实现
Apr 01 Python
Python3 socket即时通讯脚本实现代码实例(threading多线程)
Jun 01 Python
Flask中jinja2的继承实现方法及实例
Mar 03 Python
Python echarts实现数据可视化实例详解
Mar 03 Python
对于Python深浅拷贝的理解
Jul 29 #Python
PyCharm中代码字体大小调整方法
Jul 29 #Python
python pandas cumsum求累计次数的用法
Jul 29 #Python
详解Python用三种方式统计词频的方法
Jul 29 #Python
Django框架视图层URL映射与反向解析实例分析
Jul 29 #Python
Django 重写用户模型的实现
Jul 29 #Python
python写程序统计词频的方法
Jul 29 #Python
You might like
咖啡的化学
2021/03/03 咖啡文化
php生成图片缩略图的方法
2015/04/07 PHP
PHP编译安装时常见错误解决办法
2015/05/28 PHP
php头像上传预览实例代码
2017/05/02 PHP
腾讯与新浪的通过IP地址获取当前地理位置(省份)的接口
2010/07/26 Javascript
10款新鲜出炉的 jQuery 插件(Ajax 插件,有幻灯片、图片画廊、菜单等)
2011/06/08 Javascript
AeroWindow 基于JQuery的弹出窗口插件
2011/06/27 Javascript
caller和callee的区别介绍及演示结果
2013/03/10 Javascript
getAsDataURL在Firefox7.0下无法预览本地图片的解决方法
2013/11/15 Javascript
Nodejs学习笔记之测试驱动
2015/04/16 NodeJs
Windows系统下Node.js的简单入门教程
2015/06/23 Javascript
jQuery实现的数值范围range2dslider选取插件特效多款代码分享
2015/08/27 Javascript
使用JQuery在线制作ppt并在线演示源码特效
2015/09/08 Javascript
Jquery ajax基础教程
2015/11/20 Javascript
JS实现合并两个数组并去除重复项只留一个的方法
2015/12/17 Javascript
Vue 报错TypeError: this.$set is not a function 的解决方法
2018/12/17 Javascript
使用Three.js实现太阳系八大行星的自转公转示例代码
2019/04/09 Javascript
TensorFlow实现简单卷积神经网络
2018/05/24 Python
python使用scrapy发送post请求的坑
2018/09/04 Python
python多进程使用及线程池的使用方法代码详解
2018/10/24 Python
pygame实现俄罗斯方块游戏(AI篇1)
2019/10/29 Python
关于numpy中eye和identity的区别详解
2019/11/29 Python
numpy的Fancy Indexing和array比较详解
2020/06/11 Python
Python 列表推导式需要注意的地方
2020/10/23 Python
德国宠物用品、宠物食品及水族馆网上商店:ZooRoyal
2017/07/09 全球购物
英国派对礼服和连衣裙购物网站:TFNC London
2018/07/07 全球购物
经济系大学生求职信
2013/10/01 职场文书
2014年高考决心书
2014/03/11 职场文书
环保建议书600字
2014/05/14 职场文书
大学生心理活动总结
2014/07/04 职场文书
政风行风建设责任书
2014/07/23 职场文书
2014红色之旅心得体会
2014/10/07 职场文书
2014年酒店服务员工作总结
2014/12/08 职场文书
Django一小时写出账号密码管理系统
2021/04/29 Python
vue-element-admin项目导入和导出的实现
2021/05/21 Vue.js
分享Python获取本机IP地址的几种方法
2022/03/17 Python