python装饰器及functools模块
本文是笔者学习python装饰器以及functools模块的笔记。
在开始学习之前,先在此列举出python函数的一些特性:
- 一切皆对象:python函数也是对象,也就是说可以将函数赋值给变量
def hi(name='Michael'): return "Hi %s!" % name greet = hi # 将函数赋值给变量,函数名后不能加小括号 print(greet()) # output:Hi Michael!
- 在函数中定义函数:python可以让我们在一个函数中定义另一个函数
def hi(name="Michael"): print("Now, you are inside the hi() funciton!") # 在函数中定义两个函数 def greet(): return "Now, you are in the greet() function!" def welcome(): return "Now, you are in the welcome() function!" print(greet()) print(welcome()) print("Now, you are back in the hi() function!") hi() # output: Now, you are inside the hi() funciton! # Now, you are in the greet() function! # Now, you are in the welcome() function! # Now, you are back in the hi() function!
- 从函数中返回函数:python在一个函数中定义另一个函数后,可以将嵌套的函数作为返回值返回出来
def hi(name="Michael"): # 在函数中定义两个函数 def greet(): return "Now, you are in the greet() function!" def welcome(): return "Now, you are in the welcome() function!" # 返回函数,函数名后不能加小括号 if name == "Michael": return(greet) else: return(welcome) a = hi() print(a) print(a()) # output: <function hi.<locals>.greet at 0x7f393513bea0> # Now, you are in the greet() function!
- 将函数作为另一个函数的参数:python可以将一个函数作为参数传给另一个函数
def hi(): return "Hi Michael!" # 将函数hi()传递给函数do_sth_before_hi() def do_sth_before_hi(func): print("I am doing some boring work before executing hi()") print(func()) do_sth_before_hi(hi) # output: I am doing some boring work before executing hi() # Hi Michael!
python装饰器
装饰器(decorator)是一个返回函数的高阶函数,用来给增强普通函数的功能。
下面是一个使用普通的装饰器的蓝本:
import functools
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print('call %s():' % func.__name__)
print('args = {}'.format(*args))
return func(*args, **kwargs)
return wrapper
@decorator
def test(name):
print('%s param: %s' % (test.__name__, name))
test("Michael")
输出为:
call test():
args = Michael
test param: Michael
下面取关键行解释:
第3~10行
定义一个装饰器,输入的参数为一个函数,返回值是增强了功能后的函数,本质上其实就是一个返回值为函数的高阶函数。
第4行
functools.wraps()
是python提供的装饰器,它能把被装饰函数test()
的元信息(name, doc, module, dict等内部参数说明信息)拷贝到装饰器里面的封装函数wrapper()
中去。函数的元信息包括__docstring__
,__name__
,参数列表等等。如果没有第4行,在程序最后加一行print(test.__name__)
,输出是wrapper
;如果有第4行,在程序最后加一行print(test.__name__)
,输出是test
。
第12行
使用@
语法将原函数传入装饰器函数,相当于:
test = decorator(test)
带参数的装饰器
装饰器允许传入参数,一个携带了参数的装饰器将有三层函数,如下所示:
import functools
def decorator_with_param(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print('call %s():' % func.__name__)
print('args = {}'.format(*args))
print('decorator_param = {}'.format(text))
return func(*args, **kwargs)
return wrapper
return decorator
@decorator_with_param("Jane")
def test(name):
print('%s param: %s' % (test.__name__, name))
test("Michael")
输出为:
call test():
args = Michael
decorator_param = Jane
test param: Michael
下面取关键行进行解释:
第3~13行
定义一个带参数的装饰器
第15行
相当于执行:
test = decorator_with_param("Jane")(test)
首先执行decorator_with_param("Jane")
,返回的是decorator
函数,然后再调用返回的函数,即执行decorator(test)
,返回值是wrapper
函数。
装饰器类
上面我们都是使用函数来定义一个装饰器,我们也可以用类来构建装饰器,这样我们可以使用类的继承得到很多我们需要的特化的装饰器。下面我们实现一个和上面带参数的装饰器相同功能的装饰器类,代码如下所示:
import functools
class decorator_with_param():
def __init__(self, text):
self.text = text
def __call__(self, func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print('call %s():' % func.__name__)
print('args = {}'.format(*args))
print('decorator_param = {}'.format(self.text))
self.notify()
return func(*args, **kwargs)
return wrapper
def notify(self):
pass
@decorator_with_param("Jane")
def test(name):
print('%s param: %s' % (test.__name__, name))
test("Michael")
输出为:
call test():
args = Michael
decorator_param = Jane
test param: Michael
第18~19行
从新增加的notify()
函数可以看出装饰器类使用起来比嵌套函数更加整洁,代码更易懂。
functools模块
functools模块主要包含了一些方便的功能函数和函数装饰器。
functools.cmp_to_key(func)
要了解这个函数,首先需要了解python2的cmp()
函数用法、python2和python3的list.sort()
的区别、python2和python3的sorted()
的区别。
-
python2的
cmp()
函数:比较函数(comparison function)
函数语法:cmp(x, y)
作用:
cmp(x, y)
函数用于比较两个对象,如果 x < y x < y x<y返回 − 1 -1 −1,如果 x = = y x == y x==y返回 0 0 0,如果 x > y x > y x>y返回 1 1 1。
注意:python3已经没有这个内置函数了。 -
python2和python3的
list.sort()
和sorted()
sort()
与sorted()
区别:
sort()
是应用在list
上的方法,sorted()
可以对所有可迭代的对象进行排序操作。
list
的sort()
方法返回的是对已经存在的列表进行操作,而内建函数sorted()
方法返回的是一个新的list
,而不是在原来的基础上进行的操作。python2的
list.sort()
和sorted()
函数语法:list.sort(cmp=None, key=None, reverse=False) sorted(iterable, cmp=None, key=None, reverse=False)
python3的
list.sort()
函数语法:list.sort(key=None, reverse=False) sorted(iterable, key=None, reverse=False)
可以发现python3的
list.sort()
及sorted()
函数比python2的少了一个cmp
参数(又叫比较参数)。-
cmp参数的用法
cmp参数表示使用指定的方法进行比较排序。详细用法见下例(只能在python2运行):# -*-coding:utf8 -*- def numric_cmp(x, y): ''' 自己定义的比较函数,和python2的cmp()函数功能相似 x > y: 返回正数 x < y: 返回负数 x == y: 返回0 ''' return x - y def cmp_reverse(x, y): ''' 自己定义的比较函数 x > y: 返回负数 x < y: 返回正数 x == y: 返回0 ''' return y - x a = [3, 6, 2, 8, 1, -9] # sorted用法 print sorted(a, cmp=numric_cmp) print sorted(a, cmp=cmp_reverse) print sorted(a) print a print "######我是分隔符######" a.sort(cmp=numric_cmp) print(a) a.sort(cmp=cmp_reverse) print(a) a.sort() print(a)
输出为:
[-9, 1, 2, 3, 6, 8] [8, 6, 3, 2, 1, -9] [-9, 1, 2, 3, 6, 8] [3, 6, 2, 8, 1, -9] ######我是分隔符###### [-9, 1, 2, 3, 6, 8] [8, 6, 3, 2, 1, -9] [-9, 1, 2, 3, 6, 8]
-
key参数的用法
key参数(又称关键字参数)对应的值必须是一个这样的关键字函数(key function):接收一个参数,返回一个用来排序的键。最简单的:>>> sorted("This is a test string from Andrew".split(), key=str.lower) ['a', 'Andrew', 'from', 'is', 'string', 'test', 'This']
再例如按年龄给学生排序:
>>> student_tuples = [ ... ("john", 'A', 15), ... ("jane", 'B', 12), ... ("dave", 'B', 10), ... ] >>> sorted(student_tuples, key=lambda student: student[2]) [('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
-
reverse参数的用法
reverse参数表示排序规则:reverse = True
降序;reverse = False
升序(默认)。例如上一小节给字符串排序的例子如果置reverse = True
:>>> sorted("This is a test string from Andrew".split(), key=str.lower, reverse=True) ['This', 'test', 'string', 'is', 'from', 'Andrew', 'a']
-
现在我们对python2和python3的比较函数和排序函数有了一定了解。
那么有一个问题,python3的排序函数没有cmp参数,如果我想自己定义一个比较函数用于python3的排序,又没有cmp参数,该如何实现?
或者在将python2的代码迁移到python3时,python3排序函数没有了cmp参数,如何处理python2中指定了cmp参数的排序函数?
这就需要使用带装饰器的关键字函数(可以赋值给key参数)来代替比较函数(赋值给cmp参数)了,如下例:
def cmp_to_key(mycmp):
'Convert a cmp= function into a key= function'
class K(object):
def __init__(self, obj, *args):
self.obj = obj
def __lt__(self, other):
return mycmp(self.obj, other.obj) < 0
def __gt__(self, other):
return mycmp(self.obj, other.obj) > 0
def __eq__(self, other):
return mycmp(self.obj, other.obj) == 0
def __le__(self, other):
return mycmp(self.obj, other.obj) <= 0
def __ge__(self, other):
return mycmp(self.obj, other.obj) >= 0
def __ne__(self, other):
return mycmp(self.obj, other.obj) != 0
return K
def reverse_cmp(x, y):
return y - x
a = [3, 6, 2, 8, 1, -9]
print(sorted(a, key=cmp_to_key(reverse_cmp)))
print(sorted(a, key=cmp_to_key(reverse_cmp), reverse=True))
输出为:
[8, 6, 3, 2, 1, -9]
[-9, 1, 2, 3, 6, 8]
当然我们没必要自己去定义这个装饰器,因为functools模块已经有了cmp_to_key()
函数,下面我们将 讲解cmp参数时用到的只能在python2上运行的例子 改写为能在python3上运行的代码:
import functools
def numric_cmp(x, y):
'''
和python2的cmp()函数功能相似
x > y: 返回正数
x < y: 返回负数
x == y: 返回0
'''
return x - y
def cmp_reverse(x, y):
'''
x > y: 返回负数
x < y: 返回正数
x == y: 返回0
'''
return y - x
a = [3, 6, 2, 8, 1, -9]
# sorted用法
print(sorted(a, key=functools.cmp_to_key(numric_cmp)))
print(sorted(a, key=functools.cmp_to_key(cmp_reverse)))
print(sorted(a))
print(a)
print("######我是分隔符######")
a.sort(key=functools.cmp_to_key(numric_cmp))
print(a)
a.sort(key=functools.cmp_to_key(cmp_reverse))
print(a)
a.sort()
print(a)
主要的改动是之前使用cmp参数的地方都改为了key参数,并使用functools.cmp_to_key()
将之前赋值给cmp参数的比较函数转换为了可以赋值给key参数的关键字函数。
functools.partial(func[,*args][, **keywords])
partial()
是一个函数装饰器,用于为func()
函数的部分参数指定参数值,从而得到一个转换后的函数,程序以后调用转换后的函数时,就可以少传入那些己指定值的参数,只传入剩下未指定值的参数即可。
例子如下:
import functools
def func(a, b):
print('a:', a)
print('b:', b)
# 固定住a,只输入b
a_func = functools.partial(func, 3)
a_func(1)
# 固定住b, 只输入a
b_func = functools.partial(func, b=2)
b_func(5)
print(type(a_func))
print(type(b_func))
a_func.func(22, 22) # func属性,返回的是被修饰的函数
输出为:
a: 3
b: 1
a: 5
b: 2
<class 'functools.partial'>
<class 'functools.partial'>
a: 22
b: 22
可以看出返回的其实是一个partial类型的变量,实际上相当于在partial中保存了需要调用的参数以及需要固定的参数,在需要调用时将预先设置的参数传入参数列表。
注意:
functools.partial
有一个属性func
,返回的是被修饰的函数,如上例中的最后一行。
functools.update_wrapper(wrapper, wrapped[, assigned][, updated])
和上文讲的@functools.wraps(func)
功能相同但用法不同。下面是普通装饰器、使用了update_wrapper()
的装饰器和使用了@functools.wraps()
的装饰器的对比:
# 普通的装饰器
def decorator_1(func_1):
def wrapper_1(*args, **kwargs):
"""wrap func_1"""
print("before call func_1 ... ...")
return func_1(*args, **kwargs)
return wrapper_1
@decorator_1
def hello_1():
"""say hello_1"""
print("Hello, first world!")
# 带functools.update_wrapper的装饰器
from functools import update_wrapper
def decorator_2(func_2):
def wrapper_2(*args, **kwargs):
"""wrap func_2"""
print("before call func_2 ... ...")
return func_2(*args, **kwargs)
return update_wrapper(wrapper_2, func_2) # 这里用到了update_wrapper()
@decorator_2
def hello_2():
"""say hello_2"""
print("Hello, Second world!")
# 带functools.wraps()的装饰器
from functools import wraps
def decorator_3(func_3):
@wraps(func_3) # 这里用到了wraps()
def wrapper_3(*args, **kwargs):
"""wrap func_3"""
print("before call func_3 ... ...")
return func_3(*args, **kwargs)
return wrapper_3
@decorator_3
def hello_3():
"""say hello_3"""
print("Hello, third world!")
if __name__ == '__main__':
hello_1()
print("hello_1.__name__: %s" % hello_1.__name__)
print("hello_1.__doc__: %s" % hello_1.__doc__)
print("######我是分隔符######")
hello_2()
print("hello_2.__name__: %s" % hello_2.__name__)
print("hello_2.__doc__: %s" % hello_2.__doc__)
print("######我是分隔符######")
hello_3()
print("hello_3.__name__: %s" % hello_3.__name__)
print("hello_3.__doc__: %s" % hello_3.__doc__)
输出为:
before call func_1 ... ...
Hello, first world!
hello_1.__name__: wrapper_1
hello_1.__doc__: wrap func_1
######我是分隔符######
before call func_2 ... ...
Hello, Second world!
hello_2.__name__: hello_2
hello_2.__doc__: say hello_2
######我是分隔符######
before call func_3 ... ...
Hello, third world!
hello_3.__name__: hello_3
hello_3.__doc__: say hello_3
functools其他部分
functools模块还有很多其他的功能,暂时用不到,待以后补充:
@functools.lru_cache(maxsize=128, typed=False)
@functools.total_ordering
functools.partialmethod(func, *args, **keywords)
functools.reduce(function, iterable[, initializer])
@functools.singledispatch