函数从入门到超神

函数从入门到超神


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)

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函数返回值 返回给业务调用者。
posted @   姜翎  阅读(47)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示