目录
- 函数的定义
- 注释增强与函数对象
- 栈与栈帧(Stack Frame)
- 函数的参数
- 偏函数
- LEGB访问规则
函数的定义
大一学C语言时,老师说函数就是一个功能,后来学习C#后,老师却把函数又叫做方法,这里要有一定的区分,定义在类中的函数称为方法,单独分离出来的称为函数
函数叫function,可以理解为一个可以复用的功能,比如内置电池中的len()
#举例:内置电池中的len对象就是一个函数,其完成的功能是返回输入对象的元素个数
li=[1,2,3]
tu=(1,3,5)
di={1:'w'}
print(len(li),len(tu),len(di))
在python中,函数定义灵活,一般形式为:
def myfunc(param1,param2):
return param1+param2
直接写函数名得到的是对象本身,调用需要加括号与参数(),对于上面的函数myfunc,直接写函数名会得到对象本身
#>myfunc
#<function myfunc at 0x7fb9625941f0>
函数一定是有返回值的,尽管没有写关键字return,依然会返回一个NoneType类型对象:
注释增强与函数对象
在python3中,函数新增特性为注释增强,参数后加冒号‘:’,函数名后加‘->’,其对函数没有实际影响,单纯就是为了注释参数与函数返回的对象,举个例子:
def myfunc(param:'this is param')->'this is return note':
return param
对于函数对象的理解,先使用dir(function)
看一下函数包含的对象:
def myfunc(param:'this is param')->'this is return note':
return param
print(dir(myfunc))
注意到第一个__annotations__对象,其存储的就是注释增强这个特性,验证:print(myfunc.__annotations__)
得到结果为(以字典形式存储):
扩展阅读部分:下面要提及到函数对象中更为重要的__code__,它是函数调用的关键
借助dir(myfunc.__code__)
得到__code__下的对象:
在__code__下有以下几个必要的对象,并有各自的指向:
co_name:函数名
co_object:__code__本身
co_code:函数的字节码(面向解释器的字节码)
co_lnotab:字节码的与源码行号的偏移
co_filename:函数所在的文件名
co_stacksize:函数占用的栈大小
栈与栈帧(Stack Frame)
在“第一课.Python入门与环境介绍”那一篇里提到过,CPython有2个重要的栈:执行栈(存储指令和操作数)和块栈(存储循环和异常信息)
对于函数而言,python的栈又可被分为:用户栈和系统栈
1.用户栈:函数被调用时,会在栈中分配内存以保存变量
2.系统栈:用于机器执行,保存着底层的字节码
额外补充
栈对于指令执行有重要作用,栈与线程绑定,函数的调用需要线程栈存储的上下文和执行状态
重点
那python和栈帧及栈的关系到底是怎样的?
简单理解,首先定义了一个函数,当触发调用函数时,会自动在栈中新建栈帧,然后再把函数的参数和对应代码复制到栈帧中执行,并返回值,执行完毕后,栈帧被回收,相关的对象也被释放
从上文看出,两次调用时,函数的栈帧是不一样的:
#stackframe可以在函数退出后返回栈帧
def stackframe():
return id(locals())
#第一次调用
frame1=stackframe()
#第二次调用
frame2=stackframe()
print(frame1,frame2)
sys._current_frames()可返回当前栈帧,并用字典保存:
函数的参数
函数的参数分为6种,先从最简单的位置参数开始
1.位置参数
也叫定位参数,实际输入与定义时的顺序相匹配:
#位置参数:也叫定位参数,输入与参数一一匹配
def addfunc(x,y):
return x**y
2.关键字参数
关键字参数在定义函数时就已经有赋值,因此在调用函数时可以省略,为了避免和位置参数混乱,位置参数一定在参数列表的最前面:
def addfunc(x,y=6):
return x**y
关键字参数可以在调用时被覆盖,但最好加上参数名去覆盖:addfunc(2,y=8)
3.可选位置参数
有时会发现,内置电池中,某些函数的参数为[…],比如round()中就有[,ndigital],用中括号[…]包含的参数属于可选位置参数,调用时可加可不加(不要和关键字参数混淆,关键字参数是有赋值的),我们自己定义函数时是不能使用可选位置参数的,它只存在于python的内置电池的函数中
4.参数中出现的/符号与*符号
同样在内置电池中会看到,有的参数列表为(param1,/,*,param2=None)
,一个单独的/符号与*符号有着特殊的意义:
符号/
:在此之前的参数都被限制为位置参数
符号*
:在此之后的参数都被限制为关键字参数
与可选位置参数一样,这两个符号的参数只属于内置电池的函数,我们是不能用它来定义函数的
5.不定长位置参数(*args)
不定长位置参数也称为可变位置参数,不定长位置参数指的是,不限制位置参数的个数,在函数定义中用*param 表示(习惯上会写成*args):
def addfunc(param,*args):
print(args)
return sum(args)+param
addfunc(1,2,3,6,8,23)
容易看出,param就是位置参数,所以不定长位置参数应该是剩下的2,3,6,8,23;
通过print(args)
打印出此次调用中的不定长位置参数为:
可看出,不定长位置参数是用元组形式保存的
6.不定长关键字参数(**kwargs)
不定长关键字参数也称为可变关键字参数,有了前面不定长位置参数的理解,不定长关键字参数就比较容易了,不定长关键字参数可以不限制关键字参数的个数,在函数定义中用**param 表示(习惯上会写成**kwargs):
def information(**kwargs):
print(kwargs)
information(name='t',tel=139,height=178)
打印print(kwargs)
结果为:
可见,不定长关键字参数是用字典形式保存的
不定长位置参数与不定长关键字参数综合实例
先补充一个原则,无论如何,只要有位置参数,位置参数一定在参数列表最前面
下面看一个综合不定长位置参数和不定长关键字参数的实例:
def func(*args,**kwargs):
print(args)
print(kwargs)
li=[1,2,3,4]
di={'name':'t','tel':139,'height':178}
func(*li,**di)
在调用时,出现了没见过的符号*和**
,说明一下:*代表对列表或元组解包,**代表对字典解包,
相当于func(1,2,3,4,name='t',tel=139,height=178)
扩展:函数签名
函数签名针对同一文件下,两个重名的函数,通过获取参数列表判断如果调用函数的话,会执行哪个函数:
#函数签名
from inspect import signature
def func(param1,param2='value'):
return param1
def func(param1):
return 0
#获取签名
func_sig=signature(func)
print(func_sig)
对于两个重名函数func,打印签名(参数列表)为:
很显然,调用func(val)将会使用第二个func函数
解释:如果不通过签名判断,由于第一个func的param2是关键字参数已赋值,是可省略的,这样将分不清func(val)到底使用哪一个函数
偏函数
偏函数:固定了部分参数的函数,说实话,偏函数没多大作用,但是了解一下也好
int()内有个参数base,指转换前的数为几进制,a=0x24
,a是一个十六进制数,现要转换为十进制,如果每次都是十六进制转十进制,就每次都要写base=16,所以使用偏函数
import functools
a=0x24 #a是一个十六进制数,现要转换为十进制
hexint=functools.partial(int,base=16)
#下次调用直接用新函数hexint即可,hexint就是一个偏函数
hexint(str(a))
LEGB访问规则
E涉及嵌套函数,目前先说LGB三个空间,在以前的文章里大部分提到globals()和__builtins__,当在函数中时,会存在locals()空间(如果在函数外,locals()也存在,这时的locals()就是globals()空间)
假设有函数:
def func():
val=5
print(locals())
执行func(),可看到locals()空间保存了函数内部的对象:
locals()命名空间位于函数内,在最低层,因此,val变量也可以存在于G,B空间,但举个例子,如果B中有一个对象len,则locals(),globals()空间中就尽量不要再创建len对象,避免引起不必要的冲突;
因为访问规则体现在操作对象的顺序上,从locals()搜索到builtins,遇到对象直接就可以对其进行操作,这也是为什么执行时会先处理函数内部变量的原因
通过实例体会一下: