函数
函数基本概念Python函数的分类函数的定义和调用形参和实参文档字符串(函数的注释)返回值函数也是对象(内存底层分析)变量的作用域全局变量局部变量局部变量和全局变量效率测试 参数的传递传递可变对象的引用传递不可变对象的引用浅拷贝和深拷贝传递不可变对象是浅拷贝 参数的几种类型位置参数默认值参数命名参数(关键字参数)可变参数强制命名参数 lambda表达式和匿名函数eval()函数递归函数嵌套函数(内部函数)nonlocal 和 global关键字LEGB规则参考资料:《Python编程 从入门到实战》+ 尚学堂Python400集
函数
回顾我们曾经在C语言中学过的函数,在编程过程中用到的次数真的是太多了,Python当中也是一样,它是可重用的程序代码块,且具有一致性,修改函数代码,则所有调用该函数的地方都能够得到体现
基本概念
一个程序由一个个任务组成;函数就是代表一个任务或者一个功能函数是代码复用的通用机制Python函数的分类
内置函数之前所使用的str()、list()、join()等函数,可以直接拿来使用的标准库函数
通过import语句进行导入库,使用其中定义的函数(如turtle海归绘图用的函数)第三方库函数
Python社区提供的高质量的库,需要下载安装再import导入使用用户自定义函数
用户自己去定义的函数
函数的定义和调用
基本格式:
def 函数名 ([参数列表]): ‘"文档字符串’’’ 函数体 / 若干语句
【注】
参数列表部分可选,可为空
-①圆括号内是形式参数列表,有多个参数则使用逗号隔开
-②形参不需要声明类型,也不需要指定函数返回值类型
-③无参数,也必须保留空的圆括号
-④实参列表必须与形参列表一一对应用三个单引号括起来的文档字符串可以作为对函数功能等的解释说明(相当于注释)Python在执行def时,会创建一个函数对象,并绑定到函数名变量上return返回值
-①如果函数体中包含return语句,则结束函数执行并返回值
-②如果函数体中不包含return语句,则返回None值调用函数之前,必须要先定义函数,即先调用def创建函数对象
-①内置函数对象会自动创建
-②标准库和第三方库函数,通过import导入模块时,会执行模块中的def语句
# 测试函数def is_prime(n):flag = 1for i in range(2,n):if n % i == 0:flag = 0breakif flag == 1:return Trueelse:return Falsenum = int(input("Input a number:"))if is_prime(num):print("{0}是素数".format(num))else:print("{0}不是素数".format(num))# 测试函数对象print(id(is_prime))print(type(is_prime))print(is_prime)
形参和实参
# 测试函数def printMax(a,b): # 形参(定义)if a > b:print(a,"较大值")else:print(b,"较大值")printMax(10,20) # 实参(调用)printMax(25,2)
文档字符串(函数的注释)
增强程序的可读性,三个单引号 / 三个双引号
# 测试函数def printMax(a,b): # 形参(定义)'''输入两个数,判断哪个值较大并打印较大值''' # 文档字符串if a > b:print(a,"较大值")else:print(b,"较大值")printMax(10,20) # 实参(调用)printMax(25,2)
还可以通过help(函数名._ _doc_ _)可以打印输出函数的文档字符串
help(printMax.__doc__)
返回值
return返回值要点:
如果函数体中包含return语句,则结束函数执行并返回值如果函数体中不包含return语句,则返回None值要返回多个返回值,使用列表、元组、字典、集合将多个值“存起来”即可
【例】
# 测试返回值# 有参 有返回值def add(a,b):print("求两数和:{0},{1},{2}".format(a,b,(a+b)))return (a+b)**2# 无参 无返回值def pri():print("Hello")print("Stefan")return # return两个作用:1.返回具体值 2.结束函数执行print("Yes") # return之后不会执行# 返回多个值def calc(x,y,z):return [x*3,y*4,z*5] # 返回一个列表s = add(7,9)print("两数和的平方:",s)print(add(10,20)*3)a = pri()print(a) # 无返回值 输出Noneprint(calc(5,7,9))
函数也是对象(内存底层分析)
Python中,一切即为对象,在执行def所定义的函数之后,系统就创建了相应的函数对象,以后调用时就直接调用已创建好的那个函数对象,而不需要再创建一个
【例】
def func01():print("sssddd")func01()a = func01a()print(id(func01))print(id(a))
内层分析示意图:
变量的作用域
变量的作用域,即变量的作用范围
全局变量局部变量
全局变量
函数和类定义之外声明的变量。作用域:从定义位置开始直到定义模块结束全局变量降低了函数的通用性和可读性(尽量避免使用全局变量)全局变量一般做常量使用函数内要改变全局变量的值,使用global关键字声明一下局部变量
在函数体中(包含形参)声明的变量局部变量的引用比全局要快,优先考虑使用如果局部变量和全局变量同名,则在函数内隐藏全局变量,只使用同名的局部变量【例】
#全局变量 局部变量a = 3 # 全局变量def func01():b = 4 # 局部变量print(b*3)func01()print(a)print(b) # 报错
内存分析:
【如果想在函数内部改变全局变量值,用global】
#全局变量 局部变量a = 3 # 全局变量def func01():b = 4 # 局部变量print(b*3)global a # 函数内要改变全局变量的值,使用global关键字声明a = 300func01()print(a)
【输出局部变量和全局变量】
#全局变量 局部变量a = 100c = 6def fun1(): # 函数名fun1也是全局b = 5global aa = 300print(locals()) # 打印输出的局部变量print(globals()) # 打印输出的全局变量fun1()print(a)
{‘b’: 5}
{‘name’: ‘main’, ‘doc’: None, ‘package’: None, ‘loader’: <_frozen_importlib_external.SourceFileLoader object at 0x0330AF40>, ‘spec’: None, ‘annotations’: {}, ‘builtins’: <module ‘builtins’ (built-in)>, ‘file’: ‘C:/Users/Administrator/PycharmProjects/mypro01/test_if.py’, ‘cached’: None, ‘a’: 300, ‘c’: 6, ‘fun1’: <function fun1 at 0x033E7FA0>}
300
局部变量和全局变量效率测试
这里又是一处可以进行优化的地方,尤其在循环的时候,在特别强调效率的地方或者循环次数较多的地方,可以通过将全局变量转为局部变量从而提高运行速度
# 测试局部、全局效率import mathimport timedef test01():start1 = time.time()for i in range(10000000):math.sqrt(30)end1 = time.time()print("耗时:{0}".format(end1 - start1))def test02():b = math.sqrtstart2 = time.time()for i in range(10000000):b(30)end2 = time.time()print("耗时:{0}".format(end2 - start2))test01()test02()
参数的传递
本质:实参——>形参
由于Python中一切都是对象,所以在Python中的参数传递都是“引用传递”(地址),而不是“值传递”
传递可变对象的引用
Python中,可变对象有:
字典、列表、集合、自定义的对象等
对“可变对象”进行“写操作”,直接作用于原对象本身
传递参数是可变对象时,实际传递的还是对象的引用。
在函数体中不创建新的对象拷贝,而是可以直接修改所传递的对象。
b = [10,20] # 可变对象def f1(m):print("m:",id(m))m.append(30) # m = b 地址相同f1(b)print("b:",id(b))print(b)
内存分析:
传递不可变对象的引用
Python中,不可变对象有:
数字(int、float)、字符串、元组、布尔值、function等
对“不可变对象”进行“写操作”,会产生一个新的“对象空间”,并用新的值填充这块空间(注意,只是起到其他语言“值传递”的效果,但本质上不是“值传递”)
传递参数是不可变对象时,实际传递的还是对象的引用
由于不可变对象无法修改,那么系统会新创建一个对象用于“赋值操作”,没有拷贝,依旧是引用。
a = 100def f2(n):print("n:",id(n))n = n + 200print("n:",id(n))print(n)f2(a)print("a:",id(a))
内存分析:
浅拷贝和深拷贝
浅拷贝:不拷贝子对象的内容,只是拷贝子对象的引用深拷贝:会连子对象的内存也全部拷贝一份,对子对象的修改不会影响源对象使用内置函数:copy(浅拷贝)、deepcopy(深拷贝)
浅拷贝与深拷贝区别示意图:
【测试浅拷贝】
# 测试 浅拷贝import copya = [10,20,[5,6]]b = copy.copy(a)print("a:",a)print("b:",b)b.append(30)b[2].append(7)print("----浅拷贝----")print("a:",a)print("b:",b)
【浅拷贝具体分析】
【测试深拷贝】
# 测试 深拷贝import copya = [10,20,[5,6]]b = copy.deepcopy(a)print("a:",a)print("b:",b)b.append(30)b[2].append(7)print("----深拷贝----")print("a:",a)print("b:",b)
【深拷贝具体分析】
传递不可变对象是浅拷贝
【注】
传递参数是不可变对象时(如int、float、字符串、元组、布尔值),实际传递的还是对象的引用,但在“写操作”时,会创建一个新的对象拷贝,要注意的是,这个拷贝使用的是“浅拷贝”,而不是“深拷贝”。
【测试】
a = (10,20,[5,6])print("a:",id(a))def f1(m):print("m:",id(m))m[2][0] = 888print("m:",m)print("m:",id(m))f1(a)print("a",a)
参数的几种类型
位置参数
函数调用时,实参默认按位置顺序传递,需要个数和形参匹配。
按位置传递的参数,称为:位置参数
默认值参数
我们可以为某些参数设置默认值,这样这些参数在传递时就是可选的,称为“默认值参数”。
默认值参数放在位置参数后面。
【例】
def func1(a,b,c = 10,d = 20):print(a,b,c,d)func1(8,9)func1(8,9,19)func1(8,9,19,29)
命名参数(关键字参数)
在调用函数时,实参除了通过位置一一对应以外,还可以按照形参的名称去传递参数
【例】
def func1(a,b,c):print(a,b,c)func1(8,9,19)func1(c = 8,a = 9,b = 19)
可变参数
可变参数:可变数量的参数
*param(一个星号),将多个参数收集到一个“元组”对象中**param(两个星号),将多个参数收集到一个"字典"对象中
【例】
def func1(a,b,*c): # c是一个元组print(a,b,c)func1(8,9,19,20,45,6)def func2(a,b,**c): # c是一个字典print(a,b,c)func2(4,8,name = 'Stefan',age = 24)def func3(a,*b,**c):print(a,b,c)func3(2,4,6,7,9,name = 'Kitty',age = 22,address = 'New York')
强制命名参数
要想在带星号的“可变参数”后面增加新的参数,必须是“强制命名参数”
【测试】
def func1(*a,b,c):print(a,b,c)func1(2,3,4) # 报错
【必须强制命名】
def func1(*a,b,c):print(a,b,c)func1(2,7,b = 3,c = 4)
lambda表达式和匿名函数
lambda表达式可以用来声明匿名函数。
lambda表达式是一种简单的、在同一行中定义函数的方法,它实际上是生成了一个函数对象
【注】lambda表达式只允许包含一个表达式,不能包含复杂语句,该表达式的运算结果就是函数的返回值。
基本格式:
lambda arg1,arg2,arg3…: <表达式>
【注】
arg1 / arg2 / arg3 为函数的参数<表达式> 相当于函数体运算结果:表达式的运算结果
# lambda测试f = lambda a,b,c : a + b + cprint(f)print(f(2,3,4))a = [lambda x:x ** 2,lambda y:y // 5,lambda z:z + 4]print(a[2](3),a[1](10),a[0](7))
【函数也是对象】
# lambda测试f = lambda a,b,c : a + b + cprint(f)print(f(2,3,4))a = [lambda x:x ** 2,lambda y:y // 5,lambda z:z + 4]print(a[2](3),a[1](10),a[0](7))g = [f,a] # 函数也是对象print(g[0](1,6,8))print(g[1][0](4),g[1][1](25),g[1][2](9))
eval()函数
eval()函数,将字符串str当成有效的表达式来求值并返回计算结果
基本语法:
eval(source [,globals [,locals]]) -> value
source:一个Python表达式 / 函数compile()返回的代码对象globals:可选,必须是字典locals:可选,任意映射对象
通俗来讲,就是可以将文件、以及别的地方的字符串(这个字符串是一段可运行代码)传过来,传到一个变量中,通过对这个变量访问来执行这段代码字符串,当成有效表达式来执行。
# eval()函数测试s = "print('Hello yaya')"eval(s)a = 10b = 20c = eval("a+b")print(c)dict1 = dict(a = 100,b = 200)d = eval("a+b")e = eval("a+b",dict1)print(d)print(e)
递归函数
自己调用自己
在函数体内部直接或间接得自己调用自己
每个递归函数必须包含两个部分:
递归终止条件:表示递归什么时候结束,一般用于返回值,不再调用自己递归步骤:把第n步的值和第n-1步相关联
【例1】
# 递归测试def f(n):print("f:",n)if n == 0:print("over")else:f(n-1)print("f**:",n)f(4)
【函数调用内存分析】
【例2】求阶乘
# 递归求阶乘def fac(n):if n == 1 or n == 0:return 1else:return n * fac(n-1)for i in range(8):print("{0}!= {1}".format(i,fac(i)))
嵌套函数(内部函数)
嵌套函数:
在函数内部定义的函数
# 嵌套函数def outer():print("outer running!")def inner01(): # 仅限内部调用print("inner01 running!")inner01()outer()
【注】一般在什么情况下需要使用嵌套函数呢?
封装(数据隐藏)
外部无法访问内部的“嵌套函数”贯彻DRY(Don’t Repeat Yourself)原则
函数内部避免重复代码闭包
nonlocal 和 global关键字
nonlocal:用来声明外层的局部变量global:用来声明全局变量【测试】
def outer():b = 10def inner():print("inner:",b)b = 20 # 无法修改 报错inner()outer()
def outer():b = 10def inner():nonlocal b # 声明外部函数的局部变量print("inner:",b)b = 20inner()print("outer b:",b)outer()
a = 100def outer():b = 10def inner():nonlocal b # 声明外部函数的局部变量print("inner:",b)b = 20global aa = 1000inner()print("outer b:",b)outer()print("a:",a)
LEGB规则
什么是LEGB?
Python在查找“名称”时,是按照LEGB规则查找的:
Local–> Enclosed–> Global–> Built in
Local:函数或类的方法内部Enclosed:嵌套函数(一个函数包裹另一个函数,闭包)Global:模块中的全局变量Built in:Python为自己保留的特殊名称(内置)
如果某个name映射在局部(local)命名空间中没有找到,接下来就会在闭包作用域(enclosed)进行搜索,如果闭包作用域也没有找到,Python就会到全局(global)命名空间中进行查找,最后会在内建(built-in)命名空间搜索(如果一个名称在所有命名空间中都没有找到,就会产生一个NameError)。
【测试】
# 测试LEGBprint(type(str))# str = "global"def outer():# str = "outer"def inner():# str = "inner"print(str)inner()outer()
【注】这里为什么不会报错是因为,Python中还有自动创建的内置函数str()在发挥作用