Python函数式编程之返回函数、匿名函数、装饰器、偏函数

时间:2022-01-05 03:15:10

参考原文

  廖雪峰Python

返回函数

  我们已经知道了高阶函数可以接受函数作为参数外,还可以把函数作为结果值返回。我们来看一个实现可变参数的求和:

def calc_sum(*args):
    ax = 0
    for n in args:
        ax += n
    return ax

  但是,但我们不需要立即知道求和的结果,而是在后面的代码中根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数

def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax += n
        return ax
    return sum

  当我们调用lazy_sum()时,返回的不是求和结果而是求和函数,当我们调用函数f时,才真正计算求和的结果

f = lazy_sum(1,2,3,4)
print(f) # result <function lazy_sum.<locals>.sum at 0x0000025503463AE8>
print(f()) #result 10

 闭包

  在这个例子中,我们在函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数sum()中,这就是“闭包(Closure)”。

  注意:当我们调用lazy_sum()时,每次调用都会返回一个新的函数即使传入相同的参数:

f1 = lazy_sum(1,2,3,4)
f2 = lazy_sum(1,2,3,4)
print(f1==f2) #result False
Tips:当一个函数返回一个函数后其内部的局部变量还被新函数所引用,所以闭包用着简单,但实现可不容易。返回的函数也不是立即执行的,直到调用返回的函数,才会执行内部的代码。

  但此时我们不一定理解清楚了"闭包",不信来看个例子:

def count():
    fs = []
    for i in range(1, 4):
        def f():
            return i * i
        fs.append(f)
    return fs

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

  在上面的例子中,每次循环都创建了一个新的函数,然后把创建的3个函数都返回了。你可能认为调用f1(),f2()f3()的结果应该是1, 4, 9 但实际结果却是:9, 9 ,9。为什么呢?原因在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,所以结果是9。

Tips:返回闭包时要牢记一点:返回函数不要引用任何的循环变量,或者后续会发生变化的变量。

  但是如果我们一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,这样,无论该循环变量后续任何更改,已经绑定到函数参数的值不变

def count():
    def f(j):
        def g():
            return j * j
        return g
    fs = []  #容器用来存储函数
    for i in range(1, 4):
        fs.append(f(i))
    return fs

f1, f2, f3 = count()  #等价f1=count()[0], f2=count()[1], f3=count()[2]
print(f1(), f2(), f3()) #result 1 4 9
Tips:一个函数可以返回一个计算结果,也可以返回一个函数;返回一个函数时,牢记该函数并未执行,返回函数中不要引用任何可能会发生的变量。

匿名函数

  为什么要使用匿名函数?匿名函数没有名字不需要显式定义函数不必担心函数名冲突,使用起来更加的方便。还是以map()函数为例,计算f(x)=x2时,除了定义一个f(x)函数外,还可以直接传入匿名函数:

>>> list(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: x * x实际上就是:

def f(x):
    return x * x

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

Tips:匿名函数只有一个表达式不用写return,返回值就是该表达式的结果。除此之外,和其它函数一样也可以赋值给变量,也可以作为返回值返回。Python对匿名函数的支持有限,只有一些简单的情况下可以使用匿名函数。

装饰器

  装饰器本质上是一个返回函数高阶函数,它可以让其他函数在不需要做任何代码变动下增加额外的功能:包括插入日志、性能测试、事务处理、缓存、权限校验等,这种在代码运行期间动态增加功能方式,称之为装饰器Decorator)。来看一个打印日志的decorator的定义:

def log(func): #接收一个函数func作为参数--高阶函数的特性
    def wrapper(*args, **kw):  #可以接受任意参数的调用
        print('call %s():' % func.__name__) #func.__name__ 可以拿到函数额名字
        return func(*args, **kw)
    return wrapper

  已经定义好了一个decorator,怎么用呢?我们要借助Python的@语法,把decorator置于函数的定义处:

@log
def now():
    print('2018-04-19')

  调用now()函数,不仅会运行now()函数本身,还会在运行now()函数前打印一行日志

now()
'''
call now():
2018-04-19
'''

  @log放到now()函数的定义处,相当于执行了语句:

now = log(now)

  解释:由于log()是一个decorator,返回一个函数,所以原来的now()函数仍然存在,只是现在同名的now指向了新的函数,于是调用now()将执行新函数,即在log()函数中返回的wrapper()函数。

   再来看一种情况:若decorator的本身就需要传入参数的话,又该如何实现呢?此时,就应该编写一个返回decorator高阶函数。如,要自定义log的文本:

def log(text):
    def decorator(func):
        def wrapper(*args, **kw):
            print('%s %s():' % (text,func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

  这个3层嵌套的decorator的用法如下:

@log('DefineText')
def now():
    print('2018-04-19')

  执行结果如下:

now()
'''
DefineText now():
2018-04-19
'''

  和两层嵌套的decorator相比,3层嵌套的decorator效果是这样:

now = log('DefineText')(now)

  剖析:首先执行log(‘DefineText’),返回的是decorator函数,再调用返回的decorator函数,参数是now函数,最后返回值是wrapper函数。

  注意:函数也是对象,它有__name__等属性,但你看经过上面decorator装饰后的函数,它们的__name__已经从原来的'now'变成了'wrapper'。

print(now.__name__) # wrapper

  哦,原来是因为最后返回的函数wrapper()函数名字‘wrapper’替换了原来的‘now’,这样有可能依赖函数签名的代码执行可能会出错,所以应该把__name__属性改回去,幸好Python内置的functools.wraps可以帮我们实现,所以一个完整的decorator的写法如下:

#不带参数
import functools

def log(func):
    @functools.wraps(func) def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

#带参数
import functools

def log(text):
    def decorator(func):
        @functools.wraps(func) def wrapper(*args, **kw):
            print('%s %s():' % (text,func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

小结:在面向对象(OOP)的设计模式中,decorator被称为装饰模式。OOP的装饰模式需要通过继承组合来实现。而Python除了能支持OOP的decorator外,直接从语法层次支持decorator,可以用函数实现,也可以用实现。

偏函数(Partial function)

  在前面说过,通过设定函数的默认值,可以降低函数调用的难度,偏函数可以为已知的函数设定参数的默认值。来看看怎么操作:

>>> import functools  
>>> int2 = functools.partial(int, base=2)  #functools.partial帮助我们创建偏函数
>>> int2('1000000')
64

  注意到上面的int2函数,仅仅是把base参数设定默认值为2,但也可在函数调用时传入其它的值:

>>> int2('1000000', base=10)
1000000

  最后注意,创建偏函数时,实际可以接受函数对象*args、和**kw、3个参数。如上面的创建的int2函数,实际上固定了int()函数的关键字参数base,也就是:

int2('10010') 

  相当于这样:

kw = { 'base': 2 }
int('10010', **kw)
Tips:当函数的参数个数太多时,需要简化时,使用functools.partial创建一个新的函数,这个新函数固定了原函数的部分参数,使调用时更加的简洁。