一.函数是什么
函数在数学中表示一种“映射关系”,在中学中学生们会接触到各种各样的函数,例如:三角函数(sin, cos, tan),幂函数,对数函数。而在编程中,函数表示一段可以被重复使用的代码?
重复使用?那为什么不 ctrl + c, ctrl + v呢?这样不是更方便吗?
1.复制代码,必须在细节上进行调整,多次尝试,在数据不同时尤为明显。
2.一旦复制的代码需要调整,复制了几次就要调整几次,很容易漏掉。
因此,尽量不要复制代码,而是做好代码“重用”。
我们引入一个代码案例,用于代码求和。
# 1. 求 1-100 的和
theSum = 0
for i in range(1,101):
theSum += i
print(theSum)
# 2. 求 300-400 的和
theSum = 0
for i in range(300, 401):
theSum += i
print(theSum)
# 3. 求 1-1000 的和
theSum = 0
for i in range(300, 401):
theSum += i
print(theSum)
# 定义一个求和函数
def calcSum(beg, end):
theSum = 0
for i in range(beg, end + 1):
theSum += i
print(theSum)
#调用函数
#求 1-100 的和
calcSum(1, 100)
#求 300-400 的和
calcSum(300, 400)
#求 1-1000 的和
calcSum(1, 1000)
从中我们不难看出,如果是复制代码我们需要反复修改参数,而定义一个函数只需要传入参数即可,后续我们修改函数,所有的调用也会同步修改。
二.函数的定义和调用
1.函数的定义(分配任务)
格式:
def 函数名(形参列表)
函数体
return 返回值
def:define(定义)
形参列表:形参列表中,可以有多个形参(形式参数),多个形参之间可以使用 逗号 分隔。
函数体:函数体要带有一级缩进~(带有缩进的代码,才是函数内部的语句)
返回值:函数执行到 return 就意味着执行完了,return后面的值,就是函数的返回值~ return 语句 并不是必须的,可以有也可以没有。
2.函数的调用(开始完成任务)
函数名(实参列表)
实际参数:简称实参。此处的实参的个数要和形参的个数相匹配。
返回值 = 函数名(实参列表)
# 如果是定义,而不去调用,则函数体里面的代码就不会执行
def test():
print('hello')
print('hello')
print('hello')
# 函数调用才会真正执行函数体里面的代码
# 函数经过一次定义之后,可以被调用多次!
test()
test()
test()
test()
三.函数的参数
举个例子:
我有一个朋友 (函数的形参)
高中的时候,是一个学霸,同桌是一个非常漂亮也很可爱的女生,他们的关系非常好,但是因为一些原因,最终没能够走到最后。
这个朋友,就是XXX(此处为人名,也就是函数的实参)
函数的调用是可以有很多次的,同时每次调用的实参,也可以是可以不同的。函数的实参,就是在函数调用的时候,赋值给形参~
关于传递参数的几个问题:
1)少传递参数:
def test(a, b, c):
print(a, b, c)
test(10)
形参有三个,传递参数时就要传递三个进去,否则就会报错。
2)函数中可以传递各种参数类型,只要满足函数体里卖弄的操作就行。
def add(x, y):
return x + y
print(add(10, 20))
print(add(1.5, 2.5))
print(add('hello', 'world'))
四.函数的返回值
1.什么是返回值
要理解函数的返回值,我们首先要理解输入输出的概念,广义上的输入输出并不仅仅是从键盘输入,也不是从显示器输出。
举个工厂制造手机的例子:
原材料:芯片,电池,摄像头,外壳......[输入]
产品:功能完备的手机。 [输出]
也就是吃进去的是草(输入),挤出来的是奶(输出)。
2.耦合
我们引入两段代码:
def calcSum(beg, end):
theSum = 0
for i in range(beg, end + 1):
theSum += i
print(theSum)
calcSum(1, 100)
def calcSum(beg, end):
theSum = 0
for i in range(beg, end + 1):
theSum += i
return theSum
result = calcSum(1, 100)
print(result)
改进之后的代码,在 calcSum 函数内部,只是进行了计算,而把打印的逻辑放到了函数的外面,calcSum 把计算结果当作返回值,返回给“函数调用者”,即把函数里面计算好的 5050 赋值给了 result 变量。
在实际开发中,一般更倾向于第二种写法!
一个通用的编程规则:一个函数只做一件事!
第一个版本,既做了计算,又做了打印(和用户交互)
第二个版本,只是做了计算,不关心如何和用户交互!
一旦后续需要改变和用户交互的方式,第二种写法就更有优势(不必修改代码),让 逻辑和交互/界面分离-->进一步的好处:解耦合。
一个稍微复杂一些的程序中,经常会涉及多个模块,模块之间可能要进行交互,交互就会带来耦合,当然希望通过良好的设计让耦合尽量降低。
举个例子:
我和我女朋友,我们的耦合就比较强,一方产生变动,对另一方,影响就比较大。
我和我的女神,耦合几乎为 0,最多是点赞之交。
如果女神生病了,对我来说没啥影响,如果我女朋友生病了,那对我的影响就大了。
3.多个返回值
一个函数可以有多个返回值。
def test():
return 1
return 2
# 一般多个 return 语句是搭配 分支语句 / 循环语句的
def isOdd(num):
'''
用来判定 num 是不是奇数! 如果是奇数就返回 True, 不是就返回 False
:param num: 要判定的整数
:return: 返回 True False 表示是不是奇数
'''
if num % 2 == 0:
return False
return True
def isOdd(num):
'''
用来判定 num 是不是奇数! 如果是奇数就返回 True, 不是就返回 False
:param num: 要判定的整数
:return: 返回 True False 表示是不是奇数
'''
if num % 2 == 0:
return False
else:
return True
print(isOdd(10))
print(isOdd(19))
这两段代码逻辑是等价的!当函数执行到 return 的时候就不再继续执行下去了,而是回到了调用位置~~(函数结束了)
把 return True 写到 if 外面,意味着 条件是否满足 都会执行 return True 但是再仔细观察,就发现 条件满足之后,if 里面有一个 return 了。一旦条件满足后,是没有机会执行到 return True。
Python 中的一个函数可以返回多个值!这是一个非常香的特性,能馋哭C++和Java。对于他俩而言调用一个函数一次只能返回一个值,C++要想返回多个值,可以通过输出型参数(指针/引用),Java中要想返回多个值,需要把多个值给包装成一个对象,返回这个对象。
def getPoint():
x = 10
y = 20
return x, y
_, b = getPoint()
print(_, b)
虽然现在返回了多个值,但是我只想用其中的一部分,不关注其他的~~,可以使用 _ 来进行占位!
不要 x 了,只要 y ,把 y 赋值给 b 即可!
五.函数的作用域
1.作用域
def getPoint():
x = 10
y = 20
return x, y
x, y = getPoint()
print(x, y)
函数里面的 x y 和函数外面 x y 是同一组变量吗?
他们是不同的变量,只不过名字恰好相同!
就像我管我女朋友叫“宝贝”,我的朋友也管他的女朋友叫做“宝贝”。
这就叫做变量的作用域,一个变量名的有效范围是一定的!只在一个固定的区域内生效。
所以函数内部的变量名,这能在函数内部生效,出了函数,就无效了。函数内部和函数外部是可以使用同名变量的,虽然变量名相同,但是是不同的变量。
2.局部变量和全局变量
x = 10
def test():
x = 20
print(f'函数内部:{x}')
test()
print(f'函数外部:{x}')
上述代码的 x = 10 为全局变量,是在整个程序中都是有效的。
而 x = 20 是局部变量,只是在函数内部有效的。
函数里,也是可以使用全局变量的。在函数里尝试读取全局变量,是可以的!当函数中尝试访问某个变量时,会先尝试在局部变量中查找,如果找到,就直接访问,如果没找到,就会往上一级作用域中进行查找,test 再往上一级作用域,就是全局了~~
x = 10
def test():
print(f'x:{x}')
test()
global 关键字可以帮助我们在函数中修改全局变量的值。
x = 10
def test():
global x
x = 20
test()
print(f'x = {x}')
没有 global ,此时就会把 x = 20 当作是在函数内部创建一个局部变量 x 而实际上是要修改全局变量 x ,为了让 函数里面 知道 x 是个全局变量,就是用 global 关键字先声明一下。
3.其他代码块中的作用域
if, else, while, for 这些关键字也会引入“代码块”,但是这些代码块不会对变量的作用域产生影响。在上述语句代码块内部定义的变量,可以在外面被访问。
for i in range(1,5):
print(i)
print('---------------')
print(i)
if True:
x = 10
print(x)
六.函数的执行过程
def test():
print('执行函数体代码')
print('执行函数体代码')
print('执行函数体代码')
print('111111')
test()
print('222222')
test()
print('333333')
def test():
print('执行函数体代码')
print('执行函数体代码')
print('执行函数体代码')
执行这几段代码,只是在定义函数,不会执行函数体的内容。
执行到函数调用的时候,就会跳转到函数体内部来进行执行,当函数内部执行完毕(运行完了/遇到 return),就回到之前调用的位置,继续往下执行。
为了更好的观察函数的执行过程,我们可以画图,也可以使用调试器。调试执行,相比于正常的运行,最大的区别,可以随时停下来,方便程序员观察程序的中间过程。
先给第一个“print”这里加一个断点,然后进行调试执行,在断点处,程序就暂停了~就可以让代码“单步执行”。
单机左侧的行数即可打上断点。
上述图片蓝色高亮部分,为函数的调用栈,描述了当前代码是怎么跳转过去的(进一步的也就是函数之间的调用关系)。
七.函数的链式调用和嵌套调用
1.链式调用
def isOdd(x):
x = x
def add(a, b):
return a + b
print(isOdd(10))
print(isOdd(add(5, 5)))
用一个函数的返回值,作为另一个函数的参数!
链式调用中,是先执行()里面的函数,后执行外面的函数。换句话说,调用一个函数,就需要先对他的参数求值。
2.嵌套调用
def a():
print('函数 a')
def b():
print('函数 b')
a()
def c():
print('函数 c')
b()
def d():
print('函数 d')
c()
d()
上述代码为函数的嵌套调用,就是在函数体的内部调用其他的函数。
七.局部变量和栈帧
def a():
num = 10
print('函数 a')
def b():
num = 20
a()
print('函数 b')
def c():
num = 30
b()
print('函数 c')
def d():
num = 40
c()
print('函数 d')
d()
调试器的左下角,能够看到函数之间的“调用栈”,“调用栈”里面描述了当前这个代码的函数之间的调用关系是啥。每一层这个调用关系就称为“函数的栈帧”,每个函数的局部变量就在这个栈帧中体现的。
每一层栈帧,你选中之后,都能看到里面的局部变量,每个函数的局部变量就保存在对应的栈帧中。调用函数,则生成对应的栈帧,函数结束,则对应的栈帧消亡(里面的局部变量也就没了)。
上述函数中每个变量都同名,但是是不同变量的,属于不同的函数作用域。每个变量也是保存在各自栈帧中的(每个栈帧也是保存在内存上的)。 变量的本质就是一块内存空间。
八.函数递归
1.什么是递归
函数递归,就是一个函数,自己调用自己。
从前有座山
山里有座庙
庙里有个老和尚
老和尚给小和尚讲故事:从前有座山
山里有座庙
庙里有个老和尚
老和尚给小和尚讲故事:从前有座山
山里有座庙
庙里有个老和尚
老和尚给小和尚讲故事:.......
我们写一个函数,来求 n 的阶乘。
def factor1(n):
result = 1
for i in range(1, n + 1):
result *= i
return result
def factor2(n):
if n == 1:
return 1
return n * factor2(n - 1)
print(factor1(5))
print(factor2(5))
第二个函数就使用了函数的递归,函数中的 n 虽然都是 n ,但是 n 是函数形参,形参相当于函数的局部变量,局部变量在函数自己的栈帧上,虽然两个函数的局部变量名相同,但是不同的栈帧是不同的内存空间,也就是不同的变量了。(另一方面,看起来是同一个函数,但是这里的两次调用,其实是两个栈帧)。
递归的代码,不会无限的执行下去,会在每一次递归的时候,逐渐逼近递归的结束条件(if n == 1)。
递归的代码,虽然很多时候看起来写法很简单,但是执行过程可能会非常复杂!!在分析递归代码的时候,光用脑子想,太困难。所以可以画图,可以调试。
递归的代码要有两个要素:
1).递归结束条件
2).递归的递推公式
2.递归的缺点
1)执行的过程非常复杂,难以理解。
2)递归代码容易出现“栈溢出”的情况(代码不小心写错了,导致每次递归,参数不能正确的接近 递归结束条件,出现无限递归的情况)。
3)递归代码一般都是可以转换成等价的循环代码的。并且,循环的版本通常运行速度要比递归的 版本有优势。(函数的调用也是有开销的)。
3.递归的优点
代码非常简洁,尤其处理一些本身就需要通过递归方式来处理的问题(数据结构二叉树)。
九.函数形参的默认值
def add(x, y):
print(f'x = {x},y = {y}')
return x + y
result = add(10, 20)
print(result)
在函数内部加上打印信息,方便我们进行调试。但是,像这种调试信息,希望在正式发布的时候不要有,只是在调试阶段才有。
def add(x, y, debug = False):
if debug:
print(f'x = {x},y = {y}')
return x + y
result = add(10, 20, True)
print(result)
形参的默认值
带有默认值的形参,就可以在调用函数的时候不必传参,通过这样的设计,就可以在函数的设计更灵活。(像默认值这样的语法,在编程界存在争议(C++支持,Java不支持))
同时,要求带有默认值的形参,得在形参列表的后面,而不能在前面/中间。
def add(x, debug = False, y): 这样的写法并不正确。多个带有默认参数的形参,这些都得放在后面!!
十.函数的关键字传参
1.位置传参:add(10, 20),按照先后顺序来传参,这种传参风格,称为“位置参数”。这是各个编程语言中最普遍的方式。
2.关键字传参:按照形参的名字来进行传参!
test(x = 10, y = 20),非常明显的告诉程序猿,你的参数要传给谁。另外可以无视形参和实参的顺序!
位置参数和关键字参数,还能混着用,只不过混着用的时候要求位置参数在前,关键字参数在后。关键字参数,一般也就是搭配默认参数来使用的。
一个函数,可以提供很多的参数,来实现对这个函数的内部功能做出一些调整设定,为了降低调用者的使用成本,就可以把大部分参数设定出默认值。当调用者需要调整其中的一部分参数时候,就可以搭配关键字参数来进行操作。