python闭包与装饰器

说闭包和装饰器之前,先简单说下几个相关的概念

作用域

  在 Python 函数中会创建一个新的作用域,也就是有自己的命名空间。当在函数体中遇到变量时,Python 会首先在该函数的命名空间中寻找变量名,如果未找到,则会在外层区域继续寻找。外部区域变量可以被访问(如果是可变类型,甚至可以被修改)但是(默认)不能被赋值。局部变量可以和外部区域变量名字相同,这样就会“屏蔽”了该外部区域变量,无法在该局部作用域访问到。

# -*- coding: utf-8 -*-
a_str = 'aa'
b_str = 'bb'
c_str = 'cc'
L = [1,2]
def f():
    print 'a_str is %s' % a_str #a_str is aa
    # a_str = 'AA' 这里这么写的话会报错,a_str已经被当做外层变量了,这里就不能被赋值
    b_str = 'BB' #b_str 是局部变量,和外层的b_str没有关系
    print 'b_str is %s'% b_str # b_str is BB
    global c_str  #在局部作用域使用全局变量
    print 'c_str is %s' % c_str # c_str is cc
    c_str = 'CC'
    L[0] = 3 #外层变量默认不能赋值,但是可以修改
    print L # [3, 2]
f()
print 'a_str is %s' % a_str #a_str is aa
print 'b_str is %s'% b_str # b_str is bb
print 'c_str is %s' % c_str # c_str is CC
print L # [3, 2]

 

变量生命周期

值得注意的是,变量不仅是在命名空间中有效,它们也有生命周期。注意下面的代码:

这个问题不仅仅是因为 #1 处的作用域规则(虽然那是导致 NameError 的原因),也与 Python中函数调用的实现有关。函数 foo 的命名空间在每次函数被调用时重新创建,在函数结束时销毁。

 

函数的实参和形参

Python 允许向函数传递参数。形参名在函数里为局部变量。

 

内嵌函数

Python 允许创建内嵌函数。即可以在函数内部声明函数,并且所有的作用域和生命周期规则仍然适用。

  变量 x 是函数 outer 的局部变量,但函数 inner 仍然有外层作用域的访问权限(虽然默认不能赋值,但是至少有读和修改的权限)。

 函数是 Python 中的一级对象

在 Python 中有个常识:函数和其他任何东西一样,都是对象。函数包含变量,它并不那么特殊。函数可以当做实参传递给函数,或者在函数中将函数作为返回值返回。

>>> def outer():
...     def inner():
...         print "Inside inner"
...     return inner # 1
...
>>> foo = outer() #2
>>> foo # doctest:+ELLIPSIS
<function inner at 0x...>
>>> foo()
Inside inner

 

在 #1 处返回一个其实是函数标签的变量 inner。也没有什么特殊语法——函数 outer 返回了并没有被调用的函数 inner。参考变量的生命周期,每次调用函数 outer 的时候,函数 inner 会被重新定义,但是如果函数 ouer 没有返回 inner,当 inner 超出 outer 的作用域,inner 的生命周期将结束。

在 #2 处将获得返回值即函数 inner,并赋值给新变量 foo。可以看到如果鉴定 foo,它确实包含函数 inner,通过使用调用操作符(双括号)来调用它。

闭包

先看下闭包的概念,在计算机科学中,闭包英语:Closure),又称词法闭包Lexical Closure)或函数闭包function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。以上来自维基百科的解释

  

>>> def outer():
...     x = 1
...     def inner():
...         print x # 1
...     return inner
>>> foo = outer()
>>> foo.func_closure # doctest: +ELLIPSIS
(<cell at 0x...: int object at 0x...>,)

 

  inner 是 outer 返回的一个函数,存储在变量 foo 里然后用 foo() 来调用。但是它能运行吗?先来思考一下作用域规则。

  Python 中一切都按作用域规则运行, x 是函数 outer 中的一个局部变量,当函数 inner在 #1 处打印 x 时,Python 在 inner 中搜索局部变量但是没有找到,然后在外层作用域即函数 outer 中搜索找到了变量 x。但如果从变量的生命周期角度来看,变量 x 对函数 outer 来说是局部变量,即只有当 outer 运行时它才存在。只有当 outer 返回后才能调用 inner,所以依据 Python 运行机制,在调用 inner 时 x 就应该不存在了,那么这里应该有某种运行错误出现。

  结果并不是如此,返回的 inner 函数正常运行。Python 支持一种名为函数闭包的特性,意味着 在非全局作用域定义的 inner 函数在定义时记得外层命名空间是怎样的。inner 函数包含了外层作用域变量,通过查看它的 func_closure 属性可以看出这种函数闭包特性。

