函数从入门到超神


1 函数的基本概念

Python中,可通过固定的格式来定义函数。函数有具体的组成部分

  • 函数名
  • 函数体
  • 参数
  • 返回值

从分类上看,函数可分为

  • 内置函数
  • 自定义函数

为了实现不同的编程需求,还可为函数加上各种规则及作用域的限制,以完成整个功能

1.1 函数的作用

背景提要
现在老板让你写一个监控程序,监控服务器的系统状况,当cpu\memory\disk等指标的使用量超过阀值时即发邮件报警,你掏空了所有的知识量,写出了以下代码

while True:
    if cpu利用率 > 90%:
        #发送邮件提醒
        连接邮箱服务器
        发送邮件
        关闭连接
     
    if 硬盘使用空间 > 90%:
        #发送邮件提醒
        连接邮箱服务器
        发送邮件
        关闭连接
     
    if 内存占用 > 80%:
        #发送邮件提醒
        连接邮箱服务器
        发送邮件
        关闭连接

上面的代码实现了功能,但是重复代码太多了,每次报警都要重写一段发邮件的代码,太low了,如此做存在2个问题:

  1. 代码重复过多,一个劲的copy and paste不符合高端程序员的气质
  2. 若日后需要修改发邮件的这段代码,比如加入群发功能,那你就需要在所有用到这段代码的地方都修改一遍

解决这个问题的方法其实很简单,我们只需要把重复的代码提取出来,放在一个公共的地方,起个名字,以后谁想用这段代码,就通过这个名字调用就行了,如下

def 发送邮件(内容)
    #发送邮件提醒
    连接邮箱服务器
    发送邮件
    关闭连接
     
while True:
     
    if cpu利用率 > 90%:
        发送邮件('CPU报警')
     
    if 硬盘使用空间 > 90%:
        发送邮件('硬盘报警')
     
    if 内存占用 > 80%:
        发送邮件('内存报警')

函数的作用:

  • 减少重复代码
  • 方便修改、更易扩展
  • 保持代码一致性

练习1:写一个函数记录日志


日志记录案例

def action1(n):
    print ('starting action1...')

    with open('日志记录','a') as f:
        f.write('end action%s\n'%n)

def action2(n):
    print ('starting action2...')

    with open('日志记录','a') as f:
        f.write('end action%s\n'%n)

def action3(n):
    print ('starting action3...')

    with open('日志记录','a') as f:
        f.write('end action%s\n'%n)

action1(1)
action2(2)
action3(3)


##***************代码重用

def logger(n):
    with open('日志记录','a') as f:
        f.write('end action%s\n'%n)

def action1():
    print ('starting action1...')
    logger(1)


def action2():
    print ('starting action2...')
    logger(2)


def action3():
    print ('starting action3...')
    logger(3)


action1()
action2()
action3()

##***************可扩展和保持一致
##为日志加上时间
import time

def logger(n):
    time_format='%Y-%m-%d %X'
    time_current=time.strftime(time_format)

    with open('日志记录','a') as f:
        f.write('%s end action%s\n'%(time_current,n))

def action1():
    print ('starting action1...')
    logger(1)


def action2():
    print ('starting action2...')
    logger(2)


def action3():
    print ('starting action3...')
    logger(3)

action1()
action2()
action3()

1.2 函数的定义

定义函数使用关键字def,后接函数名,再后接放在()中的可选参数列表,最后是冒号。格式如下

def 函数名(参数1,参数2,...,参数N):

例如:

def hello(strname):         # 定义一个函数hello,strname是传给函数的参数
    print(strname)          # 打印参数

调用时,直接使用函数名称即可。例:

hello("I love Python!")     # 调用函数,结果输出:I love Python!

1.3 函数的命名规则

函数名的命名有其对应的规则

  • 必须以字母或下划线开头
  • 可以包含任意字母、数字或下划线的组合
  • 不能使用任何特殊字符或标点符号
  • 区分大小写
  • 不能是保留字
  • 不能以数字开头

