浅析python函数闭包和装饰器

一,闭包

       闭包这个特性我想大家都有所耳闻,但是何为闭包呢?请听我一一道来。

       要理解闭包,首先需要理解的是"变量作用域"。先来看一段代码

>>> def f1(a):
...     print(a)
...     print(b)
... 
>>> f1(5)
5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in f1
NameError: global name 'b' is not defined

      这个f1函数很简单,就是接受一个参数a,打印a和b。运行函数,报NameError错误,这并不奇怪,因为变量"b"没有定义。那么当我们定义b之后,f1就能正常执行了。

>>> b = 2
>>> f1(5)
5
2

     ok,一切都很正常!但是,任何事情都有可能会发生意外,比如下面这段代码。

>>> b = 2
>>> def f2(a):
...     print(a)
...     print(b)
...     b = 9
... 
>>> f2(5)
5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in f2
UnboundLocalError: local variable 'b' referenced before assignment

       what??? why??? 一脸懵逼有没有?我明明定义了变量"b", 但是为什么还是报错了??  

       事实上, python编译函数的定义体时, 它把b判定为函数f2的局部变量了,因为在f2中有一句 "b=2" 这样的赋值语句,所以在函数的第三行 print(b),显然会报错,因为这个时候你还没有定         义局部变量"b" 。那么这个问题如何解决呢? 其实只要加一条语句就可以,下面请看。

>>> b = 2
>>> def f3(a):
...     global b
...     print(a)
...     print(b)
...     b = 9
... 
>>> f3(5)
5
2
>>> b
9
>>> f3(6)
6
9
>>> b
9

         添加的这一条语句是 "global b", 这条语句的意思是把b当成全局变量。而我们刚好也定义了全局变量b,所以运行f3函数是不会报错的,运行完f3函数,可以发现全局变量b变成了9,这是           因为在f3函数中,全局变量b被修改了(b=9).  OK, 接下来可以讨论闭包了。

 

    定义:闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。

         乍一看闭包的定义,我相信很多人是不能 理解的,包括我自己:这定义是什么意思?很抽象有木有?为了能理解闭包(closure) 先来看一段代码。

def make_averager():
    series = []
    
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)
    
    return averager

         该函数其实不复杂,是用来计算移动平均值(比如股票均线的计算)的。 在make_averager函数中定义了一个局部变量series, 这很好理解。 接下来我们来调用这个函数

>>> def make_averager():
...     series = []
...     def averager(new_value):
...         series.append(new_value)
...         total = sum(series)
...         return total/len(series)
...     return averager
... 
>>> avg = make_averager()
>>> avg(5)
5
>>> avg(6)
5
>>> avg(12)
7

        当调用avg(5)时,make_averager函数已经返回了,那么他的本地作用域也就一去不复返了。而在averager函数中,series是自由变量(free variable)。 这个术语的意思是,未在本地作用域         绑定的变量,参见下图。

 

        averager的闭包延伸到哪个函数的作用域之外,包含自由变量series的保定。

        这下大家应该都理解了吧? OK, 关于闭包就讨论到这里,接下来讲装饰器。

 

二、装饰器

       先来看一段代码

def wraper(args):
    print("in function wraper")
    return args


def f1():
    print("in function f1")

f1 = wraper(f1)
f1()
运行结果:
in
function wraper in function f1

       这段代码是把函数f1当做参数传给wraper函数并赋值给f1,因为wraper函数返回的是参数args,所以当进行赋值"f1 = wraper(f1)" 其实是执行了wraper函数,并且返回原f1函数对象(因为这          个赋值语句的参数args是f1),最后赋值给f1,此时f1其实是wraper函数返回的对象。接下来执行语句"f1()" , 也就是说这时候调用f1(), 其实是调用了wraper返回对象的__call__(函数都是可         调用对象,都有这个方法)方法。

       这其实已经实现了一个简单的装饰器。这段代码其实等价于

def wraper(args):
    print("in function wraper")
    return args


@wraper
def f1():
    print("in function f1")

f1()

        在python中,@是一个黑魔法,它的作用是把被装饰函数(下方的那个函数,在这里是f1)当做参数传递给装饰器函数(在这里是wraper)并执行。执行wraper并且把返回值复制给新的f1,注意这里的wraper函数返回值并不是f1(),而是f1。接下来执行f1()语句,其实这时候的f1已经不是原来的那个f1函数了,它已经被wraper"装饰"过了。那么这时候调用"f1()"其实相当于执行了新的f1(被wraper装饰过的f1)。 在这个例子中,可能新的f1和原来老的f1并没有什么差别,那么接下来再来看一个例子。

import time


def clock(func):
    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
        return result
    return clocked


@clock
def f1(args):
    print("f1 is executed with args:%s" % args)
    time.sleep(0.5)
    return "hello %s" % args

f1("world")
运行结果

f1 is executed with args:world
[0.49959273s] f1('world') -> 'hello world'

       在这个例子中,f1被装饰器clock装饰。也就是说,当应用"@clock" 这个"黑魔法" 语句在函数f1上,其实相当于:把f1当做参数传递给装饰器函数(clock)并执行,执行后的返回值重新赋值给f1。  如果不用@clock这样的语句,相当于以下代码

def clock(func):
    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
        return result
    return clocked


# @clock
# def f1(args):
#     print("f1 is executed with args:%s" % args)
#     time.sleep(0.5)
#     return "hello %s" % args
# 
# f1("world")

def f1(args):
    print("f1 is executed with args:%s" % args)
    time.sleep(0.5)
    return "hello %s" % args

f1 = clock(f1)
ret = f1("world")
print(ret)

  执行clock函数(参数是f1), 返回值是clocked函数,注意是clocked函数, 而不是clocked() 。也就是说此时f1 = clocked 。而clocked函数定义如下。

def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
        return result

      那么接下来执行f1("world") 其实就相当于执行了clocked("world"), 最终得到返回值,并打印。运行结果如下

f1 is executed with args:world
[0.50013607s] f1('world') -> 'hello world'
hello world

      OK, 我相信这回大家明白装饰器是怎么回事了吧?

      在这里我们的f1函数(被装饰函数)只有一个参数,有同学可能要问,如果f1是多个参数的函数怎么办? 或者,f1包含关键字参数怎么办? 没关系,只要我们把刚才的装饰器稍微改动下,就能实现了。请看例子

import time


def clock(func):
    def clocked(*args, **kwargs):
        t0 = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
        return result
    return clocked


@clock
def f1(args):
    print("f1 is executed with args:%s" % args)
    time.sleep(0.5)
    return "hello %s" % args


@clock
def f2(name, age=20):
    print("Hello all, My name is %s, and I'm %s" % (name, age))
    time.sleep(1)
    return "hello %s" % name

ret1 = f1("world")
ret2 = f2("Ethan", age=25)
print(ret1)
print(ret2)
运行结果
f1 is executed with args:world
[0.49919400s] f1('world') -> 'hello world'
Hello all, My name is Ethan, and I'm 25
[0.99994619s] f2('Ethan') -> 'hello Ethan'
hello world
hello Ethan

       其实就是将装饰器的内部函数定义为 clocked(*args, **kwargs) 这种格式就行。如果不明白* 和** 的用法的同学,可以去看看python函数参数相关内容,这里就不做介绍了。OK,关于装饰器我就讲到这里,有兴趣的同学可以去了解下python标准包中自带的装饰器。如下

from functools import wraps
from functools import lru_cache
from functools import singledispatch

 

 

 

 

 

 

 

 

posted on 2018-03-17 15:38  hz_pythoner  阅读(125)  评论(0)    收藏  举报

导航