记住——每次调用函数 outer 时,函数 inner 都会被重新定义。此时 x 的值没有变化,所以返回的每个 inner 函数和其它的 inner 函数运行结果相同。

从这个示例可以看到闭包——函数记住其外层作用域的事实——可以用来构建本质上有一个硬编码参数的自定义函数。虽然没有直接给 inner 函数传参 1 或 2,但构建了能“记住”该打印什么数的 inner 函数自定义版本。

闭包是强大的技术——在某些方面来看可能感觉它有点像面向对象技术:outer 作为 inner的构造函数,有一个类似私有变量的 x。闭包的作用不胜枚举——例如 sorted 函数的参数 key,也可以写一个 itemgetter 函数,接收一个用于检索的索引并返回一个函数,然后就能恰当的传递给 key 参数了。

装饰器

装饰器其实就是一个以函数作为参数并返回一个替换函数的可执行函数。

请仔细看这个装饰器示例。首先,定义了一个带单个参数 some_func 的名为 outer 的函数。然后在 outer 内部定义了一个内嵌函数 innerinner 函数将打印一行字符串然后调用 some_func,并在 #1 处获取其返回值。在每次 outer 被调用时,some_func 的值可能都会不同,但不论 some_func 是什么函数,都将调用它。最后,inner 返回 some_func() 的返回值加 1。在 #2 处可以看到,当调用赋值给 decorated 的返回函数时,得到的是一行文本输出和返回值 2,而非期望的调用 foo 的返回值 1。

我们可以说变量 decorated 是 foo 的装饰版——即 foo 加上一些东西。事实上,如果写了一个实用的装饰器,可能会想用装饰版来代替 foo,这样就总能得到“附带其他东西”的 foo 版本。用不着学习任何新的语法,通过将包含函数的变量重新赋值就能轻松做到这一点:

def outer(some_func):
     def inner():
         print "before some_func"
         ret = some_func() 
         return ret + 1
     return inner
def foo():
     return 1
foo = outer(foo)

现在任意调用 foo() 都不会得到原来的 foo,而是新的装饰器版!

函数装饰器 @ 符号的应用

Python 2.4 通过在函数定义前添加一个装饰器名和 @ 符号,来实现对函数的包装。在上面代码示例中,用了一个包装的函数来替换包含函数的变量来实现了装饰函数。

@outer
def foo():
     return 1

值得注意的是,这种方式和简单的使用 wrapper 函数的返回值来替换原始变量的做法没有什么不同—— Python 只是添加了一些语法糖来使之看起来更加明确。

使用装饰器很简单!

通用的装饰器

上面的装饰器存在两个问题,一是,只接受固定参数(上面的装饰器不接受参数)。我想修饰一个有一个参数的函数就会出错,除非重新装饰器

@outer
def foo(x):
     return x
print foo(1,2) #出错

我们可以给装饰器加上通用参数来解决

def outer(some_func):
     def inner(*args, **kwargs):
         print "before some_func"
         ret = some_func(*args, ****kwargs) 
         return ret + 1
     return inner
*args接受任意多个普通参数, **kwargs接受任意关键字参数。

上面的装饰器还有第二个问题,装饰过的函数,某些属性变了,如
@outer
def foo():
     return 1
print foo.__name__
'''
inner
'''

foo的__name__改变了。python为我们提供了functools.wraps,这个也是装饰器,可以把原函数的信息拷贝到新函数中。经过改写后的装饰器如下

def outer(some_func):
    @functools.wraps(some_func)
     def inner(*args, **kwargs):
         print "before some_func"
         ret = some_func(*args, ****kwargs) 
         return ret + 1
     return inner

 

 

参考

http://simeonfranklin.com/blog/2012/jul/1/python-decorators-in-12-steps/

http://www.jianshu.com/p/d68c6da1587a

 

posted @ 2017-03-20 00:26  lwli  阅读(375)  评论(0编辑  收藏  举报