Python进阶笔记(2)_ 函数式编程 之 闭包和装饰器decorator

时间:2022-04-30 19:17:04


在了解闭包和装饰器之前,我们先来了解一下函数是如何返回函数的。看下例,是一个计算阶乘的程序:

</pre><pre name="code" class="python"><pre name="code" class="python"><pre name="code" class="python">def calc_prod(n):
    List_n = range(1,n+1)
    def lazy_prod():
        def f(x, y):
            return x * y
        return reduce(f, List_n, 1)
    return lazy_prod

f = calc_prod(5)
print f()

 
 
reduce函数在上节 Python 进阶笔记(1)中讲过了。返回函数最重要的就是不要忘记 返回语句 return lazy_prod 。要显示函数计算结果时,要用 f(),而不是 f 。

闭包

在上例中,发现没法把 lazy_sum 移到 calc_sum 的外部,因为它引用了 calc_sum 的参数 lst。

像这种内层函数引用了外层函数的变量(参数也算变量),然后返回内层函数的情况,称为闭包(Closure)。

闭包的特点是返回的函数还引用了外层函数的局部变量,所以,要正确使用闭包,就要确保引用的局部变量在函数返回后不能变。

<pre name="code" class="python">def count():
    fs = []
    for i in range(1, 5):
        def f(j):
            def g():
                return j*j
            return g
        r = f(i)
        fs.append(r)
    return fs

f1, f2, f3, f4 = count()
print f1(), f2(), f3(), f4()

 

append() 方法向列表的尾部添加一个新的元素。只接受一个参数。

匿名函数

高阶函数可以接收函数做参数,有些时候,我们不需要显式地定义函数,直接传入匿名函数更方便。

于是便引入了匿名函数的概念。

<pre name="code" class="python">map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9])
# 输出为 [1, 4, 9, 16, 25, 36, 49, 64, 81]

 
关键字lambda 表示匿名函数,冒号前面的 x 表示函数参数。 

再如

<pre name="code" class="python">def is_not_empty(s):
    return s and len(s.strip()) > 0

print filter(is_not_empty, ['test', None, '', 'str', '  ', 'END'])

 
输出 

['test', 'str', 'END']
其实还可以更简单:

<pre name="code" class="python">print filter(lambda s:s and len(s.strip()) > 0, ['test', None, '', 'str', '  ', 'END'])

 

由于对这块内容的生疏,下文内容完全摘自 imooc 

编写无参数decorator 

Python的 decorator 本质上就是一个高阶函数,它接收一个函数作为参数,然后,返回一个新函数。

使用 decorator 用Python提供的 @ 语法,这样可以避免手动编写 f = decorate(f) 这样的代码。

考察一个@log的定义:

<pre name="code" class="python"><pre name="code" class="python">def log(f):
    def fn(x):
        print 'call ' + f.__name__ + '()...'
        return f(x)
   return fn



 
对于阶乘函数,@log工作得很好:
<pre name="code" class="python">@log
def factorial(n):
    return reduce(lambda x,y: x*y, range(1, n+1))
print factorial(10)
 
结果: 
<pre name="code" class="python">call factorial()...
3628800
 
但是,对于参数不是一个的函数,调用将报错: 
<pre name="code" class="python">@log
def add(x, y):
    return x + y
print add(1, 2)
 
结果:Traceback (most recent call last):  File "test.py", line 15, in <module>    print add(1,2)TypeError: fn() takes exactly 1 argument (2 given)因为 add() 函数需要传入两个参数,但是 @log 写死了只含一个参数的返回函数。要让 @log 自适应任何参数定义的函数,可以利用Python的 *args 和 **kw,保证任意个数的参数总是能正常调用: 
<pre name="code" class="python">def log(f):
    def fn(*args, **kw):
        print 'call ' + f.__name__ + '()...'
        return f(*args, **kw)
    return fn
 
现在,对于任意函数,@log 都能正常工作。 

编写带参数decorator 

考察上一节的 @log 装饰器:

<pre name="code" class="python">def log(f):
    def fn(x):
        print 'call ' + f.__name__ + '()...'
        return f(x)
    return fn

 
发现对于被装饰的函数,log打印的语句是不能变的(除了函数名)。如果有的函数非常重要,希望打印出'[INFO] call xxx()...',有的函数不太重要,希望打印出'[DEBUG] call xxx()...',这时,log函数本身就需要传入'INFO'或'DEBUG'这样的参数,类似这样: 
<pre name="code" class="python">@log('DEBUG')
def my_func():
    pass
 
