vlambda博客
学习文章列表

第三讲 Python函数及进阶

一、函数定义

1.1 函数概述

将实现了某一功能,并需要在程序中多处使用的代码包装起来形成一个功能模块(即一个“函数”)。当程序中需要使用该项功能时,只需写一条语句调用实现该功能的 “函数”即可。

1.2 函数定义形式

在Python中,定义一个函数要使用def语句,依次写出函数名括号括号中的参数冒号:,然后,在缩进块中编写函数体,函数的返回值用return语句返回。通用形式如下:

def 函数名(参数1, 参数2 ……):
语句组(即“函数体”)
 return 返回值

也可没有参数:

def 函数名():
语句组(即“函数体”)
 return 返回值

也可以没有返回值:

def 函数名(参数1, 参数2 ……):
语句组(即“函数体”)
# 或
def 函数名():
语句组(即“函数体”)

1.3 空函数

def doNothing():
 pass

pass语句定义了一个什么也不做的空函数。

pass的作用

pass可以用来作为占位符,比如在搭建程序结构时,某个函数实现方法未确定,可先放一个pass,让代码能运行。

if score < 60:
 pass


二、函数的调用

调用一个函数,至少需要知道函数的名称和参数,多数情况下也需要了解函数的返回值。对函数的调用本质上也是一个表达式,表达式的值由函数内部的return语句决定。

2.1 参数检查

函数调用时,Python解释器会检查函数参数个数,如发现错误,会抛出TypeError

def add(x, y):
 print(x+y)

result = add(4,5,6)
# Output:
Traceback (most recent call last):
 File "/Users/hy/Desktop/test.py", line 4, in <module>
   result = add(4,5,6)
TypeError: add() takes 2 positional arguments but 3 were given

如果传入的参数类型不对,涉及到内置操作(+,-等)或内置函数的,解释器会进行参数检查,自定义函数需要使用异常处理自定义参数检查。

def myAbs(x):
 if x >= 0:
   return x
 else:
   return -x


2.2 返回值

  • return的功能是结束函数的执行,并将“返回值”作为结果返回;

  • 返回值可以是常量、变量或复杂的表达式;

  • return后面没有表达式,则返回值是None,表示“啥也不是”,如下:

    def add(x, y):
     print(x+y)
     return
    >> result = add(5,4)
    >> type(result)
    <class 'NoneType'>
  • 若函数没有return语句,返回值是None,如:

    def add(x, y):
     print(x+y)
     
    >> result = add(5,4)
    >> type(result)
    <class 'NoneType'>
  • return作为函数的出口,可以在函数中多次出现,且多个return的返回值可以不同。在哪个return结束函数执行,函数的返回值就和哪个return中的返回值相同

    def myMax(x, y):
     if x > y:
       return x
     else:
       return y
     
    n = myMax(1, 2)
    print(n, myMax(20, n))
    print(myMax("about", "take"))
  • 一个return可返回多个值

    def getMaxAndMin(x, y):
     if x > y:
       return x, y
     else:
       return y, x

    x, y = 10, 20
    maxValue, minValue = getMaxAndValue(x, y)
    print(maxValue, minValue)

    但实际上,返回的还是一个值(tuple)!但在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple,但写起来更方便。

2.3 形参 & 实参

形参:在函数定义中出现的参数

实参:调用函数时所给的参数


2.4 局部变量与全局变量

  • 在所有函数外面定义的变量,即全局变量;

  • 在函数内部定义的变量,即局部变量。局部变量在定义它的函数外部不能使用,因此,不同函数中同名的局部变量不会互相影响。

  • 若全局变量和局部变量同名,假如都为x:

    # demo.py
    def fun_0():
     print('x in fun_0: ', x) # x 是全局变量

    def fun_1():
     x = 8
     print("x in fun_1: ", x) # x 是局部变量,不会更改全局变量x

    def fun_2():
     global x
     print("x in fun_2: ", x) # x 是全局变量
     x = 5

    def fun_3():
     print("x in fun_3: ", x) # Runtime Error, 因后面有赋值而被当作局部的x,此处没赋值就先使用了
     x = 9
     
    x = 4
    fun_0()
    fun_1()
    print(x)
    fun_2()
    fun_3()
    • 如果函数中没有对x赋值,则函数中的x就是全局变量x;

    • 如果函数中对x进行赋值,且没有特别声明,则函数中全局变量x不起作用,x是局部变量;

    • 函数内部可以使用globalx进行声明,说明x是全局变量(如果要对外面的x赋值,可使用global声明)

  • python解释执行,因此函数中的语义错误,执行到时才会报告(编译型语言编译时就报错)

    如上代码中的fun_3(),只要不调用,不会有“编译器”提示错误。

