python学习笔记DAY05(函数_作用域、装饰器)
这是我个人的学习笔记,都赖于(egon老师)的分享,以下是老师博客的原地址:
https://www.cnblogs.com/xiaoyuanqujing/articles/11640888.html
python函数
我们已经可以用之前学习的python知识写一些简单的程序了。但是随着功能增多,代码量加大,如果还是吧所有代码都放在一起,代码就会显得冗长,组织结构不清晰,可读性差,可维护性差。这时候我们把一些要重复使用功能编写成一个整体,什么时候使用直接调。就不用写重复的代码了。相当于自己创建了一个可以做具体工作的工具,什么时候再做同样的工作,直接使用工具就可以了。这个工具在python中被称为函数。
一、函数的基本使用
定义函数
函数的使用必须遵循“先定义,后调用”原则。
定义函数发生的事情:
- 申请内存空间,保存函数体代码
- 将上述的内存空间地址,绑定给函数名
- 定义函数不会执行函数体代码,但是会检测函数体语法
函数名就是对这段代码的引用,这和变量的定义是很相似的。如果没有定义函数直接调用,相当于直接引用了不存在的“变量名”。
def 函数名(参数1,参数2,……) # 函数名纯小写加下划线,最好定义成动词
“”“文档描述”“” # 简单描述函数的功能作用
函数体 # 实现功能代码
return # 返回值
- 有参函数:参数是函数的调用者向函数传递数据的媒介,若函数体内代码依赖于外部的数据时,就需要给函数定义参数。
def func (x,y):
print(x+y)
retrun x,y
- 无参函数:若函数体内代码不需要外部的数据时,函数就不需要参数。
def func ():
res = input("请输入您的用户名:")
retrun res
- 空函数:在程序设计开始,都要想好程序需要完成什么功能,然后列出所有功能函数,用pass来做函数体的占位符。这样就使得程序体系结构清晰明了。
def login()
"""用户登录功能"""
pass
def look()
"""用户查看功能"""
pass
逻辑梳理清楚之后,有选择的去实现功能替换掉pass,从而提高开发效率
调用函数及返回值
调用函数发生的事情:
- 通过函数名找到函数的内存地址。
- 在内存地址后方加上(),就是触发函数体代码的执行
定义阶段:
def func():
print("你好")
f1() # 在函数内部调用函数f1
def f1(): #定义f1函数
print("nida")
调用阶段: #调用函数func(),开始运行func()代码之后,调用f1(),执行f1()代码
func() #因为是先定义后调用所以在func()调用之前,f1已经在内存中,所以这时候运行没问题
执行结果:
你好
nida
- 调用形式:按照程序出现的形式和位置,可将函数的调用形式分为三种:
- 语句形式:func()
- 表达形式:f = func() # 将函数的返回值赋值给f
- 函数调用作为参数的形式:m=my_min(1,my_min(2,3)) # 用上一次的返回值当做这一次调用的一个实参传参
- 返回值:在函数体代码执行完毕之后,如果有结果需要输出到外部,需要return返回值。return后无值直接省略,默认返回None,return的返回值没有类型限制,可以将多个值返回成一个元组。
def func(x,y,z):
return x ,y ,z #返回值为(x,y,z)
return也是一个结束函数的标志,在函数体内可以有多个return,但是只要执行到一个return,函数就结束了。
二、函数的参数
参数,相当于你制造函数这个工具的时候外部传入的原料。参数分为两种,形参和实参。
形参:函数括号内放置的参数就是一个声明说这个函数需要参数,此时这个参数就相当于一个变量名。
实参:在调用函数时,括号内放置的值,就是要传给函数的真实原料,相当于给变量赋值。
def func(x): # 此时x为形参
print(x)
func(125) #此时125为实参(实参的形式是多种多样的)
在调用有参函数时,实参会赋值给形参。在python中,变量名和值就是单纯的绑定关系,而对于函数来说,这种绑定关系只在函数调用时生效,调用结束后解除关系
位置参数
位置参数就是按照位置从左往右定义的参数。
- 位置形参:在定义函数时,按照位置顺序依次定义的形参,为位置形参。凡是位置形参,就必须被传值,否则就会报错。
- 位置实参:在定义函数时,按照位置顺序依次定义的实参,为位置实参。凡是位置实参,就从左到右依次把值传给形参,一一对应。
关键字参数
在调用函数是,实参可以是(key = value)的形式,称为关键字参数。就是指定形参名为其传值,无论位置顺序。
在调用函数时,位置实参和关键字实参也可以混合使用,但是关键字实参必须在位置实参后面,不可以对一个形参重复赋值。
func(5,6,a=15) # 关键字实参必须在位置实参后面
默认参数
在定义函数时,形参就已经被赋值,这个形参被称为默认参数。当函数需要多个参数传入时可以把变化比较多的参数定义为位置参数,每次都有调用时为其传值,可以把变化少的参数给它一个默认值,设置为默认参数。
举个栗子:def class(name,age,sex = "女") 一个班级女生较多的时候,可以直接把性别默认值设为“女”,这样大多时候都不用穿sex这个参数了。
注意:默认参数必须在位置参数之后,默认参数的值通常为不可变类型
可变长度参数 (*与**的用法)
在调用函数的时候,需要传输的参数不确定,函数就无法设置对应数量的形参。而我们所要传的参数无非就是(位置)和(关键字)两种。所以有分别处理这两种参数的可变长度参数。
-
可变长度位置参数(*args)
def func(x,y,z,*args): # 设置三个位置形参,和可变长度形参
代码体
func(1,2,3,4,5,6,7,8,9,)# 按照位置参数,1,2,3分别传给x,y,z 之后,剩下多余的实参全部会被*接收,以元组形式赋值给args。
*在实参中的作用:
def func(x,y,z,*args): # 设置三个位置形参,和可变长度形参
代码体
func(1,2*[3,4,5,6,7,8,9])# 按照位置参数,1,2分别传给x,y之后,*把列表拆分成3,4,5,6,7,8,9。把3赋值给(z)把剩下所有赋值给(args)
-
可变长度关键字参数(**kwargs)
def func(x,y,z,**kwargs): # 设置三个位置形参,和可变长度形参
代码体
func(1,2,3,a=4,b=6)# 按照位置参数,1,2,3分别传给x,y,z 之后,剩下多余的关键字实参全部会被**接收,以字典形式赋值给kwargs。
**在实参中的作用:
def func(x,y,z,**kwargs): # 设置三个位置形参,和可变长度形参
代码体
func(1,2**{a=15,b=20,c=10})# 按照位置参数,1,2分别传给x,y之后,**把字典拆分成a=15,b=20,c=10。把a=15赋值给(z)把b=20,c=10赋值给(z,kwargs)
命名关键字参数
定义函数在参数中,位置在*号之后的参数,被称为命名关键字参数。
def register(name,age,*,sex='male',height): # sex 和 height 都是命名关键字参数。
各种类型参数组合使用
综上所述,所有参数可以任意组合使用,但是定义顺序必须是:位置参数,默认参数,*args,命名关键字参数,**kwargs
可变参数(*args,*kwargs)通常是组合在一起使用的,如果一个函数的形参为(*args,*kwargs),就表示该函数可以接受任意形式和任意长度的参数。
def func(x,y,z):
def wrapper(*args,**kwargs):
func(*args,**kwargs)
warpper(1,a=6,b=9)
如上代码,其实warpper所需要的参数是以func函数为准的。(实参1,被传给*args;实参{a=6,b=9},被传给**kwargs;在调用func时,实参1,被传给x;拆分后的a=6,b=9分别被传给y,z)
三、名称空间与作用域
名称空间
名称空间就是存放名字与对象绑定关系的位置。比如x=3,python会申请内存空间把3放入内存,然后把3的内存地址指向x,名称空间里的就是x对应3的内存地址的关系说明。del x之后,名称空间内的绑定关系就被删除了,但是内存里的3这个值需要等到没有引用时被python回收。而python中的名称空间分为三大块:
-
内置名称空间
python本身有很多自己的内置函数,这些内置函数的绑定关系存在于栈区内置名称空间中。比如:print()。内置名称空间的生命周期:python解释器的启动-------->关闭
-
全局名称空间
在程序代码中不属于内置名称空间也不属于局部名称空间的,就是全局名称空间。函数级别通常是顶级。比如:无缩进定义x=25。全局名称空间生命周期:python编写的程序文件执行-------->关闭
-
局部名称空间
在函数内部所定义的名字关系,被存放与局部名称空间。比如:def func():x=1。局部名称空间的生命周期:运行函数开始-------->运行函数结束
名称空间的加载顺序为:内置名称空间 > 全局名称空间 > 局部名称空间
如果要查找一个名字,只能从自身位置开始向外查找,查找顺序为: 局部名称空间 > 全局名称空间 > 内置名称空间
作用域
按照不同名称空间中的名字在代码中对不同区域起到的作用,可以分为两种:
-
全局作用域与局部作用域
内置名称空间和全局名称空间中的名称关系的作用域都是全局范围的,任意位置都可以使用。
局部名称空间中的名称关系的作用域只有自己所在函数本身,可以在自己函数内部随意使用。
-
作用域与名字查找的优先级
python支持函数的嵌套定义,在内嵌函数查找名字时,会先从自己函数内找,没有就去外层函数找,一层一层知道找完都没有,就开始----全局---内置 ,如果都没有找到就会报错。
input = 123 # 全局变量 def f1(): input = 456 # 局部变量(外) def f2(): input = 789 # 局部变量(内) print(input) f2() f1() #调用f1最后输出的input 被赋值的顺序为:789>456>123>input内存地址x= 11 # 全局定义变量x def func(): print(x) # 打印变量x对应值 x = 22 # 函数内重新定义 x # 结果报错:函数是先定义后执行的,所有在运行前,全局名称空间有x这个名字,局部名称空间有x这个名字。 # 现在开始运行,全局名称空间有了x与11的绑定关系,接着运行函数开始打印x,从自身开始找,找到了x这个名字, # 但是x这个名字没有绑定任何内存地址,所以就报错了 -
global
如果在函数内部需要用到全局变量时,当变量的值为不可变类型时,可以用global
x= 11 # 全局定义变量x
def func(): global x # 声明在func函数内的x 所引用的就是全局变量中的x。
print(x) # 打印的是全局变量x对应值
x = 22 # 修改的是全局变量x对应值如果全局变量为可变类型时,在函数内部可以直接更改
x=[5, 6, 7]
def func():
x.append(8) # 在函数内部没有重名的情况下,可以直接改全局变量的值
print(x) # 打印的是全局变量x改之后的值
-
nonlobal
对于多层函数,使用nonlobal,可以声明此名称引用为紧跟自己的外层函数定义的变量,不是全局。
def f1(): x=2 def f2(): nonlocal x x=3 f2() #调用f2(),修改f1作用域中名字x的值 print(x) #在f1作用域查看x f1() #结果 3
四、函数对象与闭包
函数对象
对象就是把一个事物当成一个完整的数据来使用,函数对象就是把函数当成一个数据来使用。具体可使用范围大致分为以下四种:
-
函数可以被引用。可以把函数当成一个变量来使用,例如:
def add(x,y):
return x+y
func = add # 不加括号的函数名就是一个函数的内存地址,赋值给一个变量名之后,
func(1,2) # 变量名加上括号就可以调用对应函数,与直接调用add() 效果一致
-
函数可以作为容器类型的元素
dic = {"a":1,"b":2,"c":3,"d":deco} # 将函数名作为字典的值 (其实放的就是函数对应地址) print(dic) # {'a': 1, 'b': 2, 'c': 3, 'd': <function deco at 0x00000138EEBA8F70>} -
函数可以作为参数传入另一个函数
def func(a,b): print(a,b) func(5,deco) # 函数的内存地址作为参数传给另一个函数 -
函数的返回值可以是一个函数名
def deco(): def func(): print(123) return func # 函数deco 的返回值是 func的内存地址 sum = deco() # 将func的内存地址加上括号,调用函数 sum() #结果123 -
综合实例
def mybook(): # 打开我的笔记列表 print("查看我的笔记") def myfriend(): # 打开我的朋友列表 print("查看我的朋友") def myfans(): # 打开我的粉丝列表 print("查看我的粉丝") dic = { # 将用户输入提示,用户输入对应数字说触发的功能函数 "0":["1,退出",None], # 全部放入一个字典 "1":["2,查看我的笔记",mybook], "2":["3,查看我的朋友",myfriend], "3":["4,查看我的粉丝",myfans] } while True: for d in dic: # 循环打印出字典的value的第一元素(操作提示) print(dic[d][0]) usernum = input(">>>>").strip() # 接收用户输入 if not usernum.isdigit(): # 判断用户的输入必须是数字 print("请输入数字") continue if usernum == "0": # 判断用户的输入如果是0 退出 print(dic[usernum][0]) break if usernum in dic: # 判断用户输入是对应字典的那个key,并执行对应value dic[usernum][1]() # 的第二个元素 加上括号执行 对应函数 break else: print("没有对应页面") break -
函数嵌套:在一个函数内调用另外一个函数。
# 两个数比大小 def max1(a,b): if a>b: return a else: return b # 四个数字比大小 def max(x,y,z,s): res = max1(x,y) res1 = max1(res,z) res2 = max1(res1,s) return res2 print(max(5,9,6,7))
闭包函数
闭包函数概念就是之前学的知识的综合(名称空间与作用域+函数嵌套+函数对象)
核心:名字的查找关系以函数定义阶段为准
-
闭包函数
“闭”函数,指的是该函数是外层还有函数。
“包”函数,指的是该函数有引用外层函数内的数据。(只是外层函数,不是全局名称空间)
def f1(): def f2(): print(" ") return #f2函数嵌套在f1中,所以f2是“闭”函数 def f3(): s = 10 def f4(): print(s) return #f4函数嵌套在f3中,且f4代码中有引用外层代码数据,所以f4是闭包函数 -
闭包的用途
到目前为止,我们学习了两种为函数体传参的方式,一种值直接以参数的形式传入,一种就是闭包函数,用外层函数为内部函数传参。
def f1(): a = 10 def f2(): print(a) return f2 # 将函数体内调用的f2函数的函数地址当做f1的返回值 f2 = f1() # 此刻f1()其实就是拿到的 f2的内存地址 又赋值个了叫f2的变量 f2() # 变量名加括号,调用的就是原来的f2函数。 # 这样就可以 把内嵌函数当成最外层函数随意调用 # 但是现在的f2 早已不是当初的f2了。只是名字一样而已
五、装饰器
"器":指的就是工具,我们之前有说过函数就是一种我们自己创造的工具。
"装饰":就是为其他事物增加额外的点缀。
"装饰器":是定义一个函数,该函数是用来为其它函数添加额外的功能。
装饰器的概念是基于之前所学的内容,一种综合的使用。
之前所学内容回顾(*args/**kwargs/名称空间与作用域/函数对象/函数嵌套定义/闭包函数)
代码的开放封闭原则
- 开放:指的是拓展新功能是开放的
- 封闭:指的是源代码的修改是封闭的
也就是说,我允许你增加我的功能,但是不许你动我一干汗毛。那装饰器就可以做到在不修改被装饰对象的源代码和调用方式的前提下,对其增加新的功能。
无参装饰器
实例:
# 在不修改源代码的基础上为其增加统计运行时间的功能 def index(x,y): print(x+y) time.sleep(3) return "我是index返回值",x+y # 源代码index def home(x,y): print(x+y) time.sleep(2) return "我是home返回值",x+y # 源代码home def outter(func): # 创建装饰器outter def times(*args,**kwargs): # times 可以接受任何形式的参数 start = time.time() res = func(*args,**kwargs) # times接收的参数,原封不动传给func stop = time.time() # 也就是说times 的参数限制是根据func的限制来定的 print(stop-start) return res # 拿到func的返回值,再当做times的返回值 return times # outter返回的是times的内存地址 index = outter(index) # 所以调用outter就是调用 times index(4,6) # 结果 10 \n 3.00020694732666 # 此时已经完成了对源代码 统计运行时间功能的添加 且并没有动源代码。表面上看我们依然调用的是index,但是这个index只是times的化身。
-
无参装饰器模板
由上方的实例我们大概可以总结出无参装饰器的模板:
def outter(func): def times(*args,**kwargs): res = func(*args,**kwargs) return res return times index = outter("源代码内存地址") "源代码内存地址"("源代码需要参数")
语法糖
@outter # @outter相当于 index = outter(index) def index(x,y): print(x+y) time.sleep(3) return "我是index返回值",x+y
在源代码的上方@装饰器的名字,就相当于把下方的函数名字作为参数传给装饰器,并赋值给同下方函数名一样的变量。
注意:使用语法糖时,装饰器本身必须在语法糖代码之前的位置。
补充知识:除了装饰器要模仿源代码的 名称/返回值 源代码还有很多自身的属性。(from functools import wraps)导包之后,在装饰器语法糖和源代码之间增加@wraps 就可以同步源代码属性了
有参装饰器
经过之前的学习我们发现,login和outter函数都已经有固定参数,且不可更改。如果login(装饰器最内层函数)还需要其它函数,就需要再次闭包函数进行传参。
实例:
from functools import wraps def deco(db_type): # login如果除了源函数之外,还需要其它参数,就只能靠再外一层的函数传递了。就是有参装饰器 def outter(func): # 模仿源函数的login需要模仿对象的内存地址,而它自己有无法接收参数,所以需要outter传给它 @wraps(func) # 为login添加模仿源函数所有属性的语法糖,以假乱真 def login(*args,**kwargs): # 装饰器最内层函数,模仿源函数和增加功能,都是在其内部完成的 username = input("请输入您的用户名:").strip() userpwd = input("请输入您的密码:").strip() if db_type == "file": with open("name",mode="r",encoding="utf-8") as f: for line in f: name,pwd = line.strip("\n").split(":") if username == name and userpwd == pwd: print("恭喜你,登录成功") res = func(*args, **kwargs) return res else: print("您输入的用户名或密码错误") elif db_type == "mysql": print("基于mysql验证成功") res = func(*args, **kwargs) return res elif db_type == "oracle": print("基于oracle验证成功") res = func(*args, **kwargs) return res else: print("不支持此来源") return login return outter @deco("file") def index(x,y): print(x+y) return "我是index返回值",x+y @deco("mysql") def home(x,y): print(x-y) return "我是home返回值",x+y # 对源代码增加只能是登录用户才可以使用此功能 # 但是各用户数据存储的位置不一样,所以要进行不同的验证 # outter = deco("file") # index = outter(index) index(10,2) home(10,2)
有参装饰器思考过程:其实直接在outter放入源函数和其它(装饰器内层函数warpper)所需要的参数,也可达到我们最终想要的结果。但是为什么有参装饰器必须要再包一层传参给最内层函数呢?
- 在outter只需要一个被装饰对象的内存地址的时候,我们可以直接使用语法糖。(@outter语法糖把下方的函数名作为参数拿过来,outter(被装饰对象地址)---->warpper(2,3)--->把warpper的名字改为,被装饰对象函数名)
- 那如果outter被穿了两个参数,这个时候就没有办法使用语法糖了。因为此时语法糖无法处理多出来的参数。
def outter(func,a): def warpper(*args,**kwargs): print(a) res = func(*args,**kwargs) return res return warpper index = outter(index,2) index(2,3)
-
有参装饰器模板
def deco(a): def outter(func): def warpper(*args,**kwargs): print(a) res = func(*args,**kwargs) return res return warpper return outter
在外层加上deco函数以后,就可以在多加参数的情况下,使用语法糖了。(@deco(a)--->deco携带参数a----->outter(被装饰对象)----warpper---->warpper拿到(被装饰对象和另一个参数)---->修改warpper名称为---->被装饰对象的名字)
叠加多个装饰器
我们已经学会了语法糖的用法,如果一个代码需要多个装饰器时,语法糖该怎么用呢?
多层装饰器运行逻辑分析:
def outter3(func):
def warpper3(*args,**kwargs):
print("我是第三个装饰器")
res = func3(*args,**kwargs)
return res3
return warpper3
def outter2(func):
def warpper2(*args,**kwargs):
print("我是第二个装饰器")
res = func2(*args,**kwargs)
return res2
return warpper2
def outter1(func):
def warpper1(*args,**kwargs):
print("我是第一个装饰器")
res = func1(*args,**kwargs)
return res1
return warpper1
@outter3 | outter3(warpper2)-------->warpper3--------->warpper2 = warpper3
拿下方函数名当参数 得到返回值wapper3 把warpper3名字改为下方函数名
@outter2 | outter2(warpper1)-------->warpper2--------->warpper1 = warpper2
拿下方函数名当参数 得到返回值wapper2 把warpper2名字改为下方函数名
@outter1 | outter1(index)-------->warpper1--------->index = warpper1
拿下方函数名当参数 得到返回值wapper 把warpper名字改为下方函数名
def index(x,y):
print(f"我是不可变函数{x+y}")
index(2,3)
# 结果:
# 我是第三个装饰器
# 我是第二个装饰器
# 我是第一个装饰器
# 我是不可变函数5
结论:加载顺序是自下而上,运行顺序是自上而下。(从最顶层的装饰器开始运行,开始运行一行一行的装饰器,知道遇到最终被装饰对象,装饰器的工作完成)

浙公网安备 33010602011771号