函数从入门到超神
函数从入门到超神
1 函数的基本概念
Python中,可通过固定的格式来定义函数。函数有具体的组成部分
- 函数名
- 函数体
- 参数
- 返回值
从分类上看,函数可分为
- 内置函数
- 自定义函数
为了实现不同的编程需求,还可为函数加上各种规则及作用域的限制,以完成整个功能
1.1 函数的作用
背景提要
现在老板让你写一个监控程序,监控服务器的系统状况,当cpu\memory\disk等指标的使用量超过阀值时即发邮件报警,你掏空了所有的知识量,写出了以下代码
while True:
if cpu利用率 > 90%:
#发送邮件提醒
连接邮箱服务器
发送邮件
关闭连接
if 硬盘使用空间 > 90%:
#发送邮件提醒
连接邮箱服务器
发送邮件
关闭连接
if 内存占用 > 80%:
#发送邮件提醒
连接邮箱服务器
发送邮件
关闭连接
上面的代码实现了功能,但是重复代码太多了,每次报警都要重写一段发邮件的代码,太low了,如此做存在2个问题:
- 代码重复过多,一个劲的copy and paste不符合高端程序员的气质
- 若日后需要修改发邮件的这段代码,比如加入群发功能,那你就需要在所有用到这段代码的地方都修改一遍
解决这个问题的方法其实很简单,我们只需要把重复的代码提取出来,放在一个公共的地方,起个名字,以后谁想用这段代码,就通过这个名字调用就行了,如下
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解释器就会从上到下解释代码,步骤如下:
- def auth(auth_type='local'): ==>将auth函数加载到内存
- def out_wrapper(func): ==>将out_wrapper函数加载到内存
- @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函数返回值 返回给业务调用者。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!