三、内置函数

各种程序设计语言都会提供大量函数,可以直接在程序中使用,这些函数称为内置函数(库函数)。之前用到的print(),len()都是内置函数,常用的内置函数如下表:

函数 功能
int(x) x转为整数
float(x) x转为小数
str(x) x转为字符串
ord(x) x的字符编码
chr(x) 求编码为x的字符
abs(x) x的绝对值
len(x) 求序列x的长度(元素个数)
max(x) 求序列x中的最大值
min(x) 求序列x中的最小值
max(x1,x2,x3,...) 求多个参数中最大的
min(x1,x2,x3,...) 求多个参数中最小的
type(x) 返回变量x或表达是x的值的类型
exit() 终止程序执行
dir(x) 返回类x或对象x的成员函数名构成的列表
help(x) 返回函数x或类x的使用说明


四、函数的参数

定义函数时,会确定函数名、参数名字和位置,此时便定义好了函数的接口。对于函数的使用者,只需要按照接口传递正确的参数,并了解函数返回值即可,无需关注内部实现。

Python的定义虽简单,但参数的灵活度却很大,除了定义一些必要的参数,还可以使用默认参数、可变参数和关键字参数。

4.1 位置参数

考虑如下函数(版本一):

# 版本一
def power(x):
 return x**2

该函数计算x的平方,power(x)中的x就是一个位置参数,调用该函数时,必须传入且只能传入一个参数:x

power(2)  # 4
power(10) # 100

若要提高该函数的泛化能力,计算x的n次方,可做如下修改(版本二):

# 版本二
def power(x, n):
 if not isinstance(n, (int, float)):
   raise TypeError("n should be a number.")
 else:
 return x ** n

调用时,通过传递正确的参数,计算x的任意n次方:

power(2, 3)  # 8
power(2, 10) # 1024
power(2, 'a')
'''
Traceback (most recent call last):
File "/Users/hy/Desktop/test.py", line 7, in <module>
  print(power(3, 'a'))
File "/Users/hy/Desktop/test.py", line 3, in power
  raise TypeError("n should be type int.")
TypeError: n should be type int.
'''

修改后的power(x, n)有两个参数,均为位置参数,调用时,传入的两个值要按照位置顺序依次赋值给xn

4.2 默认参数

修改后的版本二使用时需传入两个参数,假设多数情况下,只需计算x的平方,此时,每次调用均需要传入第二个参数,此时可使用默认参数对版本二进一步改进(版本三):

# 版本三
def power(x, n=2):
 if not isinstance(n, (int, float)):
   raise TypeError("n should be a number.")
 else:
 return x ** n

除了函数接口,函数体内没有任何变化,调用情形如下:

power(2)   # 4
power(2, 5) # 32

可见,此时对于n!=2的情况,必须传入两个位置参数;对于n=2的情况,只需传入x即可,因为,在函数定义时给参数n了一个默认值。

默认参数设计原则及注意事项:

  • 必选参数在前,默认参数在后

    # 错误
    def power(n=2, x):
     pass
  • 设计有多个参数的函数接口时,变化大的在前,变化小的在后,考虑将变化小的作为默认参数

    def enroll(name, gender, age = 6, city="Zhengzhou"):
     pass
  • 调用具有多个默认参数的函数时,可以按顺序提供默认参数,也可以不按顺序提供部分默认参数。

    enroll("Zhang San", "Male", 7)

    # city参数用传进去的值,其它默认参数使用默认值
    enroll("Li Si", "Male", city="Xinxiang")
  • 默认参数必须指向不变对象!

    考虑如下代码:

    def add_end(L=[]):
     L.append('OVER')
     return L

    正常调用时:

    myList_1 = add_end([1,2,3])
    print(myList_1) # [1, 2, 3, 'OVER']
    myList_2 = add_end(['x', 'y', 'z'])
    print(myList_2) # ['x', 'y', 'z', 'OVER']
    myList_3 = add_end()
    print(myList_3) # ['OVER']

    继续调用:

    myList_4 = add_end()
    print(myList_4) # ['OVER', 'OVER']
    myList_5 = add_end()
    print(myList_5) # ['OVER', 'OVER', 'OVER']

    似乎每次都“记住了”上次添加了'OVER'后的list。

    原因:Python函数在定义的时候,默认参数L的值就被计算出来了,即[]因为默认参数L也是一个变量,它指向对象[]每次调用该函数,如果改变了L的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]了。

    不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。

