Python高阶函数之装饰器和偏函数

Posted 礁之

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python高阶函数之装饰器和偏函数相关的知识,希望对你有一定的参考价值。

文章目录


此文章参考:装饰器 - 廖雪峰的官方网站 (liaoxuefeng.com)

一、装饰器

  • 由于函数也是一个对象,并且函数可以赋值给变量,赋值后的变量可以直接调用函数:
>>> def test():
...     print("aaaaaa1") 
... 
>>> f = test
>>> f
<function test at 0x0000022862EBE7A0>
>>> f()

使用__name__可以获取函数的函数名:
>>> test.__name__
'test'
>>> f.__name__
'test'
  • 如果要增加test( )函数的功能,例如在函数调用前后增加输出内容,想要达到这种效果,我们可以直接修改函数,但是如果说不允许修改源函数,又需要增加函数功能该怎么做呢,这种情况我们可以使用装饰器(Decorator)
  • 装饰器其实就是在不修改源函数的情况下,在代码运行期间动态增加函数功能的方式,而装饰器本质上就是一个返回函数的高阶函数
  • 下面就来看一下装饰器的使用案例:
# -*- coding: utf-8 -*-
def log(func):    #装饰器,接收一个函数,返回一个函数
    def wrapper(*args,**kw):
        func()  #这里调用了函数,因为没有定义参数,所以最后输出args= (),kw= 
        print("name is %s()" % func.__name__)
        return func(*args,**kw)    #这里调用了传入的函数,也就是now函数,并且定义了参数,最后输出args= (1, 2, 3),kw= 'a': 'aaa'
    print("aaa")
    return wrapper

@log    #@符号把装饰器log置于now函数定义处,相当于执行了"now = log(now)"
def now(*args,**kw):
    print("args= %s,kw= %s" % (args,kw))


if __name__=='__main__':   
    now(1,2,3,a="aaa")   #调用函数并且传入参数
    print(now.__name__)
    
#输出:
aaa
args= (),kw= 
name is now()
args= (1, 2, 3),kw= 'a': 'aaa'
wrapper

#解析
由于log()是一个装饰器,会返回一个函数,但是原来的now函数依然存在,上面只是把同名的now变量指向了新的函数,然后调用now执行新的函数,也就是在log函数中返回wrapper函数
wrapper函数的参数定义是(*args,**kw),这样定义参数可以使wrapper函数接受任意参数的调用
  • 如果装饰器本身需要传入参数,那就需要编写一个返回装饰器的高阶函数:
# -*- coding: utf-8 -*-
def log(test):
    def deco(func):
        def wrapper(*args,**kw):
            print("%s %s()" % (test,func.__name__))
            return func(*args,**kw)
        return wrapper
    return deco

@log('name is ')
def now():
    print("aaaaa")

if __name__=='__main__':
    now()
    print(now.__name__)
    
#输出:
name is  now()
aaaaa
wrapper
#解析:
其实看过上面的案例后,这里传入装饰器参数就很好理解了,“name is”赋值给了test参数,最终返回的还是wrapper函数,和上面的两层嵌套相比,这里的三层嵌套其实就是now = log('name is ')(now),log('name is ')返回的是装饰器函数deco,然后装饰器函数deco再调用now即deco(now),最终返回wrapper函数,先输出“name is  now()”,然后调用now函数输出”aaaaa“
  • 看过上面的案例之后,可以看到最后一步输出print(now.__name__)时,输出了wrappernow的函数名称从now变成了wrapper,这是因为装饰器返回的wrapper函数的函数名就是wrapper,所以,需要把原始函数now__name__等属性复制到wrapper函数中,不然有些依赖函数签名的代码执行时就会报错
  • 我们可以使用wrapper.__name__ = func.__name__这种代码来解决上面的问题,但是不需要,因为Python内置的functools.wraps就是做这个事情的,例如:
1、正常装饰器:
# -*- coding: utf-8 -*-
import functools
def log(func):
    @functools.wraps(func)
    def wrapper(*args,**kw):
        print("name is %s()" % func.__name__)
        return func(*args,**kw)
    return wrapper

@log
def now(*args,**kw):
    print("%s %s" % (args,kw))

if __name__=='__main__':
    now(1,2,3,a='aaa')
    print(now.__name__)
    
#输出:
name is now()
(1, 2, 3) 'a': 'aaa'
now


