2020.9.11
小白学习,如有错误欢迎指点
参考自egon大佬
Python快速入门神器www.zhihu.com使用函数的原因:
函数的概念:事先将具有某一功能的代码块封装起来,然后将其内存地址赋值给函数名,函数名就是对这段代码的引用,每当需要用到这一功能的时候,就直接调用该函数即可。
定义函数语法:根据上面可知函数的概念必须遵循“先定义后调用”的原则。(和变量类似)
格式:
def 函数名(参数1,参数2,……):
"""文档描述"""
函数体代码
return 值
参数是函数的调用者向函数体传值的媒介,若函数体代码逻辑依赖外部传来的参数时则需要定义为有参函数,否则定义为无参函数
函数体为pass代表什么都不做,称之为空函数。空函数一般用于程序初始设计,通常编程要先想好程序都需要完成什么功能,然后把所有功能都列举出来用pass充当函数体“占位符”,使得程序的体系结构清晰且可读性强,然后可以统筹分工,从而提高开发效率。
函数的使用分为定义和调用两阶段
函数的调用形式可分为三种
①语句形式:直接加括号
foo()
②表达式形式:
m=my_min(1,2) #将调用函数的返回值赋值给x
n=10*my_min(1,2) #将调用函数的返回值乘以10的结果赋值给n
③函数调用作为函数参数的形式:
# 如:my_min(2,3)作为函数my_min的第二个参数,实现了取1,2,3中的较小者赋值给m
m=my_min(1,my_min(2,3))
若需要将函数体代码执行的结果返回给调用者,则需要用到return。return后无值或直接省略return,则默认返回None,return的返回值无类型限制,且如有多个返回值则会放到一个元组内
return是一个函数的结束的标志,一个函数可以有多个return,但只要执行到第一个return函数就会终止执行了,并把return值作为本次调用的结果返回。
#返回值也是值,所以如果调用函数时没有及时将返回值赋值给变量的话,就会被当做垃圾被清空
函数的参数分为形式参数和实际参数,简称形参和实参:
实参示例
①实参是常量
res=my_min(1,2)
②实参是变量
a=1
b=2
res=my_min(a,b)
③实参是表达式
res=my_min(10*2,10*my_min(3,4))
④实参可以是常量、变量、表达式的任意组合
a=2
my_min(1,a,10*my_min(3,4))
在调用有参函数时,实参(值)会赋值给形参(变量名)。在Python中,变量名与值只是单纯的绑定关系,而对于函数来说,这种绑定关系只在函数调用时生效,在调用结束后解除。
位置参数
位置即顺序,位置参数是指按顺序定义的参数,需要从两个角度去看:
def register(name,age,sex): #定义位置形参:name,age,sex,三者都必须被传值
print('Name:%s Age:%s Sex:%s' %(name,age,sex))
register("sakura","20","male") 3个位置参数(实参)
关键字参数
在调用函数时,实参可以是key=value的形式,称为关键字参数,凡是按照这种形式定义的实参,可以完全不按照从左到右的顺序定义,但仍能为指定的形参赋值
需要注意在调用函数时,实参也可以是按位置或按关键字的混合使用,但必须保证关键字参数在位置参数后面,且不可以对一个形参重复赋值
def register(name,age,sex): #定义位置形参:name,age,sex,三者都必须被传值
print('Name:%s Age:%s Sex:%s' %(name,age,sex))
register(sex='male',name='sakura',age=20) 3个关键字参数
register('sakura',sex='male',age=20) 位置参数和关键字参数混合使用
默认参数
在定义函数时,就已经为形参赋值,这类形参称之为默认参数,当函数有多个参数时,需要将值经常改变的参数可定义成位置参数,而值改变较少的参数可定义成默认参数。
例如编写一个注册学生信息的函数,如果大多数学生的性别都为男,那完全可以将形参sex定义成默认参数为男。如果定义时就已经为参数sex赋值,意味着调用时可以不对sex赋值,这降低了函数调用的复杂度
>>> def register(name,age,sex='male'): #默认sex的值为male
>>> register('tom',17) #大多数情况,无需为sex传值,默认值为male
Name:tom Age:17 Sex:male
>>> register('Lili',18,'female') #少数情况,可以为sex传值female
Name:Lili Age:18 Sex:female
需要注意:
>>> x=1
>>> def foo(arg=x):
... print(arg)
...
>>> x=5 #定义阶段arg已被赋值为1,此处的修改与默认参数arg无任何关系
>>> foo()
1
参数的长度可变指的是在调用函数时,实参的个数可以不固定。而在调用函数时,实参的定义无非是按位置或者按关键字两种形式,这就要求形参提供两种解决方案来分别处理两种形式的可变长度的参数
①可变长度的位置参数(*)
如果在最后一个形参名前加*号,那么在调用函数时,溢出的位置实参,都会被接收并以元组的形式保存下来赋值给该形参,如果实参是列表,仍然可以传值给该形参
>>> def foo(x,y,z=1,*args): #在最后一个形参名args前加*号
... print(x)
... print(y)
... print(z)
... print(args)
>>> foo(1,2,3,4,5,6,7) #实参1、2、3按位置为形参x、y、z赋值,多余的位置实参4、5、6、7都被*接收,以元组的形式保存下来,赋值给args,所以args=(4, 5, 6,7)
1
2
3
(4, 5, 6, 7)
>>> def foo(x,y,*args):
... print(x)
... print(y)
... print(args)
>>> L=[3,4,5]
>>> foo(1,2,*L) # *L就相当于位置参数3,4,5, foo(1,2,*L)就等同于foo(1,2,3,4,5)
1
2
(3, 4, 5)
注意:
如果在传入实参L时没有加*,那L就只是一个普通的位置参数了
>>> foo(1,2,L) #仅多出一个位置实参L
1
2
([1, 2, 3],)
如果形参为常规的参数(位置或默认),实参仍可以是*的形式
>>> def foo(x,y,z=3):
... print(x)
... print(y)
... print(z)
>>> foo(*[1,2]) #等同于foo(1,2)
1
2
3
如果我们想要求多个值的和,*args就派上用场了
>>> def add(*args):
... res=0
... for i in args:
... res+=i
... return res
>>> add(1,2,3,4,5)
15
②可变长度的关键字参数(**)
如果在最后一个形参名前加**号,那么在调用函数时,溢出的关键字参数,都会被接收并以字典的形式保存下来赋值给该形参,如果实参是字典,仍然可以传值给**kwargs
>>> def foo(x,**kwargs): #在最后一个参数kwargs前加**
... print(x)
... print(kwargs)
>>> foo(y=2,x=1,z=3) #溢出的关键字实参y=2,z=3都被**接收,以字典的形式保存下来,赋值给kwargs
1
{'z': 3, 'y': 2}
>>> def foo(x,y,**kwargs):
... print(x)
... print(y)
... print(kwargs)
>>> dic={'a':1,'b':2}
>>> foo(1,2,**dic) #**dic就相当于关键字参数a=1,b=2,foo(1,2,**dic)等同foo(1,2,a=1,b=2)
1
2
{'a': 1, 'b': 2}
注意:
如果在传入dic时没有加**,那dic就只是一个普通的位置参数了
>>> foo(1,2,dic) #TypeError:函数foo只需要2个位置参数,但是传了3个,**是用于关键字参数的
如果在传入dic时没有加**,将dic写成关键字参数
>>> foo(1,2,dic={'a':1,'b':2})#此时dic为一个普通的关键字参数
1
2
{'dic': {'a': 1, 'b': 2}}
如果形参为常规参数(位置或默认),实参仍可以是**的形式
>>> def foo(x,y,z=3):
... print(x)
... print(y)
... print(z)
>>> foo(**{'x':1,'y':2}) #等同于foo(y=2,x=1)
1
2
3
如果我们要编写一个用户认证的函数,起初可能只基于用户名密码的验证就可以了,可以使用**kwargs为日后的扩展供良好的环境,同时保持了函数的简洁性。
>>> def auth(user,password,**kwargs):
... pass
命名关键字参数(*)
在定义了**kwargs参数后,函数调用者就可以传入任意的关键字参数key=value,如果函数体代码的执行必须需要依赖某个key,此时可以通过命名关键字参数在函数调用时进行判断
想要规定函数的调用者必须以key=value的形式传值,Python3提供了专门的语法:需要在定义形参时,用*作为一个分隔符号,*号之后的形参称为命名关键字参数。对于这类参数,在函数调用时,必须按照key=value的形式为其传值,且必须被传值。命名关键字参数也可以有默认值,从而简化调用
>>> def register(name,age,*,sex,height): #sex,height为命名关键字参数
... pass
>>> register('lili',18,sex='male',height='1.8m') #正确使用
>>> register('lili',18,'male','1.8m') # TypeError:未使用关键字的形式为sex和height传值
>>> register('lili',18,height='1.8m') # TypeError:没有为命名关键字参数sex传值
命名关键字参数可以有默认值
>>> def register(name,age,*,sex='male',height):
... print('Name:%s,Age:%s,Sex:%s,Height:%s' %(name,age,sex,height))
>>> register('lili',18,height='1.8m')
Name:lili,Age:18,Sex:male,Height:1.8m
需要强调的是:sex不是默认参数,height也不是位置参数,因为二者均在后,所以都是命名关键字参数,形参sex="male"仅仅是命名关键字参数的默认值,因而即便是放到形参height之前也不会有问题。另外,如果形参中已经有一个args了,命名关键字参数就不再需要一个单独的*作为分隔符号了
>>> def register(name,age,*args,sex='male',height):
... print('Name:%s,Age:%s,Args:%s,Sex:%s,Height:%s' %(name,age,args,sex,height))
>>> register('lili',18,1,2,3,height='1.8m') #sex与height仍为命名关键字参数
Name:lili,Age:18,Args:(1, 2, 3),Sex:male,Height:1.8m
组合使用
综上所述所有参数可任意组合使用,但定义顺序必须是:位置参数、默认参数、*args、命名关键字参数、**kwargs
可变位置参数*args与可变关键字参数**kwargs通常是组合在一起使用的,如果一个函数的形参为*args与**kwargs,那么代表该函数可以接收任何形式、任意长度的参数
*args、**kwargs中的args和kwargs被替换成其他名字并无语法错误,但使用args、kwargs是约定俗成的
传参:在该函数内部还可以把接收到的参数传给另外一个函数(这在装饰器的实现中大有用处)
def wrapper(*args,**kwargs):
例子:
>>> def wrapper(*args,**kwargs):
... print(args)
... print(kwargs)
>>> wrapper(1,z=3,y=2)
(1,) #位置实参1被*接收,以元组的形式保存下来,赋值给args
{'z': 3, 'y': 2} #关键字实参z=3,y=2被**接收,以字典的形式保存下来,赋值给kwargs
>>>def func(x,y,z):
... print(x,y,z)
>>>func(*(1,),**{'z': 3, 'y': 2}) #等同于func(1,z=3,y=2)
1 2 3
以上两者结合起来:
>>> def func(x,y,z):
... print(x,y,z)
...
>>> def wrapper(*args,**kwargs):
... func(*args,**kwargs)
...
>>> wrapper(1,z=3,y=2) # 在为函数wrapper传参时,其实遵循的是函数func的参数规则
1 2 3
名称空间是存放名字与对象映射/绑定关系的地方。对于x=3,Python会申请内存空间存放对象3,然后将名字x与3的绑定关系存放于名称空间中,del x可清除该绑定关系
在程序执行期间最多会存在三种名称空间
伴随python解释器的启动/关闭而产生/回收,所以是第一个被加载的名称空间,主要用来存放一些内置的名字,比如内建函数名
print(max)
运行结果:
伴随python文件的开始执行/执行完毕而产生/回收,是第二个被加载的名称空间,文件执行过程中产生的名字都会存放于该名称空间中,如下名字
import sys #模块名sys
x=1 #变量名x
if x == 1:
y=2 #变量名y
def foo(x): #函数名foo
y=1
def bar():
pass
Class Bar: #类名Bar
pass
伴随函数的调用/结束而临时产生/回收,函数的形参、函数内定义的名字都会被存放于该名称空间中
def foo(x):
y=3 #调用函数时,才会执行函数代码,名字x和y都存放于该函数的局部名称空间中
综上所述:
按照名字作用范围的不同可以将三个名称空间划分为两个区域:
在局部作用域查找名字时,起始位置是局部作用域,所以先查找局部名称空间,没有找到,再去全局作用域查找:先查找全局名称空间,没有找到,再查找内置名称空间,最后都没有找到就会抛出异常
x=100 #全局作用域的名字x
def foo():
x=300 #局部作用域的名字x
print(x) #在局部找x
foo()
运行结果:
300
在全局作用域查找名字时,起始位置便是全局作用域,所以先查找全局名称空间,没有找到,再查找内置名称空间,最后都没有找到就会抛出异常
x=100
def foo():
x=300 #只在函数调用时才产生局部作用域的名字x
foo()
print(x)
运行结果:
100
ps:可以调用内建函数locals()和globals()来分别查看局部作用域和全局作用域的名字,查看的结果都是字典格式。在全局作用域查看到的locals()的结果等于globals()
在内嵌的函数内查找名字时,会优先查找自己所在层的局部作用域的名字,然后由内而外一层层查找外部嵌套函数定义的作用域,没有找到,则查找全局作用域
x=1
def outer():
x=2
def inner(): # 函数名inner属于outer这一层作用域的名字
x=3
print('inner x:%s' %x)
inner()
print('outer x:%s' %x)
outer()
运行结果:
inner x:3
outer x:2
在函数内,无论嵌套多少层,都可以查看到全局作用域的名字,若要在函数内修改全局名称空间中名字的值,当值为不可变类型时,则需要用到global关键字
x=1
def foo():
global x #声明x为全局名称空间的名字
x=2 #修改了全局名称空间中x的值
foo()
print(x)
运行结果:
2
当实参的值为可变类型时,函数体内对该值的修改将直接反映到原值上即可直接修改全局名称空间中名字的值
num_list=[1,2,3]
def foo(nums):
nums.append(5)
foo(num_list)
print(num_list)
运行结果:
[1, 2, 3, 5]
对于嵌套多层的函数,使用nonlocal关键字可以将名字声明为来自外部嵌套函数定义的作用域(非全局),nonlocal x会从当前函数的外层函数开始一层层去查找名字x,若是一直到最外层函数都找不到,则会抛出异常。
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函数引用
>>> func(1,2)
3
②函数可以作为容器类型的元素
>>> dic={'add':add,'max':max} # add和max函数(内存地址)作为字典的值
>>> dic
{'add': , 'max': }
>>> dic['add'](1,2)
3
③函数可以作为参数传入另外一个函数
>>> def foo(x,y,func):
... return func(x,y)
...
>>> foo(1,2,add) # add函数作为实参传给了func
3
④函数的返回值可以是一个函数
def bar():
return add
func=bar() #返回值add函数被func引用
func(1,2)
3
闭与包
基于函数对象的概念,可以将函数返回到任意位置去调用,但作用域的关系是在定义完函数时就已经被确定了的,与函数的调用位置无关。
x=1
def f1():
def f2():
print(x) #找到x=1
return f2
def f3():
x=3
f2=f1() #调用f1()返回函数f2
f2() #需要按照函数定义时的作用关系去执行,与调用位置无关
f3()
运行结果:
1
也就是说函数被当做数据处理时,始终以自带的作用域为准。若内嵌函数包含对外部函数作用域(而非全局作用域)中变量的引用,那么该“内嵌函数”就是闭包函数,简称闭包(Closures)
x=1
def outer():
x=2
def inner():
print(x) # 找到x=2
return inner
func=outer()
func()
运行结果:
2
可以通过函数的closure属性,查看到闭包函数所包裹的外部变量
>>> func.__closure__
(,)
>>> func.__closure__[0].cell_contents # 显示的是外部变量的值
2 |
“闭”代表函数是内部的,“包”代表函数外’包裹’着对外层作用域的引用。因而无论在何处调用闭包函数,使用的仍然是包裹在其外层的变量。(定义函数时已经固定)
闭包的用途
目前为止,我们得到了两种为函数体传值的方式,一种是直接将值以参数的形式传入,另外一种就是将值包给函数
import requests
提示:requests模块是用来模拟浏览器向网站发送请求并将页面内容下载到本地,
需要事先安装:pip3 install requests
#方式一:
def get(url):
return requests.get(url).text
#方式二:
def page(url):
def get():
return requests.get(url).text
return get
# 方式一下载同一页面
get('https://www.python.org')
get('https://www.python.org')
get('https://www.python.org')
……
# 方式二下载同一页面
python=page('https://www.python.org')
python()
python()
python()
……
对比两种方式,方式一在下载同一页面时需要重复传入url,而方式二只需要传一次值,就会得到一个包含指定url的闭包函数,以后调用该闭包函数无需再传url
闭包函数的这种特性有时又称为惰性计算。使用将值包给函数的方式,在接下来的装饰器中也将大有用处
装饰器的由来
软件的设计应该遵循开放封闭原则,即对扩展是开放的,而对修改是封闭的。
软件包含的所有功能的源代码以及调用方式,都应该避免修改,否则一旦改错,则极有可能产生连锁反应,最终导致程序崩溃,而对于上线后的软件,新需求或者变化又层出不穷,我们必须为程序提供扩展的可能性,这就用到了装饰器。
'装饰’是指为被装饰对象添加新的功能,'器’指器具/工具,装饰器与被装饰的对象均可以是任意可调用对象。装饰器的作用就是在不修改被装饰对象源代码和调用方式的前提下为被装饰对象添加额外的功能。装饰器经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等应用场景,装饰器是解决这类问题的绝佳设计,有了装饰器,就可以抽离出大量与函数功能本身无关的雷同代码并继续重用即为多个函数提供相同的功能
提示:可调用对象有函数,方法或者类,此处以函数来介绍函数装饰器,并且被装饰的对象也是函数
函数装饰器分为:无参装饰器和有参装饰两种,二者的实现原理一样,都是'函数嵌套+闭包+函数对象'的组合使用的产物。
什么时候需要用到装饰器:需要对源代码进行扩展时加装饰器
def timer(func): # 为函数体传参(闭包)
def wrapper(*args,**kwargs): # 引用外部作用域的变量func
(增加的功能代码)
res=func(*args,**kwargs) #满足被装饰函数有参数的情况
(增加的功能代码)
return res #wrapper之间就是对原函数func的装饰
return wrapper
@timmer
def foo():
time.sleep(3)
print('from foo')
foo()
当然我们定义好装饰器后,每次都需要事先执行一次timer将被装饰的函数传入,返回一个闭包函数wrapper重新赋值给变量名/函数名index,再执行(两步)
index=timer(index) #得到index=wrapper,wrapper携带对外作用域的引用:func=原始的index
index() # 执行的是wrapper(),在wrapper的函数体内再执行最原始的index
因此一般装饰器最多嵌套三层函数,最外层函数用于传增加的代码体中所需要的参数,中间传原函数,最内层传原函数所需的参数(固定为*args,**kwargs)
def auth(driver='file'): #传增加的代码体中所需要的参数
def auth2(func): #传原函数
def wrapper(*args,**kwargs): #传原函数所需的参数(固定为*args,**kwargs)
name=input("user: ")
pwd=input("pwd: ")
if driver == 'file':
if name == 'egon' and pwd == '123':
print('login successful')
res=func(*args,**kwargs)
return res
elif driver == 'ldap':
print('ldap')
return wrapper
return auth2
@auth(driver='file') # 此时语法糖先执行最外层的函数auth(driver='file'),返回出auth2,再使用语法糖语法
def foo(name):
print(name)
foo('egon')
对于被装饰之后的函数,使用help(函数名)来查看函数的文档注释,结果不是原函数,且在被装饰之后原函数其实变成了wrapper,查看原函数名.name也可以发现函数名变为wrapper,想要保留原函数的文档和函数名属性,需要修正装饰器,functools模块下提供一个装饰器wraps专门用来实现这件事(将@wraps(func)加在最内层函数正上方即可)
def timer(func):
def wrapper(*args,**kwargs):
start_time=time.time()
res=func(*args,**kwargs)
stop_time=time.time()
print('run time is %s' %(stop_time-start_time))
return res
wrapper.__doc__=func.__doc__
wrapper.__name__=func.__name__ #此方式来实现保留原函数属性过于麻烦
return wrapper
wraps的使用:
from functools import wraps
def timer(func):
@wraps(func) #加在最内层函数正上方
def wrapper(*args,**kwargs):
(增加的功能代码)
res=func(*args,**kwargs)
(增加的功能代码)
return res
return wrapper
语法糖(Syntactic sugar):也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。一般使用语法糖是通过降低程序的可读性来换取效率,无论是写程序的效率还是程序运行的效率,从而减少程序代码出错的机会,代码更简洁流畅。ps:语法糖不是什么坏事,第一个就是吃不吃随你,第二个就是最好要知道这些糖在底层都做了些什么
与之相对的还有一种名为语法盐,是指在计算机语言中为了降低程序员撰写出不良代码的一种设计,但其中仍会有潜藏错误存在的可能。这些特性强迫程序员做出一些不用于描述程序行为,而用来证明他们知道自己在做什么的额外举动。所以当你编程时,必须加上无关程序的代码,才不会报错时,那就是语法盐。语法盐的不足:使代码的可读性降低,占用空间
为了简洁而优雅地使用装饰器,Python提供了专门的装饰器语法来取代index=timer(index)的形式,需要在定义被装饰对象的正上方单独一行添加@timer,当解释器解释到@timer时就会调用timer函数,且把它正下方的函数名当做实参传入,然后将返回的结果重新赋值给原函数名
ps(重点):注意语法糖只能传一个参数,这就是有参装饰器要单独套一层函数传参数的原因
如果我们有多个装饰器,可以叠加多个
@deco3 # deco3的参数func3=wrapper2函数的内存地址,返回wrapper3的内存地址=index(装饰后的index)
@deco2 # deco2的参数func2=wrapper1函数的内存地址,返回wrapper2的内存地址供deco3使用
@deco1 # deco1的参数func1=最原始的那个index函数的内存地址,返回wrapper1的内存地址供deco2使用
def index():
pass
index()
先加载,后执行
以上代码语义等同于如下:
index=deco3(deco2(deco1(index))) # 先加载deco1中国内层函数以外的代码,再加载deco2,最后加载deco3即按自下而上的顺序依次加载代码
index() # 先执行wrapper3中的代码即deco3最内层的函数代码,再执行wrapper2,最后执行wrapper1即按自上而下的顺序依次运行代码
总结:遇到多个装饰器叠加,运行直接自上而下依次运行每个代码中的最内层函数,遇到func(*args,**kwargs),转而运行下方函数,到最下方后再依次返回至最上方(找到"心脏"即可)