def add_end(L=None):
 if L is None:
   L = []
 L.append('OVER')
 return L

4.3 可变参数

可变参数就是传入的参数个数是可变的,可以是1个、2个到任意个,还可以是0个。

假如需要计算x1**2 + x2**2 + ...,要在Python中定义该函数,必须确定输入的参数,但由于参数个数不确定,可将参数作为列表或元组传入,可如下定义:

def cal(numSeq):
 sum = 0
 for i in numSeq:
   sum += i**2
 return sum

调用时,先组装一个list或tuple:

print(cal([1,2,3]))  # 14
print(cal((1,2,3)))

此时,调用仍觉繁琐,期望的调用形式如下:

print(cal(1,2,3))

定义可如下修改,把函数参数作为可变参数:

def cal(*numSeq):
 sum = 0
 for i in numSeq:
   sum += n**2
return sum

定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*号。在函数内部,参数numSeq接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数:

print(cal(1,2))
print(cal(1,2,3))
print(cal())

但是,如果已经有了一个list或tuple,此时要调用一个可变参数的函数,需要如下调用

numSeq = [1, 2, 3]
print(cal(numSeq[0], numSeq[1], numSeq[2]))

繁琐!!!Python允许在list或tuple前加一个*,把list或tuple的元素变成可变参数传入:

numSeq = [1, 2, 3]
print(cal(*numSeq))
# *numSeq表示把numSeq这个list的所有元素作为可变参数传进去。这种写法相当有用,而且很常见。

可变参数允许传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。

4.4 关键字参数

关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。

def person(name, age, **kwargs):
 print('name: ', name, 'age: ', age, 'other: ', kwargs)

函数person除了必选参数nameage外,还接受关键字参数kwargs。在调用该函数时,可以只传入必选参数:

person('Zhang San', 20)
# name: Michael age: 30 other: {}

也可以传入任意个数的关键字参数:

person('Li Si', 19, city='Beijing') # name: Li Si age: 19 other: {'city': 'Beijing'}
person('Han Meimei', 19, gender='Female', job='Dancer')

这些关键字参数可以扩展函数的功能。比如,在person函数里,保证能接收到nameage这两个参数,但是,如果调用者愿意提供更多的参数,该函数也能收到。

比如用户注册功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数定义函数就能满足注册的需求。

和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去

extra = {'city': 'Beijing', 'job': 'Engineer'}

person('Jack', 24, city=extra['city'], job=extra['job'])
# 简单写法:
person('Jack', 24, **extra)
  • **extra表示把extra这个dict的所有key-value用关键字参数传入函数.

  • **kwargs参数,kwargs将获得一个dict,kwargs获得的dict是extra的一份拷贝,对kwargs的改动不会影响到函数外的extra.

4.5 命名关键字参数

对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,只能在函数内部通过kwargs检查。

person()函数为例,我们希望检查是否有cityjob参数:

def person(name, age, **kwargs):
   if 'city' in kwargs:
       # 有city参数
       pass
   if 'job' in kwargs:
       # 有job参数
       pass
   print('name:', name, 'age:', age, 'other:', kwargs)

但是调用者仍可以传入不受限制的关键字参数:

