实例详解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 读取竖线分隔符的文本方法
Dec 20 Python
Python多线程同步---文件读写控制方法
Feb 12 Python
python中yield的用法详解——最简单,最清晰的解释
Apr 04 Python
python解析yaml文件过程详解
Aug 30 Python
详解Python3迁移接口变化采坑记
Oct 11 Python
pytorch 改变tensor尺寸的实现
Jan 03 Python
基于python实现文件加密功能
Jan 06 Python
keras打印loss对权重的导数方式
Jun 10 Python
python 读取、写入txt文件的示例
Sep 27 Python
Python之字典对象的几种创建方法
Sep 30 Python
python3爬虫中多线程的优势总结
Nov 24 Python
用gpu训练好的神经网络,用tensorflow-cpu跑出错的原因及解决方案
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
php中的时间处理
2006/10/09 PHP
PHP伪静态Rewrite设置之APACHE篇
2014/07/30 PHP
兼容FF和IE的动态table示例自写
2013/10/21 Javascript
javascript去除字符串中所有标点符号和提取纯文本的正则
2014/06/07 Javascript
js实现select组件的选择输入过滤代码
2014/10/14 Javascript
JQuery记住用户名密码实现下次自动登录功能
2015/04/27 Javascript
jQuery自动添加表单项的方法
2015/07/13 Javascript
每天一篇javascript学习小结(RegExp对象)
2015/11/17 Javascript
jQuery基于cookie实现的购物车实例分析
2015/12/24 Javascript
vue实现表格增删改查效果的实例代码
2017/07/18 Javascript
Javascript中将变量转换为字符串的三种方法
2017/09/19 Javascript
JS去掉字符串中所有的逗号
2017/10/18 Javascript
webpack配置打包后图片路径出错的解决
2018/04/26 Javascript
JavaScript常用数组操作方法,包含ES6方法
2020/05/10 Javascript
详解如何为你的angular app构建一个第三方库
2018/12/07 Javascript
微信小程序实现简单跑马灯效果
2020/05/26 Javascript
原生JavaScript实现购物车
2021/01/10 Javascript
python获得文件创建时间和修改时间的方法
2015/06/30 Python
python单例模式实例解析
2018/08/28 Python
python实现图片转字符小工具
2019/04/30 Python
分享8个非常流行的 Python 可视化工具包
2019/06/05 Python
在Qt5和PyQt5中设置支持高分辨率屏幕自适应的方法
2019/06/18 Python
在Django model中设置多个字段联合唯一约束的实例
2019/07/17 Python
python实现知乎高颜值图片爬取
2019/08/12 Python
Python pysnmp使用方法及代码实例
2020/08/24 Python
Python SQLAlchemy库的使用方法
2020/10/13 Python
python实现网页录音效果
2020/10/26 Python
在HTML5 Canvas中放入图片和保存为图片的方法
2014/05/03 HTML / CSS
幼儿园美术教学反思
2014/01/31 职场文书
行政执法作风整顿剖析材料
2014/10/11 职场文书
2014年驻村干部工作总结
2014/11/17 职场文书
给上级领导的感谢信
2015/01/22 职场文书
学期个人自我总结
2015/02/13 职场文书
2015年乡镇纪检工作总结
2015/04/22 职场文书
计算机实训心得体会
2016/01/14 职场文书
CSS中实现动画效果-附案例
2022/02/28 HTML / CSS