文章内容
2017/9/22 10:47:28,作 者: 黄兵
论函数不得不说的秘密(一)
1 函数的定义
因为Python用的人越来越多,功能也越来越强大。为了方便于管理又能简单的调用,需要程序分解成许多较小的组成部分,这里可以用三个方法实现:函数,对象和模块。
1.1 创建和调用函数
函数就是把代码打包,通过打包的代码在调用的时候返回所需要的。代码可以随意拼装和反复使用,根本不需要知道原理,就可以把复杂变成简单。
简单的说就是一个程序按照不同的功能实现,分隔成许许多多的代码块,每一个代码块都可以封装成一个函数。在Python中创建函数的关键字是def:
>>> def func():
... print("你好")
... print("你很好")
... print("你很好呀")
...
>>> func()
你好
你很好
你很好呀
注意:def是函数的关键字,func是函数名,后面的小括号是传参数,括号后面有一个冒号,然后是函数的组成部分。函数创建完成,如果不调用,那它就不会执行,调用方法直接是一个函数,这里没有传入参数所以直接打印函数名即可。
函数的运行机制就是:当函数发生调用的时候,Python自动会往上找到所定义的函数,然后依次执行该函数所包含的那块部分,也就是冒号后面缩紧部分的内容,这里要注意的是,Python是从上到下的执行顺序,如果被调的函数在下面就会出现报错的情况。如果想多次调用,哪就执行多次。
>>> for i in range(2):
... func()
...
你好
你很好
你很好呀
你好
你很好
你很好呀
1.2 函数的参数
函数刚发明出来的时候是没有参数的,后来因为发现没有传参的函数和使用循环没有本质的区别。所以,为了实现每次调用函数可以有不同的体现,才加入了参数的概念。比如一个成绩表,我只需要输入我的分数,我就能知道我得的是A还是B,或者是C:
>>> def student(study):
... if study >= 60 and study < 90:
... study = 'B'
... elif study >= 90:
... study = 'A'
... else:
... study = 'C'
... return study
...
>>> student(30)
'C'
>>> student(80)
'B'
>>> student(99)
'A'
注意:函数用的return就是给调用函数返回一个结果,如果想传入多个参数用逗号隔开即可,下面看看传入两个参数:
>>> def student(name, study):
... if study >= 60 and study < 90:
... study = 'B'
... elif study >= 90:
... study = 'A'
... else:
... study = 'C'
... return name,study
...
>>> student('Python3v', 99)
('Python3v', 'A')
函数可以支持很多参数,但是建议参数定义的不要太多。函数的功能和参数的意义也要做好响应的注解,这样让别人看起来才会清晰。
1.3 函数的返回值
函数的返回值就是return,如果你没有return在使用pycharm的时候打印的最后一行会有一个None,如果加上return就不会有了。
2 灵活即强大
看一个编程语言是否强大,就看是否灵活。Python灵活多变的一个就是函数因为参数而灵活。如果没有参数,一个函数就只能死板的完成一个功能,一个任务。
2.1 形参和实参
参数从调用的角度分为形式参数(parameter)和实际参数(argument),形参指的是函数在创建和定义的过程中小括号里的参数。实参则指的是函数在被调用的过程中传递进来的参数。举个例子:
>>> def function(name):
... print(name)
...
>>> function("python3v")
python3v
function(name)的name是形参,因为name只是代表了一个位置、一个变量名;而调用function("python3v")传递的是“python3v”是实参,因为它是一个具体的内容,是赋值变量名中的值
2.2 函数文档
给函数写文档是为了让自己或者别人更好的理解你的函数,所以这是一个好习惯。因为在实际的开发过程中一套程序不是一个人来完成的,相互之间需要链接,就需要别人来阅读自己的代码,适当的文档说明非常重要。也或者是自己写的代码时间长了,自己也不一定就记得,所以给函数写文档很有必要。看下例子:
>>> def exchangeRate(dollar):
... """美元 - > 人民币
... 汇率暂定为7.2
... """
... return dollar * 7.2
...
>>> exchangeRate(10)
72.0
在函数开头写下的字符串是不会打印出来的,但它会作为函数的一部分存储起来。这个称为函数文档字符串,它的功能跟注释是一样的。我们也可以理解为注释,单行用“#”,多行用“"""””用三个双引号。
其实,函数的文档字符串可以通过特殊属性doc获取(注:doc两边分别是两条下划线):
>>> exchangeRate.__doc__
'\xe7\xbe\x8e\xe5\x85\x83 - > \xe4\xba\xba\xe6\xb0\x91\xe5\xb8\x81\n \xe6\xb1\x87\xe7\x8e\x87\xe6\x9a\x82\xe5\xae\x9a\xe4\xb8\xba7.2\n '
>>> print(unicode('\xe7\xbe\x8e\xe5\x85\x83 - > \xe4\xba\xba\xe6\xb0\x91\xe5\xb8\x81\n \xe6\xb1\x87\xe7\x8e\x87\xe6\x9a\x82\xe5\xae\x9a\xe4\xb8\xba7.2\n ', 'utf-8'))
美元 - > 人民币
汇率暂定为7.2
这个是因为编码的问题,需要转换一下,关于编码后面会说。另外,想使用一个函数,但是不确定其用法的时候,可以通过help()函数来查看函数的文档,因此,对我们自己的函数也可以这样使用:
>>> help(exchangeRate)
Help on function exchangeRate in module __main__:
exchangeRate(dollar)
美元 - > 人民币
汇率暂定为7.2
2.3 关键字参数
普通的参数叫位置参数,通常在调用一个函数的时候,粗心的程序员会很容易搞乱位置参数的顺序,以至于函数无法按照预期实现。因此,有了关键字参数。使用关键字参数,就可以很简单地解决这个潜在的问题,看下例子:
>>> def saysomething(name, words):
... print(name + '->' + words)
...
>>> saysomething("编程", "改变世界")
编程->改变世界
>>> saysomething("世界", "改变编程")
世界->改变编程
>>>
>>>
>>> saysomething(words="改变世界", name="编程")
编程->改变世界
关键字参数其实就是在传入实参时指定形参的变量名,尽管使用这种技巧要多打一些字,但随着程序的规模越来越大、参数越来越多,关键字也起作用也就越明显。毕竟宁可多打个字符,也不希望出现BUG
2.4 默认参数
初学者最容易搞混什么是关键参数,什么是默认参数,其实默认参数就是在定义的时候赋予了默认的参数
>>> def saysomething(words="改变世界", name="编程"):
... print(name + '->' + words)
...
>>>
>>> saysomething()
编程->改变世界
>>> saysomething("是学习python的订阅号", "python3v")
python3v->是学习python的订阅号
>>> saysomething(words="是学习python的订阅号", name="python3v")
python3v->是学习python的订阅号
使用默认参数的话,就可以不带参数去调用函数。所以,它们之间的区别就是:关键字参数是在函数调用的时候,通过参数名指定要赋值的参数,这样做就不怕因为搞不清楚参数的顺序导致函数调用出错;而默认参数是在参数定义的过程中,为形参赋初值,当函数调用的时候不传递实参,则默认使用形参的初始值代替。
2.5 收集参数
收集参数在大多数时候也被叫作可变参数。发明这种机制的动机就是函数的作者有时候也不知道这个函数到底需要多少个参数......这个听起来有点令人不解,但是确实有此类情况。这个时候,只需要在参数的前面加上星号(*)即可:
>>> def test(*params):
... print("共有 %d个参数" % len(params))
... print("第三个参数是:", params[2])
...
>>> test("a","b","c","d","e")
共有 5个参数
第三个参数是: c
>>> test("a","b","c")
共有 3个参数
第三个参数是: c
看完例子不难理解,Python就是把标志为收集参数的参数们打包成一个元组。不过这里需要注意一下,如果收集参数后边还需要指定其他参数,在调用的函数的时候就应该使用关键参数来指定,否则Python就都会把你的实参都收集参数的范畴。举个例子:
这里我不在用linux的终端操作,因为后面会写多个函数用终端太麻烦,从这里开始使用pycharm
def test(*params, extra):
print("收集参数是:", params)
print("位置参数是:", extra)
test(1,2,3,4,5,6,7,8)
报错结果:
test(1,2,3,4,5,6,7,8)
TypeError: test() missing 1 required keyword-only argument: 'extra'
test(1,2,3,4,5,6,7,8, extra=9)
收集参数是: (1, 2, 3, 4, 5, 6, 7, 8)
位置参数是: 9
建议大家如果你的参数中带有收集参数,那么可以将其他参数设置为默认参数,这样不容易出错:
def test(*params, extra="9"):
print("收集参数是:", params)
print("位置参数是:", extra)
test(1,2,3,4,5,6,7,8, extra=9)
星号(*)其实即可以打包又可以"解包"。“解包”怎么说呢?举个例子,假如你需要将一个列表a传入test参数的收集参数param中,那么调用test(a)时便会出错,此时需要在a前面加上一个星号(*)表示实参需要"解包"后才能使用:
def test(*params):
print("有 %d 个参数:" % len(params))
print("第三个参数是:", params[2])
a = [1,2,3,4,5,6,7,8,9]
test(a) #直接将列表名a作为实参将会出错
打印结果:
Traceback (most recent call last):
File "E:/Python3v/python3v.py", line 9, in <module>
test(a)
File "E:/Python3v/python3v.py", line 6, in test
print("第三个参数是:", params[2])
IndexError: tuple index out of range
test(*a) #实参前边加上星号(*)表示解包
打印结果:
有 9 个参数:
第三个参数是: 3
Python还有另一种收集方式,就是用两个星号(**)表示。跟前面介绍不同,两个星号的收集参数表示将参数们打包成字典的形式。
3 我的地盘听我的
在很多编程语言中:函数和过程其实是区分开的,一般认为函数(functions)是有返回值的,而过程(procedure)是简单、特殊并且没有返回值的。也就是说函数在干完事必须要返回一个结果,而过程就是干完事拍拍屁股走人。
在Python中,严格的来说是只有函数,没有过程。有的同学会说在没有return之前,函数不也没有返回值吗?为了证明看一下下面的例子:
def Hello():
print("Hello Word")
print(Hello())
打印结果:
Hello Word
None
调用print(Hello())之后打印了两行,第一行是函数执行的,第二行的None是在函数不写return的时候,默认Python会认为函数是return None.所以说Python所有的函数都有返回值。
3.1 函数的过程
在许多编程语言中,我们说一个函数是整型,就是指这个函数会返回一个整型的返回值。而Python不这么干,Python可以动态的确定函数的类型,而且函数还能返回不同类型的值。这就是前面说过的“Python没有变量,只有名字”。只需要知道Python会返回一个东西,然后拿来使用就可以了。另外,Python也可以同时返回多个值:
def test():
return [1,"Python3v", 3.14]
print(test())
打印结果:
[1, 'Python3v', 3.14]
Python也可以利用列表打包多种类型的值一次性返回。当然,也可以用直接用元组的形式返回多个值:
def test():
return 1,"Python3v", 3.14
print(test())
打印结果:
(1, 'Python3v', 3.14)
3.3 函数变量的作用域
一般的编程语言都有局部变量(Local Variable)和全局变量(Global Variable)之分,举个例子:
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)
打印结果:
请输入原价:90
请输入折扣率:0.3
打折后价格是: 27.0
这里分析一下代码:在函数discounts()中,两个参数price和rate,还有一个final_price,它们都是discounts()函数中的局部变量。因为这些变量在函数以外不能使用,看一下例子:
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)
打印结果:
请输入原价:90
请输入折扣率:0.3
Traceback (most recent call last):
File "E:/Python3v/python3v.py", line 12, in <module>
print("这里试图打印局部变量final_price的值:", final_price)
NameError: name 'final_price' is not defined
打折后价格是: 27.0
错误原因:NameError: name 'finalprice' is not defined,finalprice没有定义过,也就是说,Python找不到finalprice这个变量。这是因为finalprice只是一个局部变量,它的作用范围只是在它的地盘上------discounts()函数的定义范围内有效,出了这个范围就不行了。
总结一下:在函数里边定义的参数以及变量,都称为局部变量,出了函数,这些变量都是无效的。事实上的原理是,Python在运行函数的时候,利用栈(Stack)进行存储,当执行完该函数后,函数中的所有数据都会被自动删除。所以函数外边是无法访问到函数内部的局部变量的。
与局部变量相对的是全局变量,程序中oldprice、newprice、rate都是在函数外边定义的,它们都是全局变量,全局变量拥有更大的作用域,例如在函数中可以访问到它们:
def discounts(price, rate):
final_price = price * rate
print("这里试图打印全局变量old_price的值是:", old_price)
return final_price
old_price = float(input("请输入原价:"))
rate = float(input("请输入折扣率:"))
new_price = discounts(old_price, rate)
print("打折后价格是:", new_price)
打印结果:
请输入原价:90
请输入折扣率:0.3
这里试图打印全局变量old_price的值是: 90.0
打折后价格是: 27.0
看样子全局变量更为霸道,不过在使用全局变量的时候也要小心了,在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)
打印结果:
请输入原价:90
请输入折扣率:0.3
在局部变量中修改后的old_price的值是: 50
全局变量old_price现在的值是: 90.0
打折后价格是: 27.0
可以看出,如果在函数内部试图修改全局变量,那么Python就会创建一个新的局部变量代替(名字跟全局变量相同),但真正的全局变量是不会改变的,所以实现的效果和大家的预期是不一样的。
全局变量就是在整个代码段中都可以访问到的,但是不要试图在函数内部修改全局变量的值,因为那样Python会自动在函数内部新建一个名字一样的局部变量代替。
很多人容易在局部变量和全局变量的使用上容易犯错,尤其是很多朋友都试图去建立一个跟全局变量同名的局部变量,这类做法是强烈反对的,那如果在函数里去修改全局变量的值有实现的方法吗?这里会说一个嵌套定义一个新的函数。
4 内嵌函数和闭包
4.1 global关键字
全局变量的作用域是整个模块(整个代码段),也就是代码段内的所有函数都可以访问到全局变量,但是要注意的是,子函数内部仅仅去访问全局变量就好,不要试图去修改。
Python会使用屏蔽(Shadowing)的方式“保护”全局变量,一旦函数内部试图修改全局变量,Python就会在函数内部自动创建一个名字一模一样的局部变量,这样修改的结果就会修改到局部变量,而不会修改到全局变量。看下面的例子:
count = 10
def myfun():
count = 5
print(count)
print(myfun())
打印结果:
5
None
10
人毕竟是灵活多应变的,如果你已经完全了解在函数中修改全局变量,可能会导致程序可读性变差、出现莫名其妙的BUG、代码的维护成本提高,但是你还是坚持“虚心接受、死性不改”这八个字的原则,仍然觉得有必要在函数中去修改全局变量,那么你就可以使用global关键字来达到目的,修改一下程序:
count = 10
def myfun():
global count
count = 5
print(count)
print(myfun())
print(count)
打印结果:
5
None
5
4.2 内嵌函数
Python的函数定义是可以嵌套的,也就是允许在函数内部创建另一个函数,这种函数叫作内嵌套函数或者内部函数。看一下例子:
def fun1():
print("fun1()正在被调用....")
def fun2():
print("fun2()正在被调用.....")
fun2()
print(fun1())
打印结果:
fun1()正在被调用....
fun2()正在被调用.....
None
这是函数嵌套最简单的例子,虽然看起来没有什么用,不过麻雀虽小,五脏俱全。关于内部函数的使用,有一个比较值得注意的地方,就是内部函数整个作用域都在外部函数之内。就像刚才例子中fun2()整个函数的作用域都在fun1()里边。
需要注意的地方是,除了在fun1()这个函数体中可以随意的调用fun2()这个内部函数外,出来fun1(),就没有任何可以对fun2()进行的调用。如果在fun1()外部试图调用内部函数fun2(),就会出错:
def fun1():
print("fun1()正在被调用....")
def fun2():
print("fun2()正在被调用.....")
fun2()
print(fun1())
print(fun2())
打印结果:
Traceback (most recent call last):
fun1()正在被调用....
File "E:/Python3v/python3v.py", line 14, in <module>
fun2()正在被调用.....
print(fun2())
None
NameError: name 'fun2' is not defined
评论列表