1.4 函数的组成部分

Python 中有多种不同类型的函数。无论它们的功能差别有多大,其组成部分是相同的。

函数有四个组成部分

  • 函数名:def后面的名字
  • 函数参数:函数名后面的变量
  • 函数体:函数名的下一行代码,起始需要缩进
  • 返回值:函数执行完的返回内容,用 return 语句返回。若无返回值,可不写 return,但即使不写 return 默认也会返回一个None

对于一般类型的函数来说,函数名和函数体是必须有的,函数的参数和返回值是可选的

文档字符串
在函数体中,常会在开始位置放一个多行字符串,用来说明函数的功能及调用方法。这个字符串为函数的文档字符串(docstring),说白了就是帮助文档。可使用代码print(函数名.__doc__)将其输出

def get_info():                         # 定义一个函数 get_info
    '''本函数用于返回用户姓名和年龄'''
    name = 'tom'
    age = 23
    return name, age                     # 返回 name 和 age 的值
    
print(get_info.__doc__)                 # 用 __doc__ 返回文档字符串
help(get_info)                          # 用内置函数 help 来查看文档帮助信息

在实际应用中,文档字符串主要用于描述函数的相关信息,以便用户更好的浏览和输出。建议养成在代码中添加文档字符串的好习惯

此处文档字符串虽然我们用的是三引号,但其实单引号和双引号都可以实现相同的功能,只是因为在实际应用中我们要描述的内容比较多,会占据多行,所以用的三引号引用多行文本,若内容很短,只有一行的情况下用单引号、双引号与用三引号无甚区别

但要注意的是文档字符串必须在函数体的第一行

1.5 函数的参数:形参与实参

形参是从函数的角度来说的,在定义函数时写到括号中的参数就是形参。所谓形参就是形式上的参数

实参是从调用的角度来说的,在调用函数时写到括号里传给函数的参数就是实参,实参可以是常量、变量、表达式、函数。
实参的个数与类型应与形参一一对应,不能多也不能少

形参与实参的区别:

  • 形参是虚拟的,不占用内存空间。形参变量只有在被调用时才分配内存单元
  • 实参是一个变量,会占用内存空间,数据传送单向,由实参传给形参,不能由形参传给实参
import time
times = time.strftime('%Y--%m--%d')
def f(time):
    print('Now is : %s' % time)
f(times)

1.6 函数的返回值

函数不需要返回值时可以什么都不做。若需要返回值时,就要使用 return 语句将具体的值返回。使用 return 语句可以一次返回多个值。调用时,可定义多个变量来接收,也可用一个元组来接收

def get_info():
    name = 'tom'
    age = 23
    return name,age
    
myname,myage = get_info()
print(myname,myage)         # tom 23

person = get_info()
print(person)               # ('tom', 23)

有时可能只需要用到返回值中的一个,而将其他的忽略掉。这种情况下,可使用下划线(_)来接收对应返回值

personname,_ = get_info()       # 在调用时,使用_来接收不需要的返回值
print(personname)

返回值必须要有一个变量接收,若无变量接收将无法展示给用户看
返回值与打印的区别:

def get_info():
    name = 'tom'
    age = 23
    return name,age
    
get_info()      # 无任何输出

a = get_info()
print(a)        # tom 23

##########################################
def get_info():
    name = 'tom'
    age = 23
    print(name,age)

get_info()      # tom 23

返回值的作用:

  • 结束函数。函数在执行过程中只要遇到 return 语句,就会停止执行并返回结果,所以可以理解为 return 语句代表着函数的结束
  • 返回对象

注意:

  • 若函数里没有明确指定return,则函数会默认返回一个None
  • 若return多个对象,则python将会帮我们把这些对象封装成一个元组返回给我们

2 函数的参数

函数的参数有几种:

  • 必备参数
  • 关键字参数
  • 默认参数
  • 不定长参数

2.1 必备参数

必备参数须以正确的顺序传入函数。调用时的数量必须和声明时的一样