person('Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456)

如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收cityjob作为关键字参数。这种方式定义的函数如下:

def person(name, age, *, city, job):
   print(name, age, city, job)

命名关键字参数需要一个特殊分隔符**后面的参数被视为命名关键字参数。调用方法如下:

person('Jack', 24, city='Beijing', job='Engineer')

注意事项:

  • 如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数不再需要特殊分隔符*

    def person(name, age, *args, city, job):
       print(name, age, args, city, job)
  • 命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错

    def person(name, age, *args, city, job):
       print(name, age, args, city, job)
       
    person('Jack', 24, 'Beijing', 'Engineer')

    '''
    Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    TypeError: person() missing 2 required keyword-only arguments: 'city' and 'job'
    '''
    '''
    原因:
    调用时缺少参数名city和job,Python解释器把前两个参数视为位置参数,后两个参数传给*args,但缺少命名关键字参数导致报错。
    '''
  • 命名关键字参数可以有缺省值,从而简化调用

    def person(name, age, *, city='Beijing', job):
       print(name, age, city, job)
       
    person('Jack', 24, job='Engineer')
  • 如果没有可变参数,就必须加一个*作为特殊分隔符。如果缺少*,Python解释器将无法识别位置参数和命名关键字参数:

    def person(name, age, city, job):
       # 缺少 *,city和job被视为位置参数
       pass

4.6 参数组合

定义函数时可以组合使用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,但必须遵循如下顺序:必选参数、默认参数、可变参数、命名关键字参数和关键字参数

def f1(a, b, c=0, *args, **kwargs):
   print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kwargs =', kwargs)

def f2(a, b, c=0, *, d, **kwargs):
   print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kwargs =', kwargs)

函数调用的时候,Python解释器自动按照参数位置和参数名把对应的参数传进去:

>>> f1(1, 2)
a = 1 b = 2 c = 0 args = () kwargs = {}

>>> f1(1, 2, c=3)
a = 1 b = 2 c = 3 args = () kwargs = {}

>>> f1(1, 2, 3, 'a', 'b')
a = 1 b = 2 c = 3 args = ('a', 'b') kwargs = {}

>>> f1(1, 2, 3, 'a', 'b', x=99)
a = 1 b = 2 c = 3 args = ('a', 'b') kwargs = {'x': 99}

>>> f2(1, 2, d=99, ext=None)
a = 1 b = 2 c = 0 d = 99 kwargs = {'ext': None}

通过tuple,dict调用上述函数:

# f1(a, b, c=0, *args, **kw)
args = (1, 2, 3, 4)
kwargs = {'d': 99, 'x': '#'}
f1(*args, **kwargs)
"""
Output:
a = 1 b = 2 c = 3 args = (4,) kwargs = {'d': 99, 'x': '#'}
"""

# f2(a, b, c=0, *, d, **kwargs)
args = (1, 2, 3)
kw = {'d': 88, 'x': '#'}
f2(*args, **kwargs)
"""
Output:
a = 1 b = 2 c = 3 d = 88 kwargs = {'x': '#'}
"""

对于任意函数,都可以通过类似func(*args, **kwargs)的形式调用它,无论它的参数是如何定义的。

4.7 函数参数小结

  • 虽然可以组合多种参数,但不要同时使用太多的组合,否则函数接口的可理解性很差;

  • 默认参数一定要用不可变对象

  • *args是可变参数,args接收的是一个tuple

  • **kwargs是关键字参数,kwargs接收的是一个dict

  • 可变参数既可以直接传入:func(1, 2, 3),又可以先组装list或tuple,再通过*args传入:func(*(1, 2, 3))

  • 关键字参数既可以直接传入:func(a=1, b=2),又可以先组装dict,再通过**kwargs传入:func(**{'a': 1, 'b': 2})

  • 使用*args**kwargs是Python的习惯写法

  • 命名的关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值

  • 定义命名的关键字参数在没有可变参数的情况下要写分隔符*,否则定义的将是位置参数

五、高阶函数

5.1 函数式编程

函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。

函数式编程(Functional Programming)虽然也可以归结到面向过程的程序设计,但其思想更接近数学计算。

函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的

函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!

Python对函数式编程提供部分支持。(函数式编程没有变量)

5.2 高阶函数

把函数作为参数传入,这样的函数称为高阶函数。

5.2.1 变量可以指向函数
abs(-10)
print(abs)
# <built-in function abs>

abs(-10)是函数调用,而abs是函数本身。能否把函数本身赋值给变量?

f = abs
print(f)
# <built-in function abs>

# 用该变量调用函数
f(-10) # 10

说明变量f现在已经指向了abs函数本身。直接调用abs()函数和调用变量f()完全相同。

5.2.2 函数名也是变量

函数名其实就是指向函数的变量!对于abs()这个函数,完全可以把函数名abs看成变量,它指向一个可以计算绝对值的函数!

abs = 10
abs(-10)
"""
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable
"""

abs指向10后,abs这个变量已经不指向求绝对值函数而是指向一个整数10!(恢复abs函数,重启Python环境。)

5.2.3 传入函数

变量可以指向函数,函数的参数能接收变量,那么可以把一个函数作为变量传递给另一个函数作为参数,这种函数就称之为高阶函数

def add(x, y, f):
 return f(x) + f(y)

add(-5, 6, abs)
5.2.4 匿名函数(Lambda表达式)

关键字lambda表示匿名函数,其形式为:lambda 参数1,参数2,...:返回值,如:lambda x,y:x+y,实际上就是:

def f(x, y):
 return x + y

匿名函数有个限制,即只能有一个表达式,不用写return,返回值就是该表达式的结果。由于匿名函数没有名字,不必担心函数名冲突;此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:

f = lambda x, y : x + y
print(f(1,2)) # 3

print((lambda x, y : x + y)(1,2))

同样,也可以把匿名函数作为返回值返回,如:

def func(x, y):
 return lambda: x**2 + y**2


5.2.5 返回函数
5.2.5.1 函数作为返回值

高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回

# 可变参数的求和
def cal_sum(*args):
 sum = 0
 for i in args:
   sum += i
 return sum

cal_sum(1,2,3) # 6

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

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

当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数,且返回的函数并没有立即执行,而是调用才执行:

f = lazy_sum(1, 2, 3)
print(f)
"""
Output:
<function lazy_sum.<locals>.sum at 0x7f84b552f310>
"""

# 真正计算结果时:
print(f())
"""
6
"""

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

f1 = lazy_sum(1, 3, 5, 7, 9)
f2 = lazy_sum(1, 3, 5, 7, 9)
f1==f2 # False
5.2.5.2  闭包

闭包是一种程序结构,在一个函数内部定义了另一个函数,内部函数中用了外部函数的局部变量,并且外部函数的返回值是是内部函数,这就构成了一个闭包,内部函数称为闭包函数。

# outer是外部函数,a和b都是外部函数的局部变量
def outer(a):
 b = 10
 # inner是内部函数
 def inner():
   # 在内部函数中用到了外部函数的临时变量
   print(a+b)
 # 外部函数的返回值是内部函数
 return inner

一般情况下,如果一个函数结束,函数的内部所有东西都会释放掉,还给内存,局部变量都会消失。但是闭包是一种特殊情况,如果外部函数在结束的时候发现有自己的临时变量将来会在内部函数中用到,就把这个临时变量绑定给了内部函数,然后再结束自己。

# 调用外部函数传入参数5
# 此时外部函数两个临时变量 a是5 b是10 ,并创建了内函数,然后把内函数的引用返回存给了demo
# 外函数结束的时候发现内部函数将会用到自己的临时变量,这两个临时变量就不会释放,会绑定给这个内部函数
demo = outer(5)

# 调用内部函数,内部函数确实能使用外部函数的临时变量
demo() # 15

或者说,内部函数(闭包函数)记住了外部函数的局部变量,所以,在一些资料中称,闭包具有记忆功能。

在闭包内函数中,我们可以随意使用外函数绑定来的临时变量,但是如果我们想修改外函数临时变量数值的时候就会出现问题:

def outer(a):
 b = 10
 def inner():
   # nonlocal b
   b += 1
   print(a+b)
 return inner

类比理解:在基本的python语法当中,一个函数可以随意读取全局数据,但是要修改全局数据的时候有两种方法:1 global 声明全局变量;2.全局变量是可变类型数据,如列表:

  1. 在python3中,可以用nonlocal 关键字声明 一个变量, 表示这个变量不是局部变量空间的变量,需要向上一层变量空间找这个变量;

  2. 在python2中,没有nonlocal这个关键字,我们可以把闭包变量改成可变类型数据进行修改,比如列表。

原因是b作为局部变量并没有初始化,直接计算b+1是不行的。但我们其实是想引用outer()函数中的b,所以需要在inner()函数中的变量b前加一个nonlocal声明。加上这个声明后,解释器把inner()中的b看作外层函数的局部变量,它已经被初始化了,可以正确计算b+1


闭包中的坑

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

f1, f2, f3 = count()

上面的代码,每次循环都创建了一个新的函数,然后,把创建的3个函数都返回了。

依次调用f1(),f2(),f3()

>>> f1()  # 1

>>> f2() # 4

>>> f3() # 9

全部都是9!原因就在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果为9

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

如果一定要引用循环变量怎么办?--- 再创建一个函数,用该函数的参数绑定循环变量当前的值,并且在最外层函数中调用该函数使其执行,那么,无论该循环变量后续如何更改,已绑定到函数参数的值不变

def count():
   def g(j):
       def f():
           return j*j
       return f
   fs = []
   for i in range(1, 4):
       fs.append(g(i)) # f(i)立刻被执行,因此i的当前值被传入f()
   return fs
>>> f1, f2, f3 = count()
>>> print(f1())
1
>>> print(f2())
4
>>> print(f3())
9


5.2.6 装饰器

装饰器就是一个闭包,装饰器是闭包的一种应用。

简言之,python装饰器就是用于拓展原来函数功能的一种函数,这个函数的特殊之处在于它的返回值也是一个函数,使用python装饰器的好处就是在不用更改原函数的代码前提下给函数增加新的功能。

装饰器函数的外部函数传入要装饰的函数名字,返回经过修饰后函数的名字;内层函数(闭包)负责修饰被修饰函数。

从上面的描述中,可以总结装饰器的几个属性:

  • 实质:函数

  • 参数:要装饰的函数名

  • 返回:装饰完的函数名

  • 作用:为已经存在的对象(函数)添加额外功能

  • 特点:不需要对对象做任何改动

用时,只需在被修饰的函数前加上@装饰器名即可。

使用场景分析

需求:在下面函数中添加记录(输出)该函数运行时间的功能:

import time
def func():
   print("hello")
   time.sleep(1)
   print("world")

最简单的解决方案:

import time
def func():
   startTime = time.time()
   
   print("hello")
   time.sleep(1)
   print("world")
   
   endTime = time.time()
   
   duration = endTime - startTime
   print("Duration is %d s." % duration)

新需求:对所有函数都增加该功能。

此时可使用装饰器解决,先来看使用装饰器的最原始代码:

import time
# func是需要装饰器的函数
def my_decorator(func):
 # 内层函数wrapper,相当于闭包函数,它起到装饰给定函数的作用
 def wrapper(*args, **kwargs):
   startTime = time.time()
   func()
   endTime = time.time()
   print(endTime - startTime)
 return wrapper

# @my_decorator语法 相当于 执行 func = my_decorator(func),为func函数装饰并返回
@my_decorator
def func():
 print("hello")
 time.sleep(1)
 print("world")
 
func()
"""
hello
world
1.0052070617675781
"""

# 问题
print(func.__name__) # wrapper

以上代码确实起到了装饰func的效果,但是会带来一些副作用:被装饰后的函数变成了另一个函数。(可通函数名等属性查看,如func.__name__)。该副作用有时会带来一些不便,如对flask框架中的一些函数添加一些自定义的装饰器,添加后由于函数名发生了变化,会对测试结果产生影响。

Python的functools包中提供了一个wraps装饰器来消除该副作用,在实际代码中写装饰器时,为消除该副作用,会实现之前添加functools的wrap,它会被装饰函数的原有属性,修改后的代码如下:

import imp
import time
# 导入functools的wraps装饰器
from functools import wraps

# func是需要装饰器的函数
def my_decorator(func):
 # 内层函数wrapper,相当于闭包函数,它起到装饰给定函数的作用
 # 添加functools的wraps装饰器
 @wraps(func)
 def wrapper(*args, **kwargs):
   startTime = time.time()
   func()
   endTime = time.time()
   print(endTime - startTime)
 return wrapper

# @my_decorator语法 相当于 执行 func = my_decorator(func),为func函数装饰并返回
@my_decorator
def func():
 print("hello")
 time.sleep(1)
 print("world")
 
func()
"""
hello
world
1.001641035079956
"""

# 问题
print(func.__name__) # func

装饰器链

一个Python函数可以被多个装饰器修饰,如有多个装饰器,由近到远执行。

def makebold(f):
 return lambda: "<b>" + f() + "</b>"

def makeitalic(f):
 return lambda: "<i>" + f() + "</i>"

@makebold
@makeitalic
def say():
 return "Hello"

print(say())