实例详解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读写文本文件及编写简单的文本编辑器
Mar 11 Python
Python金融数据可视化汇总
Nov 17 Python
Django在win10下的安装并创建工程
Nov 20 Python
Python装饰器用法示例小结
Feb 11 Python
基于Python3.6+splinter实现自动抢火车票
Sep 25 Python
int在python中的含义以及用法
Jun 27 Python
对django的User模型和四种扩展/重写方法小结
Aug 17 Python
python默认参数调用方法解析
Feb 09 Python
Python实现名片管理系统
Feb 14 Python
python计算auc的方法
Sep 09 Python
Python中的min及返回最小值索引的操作
May 10 Python
梳理总结Python开发中需要摒弃的18个坏习惯
Jan 22 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
批量获取memcache值并按key的顺序返回的实现代码
2011/06/14 PHP
php限制上传文件类型并保存上传文件的方法
2015/03/13 PHP
php获取图片信息的方法详解
2015/12/10 PHP
PHP AjaxForm提交图片上传并显示图片源码
2016/11/29 PHP
PHP利用缓存处理用户注册时的邮箱验证,成功后用户数据存入数据库操作示例
2019/12/31 PHP
PNGHandler-借助JS让PNG图在IE下实现透明(包括背景图)
2007/08/31 Javascript
javascript onmouseout 解决办法
2010/07/17 Javascript
js中的referrer返回上一页使用介绍
2013/09/26 Javascript
javascript实现表格排序 编辑 拖拽 缩放
2015/01/02 Javascript
jquery插件jSignature实现手动签名
2015/05/04 Javascript
纯js代码实现未知宽高的元素在指定元素中垂直水平居中显示
2015/09/12 Javascript
深入理解React中es6创建组件this的方法
2016/08/29 Javascript
利用原生js和jQuery实现单选框的勾选和取消操作的方法
2016/09/04 Javascript
前端分页功能的实现以及原理(jQuery)
2017/01/22 Javascript
Javascript封装id、class与元素选择器方法示例
2017/03/13 Javascript
深入理解Vuex 模块化(module)
2017/09/26 Javascript
基于vue中css预加载使用sass的配置方式详解
2018/03/13 Javascript
使用vue-route 的 beforeEach 实现导航守卫(路由跳转前验证登录)功能
2018/03/22 Javascript
浅析Vue 生命周期
2018/06/21 Javascript
解决vue点击控制单个样式的问题
2018/09/05 Javascript
[03:56]还原FTP电影首映式 DOTA2群星拼出遗迹世界
2014/03/26 DOTA
[01:01:18]DOTA2上海特级锦标赛主赛事日 - 2 败者组第二轮#2COL VS LGD
2016/03/03 DOTA
python3实现短网址和数字相互转换的方法
2015/04/28 Python
Python面向对象之Web静态服务器
2019/09/03 Python
python 6.7 编写printTable()函数表格打印(完整代码)
2020/03/25 Python
python用Tkinter做自己的中文代码编辑器
2020/09/07 Python
ffmpeg+Python实现B站MP4格式音频与视频的合并示例代码
2020/10/21 Python
纯CSS绘制漂亮的圆形图案效果
2014/05/07 HTML / CSS
css3 border旋转时的动画应用
2016/01/22 HTML / CSS
J2ee常用的设计模式?说明工厂模式
2015/05/21 面试题
银行会计业务的个人自我评价
2013/11/02 职场文书
火车来了教学反思
2014/02/11 职场文书
个人四风问题对照检查材料
2014/09/26 职场文书
个园导游词
2015/02/04 职场文书
win10壁纸在哪个文件夹 win10桌面背景图片文件位置分享
2022/08/05 数码科技
Zabbix6通过ODBC方式监控Oracle 19C的详细过程
2022/09/23 Servers