def get_info(name, age):
    print("My name is %s, I'm %d years old." %(name, age))
 
get_info('tom',25)
get_info('jerry',16)

2.2 关键字参数

关键字参数和函数调用关系紧密,函数调用使用关键字参数来确定传入的参数值。使用关键字参数允许函数调用时参数的顺序与声明时不一致,因为 Python 解释器能够用参数名匹配参数值

def get_info(name, age):
    print("My name is %s, I'm %d years old." %(name, age))
 
# get_info(16, 'jerry')      # TypeError: %d format: a number is required, not str
get_info(age=16, name='jerry')  # My name is jerry, I'm 16 years old.

2.3 默认参数

调用函数时,缺省参数的值如果没有传入,则被认为是默认值。下例会打印默认的sex,如果sex没有被传入

def get_info(name, age, sex='male'):
    print('Name: %s' %name)
    print('age: %d' %age)
    print('Sex: %s' %sex)
 
get_info('tom',23)
get_info('jerry',30,'female')

2.4 不定长参数

你可能需要一个函数能处理比当初声明时更多的参数。这些参数叫做不定长参数,和上述2种参数不同,声明时不会命名

# def add(x, y):
#     return x + y
 
def add(*tuples):
    sum=0
    for v in tuples:
        sum += v
    return sum
 
print(add(1,4,6,9))         # 20
print(add(1,4,6,9,5))       # 25

加了星号(*)的变量名会存放所有未命名的变量参数。而加(**)的变量名会存放命名的变量参数

def get_info(**kwargs):
    print(kwargs)
    for i in kwargs:
        print('%s: %s' %(i,kwargs[i]))      # 根据参数可以打印任意相关信息了
 
get_info(name='tom',age=20,sex='female',hobby='book',nationality='Chinese')
 
##############################################################################
 
def get_info(name,*args,**kwargs):  # def print_info(name,**kwargs,*args):  报错
    print('Name: %s' %name)
    print('args: ',args)
    print('kwargs: ', kwargs)
 
get_info('tom',20,hobby='book',nationality='Chinese')
# get_info(hobby='book','tom',20,nationality='Chinese')  # 报错
# get_info('tom',hobby='book',20,nationality='Chinese')  # 报错

注意,还可以这样传参

def f(*args):
    print(args)
 
f(*[1,2,5])             # 直接给函数传一个序列参数
 
def f(**kargs):
    print(kargs)
 
f(**{'name':'tom'})     # 直接给函数传一个字典参数

2.5 参数的位置优先级

参数的位置优先级:

  • 关键字参数
  • 默认参数
  • args不定长参数
  • kwargs不定长参数

练习:写一个计算器函数

3 函数的作用域

3.1 作用域介绍

Python中的作用域分4种情况:

  • L:local,局部作用域,即函数中定义的变量;
  • E:enclosing,嵌套的父级函数的局部作用域,即包含此函数的上级函数的局部作用域,但不是全局的;
  • G:globa,全局变量,就是模块级别定义的变量;
  • B:built-in,系统固定模块里面的变量。

搜索变量的优先级顺序依次是:
作用域局部 > 外层作用域 > 当前模块中的全局 > python内置作用域,也就是LEGB。

x = int(2.9)  # int built-in
 
g_count = 0  # global
def outer():
    o_count = 1  # enclosing
    def inner():
        i_count = 2  # local
        print(o_count)
    # print(i_count) # 找不到
    inner() 
outer()
 
# print(o_count) # 找不到

当然,local和enclosing是相对的,enclosing变量相对上层来说也是local

3.2 作用域产生

在Python中,只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域,其它的代码块(如if、try、for等)是不会引入新的作用域的,如下代码

if 2 > 1:
    x = 1
print(x)  # 1

这个是没有问题的,if并没有引入一个新的作用域,x仍处在当前作用域中,后面代码可以使用

def test():
    x = 2
print(x)    # NameError: name 'x2' is not defined

def、class、lambda是可以引入新作用域的

3.3 变量的修改

x = 6
def f2():
    print(x)
    x = 5
f2()
  
# 错误的原因在于print(x)时,解释器会在局部作用域找,
# 会找到x = 5(函数已经加载到内存),但 x 使用在声明前了,所以报错:
# local variable 'x' referenced before assignment.
# 如何证明找到了x=5呢?简单:注释掉 x=5,x=6
# 报错为:name 'x' is not defined
# 同理

x = 6
def f2():
    x+=1    # local variable 'x' referenced before assignment.
f2()

3.4 global关键字

当内部作用域想修改外部作用域的变量时,就要用到global和nonlocal关键字了,当修改的变量是在全局作用域(global作用域)上的,就要使用global先声明一下,代码如下

count = 10
def outer():
    global count
    print(count) 
    count = 100
    print(count)
outer()

3.5 nonlocal关键字

global 关键字声明的变量必须在全局作用域上,不能嵌套作用域上,当要修改嵌套作用域(enclosing作用域,外层非全局作用域)中的变量怎么办呢,这时就需要nonlocal关键字了

def outer():
    count = 10
    def inner():
        nonlocal count
        count = 20
        print(count)
    inner()
    print(count)
outer()

3.6 总结

  • 变量查找顺序:LEGB,作用域局部>外层作用域>当前模块中的全局>python内置作用域;
  • 只有模块、类、及函数才能引入新作用域;
  • 对于一个变量,内部作用域先声明就会覆盖外部变量,不声明直接使用,就会使用外部作用域的变量;
  • 内部作用域要修改外部作用域变量的值时,全局变量要使用global关键字,嵌套作用域变量要使用nonlocal关键字。nonlocal是python3新增的关键字,有了这个 关键字,就能完美的实现闭包了。

4 高阶函数

高阶函数是至少满足下列一个条件的函数:

  • 接受一个或多个函数作为输入
  • 输出一个函数

高阶函数的特点:

  • 函数名可以进行赋值
  • 函数名可以作为函数的参数
  • 函数名可以作为函数的返回值
def add(x,y,f):
    return f(x) + f(y)
 
res = add(3,-6,abs)
print(res)
###############
def foo():
    x=3
    def bar():
        return x
    return bar

5 递归函数

定义:在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。

实例1(阶乘)

# 用for循环实现阶乘
def factorial(n):
 
    result=n
    for i in range(1,n):
        result*=i
 
    return result
 
print(factorial(4))
 
 
# 用递归函数实现阶乘
def factorial_new(n):
 
    if n==1:
        return 1
    return n*factorial_new(n-1)
 
print(factorial_new(3))

实例2(斐波那契数列)

def fibo(n):
    before = 0
    after = 1
    for i in range(n - 1):
        ret = before + after
        before = after
        after = ret

    return ret


print(fibo(3))


# **************递归*********************
def fibo_new(n):  # n可以为零,数列有[0]

    if n <= 1:
        return n
    return (fibo_new(n - 1) + fibo_new(n - 2))


print(fibo_new(3))

# print(fibo_new(30000))  # maximum recursion depth exceeded in comparison

递归函数的特点

  • 调用自身函数
  • 必须有一个结束条件
  • 递归能实现的循环均可实现
  • 递归效率在很多情况下会很低,不推荐使用。但在用for循环实现非常复杂时可以使用递归实现,结构比较清晰

6 常用的内置函数

6.1 内置函数(py3.7.4)

Built-in
Functions
abs() delattr() hash() memoryview() set()
all() dict() help() min() setattr()
any() dir() hex() next() slice()
ascii() divmod() id() object() sorted()
bin() enumerate() input() oct() staticmethod()
bool() eval() int() open() str()
breakpoint() exec() isinstance() ord() sum()
bytearray() filter() issubclass() pow() super()
bytes() float() iter() print() tuple()
callable() format() len() property() type()
chr() frozenset() list() range() vars()
classmethod() getattr() locals() repr() zip()
compile() globals() map() reversed() import()
complex() hasattr() max() round()

