实例详解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中的闭包实例详解
Aug 29 Python
python里将list中元素依次向前移动一位
Sep 12 Python
python通过urllib2获取带有中文参数url内容的方法
Mar 13 Python
Python中的zipfile模块使用详解
Jun 25 Python
Python argv用法详解
Jan 08 Python
基于Python log 的正确打开方式
Apr 28 Python
python 应用之Pycharm 新建模板默认添加编码格式-作者-时间等信息【推荐】
Jun 17 Python
Pytorch中实现只导入部分模型参数的方式
Jan 02 Python
在tensorflow中设置使用某一块GPU、多GPU、CPU的操作
Feb 07 Python
浅谈Python程序的错误:变量未定义
Jun 02 Python
python线性插值解析
Jul 05 Python
python 安全地删除列表元素的方法
Mar 16 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
用来解析.htgroup文件的PHP类
2012/09/05 PHP
PHP中file_get_contents高?用法实例
2014/09/24 PHP
php判断类是否存在函数class_exists用法分析
2014/11/14 PHP
PHP中的静态变量及static静态变量使用详解
2015/11/05 PHP
如何判断php mysqli扩展类是否开启
2016/12/24 PHP
Firefox中autocomplete=&quot;off&quot; 设置不起作用Bug的解决方法
2011/03/25 Javascript
基于Jquery插件开发之图片放大镜效果(仿淘宝)
2011/11/19 Javascript
jQuery 遍历-nextUntil()方法以及prevUntil()方法的使用介绍
2013/04/26 Javascript
JQuery的自定义事件代码,触发,绑定简单实例
2013/08/01 Javascript
js从Cookies里面取值的简单实现
2014/06/30 Javascript
jQuery Form 表单提交插件之formSerialize,fieldSerialize,fieldValue,resetForm,clearForm,clearFields的应用
2016/01/23 Javascript
jQuery+css实现的切换图片功能代码
2016/01/27 Javascript
使用struts2+Ajax+jquery验证用户名是否已被注册
2016/03/22 Javascript
jQuery短信验证倒计时功能实现方法详解
2016/05/25 Javascript
关于JavaScript 原型链的一点个人理解
2016/07/31 Javascript
WEB 前端开发中防治重复提交的实现方法
2016/10/26 Javascript
angularjs实现柱状图动态加载的示例
2017/12/11 Javascript
Swiper自定义分页器使用详解
2017/12/28 Javascript
Vue-cli3.x + axios 跨域方案踩坑指北
2019/07/04 Javascript
element form 校验数组每一项实例代码
2019/10/10 Javascript
js实现复制粘贴的两种方法
2020/12/04 Javascript
[05:04]完美世界携手游戏风云打造 卡尔工作室地图界面篇
2013/04/23 DOTA
python采用requests库模拟登录和抓取数据的简单示例
2014/07/05 Python
Python捕捉和模拟鼠标事件的方法
2015/06/03 Python
Python 合并多个TXT文件并统计词频的实现
2019/08/23 Python
python发qq消息轰炸虐狗好友思路详解(完整代码)
2020/02/15 Python
Python如何向SQLServer存储二进制图片
2020/06/08 Python
JSF面试题:Jsf中的核心类用那些?有什么作用?LiftCycle六大生命周期是什么?
2014/07/17 面试题
Delphi工程师笔试题
2013/09/21 面试题
公司营业员的工作总结自我评价
2013/10/05 职场文书
工作违纪检讨书范文
2015/01/26 职场文书
财务工作失误检讨书
2015/02/19 职场文书
小学毕业教师寄语
2019/06/21 职场文书
Pytorch中Softmax与LogSigmoid的对比分析
2021/06/05 Python
Nginx使用Lua模块实现WAF的原理解析
2021/09/04 Servers
MySQL 条件查询的常用操作
2022/04/28 MySQL