文章目录
Python函数用法和底层分析
Python函数的分类
1.内置函数
2.标准库函数:通过import导入库并使用里面的函数
3.第三方库函数:下载安装第三方库后,导入并使用里面的函数
4.用户自定义函数
函数的定义和调用
1.使用def定义函数。Python执行def时,会创建一个函数对象(存储在堆内存中),并绑定到函数名变量上。
2.参数列表:圆括号内是形式参数列表,形参不需要声明类型,也不需要指定函数返回值类型。即使无形参,也要有()。实参列表必须与形参一一对应。
3.若函数体中包含return语句,则结束函数执行并返回值;若无return语句,则返回None。
4.函数应先定义再调用。对于内置函数,其函数对象会自动创建;对于导入的库函数,通过import导入模块时,会执行模块中的def语句。
文档字符串(函数的注释)
建议在函数体开始的部分附上对函数的解释说明。用三个单引号或三个双引号实现,中间加入多行文字。
打印输出函数的文档字符串:help(函数名.doc)
函数也是对象(内存底层分析)
def test1():
print('haha')
c=test1
c()
执行def定义函数后,系统就在堆中创建了相应的函数对象,并通过test1这个变量进行引用,test1的值即为函数对象的地址。test1的值赋给了c,则c的值也为函数对象的地址,因此c也可用于引用该函数。
print(id(test1))
print(id(c))
print(c)
print(test1)
函数是对象,因此函数既可以作为参数传递,也可作为返回值。
变量的作用域(全局变量与局部变量)
变量作用域
变量起作用的范围。不同作用域内的同名变量不互相影响。
全局变量
1.在函数和类定义之外声明的变量。其作用域为定义的模块,从模块开始至模块结束。
2.全局变量降低了函数的通用性和可读性,因此应尽量避免全局变量的使用。
3.全局变量一般作为常量使用。
4.若在函数内需改变全局变量的值,要用global关键字声明。
局部变量
1.在函数体中(含形式参数)声明的变量
2.局部变量的查询和访问速度比全局变量快,应优先使用,尤其在循环的时候。在特别强调效率或循环次数多的情况下,可将全局变量转为局部变量以提高效率。
3.若局部变量与全局变量同名,则在函数内隐藏全局变量,仅使用同名的局部变量。
栈帧(stack frame)内存分析
调用函数时,系统在栈中开辟一块”栈帧“,内有局部变量的引用。函数调用完成后,栈帧便被删除。因此,有方法在函数内部访问全局变量,而函数外不可访问局部变量。
a=3
def test2():
b=10
print(b*10)
a=5
print(a)
print(a)
test2()
a=3
def test2():
b=10
print(b*10)
global a
a=5
print(a)
print(a)
test2()
a=3
def test2():
b=10
print(b*10)
a=5
print(a)
print(locals()) #查看函数内的局部变量(若要查看全局变量,则使用globals())
test2()
print(a)
局部变量和全局变量效率测试
import math
import time
def quanju():
start=time.time()
for i in range(1000000):
math.sqrt(30) #引用全局变量math.sqrt
end=time.time()
return end-start
def jubu():
a=math.sqrt #将全局变量math.sqrt转为仅在函数内部使用的局部变量a
start=time.time()
for i in range(1000000):
a(30) #引用局部变量a
end=time.time()
return end-start
print(quanju())
print(jubu())
由输出结果可知,引用局部变量比引用全局变量快。因此,可采用将全局变量转为局部变量的方法来提升效率。
参数的传递
函数的参数传递,本质即为由实参到形参的赋值操作。
实参和形参都是对象的引用。因此,Python函数中的参数传递并非“值传递”,而是“引用的传递”。
具体操作时分为两类:
传递可变对象的引用
1.对可变对象(字典、列表、集合、自定义的对象等)进行“写操作”,直接作用于原对象本身。在函数体中并不创建新的对象拷贝,而是直接修改所传递的对象。
b=[10,20]
def f(m):
print('m地址为:',id(m))
m.append(30)
return m
print(f(b))
print('b地址为:',id(b))
print(b)
分析:
第一步,在堆中创建列表对象,b为其引用。
第二步,在堆中创建函数对象,f为其引用。
第三步,调用函数,在栈中创建栈帧,内有m,并将b赋值于m,则m也成为该列表对象的引用。m和b的id一致。
第四步,执行函数,使列表对象发生变化。因此,打印b时,会发现输出的列表也发生了变化。
传递不可变对象的引用
2.对不可变对象(数字、字符串、元组、函数等)进行“写操作”,会产生一个新的“对象空间”,并用新的值填充这块空间。
b=100
def f(m):
print('m地址为:',id(m))
m+=200
print('m地址变为:',id(m))
return m
print(f(b))
print('b地址为:',id(b))
print(b)
分析:
第一步,在堆中创建整数对象100,b为其引用。
第二步,在堆中创建函数对象,f为其引用。
第三步,调用函数,在栈中创建栈帧,内有m,并将b赋值于m,则m也成为该列表对象的引用。m和b的id一致。
第四步,执行函数。整数对象本身并不会发生变化,因此系统在堆中创建一个新的整数对象300,m变为新整数对象的引用。于是,m的id发生变化,不再与b一致。而b仍为原整数对象100的引用.
传递包含可变子对象的不可变对象的引用
a=(10,20,[5,6])
print('a:',id(a))
def f1(m):
print('m:',id(m))
m[2][0]=10
print('m:',id(m))
f1(a)
print(a)
分析:
第一步,在堆中创建元组对象,a为其引用。
第二步,在堆中创建函数对象,f1为其引用。
第三步,调用函数,在栈中创建栈帧,内有m,并将a赋值于m,则m也成为该列表对象的引用。m和b的id一致。
第四步,执行函数。元组对象本身并不会发生变化,但其指向的列表子对象的子对象发生了变化。m仍是该元组对象的引用,因此id并不变化,但元组对象的子对象的子对象值发生了变化。于是,m、a的打印输出结果均发生变化。
浅拷贝、深拷贝
copy(浅拷贝):不拷贝子对象的内容,只拷贝子对象的引用。
import copy
a=[10,20,[5,6]]
b=copy.copy(a)
print('a:',a)
print('b:',b)
b.append(30)
b[2].append(7)
print('a:',a)
print('b:',b)
过程如下图所示:
b为a的浅拷贝,则b只复制了a所指向列表对象的引用。
deepcopy(深拷贝):连子对象的内存一起拷贝。对子对象的修改不会影响源对象。
import copy
a=[10,20,[5,6]]
b=copy.copy(a)
print('a:',a)
print('b:',b)
b.append(30)
b[2].append(7)
print('a:',a)
print('b:',b)
过程如下图:
b为a的深拷贝,则b连a的内存一起复制,此后对b的任何修改都不会对a产生影响。
参数类型
位置参数
按位置传递的参数称为位置参数。函数调用时,实参默认按位置顺序传递,需要个数和形参匹配。
默认值参数
为某些参数设定默认值。默认值参数放在位置参数之后。
如:def f(a,b,c=10,d=20):
print(a,b,c,d)
则f(2,3): 2 3 10 20
f(2,3,4): 2 3 4 20
f(2,3,4,5): 2 3 4 5
命名参数
按形参的名称传递参数。也称关键字参数。
如:def f(a,b,c,d):
print(a,b,c,d)
则f(b=3,a=2,d=5,c=4): 2 3 4 5
可变参数
1.*param(一个星号):将多个参数收集进一个元组对象。
如:def f1(a,b,*c):
print(a,b,c)
则f1(2,3,4,5): 2 3 (4,5)
2.**param(两个星号):将多个参数收集进一个字典对象。
如:def f2(a,b,**c)
print(a,b,c)
则f2(2,3,‘a’=‘bc’,‘d’=‘ef’): 2 3 {‘a’:‘bc’,‘d’:‘ef’}
强制命名参数
若在带星号的可变参数后添加新的参数,则必须是强制命名参数。
如:def f3(*a,b,c):
print(a,b,c)
若f3(2,3,4): 报错
若f3(2,b=3,c=4): (2,) 3 4
lambda表达式和匿名函数
lambda表达式可以用来声明匿名函数。lambda函数是一种简单的、在同一行内定义函数的方法,实际生成一个函数对象,
lambda表达式只能包含一个表达式,不可包含复杂语句,该表达式计算值就是函数返回值。
基本语法: lambda arg1,arg2,arg3…: <表达式>
arg1,arg2,arg3…即为函数参数,表达式相当于函数体。
若f引用lambda表达式,则f即为函数的引用。
f=lambda a,b,c:a*b*c
print(f)
print(f(1,2,3))
g=[lambda a:a*2,lambda b:b*3,lambda c:c*4]
print(g[0](2),g[1](3),g[2](4))
print(g)
eval()函数
功能:将字符串当作有效表达式,求值并返回计算结果
如:s=“print(‘abcd’)”
eval(s) : ‘abcd’
又如:a=‘10+20’
eval(a) : 30
又如:dict1=dict(a=100,b=200)
d=eval(‘a+b’,dict1)
print(d) : 300
递归函数调用内存分析
递归函数指的是在函数体内部直接或间接调用自己的函数。
递归函数必须包含以下两部分:
1.终止条件:表示递归什么时候结束。一般用于返回值,不再调用自己;
2.递归步骤:把第n步的值和第n-1步相关联。
print('digui:',n)
if n==0:
print('over')
else:
digui(n-1)
print('digui:',n)
digui(2)
内存分析:
第一步:调用函数,创建栈帧,内含对象2的引用。
第二步:函数执行至调用自身的语句时,创建第二个栈帧(压在第一个栈帧之上),内含对象1的引用。
第三步:函数执行至函数执行至调用自身的语句时,创建第三个栈帧(压在第二个栈帧之上),内含对象0的引用。
第四步:print(‘over’),函数停止调用自身,而是继续向下执行并输出。执行完毕后,第三个栈帧删除。
第五步:函数继续向下执行并输出,此时栈帧内含对象1的引用。执行完毕后,第二个栈帧删除。
第六步:函数继续向下执行并输出,此时栈帧内含对象2的引用。执行完毕后,第一个栈帧删除。函数执行完成。
注意:递归函数会大量创建函数对象,过量消耗内存和运算能力,处理大量数据时应谨慎使用。
嵌套函数(内部函数)
例:inner()的定义和调用都在outer()内部。
def outer():
print('outer running')
def inner():
print('inner running')
inner()
outer()
嵌套函数的功能:
1.封装-数据隐藏(外部无法访问内部函数)。
2.在函数内部避免重复代码。
3.闭包。
def ChineseName(name,family_name):
print('{0} {1}'.format(family_name,name))
def EnglishName(name,family_name):
print('{0} {1}'.format(name,family_name))
可通过嵌套函数,将以上两个函数写成一个函数。
def printName(is_Chinese,name,family_name):
def inner_print(a,b):
print('{0} {1}'.format(a,b))
if is_Chinese:
inner_print(family_name,name)
else:
inner_print(name,family_name)
printName(True,'hong','xiao')
printName(False,'hong','xiao')
内部函数可访问外部函数中的变量,但不可修改。若需在内部函数内修改,需使用nonlocal关键字对变量进行声明。
LEGB规则
Python在查找名称时,是按照LEGB规则查找的:Local(函数或类的方法内部)——>Enclosed(嵌套函数(一个函数包裹另一个函数,闭包))——>Global(模块中的全局变量)——>Built in(Python为自己保留的特殊名称)