6.2 重要的内置函数

filter(function, sequence)

str = ['a', 'b','c', 'd']
 
def fun1(s):
    if s != 'a':
        return s
 
 
ret = filter(fun1, str)
 
print(list(ret))        # ret是一个迭代器对象  

对sequence中的item依次执行function(item),将执行结果为True的item做成一个filter object的迭代器返回。可以看作是过滤函数。

map(function, sequence)

str = ['a', 'b' ,'c','d']
# str = [1, 2, 'a', 'b']


def fun2(s):
    return s + "sean"


ret = map(fun2, str)

print(ret)  # map object的迭代器
print(list(ret))  # ['asean', 'bsean', 'csean', 'dsean']

对sequence中的item依次执行function(item),将执行结果组成一个map object迭代器返回.
map也支持多个sequence,这就要求function也支持相应数量的参数输入

def add(x,y):
    return x+y
print(list(map(add, range(10), range(10))))    # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

reduce(function, sequence, starting_value)

from functools import reduce

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


print(reduce(add, range(1, 101)))  # 5050 (注:1+2+...+99)

print(reduce(add, range(1, 101), 20))  # 5070 (注:1+2+...+99+20)

对sequence中的item顺序迭代调用function,如果有starting_value,还可以作为初始值调用。

7 lamda匿名函数

普通函数与匿名函数的对比

# 普通函数
def add(a,b):
    return a + b
 
print(add(2,3))
 
  
#匿名函数
add = lambda a,b : a + b
print(add(2,3))

匿名函数的命名规则,用lamdba 关键字标识,冒号左侧表示函数接收的参数(a,b) ,冒号右侧表示函数的返回值(a+b)。

因为lamdba在创建时不需要命名,所以叫匿名函数。

8 函数式编程与函数闭包

8.1 函数式编程

函数式编程也称作泛函编程,是一种编程范型,说白了就是实现可以把函数当参数传递给另一个函数,它将电脑运算视为数学上的函数计算,并且避免状态以及可变数据,函数式编程语言最重要的基础是lambda演算,而且lambda演算的函数可以接受函数当作输入和输出。

前面讲到的重要函数filter、map、reduce等都属于函数式编程。

常见的编程范式有命令式编程和函数式编程两种。

命令式编程案例:
假如,现在你来到 baidu面试,面试官让你把number =[2, -5, 9, -7, 2, 5, 4, -1, 0, -3, 8]中的正数的平均值,你肯定可以写出:

# 计算数组中正整数的平均值

number = [2, -5, 9, -7, 2, 5, 4, -1, 0, -3, 8]
count = 0
sum = 0

for i in range(len(number)):
    if number[i] > 0:
        count += 1
        sum += number[i]

print(sum, count)

if count > 0:
    average = sum / count

print(average)

首先循环列表中的值,累计次数,并对大于0的数进行累加,最后求取平均值。  

这就是命令式编程——你要做什么事情,你得把达到目的的步骤详细的描述出来,然后交给机器去运行。

这也正是命令式编程的理论模型——图灵机的特点。一条写满数据的纸带,一条根据纸带内容运动的机器,机器每动一步都需要纸带上写着如何达到。

那么,不用这种方式如何做到呢?

from functools import reduce

number = [2, -5, 9, -7, 2, 5, 4, -1, 0, -3, 8]

positive = list(filter(lambda x: x > 0, number))

average = reduce(lambda x, y: x + y, positive) / len(positive)

print(average)

这段代码最终达到的目的同样是求取正数平均值,但是它得到结果的方式和之前有着本质的差别:
通过描述一个列表 -> 正数平均值的映射,而不是描述“从列表得到正数平均值应该怎样做”来达到目的。

再比如,求阶乘。通过Reduce函数加lambda表达式式实现阶乘是如何简单:

from functools import reduce
print(reduce(lambda x,y: x*y, range(1,6)))

8.2 函数闭包