把上面的定义翻译成高阶函数的调用,就是: 
<pre name="code" class="python">my_func = log('DEBUG')(my_func)
 
上面的语句看上去还是比较绕,再展开一下: 
<pre name="code" class="python">log_decorator = log('DEBUG')
my_func = log_decorator(my_func)
 
上面的语句又相当于: 
<pre name="code" class="python">log_decorator = log('DEBUG')
@log_decorator
def my_func():
    pass
 
所以,带参数的log函数首先返回一个decorator函数,再让这个decorator函数接收my_func并返回新函数:

<pre name="code" class="python">def log(prefix):
    def log_decorator(f):
        def wrapper(*args, **kw):
            print '[%s] %s()...' % (prefix, f.__name__)
            return f(*args, **kw)
        return wrapper
    return log_decorator

@log('DEBUG')
def test():
    pass
print test()
 
执行结果:[DEBUG] test()...None对于这种3层嵌套的decorator定义,你可以先把它拆开: 
<pre name="code" class="python"># 标准decorator:
def log_decorator(f):
    def wrapper(*args, **kw):
        print '[%s] %s()...' % (prefix, f.__name__)
        return f(*args, **kw)
    return wrapper
return log_decorator

# 返回decorator:
def log(prefix):
    return log_decorator(f)
 
拆开以后会发现,调用会失败,因为在3层嵌套的decorator定义中,最内层的wrapper引用了最外层的参数prefix,所以,把一个闭包拆成普通的函数调用会比较困难。不支持闭包的编程语言要实现同样的功能就需要更多的代码。 

完善decorator 

@decorator可以动态实现函数功能的增加,但是,经过@decorator“改造”后的函数,和原函数相比,除了功能多一点外,有没有其它不同的地方?

在没有decorator的情况下,打印函数名:

<pre name="code" class="python">def f1(x):
    pass
print f1.__name__

 
输出: f1有decorator的情况下,再打印函数名: 
<pre name="code" class="python">def log(f):
    def wrapper(*args, **kw):
        print 'call...'
        return f(*args, **kw)
    return wrapper
@log
def f2(x):
    pass
print f2.__name__
 
输出: wrapper可见,由于decorator返回的新函数函数名已经不是'f2',而是@log内部定义的'wrapper'。这对于那些依赖函数名的代码就会失效。decorator还改变了函数的__doc__等其它属性。如果要让调用者看不出一个函数经过了@decorator的“改造”,就需要把原函数的一些属性复制到新函数中: 
<pre name="code" class="python">def log(f):
    def wrapper(*args, **kw):
        print 'call...'
        return f(*args, **kw)
    wrapper.__name__ = f.__name__
    wrapper.__doc__ = f.__doc__
    return wrapper
 
这样写decorator很不方便,因为我们也很难把原函数的所有必要属性都一个一个复制到新函数上,所以Python内置的functools可以用来自动化完成这个“复制”的任务:

<pre name="code" class="python">import functools
def log(f):
    @functools.wraps(f)
    def wrapper(*args, **kw):
        print 'call...'
        return f(*args, **kw)
    return wrapper
 

最后需要指出,由于我们把原函数签名改成了(*args, **kw),因此,无法获得原函数的原始参数信息。即便我们采用固定参数来装饰只有一个参数的函数:
<pre name="code" class="python">def log(f):
    @functools.wraps(f)
    def wrapper(x):
        print 'call...'
        return f(x)
    return wrapper
 
也可能改变原函数的参数名,因为新函数的参数名始终是 'x',原函数定义的参数名不一定叫 'x'。 

偏函数 

当一个函数有很多参数时,调用者就需要提供多个参数。如果减少参数个数,就可以简化调用者的负担。

比如,int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换:

<pre name="code" class="python">>>> int('12345')
12345

 
但int()函数还提供额外的base参数,默认值为10。如果传入base参数,就可以做 N 进制的转换:

<pre name="code" class="python">>>> int('12345', base=8)
5349
>>> int('12345', 16)
74565
 

假设要转换大量的二进制字符串,每次都传入int(x, base=2)非常麻烦,于是,我们想到,可以定义一个int2()的函数,默认把base=2传进去:
<pre name="code" class="python">def int2(x, base=2):
    return int(x, base)
 
这样,我们转换二进制就非常方便了: 
<pre name="code" class="python">>>> int2('1000000')
64
>>> int2('1010101')
85
 
functools.partial就是帮助我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2: 
<pre name="code" class="python">>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
>>> int2('1010101')
85
 
所以,functools.partial可以把一个参数多的函数变成一个参数少的新函数,少的参数需要在创建时指定默认值,这样,新函数调用的难度就降低了。