第六章 函数-下
原文再续,书接上一回
6.3 我的地盘听我的
6.3.1 函数和过程
函数和过程的区别
- 函数是有返回值的
- 过程是简单、特殊并且没有返回值的
Python严格来说只有函数,没有过程。函数默认会有返回值
>>> def hello():
print('Hello')
>>> print(hello())
Hello
None
通过上面小demo可以看出当不写return语句时,默认Python会认为函数是return None。所以说Python所有的函数都有返回值。
6.3.2 再谈一谈返回值
Python同时返回多个值,使用列表或元组打包
Python利用列表打包多种类型的值一次性返回
>>> def test():
return [1,2,3,4,5]
>>> test()
[1, 2, 3, 4, 5]
6.3.3 函数变量的作用域
变量的作用域就是平时所说的变量可见性。一般的编程语言都有局部变量(Local Variable)和全局变量(Global Variable)之分
通过下面的小demo来学习(可以先把最后面那一条语句注释掉,运行一下先):
def discounts(price,rate):
final_price = price * rate
return final_price
old_price = float(input('请输入原价:'))
rate = float(input('请输入折扣率:'))
new_price = discounts(old_price,rate)
print('打折后价格是:',new_price)
print('这里试图打印局部变量final_price的值:',final_price)
运行整个文件的结果,程序有出错:
请输入原价:80
请输入折扣率:0.75
打折后价格是: 60.0
Traceback (most recent call last):
File "F:/零基础入门学习python-小甲鱼/p6_2.py", line 9, in <module>
print('这里试图打印局部变量final_price的值:',final_price)
NameError: name 'final_price' is not defined
解释一下函数discounts里面的三个参数都是局部变量。
错误原因:final_price没有被定义过,也就是说Python找不到final_price这个变量。这是因为final_price只是一个局部变量,它的作用范围只在它的地盘上,超出了这个范围,它将什么都不是。
总结:
- 在函数里边定义的参数以及变量,都称为局部变量,出了这个函数,这些变量都是无效的。
- 事实上的原理是,Python在运行函数的时候,利用栈(Stack)进行存储,当执行该函数后,函数中的所有数据都会被自动删除。所以在函数外边是无法访问到函数内部的局部变量的。
全局变量:
全局变量拥有更大的作用域,例如可以在函数中可以访问到它们
如果在函数内部试图修改全局变量,那么Python会创建一个新的局部变量替代(名字跟全局变量相同),但是真正的全局变量是没有被改变的。
def discounts(price,rate):
final_price = price * rate
old_price = 50 #在这里试图修改全局变量
print('在局部变量中修改后的old_price的值:',old_price)
return final_price
old_price = float(input('请输入原价:'))
rate = float(input('请输入折扣率:'))
new_price = discounts(old_price,rate)
print('全局变量old_price的值:',old_price)
print('打折后价格是:',new_price)
运行整个文件的结果:
请输入原价:80
请输入折扣率:0.75
在局部变量中修改后的old_price的值: 50
全局变量old_price的值: 80.0
打折后价格是: 60.0
全局变量的总结:
全局变量在整个代码段中都是可以访问到的,但是不要试图在函数内部去修改全局变量的值
6.4 内嵌函数和闭包
6.4.1 global 关键字
全局变量的作用域是整个模块(整个代码段),也就是代码段内所有的函数内部都可以访问全局变量。
PS.函数内部仅仅去访问全局变量就好,不要试图去修改它。
再说一下:
Python会使用屏蔽的方式”保护”全局变量,一旦函数内部试图修改全局变量,Python就会在函数内部自动创建一个名字一模一样的局部变量,这样修改的结果只会修改到局部变量,而不会影响到全局变量。
小demo:
>>> count = 5
>>> def myFun():
count = 10
print(count)
>>> myFun()
10
>>> count
5
在函数中修改全局变量可能会导致程序可读性变差、出现莫名其妙的BUG、代码的维护成本提高
如果是有必要在函数中去修改这个全局变量,那么你可以使用global关键字来达到目的!!
>>> count = 5
>>> def myFun():
global count
count = 10
print(count)
>>> myFun()
10
>>> count
10
6.4.2 内嵌函数
Python你的函数定义是可以嵌套的,也就是允许在函数内部创建令一个函数,这种函数叫做内嵌函数或者是内部函数。
下面通过一个小demo(函数嵌套最简单的例子):
>>> def fun1():
print('fun1()正在被调用')
def fun2():
print('fun2()正在被调用')
fun2()
>>> fun1()
fun1()正在被调用
fun2()正在被调用
PS.内部函数整个作用域(fn2()整个函数)都在外部函数(fun1()函数)之内。
fun1()这个函数体可以随意调用fun2()这个内部函数外,出了fun1(),就没有任何可以对fun2()进行的调用。
如果在fun1()外部试图内部函数fun2(),就会报错:
>>> fun2()
Traceback (most recent call last):
File "<pyshell#59>", line 1, in <module>
fun2()
NameError: name 'fun2' is not defined
上面的结果说明fun2()只能在fun1()的地盘上面的被访问。
6.4.3 闭包(closure)
闭包是函数式编程的一个重要的语法结构,函数式编程是一种编程分范式。
Python的闭包从表现形式上定义为:
如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包。
小demo:
>>> def funX(x):
def funY(y):
return x * y
return funY
>>> i =funX(8)
>>> i(5)
40
#funX(8)(5)也可以实现上面的效果
如果在一个内部函数里(FunY就是这个内部函数),对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包。
PS.因为闭包的概念是由内部函数而来的,所以你也不能在外部函数以外的地方对内部函数进行调用,下面的做法是错误的:
>>> funY(5)
Traceback (most recent call last):
File "<pyshell#68>", line 1, in <module>
funY(5)
NameError: name 'funY' is not defined
在闭包中,外部函数的局部变量对应内部的函数的局部变量,事实上也相当于之前的全局变量跟局部变量的对应的关系,在内部函数中,你只能对外部函数的局部变量进行访问,但不能进行修改。
>>> def funX(x):
x = 5
def funY(y):
x *= x
return x * y
return funY
>>> funX()()
Traceback (most recent call last):
File "<pyshell#71>", line 1, in <module>
funX()()
TypeError: funX() missing 1 required positional argument: 'x'
报错原因:Python认为在内部函数的x是局部变量的时候,外部函数的x被屏蔽了起来,所以执行x *= x的时候,在右边根本就找不到局部变量x的值
解决方法:
- 方法1:间接地通过容器类型(字符串,列表,元组)来存放,因为容器类型不是放在栈里,所以不会被”屏蔽”掉。
- 方法2:使用关键字nonlocal
方法1:
>>> def funX():
x = [5]
def funY():
x[0] *= x[0]
return x[0]
return funY
>>> funX()()
25
方法2:
>>> def funX():
x = 5
def funY():
nonlocal x
x *= x
return x
return funY
>>> funX()()
25
6.6 lambda 表达式
Python允许使用lambda关键字来创建匿名函数。
使用匿名函数的有什么优势?lambda表达式语法非常精简
匿名函数的语法:
- 在冒号(:)左边放原函数的参数,可以有多个参数,用逗号(,)隔开即可
- 冒号右边是返回值
普通函数:
>>> def ds(x):
return 2 * x +1
>>> ds(5)
11
lambda来定义这个函数:
>>> lambda x:2 * x +1
<function <lambda> at 0x05983CD8>
#lambda语句是实际上是返回一个函数的对象,如果是要使用它,只需要进行简单的赋值操作即可
>>> g = lambda x:2 * x +1
>>> g(5)
11
lambda表达是带两个参数:
#普通函数
>>> g(5)
11
>>> def add(x,y):
return x + y
>>> add(3,4)
7
#把它转换为lambda表达式
>>> g = lambda x,y:x + y
>>> g(3,4)
7
lambda表达式的作用:
- Python写一些执行脚本时,使用lambda就可以省下定义函数过程。
- 使用lambda就不需要考虑命名的问题。对于一些比较抽象并且整个程序执行下来执行要调用一两次的函数,有时候给函数起一个名字也是比较头疼的问题。
- 简化代码的可读性。
介绍两个BIF :filter() 和map()
1.filter()
这个函数是一个过滤器。通过过滤器,就可以保留你想要的信息(你认为有趣的事情),把其他不感兴趣的东西直接丢掉。
>>> help(filter)
Help on class filter in module builtins:
class filter(object)
| filter(function or None, iterable) --> filter object
|
| Return an iterator yielding those items of iterable for which function(item)
| is true. If function is None, return the items that are true.
filter有两个参数。第一个参数是函数/None
- 如果是一个函数的话,则将第二个可迭代数据里面的每一个元素作为函数的参数进行计算,把返回True的值筛选出来的
- 如果是None,则直接将第二参数中的True的值筛选。
下面通过小demo:
>>> temp = filter(None,[1,0,False,True])
>>> list(temp)
[1, True]
利用filter(),尝试写一个筛选奇数的过滤器:
>>> def odd(x):
return x % 2
>>> temp = filter(odd,range(10))
>>> list(temp)
[1, 3, 5, 7, 9]
使用lambda表达式:
>>> list(filter(lambda x : x %2,range(10)))
[1, 3, 5, 7, 9]
2.map()
map一般作作”映射”来解释。map()这个内置函数也有两个参数,仍然是一个函数和一个可迭代序列,将序列的每一个元素作为函数的参数进行加工,直到可迭代序列每个元素都加工完毕,返回所有加工后的元素构成的新的序列。
>>> list(map(lambda x :x * 2,range(10)))
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
6.6 递归
6.6.1 递归是”神马”
递归:从原理上来说就是函数调用自身这么一个行为(在函数内部你可以调用所有可见的函数,当然也包括自己)
举个栗子:
def recursion():
recursion()
这个程序将会永远执行下去直至耗尽所有的内存资源。python3对递归的深度默认限制是100层,所以你的代码才会停下来。
方法如下:
import sys
sys.setrecursionlimit(1000000) #将递归限制设置为100万层
如果你的递归不能停下啦,可以是通过Ctrl+C让它停止下来。
6.6.2 写一个求阶乘的函数
正整数的阶乘是指从1乘以2乘以3乘以4一直乘到所有的所要求的数。
所要求的的数是5,则阶乘式是1×2×3×4×5
非递归版本:
def recursion(n):
result = n
for i in range(1,n):
result *= i
return result
number = int(input("请输入一个整数:"))
result = recursion(number)
print("%d的阶乘:%d"%(number,result))
程序实现的结果如下:
请输入一个整数:5
5的阶乘:120
递归版本(装逼的版本):
def factorial(n):
if n == 1:
return 1
else:
return n * factorial(n-1)
number = int(input("请输入一个整数:"))
result = factorial(number)
print("%d的阶乘: %d"%(number,result))
该程序实现的效果跟上面非递归的版本一样的。
递归的条件(麻雀虽小,五脏俱全):
调用函数本身
设置了正确的返回条件
普通的程序员用迭代,天才程序员用递归
递归的实现可以是函数自个儿调用自个儿,每次函数的调用都需要压栈、弹栈、保存和恢复寄存器的栈操作,所以在上边是非常消耗时间和空间。
救使用口诀:递归递归,归去来兮!
递归妙在自然简洁、精炼。
6.6.3 这帮兔崽子
斐波那契数列的发明者,是意大利数学家列昂纳多.斐波那契。小甲鱼注重过程和细节,而那个数学家更关心结果。
如果说兔子在出生两个月后,就有了繁殖能力,在拥有繁殖能力之后,这对兔子每个月能生出一对小兔子来。假设所有兔子都不会死去,能够一直繁衍下去,那么一年之后可以繁殖多少对兔子呢?
所经过的月数 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
兔子的总对数 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 | 89 | 144 |
可以用数学函数来定义:
当n = 1时,F(n) = 1
当n = 2时,F(n) = 1
当n > 2时,F(n) = F(n-1) + F(n-2)
本节使用递归来实现斐波那契数列。
假设需要求出经历了20个月后,总共有多少对小兔崽子,不妨一起来考虑一下分别用迭代和递归如何实现?
迭代实现:
def fab(n):
a1 = 1
a2 = 1
a3 = 1
if n < 1:
print('输入有误!')
return -1
while(n -2) > 0:
a3 = a1 + a2
a1 = a2
a2 = a3
n -= 1
return a3
result = fab(20)
if result != -1:
print('总共有%d只小兔崽子诞生!'%result)
递归实现:
def fab(n):
if n < 1:
print('输入有误!')
return -1
if n == 1 or n == 2:
return 1
else:
return fab(n-1) + fab(n-2)
result = fab(20)
if result != -1:
print('总共有%d只小兔崽子诞生!'%result)
用迭代代码来实现基本是毫秒级的.而使用递归来实现就考验你的CPU能力啦(N秒-N分钟不等)
这就是小甲鱼不支持大家所有东西都用递归求解的原因,本来好好一个代码,给你用了递归,效率反而拉下了一大截。
不懂得递归,试图想要写一个程序来解决问题是相当困难的,如果是是用来递归,你会发现问题变得奇迹般的简单。
6.6.4 汉诺塔
一位法国数学家曾编写过一个印度的古老传说:说的是,在世界中心贝拿勒斯的圣庙里边,有一块黄铜板,上边插着三根宝针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这就是所谓的汉诺塔。然后不论白天或者黑夜,总有一个僧侣在按照下面的法则来移动这些金片:”一次只移动一片,不管在哪根针上,小片必须在大片上面。”规则很简单,另外僧侣们预言,当所有的金片都从梵天穿好的那根针上移到另外一根针上时,世界就将在一声霹雳中消灭,而梵塔、庙宇和众生也都将同归于尽。
对于游戏的玩法,可以简单分解为三个步骤:
- 将前63个盘子从X移动到Y上,确保大盘在小盘下。
- 将最底下的第64个盘子从X移动到Z上。
- 将Y上的63个盘子移动到Z上。
在游戏中我们发现由于每一次只能移动一个圆盘,所以在移动的过程中显然要借助另外一根针才可以实施。
思路:步骤(1)将1-63个盘子需要借助Z移到Y上,步骤(3)将Y针上的63个盘子需要借助X移到Z针上。
新思路聚集为以下两个问题。
- 问题一:将X上的63个盘子借助Z移到Y上
- 问题二:将Y上的63个盘子借助X移到Z上
解决这两个问题的方法跟刚才第一个问题的思路是一样的,都可以才拆分为3个步骤来实现。
问题一:将X上的63个盘子借助Z移到Y上 拆解为:
- 将前62个盘子从X移动到Z上,确保大盘在小盘下句号
- 将最底下的第63个盘子移动到Y上
- 将Z上的62个盘子移动到Y上
汉诺塔的拆解过程刚好满足递归算法的定义,使用递归来解决
def hanoi(n,x,y,z):
if n == 1:
print(x,'--->',z) #如果只有一层,直接从x移动到z
else:
hanoi(n-1,x,z,y) #将前n-1个盘子从x移动到y上,根据移动的顺序排列
print(x,'--->',z) #将最底下的64个盘子从x移动到z上
hanoi(n-1,y,x,z) #将y上的63个盘子移动到z上
n = int(input('请输入汉诺塔的层数:'))
hanoi(n,'X','Y','Z')
参考资料是:零基础入门学习Python小甲鱼的书和视频教程