闭包叫lexical closure(词法闭包)。是指函数及相关的环境组成的整体。

闭包指的就是一个内层函数和所处的环境(外层函数)所构成的内容所组成的整体。

闭包只是在形式和表现上像函数,但事实上闭包自身并不是函数。

闭包从其表现的形式上可以解释为函数在嵌套环境中,如果在一个内层函数里对外层函数作用域中的变量进行了引用,那么在外层函数返回后,内层函数依然可以使用其外层函数中被引用的变量,这种变量就构成了内层函数可以使用的环境

def func1(x):               # 外层函数
    def func2(y):           # 内层函数
        return y ** x
    return func2

f4 = func1(4)
print(type(f4))
print(f4(2))
print(f4(3))

棋盘走位:

def startPos(m,n):
    def newPos(x,y):
        return "The old position is (%d,%d),and the new position is (%d,%d)." % (m, n, m + x, n + y)
    return newPos

action = startPos(10,10)

print(type(action))

print(action(1,2))

action = startPos(11,12)
print(action(3,-2))

对于外层函数中的变量施加了修改,内层函数也就相应的受到影响,所以说外层函数给内层函数提供了一个运行环境,这就叫做闭包。

9 函数高阶应用(装饰器)

装饰器定义:

  • 本质上是一个函数
  • 功能是用来装饰其他函数。就是为其他函数添加附加功能

装饰器 = 高阶函数 + 嵌套函数

装饰器特定的原则:

  • 不能修改被装饰的函数的源代码(线上环境)
  • 不能修改被装饰的函数的调用方式
  • 不能修改被装饰的函数的返回值

装饰器可以抽离出大量的函数中与函数无关的功能,把函数本身只作为一个核心,在必要时如果函数的核心功能不够,就用装饰器装饰一下本次调用所需要的功能,于是运行结束了,下次当需要其它功能时再用装饰器给重新装饰一下就可以了,这就是装饰器。

装饰器需要接受一个函数对象作为其参数,而后对此函数做包装,以对此函数进行增强。

9.1 实现装饰器的知识储备

9.1.1 函数即“变量”

#### 第一波 ####
def foo():
    print('foo')


# foo       表示是函数
# foo()     表示执行foo函数


#### 第二波 ####
def foo():
    print('foo')


foo = lambda x: x + 1

foo(10)  # 执行下面的lambda表达式,而不再是原来的foo函数,因为函数 foo 被重新定义了

9.1.2 高阶函数

把一个函数名当做实参传给另一个函数(可以实现在不修改被装饰函数源代码的情况下为其添加功能)

import time

def bar():
    time.sleep(3)
    print('in the bar')
    
def test1(func):
    start_time = time.time()
    func()
    stop_time = time.time()
    print('The func run time is %s'% (stop_time-start_time))
    
test1(bar)

返回值中包含函数名(可以实现不修改被装饰函数的调用方式)

import time

def bar():
    time.sleep(3)
    print('in the bar')
    
def test2(func):
    print(func)
    return func
    
x = test2(bar)    #此处也可以改成:bar = test2(bar)

bar()

当用bar = test2(bar)时,此处定义的bar变量名就会覆盖之前定义bar函数时生成的变量名bar。

如此的话,那之前定义的bar函数进行调用时就是使用新定义的bar变量名引用其在内存中的位置,从而达到不修改bar函数调用方式的目的。

9.1.3 嵌套函数

def bar():
    print('in the bar')


def foo(func):
    print('in the foo')

    def inner():
        return func()

    return inner


foo(bar)
# foo(bar)()

9.2 装饰器之不带参数的func(被装饰的函数)

def decorative(func):
    def wrapper():    # 定义一个包装器
        print("Please say something: ")
        func()        # 调用func,这个func是我们自己定义的
        print("No zuo no die...")
    return wrapper

@decorative    # 使用@符号调用装饰器
def show():    # 定义func,名字取什么都无所谓,它只是用来传给装饰器中的func参数
    print("I'm from Mars.")