2、带参数的装饰器:
# -*- coding: utf-8 -*-
import functools
def log(test):
    def deco(func):
        @functools.wraps(func)
        def wrapper(*args,**kw):
            print("%s is %s()" % (test,func.__name__))
            return func(*args,**kw)
        return wrapper
    return deco

@log('name')
def now(*args,**kw):
    print("%s %s" % (args,kw))

if __name__ == '__main__':
    now(1,2,3,a="ccc")
    print(now.__name__)

#输出:
name is now()
(1, 2, 3) 'a': 'ccc'
now


可以看到,不管是带参数还是不带参数的装饰器,在加了@functools.wraps(func)后,最后输出now.__name__时,函数名称还是now
  • 这里引用一个小练习:
# -*- coding: utf-8 -*-
import time, functools
def metric(fn):
    @functools.wraps(fn)
    def wrapper(*args,**kw):
        start_time = time.time()
        res = fn(*args,**kw)
        end_time = time.time()
        print('%s executed in %s ms' % (fn.__name__, end_time-start_time))
        return res
    return wrapper


# 测试
@metric
def fast(x, y):
    time.sleep(0.0012)
    return x + y;

@metric
def slow(x, y, z):
    time.sleep(0.1234)
    return x * y * z;

f = fast(11, 22)
s = slow(11, 22, 33)
if f == 33 and s == 7986:
    print('测试成功')
    
#输出:
fast executed in 0.013970375061035156 ms
slow executed in 0.12478971481323242 ms
测试成功

二、偏函数——functools.partial

  • 在上面的装饰器中,使用了模块functools,除了functools.wraps,还有很多功能,其中有一个就是偏函数(Partial function),注意,这里的偏函数和数学意义上的偏函数不一样
  • 在定义函数时,通过设置参数的默认值,也就是设置默认参数,可以降低调用函数的难度,而偏函数也可以做到这一点,例如:
1int()函数可以把字符串转换为整数,当只传入字符串时,int函数默认按照十进制转换,也可以加base参数,执行进制的转换,要注意,int转换需要是字符串
>>> int('12345') 
12345
>>> int('12345',base=8) 
5349
>>> int('12345',base=16) 
74565
>>> int('111000',2) 
56


2、如果需要转换大量的字符串为指定进制,那么我们可以定义一个函数
>>> def test(x,base=2):    
...     return int(x,base) 
... 
>>> test("11100") 
28
>>> test("111")   
7


3、而functools.partial就是帮助我们创建一个偏函数的,像上面那种情况,不需要我们自己定义函数,可以直接使用functools.partial创建一个新函数
>>> import functools
>>> test = functools.partial(int,base=2) 
>>> test("1111") 
15
>>> test("1111000") 
120
>>> test("1111000",base=10)   #可以看到,在调用test函数添加参数base后也是可以正常调用的,说明functools.partial只是定义了默认参数
1111000


4、在创建偏函数时,可以接收不同的参数,例如可变参数*args和关键字参数**kw
>>> test = functools.partial(int,base=2) 
>>> kw = 'base':10 
>>> test("11111",**kw) 
11111
>>> test_1 = functools.partial(max,10) 
>>> args = (12,55,4)  
>>> test_1(*args)    
55
  • 当函数的参数个数太多,需要简化时,使用functools.partial可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。
  • 在内置函数或自定义函数在执行时,要带上所有必要的参数进行调用。但是,有时参数可以在函数被调用之前提前获知。如果是自定义函数,我们可以定义默认参数,而内置函数在这种情况下,我们就可以使用偏函数,定义一个新的函数,并且指定默认参数
  • 这里偏函数的作用其实就是在原函数的基础上创建一个新的函数,并且指定默认参数

太多,需要简化时,使用functools.partial可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。

  • 在内置函数或自定义函数在执行时,要带上所有必要的参数进行调用。但是,有时参数可以在函数被调用之前提前获知。如果是自定义函数,我们可以定义默认参数,而内置函数在这种情况下,我们就可以使用偏函数,定义一个新的函数,并且指定默认参数
  • 这里偏函数的作用其实就是在原函数的基础上创建一个新的函数,并且指定默认参数

以上是关于Python高阶函数之装饰器和偏函数的主要内容,如果未能解决你的问题,请参考以下文章

python基础-装饰器和偏函数

装饰器和高阶函数

python函数下篇装饰器和闭包,外加作用域

python高级之装饰器

Python高阶函数之 - 装饰器

day4-装饰器和模块导入