show()

如上例所示,show函数本身只有一个print语句,而使用装饰器以后,就变成了三个print,这里的print可以改成任何其它的语句,这就是函数的装饰器。

9.3 装饰器之带参数的func(被装饰的函数)

def decorative(func):
    def wrapper(x):
        print("Please say something...>")
        func(x)
        print("no zuo no die...")
    return wrapper

@decorative
def show(x):
    print(x)

show("hello,mars.")

9.4 有参数的装饰器

一个参数的装饰器

def foo(func):
    def inner(arg):
        # 验证
        return func(arg)
    return inner

@foo
def bar(arg):
    print('bar')

两个参数的装饰器

def foo(func):
    def inner(arg1,arg2):
        # 验证
        return func(arg1,arg2)
    return inner

@foo
def bar(arg1,arg2):
    print('bar')

三个参数的装饰器

def foo(func):
    def inner(arg1,arg2,arg3):
        # 验证
        return func(arg1,arg2,arg3)
    return inner

@foo
def bar(arg1,arg2,arg3):
    print('bar')

不固定参数个数的装饰器

def foo(func):
    def inner(*args,**kwargs):
        # 验证
        return func(*args,**kwargs)
    return inner
 
@foo
def bar(arg1,arg2,arg3):
    print('bar')

一个函数可以被多个装饰器装饰:

def foo(func):
    def inner(*args,**kwargs):
        # 验证
        return func(*args,**kwargs)
    return inner
 
def foo1(func):
    def inner(*args,**kwargs):
        # 验证
        return func(*args,**kwargs)
    return inner
 
 
@foo
@foo1
def bar(arg1,arg2,arg3):
    print('bar')

想当于bar = foo(foo1(bar))

9.5 装饰器案例

9.5.1 为函数添加执行时间的装饰器函数

import time

def timmer(func):
    def wrapper(*args,**kwargs):
        start_time = time.time()
        a = func()
        stop_time = time.time()
        print('The func run time is %s'% (stop_time-start_time))
        return a
    return wrapper
    
@timmer
def foo():
    time.sleep(3)
    print('in the foo')
    
print(foo())

9.5.2 页面验证装饰器

假定有三个页面,现在要实现其中2个页面验证登录之后才能访问,另一个页面不用验证即可访问

首先定义三个页面函数:

def index():
    print('Welcome to index page')
    return 'from index page'
    
def home():
    print('Welcome to home page')
    return 'from home page'
    
def bbs():
    print('Welcome to bbs page')
    return 'from bbs page'

然后定义装饰器函数:

import getpass

user = 'sean'
passwd = 'abc123'
def auth(auth_type='local'):
    def out_wrapper(func):
        def wrapper(*args,**kwargs):
            if auth_type == 'local':
                username = input('Username: ').strip()
                password = getpass.getpass("Password: ").strip()
                if username == user and password == passwd:
                    print('authentication passed')
                    func(*args,**kwargs)
            elif auth_type == 'ldap':
                print('This is ldap authentication')
                func(*args,**kwargs)
        return wrapper
    return out_wrapper

接下来将装饰器分别应用于home函数与bbs函数:

def index():
    print('Welcome to index page')
    return 'from index page'

@auth('local')
def home():
    print('Welcome to home page')
    return 'from home page'

@auth('ldap')
def bbs():
    print('Welcome to bbs page')
    return 'from bbs page'

# 调用函数
index()
home()
bbs()

9.6 装饰器原理

写代码要遵循开放封闭原则,虽然在这个原则是用的面向对象开发,但是也适用于函数式编程,简单来说,它规定已经实现的功能代码不允许被修改,但可以被扩展,即:

  • 封闭:已实现的功能代码块
  • 开放:对扩展开发

对于装饰器的原理,这里以页面验证装饰器为例来讲解。

import getpass

user = 'sean'
passwd = 'abc123'
def auth(auth_type='local'):
    """
    :param auth_type: 接收装饰器的参数
    :return:
    """
    def out_wrapper(func):
        """
        :param func: 接收被装饰的函数
        :return:
        """
        # 接收被装饰函数的参数
        def wrapper(*args,**kwargs):
            """
            :param args: 收集被装饰函数的参数
            :param kwargs: 收集被装饰函数的关键字参数
            :return:
            """
            if auth_type == 'local':
                username = input('Username: ').strip()
                password = getpass.getpass("Password: ").strip()
                if username == user and password == passwd:
                    print('authentication passed')
                    func(*args,**kwargs)
            elif auth_type == 'ldap':
                print('This is ldap authentication')
                func(*args,**kwargs)
        return wrapper
    return out_wrapper
    
@auth('local')
def home():
    print('Welcome to home page')
    return 'from home page'

当写完这段代码后(函数未被执行、未被执行、未被执行),python解释器就会从上到下解释代码,步骤如下:

  1. def auth(auth_type='local'): ==>将auth函数加载到内存
  2. def out_wrapper(func): ==>将out_wrapper函数加载到内存
  3. @auth('local')

没错,从表面上看解释器仅仅会解释这三句代码,因为函数在没有被调用之前其内部代码不会被执行。

从表面上看解释器着实会执行这三句,但是 @auth('local') 这一句代码里却有大文章,@函数名 是python的一种语法糖。

如上例@auth('local')内部会执行一下操作:

  • 执行auth函数,并将 @auth('local') 下面的 函数 作为auth函数的参数,即:@auth('local') 等价于 @auth('local')(home)
# 所以,内部就会去执行:
    def out_wrapper(func):      # 此时的func为home,将home当作参数传给out_wrapper函数
        def wrapper(*args,**kwargs):
            if auth_type == 'local':
                username = input('Username: ').strip()
                password = getpass.getpass("Password: ").strip()
                if username == user and password == passwd:
                    print('authentication passed')
                    func(*args,**kwargs)      # 调用func函数,此时应该调用home()
            elif auth_type == 'ldap':
                print('This is ldap authentication')
                func(*args,**kwargs)      # 调用func函数,此时应该调用home()
        return wrapper      # 返回的wrapper,wrapper代表的是函数,非执行函数
    return out_wrapper      # 返回的out_wrapper,out_wrapper代表的是函数,非执行函数
  • 将执行完的 auth('local') 函数返回值赋值给@auth('local')下面的函数的函数名
auth('local')函数的返回值是:
    def out_wrapper(func):      # 此时的func为home,将home当作参数传给out_wrapper函数
        def wrapper(*args,**kwargs):
            if auth_type == 'local':
                username = input('Username: ').strip()
                password = getpass.getpass("Password: ").strip()
                if username == user and password == passwd:
                    print('authentication passed')
                    func(*args,**kwargs)      # 调用func函数,此时应该调用home()
            elif auth_type == 'ldap':
                print('This is ldap authentication')
                func(*args,**kwargs)      # 调用func函数,此时应该调用home()
        return wrapper      # 返回的wrapper,wrapper代表的是函数,非执行函数
    return out_wrapper

然后,将此返回值再重新赋值给 home,即:
新home = def wrapper(*args,**kwargs):
            if auth_type == 'local':
                username = input('Username: ').strip()
                password = getpass.getpass("Password: ").strip()
                if username == user and password == passwd:
                    print('authentication passed')
                    func(*args,**kwargs)      # 调用func函数,此时应该调用home()
            elif auth_type == 'ldap':
                print('This is ldap authentication')
                func(*args,**kwargs)      # 调用func函数,此时应该调用home()
        return wrapper      # 返回的wrapper,wrapper代表的是函数,非执行函数
 
所以,以后想要执行 home 函数时,就会执行 新home 函数,在 新home 函数内部先执行验证,\
再执行原来的home函数,然后将 原来home 函数的返回值 返回给了业务调用者。\
如此一来, 即执行了验证的功能,又执行了原来home函数的内容,\
并将原home函数返回值 返回给业务调用者。