Python学习之路day3-函数
一、函数基础
- 编程方法
典型的编程方法有面向过程、面向对象和函数式编程。
面向过程是把编程的重点放在实现过程上,分析出结局问题所需的步骤过程,然后通过语句来一一定义实现。
面向对象是把构成问题的事务分界成若干个对象,然后定义出每个对象在解决问题步骤中具备的属性和行为。
函数式编程是一种编程范式,主要思想是把运算过程尽量定义成一系列函数来进行调用(更多内容会在下面章节展开)。 - 函数的概念
编程语言中对函数的定义是:函数是逻辑结构化和过程化的一种编程方法。实际应用时是把一组语句的集合(代码块)通过函数名进行封装,以便在程序中其他位置灵活调用。 - 如何定义及使用函数
Python中定义函数的方法如下:
关于return的返回值:1 def test(x): 2 "description" 3 x += 1 4 return x 5 6 说明: 7 def: 定义函数的关键字 8 test: 函数名 9 () 用于定义参数(形参,参数非必需但括号必需得有) 10 "description" 函数附加说明,非必需,但强烈建议添加以提高代码可读性 11 X += 1 泛指代码块或程序处理逻辑 12 return 定义函数的返回值,返回函数中全部逻辑的执行结果并结束函数的执行
作用:
返回函数中全部逻辑的执行结果并结束函数的执行
返回值存在以下情况:
(1) 没有显式定义,返回None
(2) 定义了一个返回值,返回定义的值
(3) 定义了多个函数返回值,返回一个以所有返回值为元素的元组
最后,函数的返回值可以是任意对象,字符串、函数等均可以作为函数的返回值。
定义函数的目的是便于其他地方可以调用,那么定义后如何使用函数呢?
调用函数方法: 函数名(传入的参数)
还是来几个栗子:
1 def func0(a): 2 a +=1 3 print('This is func0') #此处没有显式定义返回值 4 5 6 def func1(x): 7 x += 1 8 return x 9 10 11 def func2(): 12 return 'Hello,world', ('Python', 'PHP'), [1, 2, 3], func0(2) #把函数作为返回值了 13 14 15 print(func0(0)) 16 print(func1(1)) 17 print(func2()) 18 19 输出结果: 20 This is func0 21 None # 没有显式定义返回值就返回None 22 2 23 This is func0 24 ('Hello,world', ('Python', 'PHP'), [1, 2, 3], None) #最后一个返回值是func0(2)的返回值
- 使用函数的好处
使用函数有以下四个典型的好处:
(1) 提高代码的重用性,避免代码的重复冗余
(2) 保持一致性,相同的过程执行相同的逻辑
(3) 可扩展性好,函数中的代码块可根据需要灵活扩展
(4) 可维护性高,一旦需要改变逻辑时只需要修改函数中的代码块,一处修改后调用的地方都同步更新
二、函数的参数及其传入方法
定义函数的目的是便于在程序的其他地方调用,而对于很多函数或执行逻辑来讲,不同的输入往往会导致截然不同的输出,因此函数是需要能接收输入的参数的。函数中的参数有以下两类:
形参:即形式参数,在定义函数时引入的参数,只有在函数被调用时才会存在并为其分配内存空间
实参:调用函数时实际传入的参数,即执行函数定义的逻辑时给定的输入,占用内存空间。调用函数时实参的值会根据一定的规则传递给形参来执行函数中定义的逻辑过程。
那么有把实参传给形参有哪些规则或者传入方法呢?
- 位置参数传入
位置参数传入是一种最简单的参数传入方式,传入是实参与形参的相对位置一对一对应。
注意:1 def get_list(x, y): #定义两个形参 2 return [x, y] 3 4 5 6 z = get_list(1, 2) #调用函数并传入两个实参 7 print(z) 8 9 输出: 10 [1, 2] #输出结果表明传入的实参通过位置的对应关系传递给了形参
位置参数传入时实参的个数与形参的个数必须严格一致,多一个或少一个都会报错
1 def get_list(x, y): 2 return [x, y] 3 4 5 z = get_list(1) #传入的实参比形参少了一个 6 print(z) 7 8 报错信息: 9 Traceback (most recent call last): 10 File "D:/python/S13/Day3/function.py", line 10, in <module> 11 z = get_list(1) 12 TypeError: get_list() missing 1 required positional argument: 'y' 13 14 =============================我是分割线=========================== 15 16 def get_list(x, y): 17 return [x, y] 18 19 20 u = get_list(1, 2, 3) #传入的实参比形参多了一个 21 print(u) 22 23 报错信息: 24 Traceback (most recent call last): 25 File "D:/python/S13/Day3/function.py", line 10, in <module> 26 u = get_list(1, 2, 3) 27 TypeError: get_list() takes 2 positional arguments but 3 were given
- 关键字参数传入
在调用函数传入参数时,直接使用形参的名字对其进行赋值。传入实参的顺序与形参定义的顺序可以不一致,传入时通过参数的名字来对号入座(相对位置已经失效)。
1 def get_list(x, y): 2 return [x, y] 3 4 5 z = get_list(y='Python', x='PHP') #传入的实参顺序与形参定义的顺序不同 6 print(z) 7 8 输出: 9 ['PHP', 'Python'] #程序可以正常执行,且严格按照函数定义的返回顺序来给出返回值
如果把位置参数和关键字参数混合使用,会按照位置参数调用来进行处理,或者说各自为政,互不干扰,但前提是位置参数不能放在关键字参数后面:
为什么说位置参数不能放在关键字参数后面呢?举证如下:1 def get_list(x, y): 2 return [x, y] 3 z = get_list(2, y=3) #位置参数和关键字参数混合使用 4 print(z) 5 6 输出: 7 [2, 3]
报错信息很明确了,位置参数跟在关键字参数后就是语法错误!1 def get_list(x, y): 2 return [x, y] 3 z = get_list(x=2, 3) 4 print(z) 5 6 报错信息: 7 File "E:/Python_Programs/S13/day3/function.py", line 8 8 z = get_list(x=2, 3) 9 ^ 10 SyntaxError: positional argument follows keyword argument
- 默认参数传入
在定义函数时还可以直接对引入的形参进行赋值来作为参数的默认值,这种情况下在调用函数时是否需要传入实参已经不重要了,如果不传入实参则使用则使用对应形参的默认值,否则则传入实参执行函数(覆盖参数的默认值)。这就是默认参数传入方式。
默认参数传入方式接收位置调用和关键字调用,主要用于以下场景:
(1) 为软件的安装预设默认安装路径
(2) 设定某些应用的默认参数,如数据的默认连接端口为3306
程序示例:
1 def get_list(x=1, y=2): 2 return [x, y] 3 z = get_list() 4 print(z) 5 u = get_list(2, y=3) 6 print(u) 7 8 输出: 9 [1, 2] 10 [2, 3]
- 参数组参数传入
参数组参数传入用于传递非固定长度的参数,实参会被转换为元组或字典传递给形参。详情如下:
(1) 在定义函数的形参时,如果形参以一个星号*开头然后接上其它的有效字符串,并且调用函数时实参以位置参数方式传入,实参会被转换为元组执行函数
def func1(*args):
… #省略代码块
return … #省略return的值
a = func1(x, y, z) #x,y,z表示实参
实例:
如果改用关键字方式传入参数呢?1 def get_list(*var): 2 return [var] 3 z = get_list(1, 2, 3) 4 print(z) 5 6 输出: 7 [(1, 2, 3)]
事实证明如果通过参数组方式传入参数,并且想以元组方式传入,实参必须以位置参数方式传入,否则直接报错。实际在Pycharm里面定义y=3这个参数时,Pycharm已经开始报错了再次体会到了Pycharm的强大:1 def get_list(*var): 2 return [var] 3 z = get_list(1, 2, y=3) 4 print(z) 5 6 提示信息: 7 Traceback (most recent call last): 8 File "E:/Python_Programs/S13/day3/function.py", line 8, in <module> 9 z = get_list(1, 2, y=3) 10 TypeError: get_list() got an unexpected keyword argument 'y'
(2) 在定义函数的形参时,如果形参以两个星号**开头然后接上其它的有效字符串,并且调用函数时实参以关键字参数方式传入,实参会被转换为字典执行函数
def func2(**args):
… #省略代码块
return … #省略return的值
a = func1(x=1, y=2, z=3) #x,y,z表示实参
实例:
同理,一旦形参定义为两个星号开头的格式,那么实参就只能是以关键字参数方式传入(只有关键字参数传入才符合字典的key-value特征):1 def display(**args): 2 return args 3 4 u = display(a=1, b=2, c=3) 5 print(u) 6 7 输出: 8 {'a': 1, 'b': 2, 'c': 3} #把实参以字典形式返回了
强大的是,参数组参数传入方式也可以与其他形式的参数传入方式混合使用,但有一些原则需要遵循,总结起来就是参数的传递不能让Python解释器看来存在二义性:1 def display(**args): 2 return args 3 4 u = display(1, 2, c=3) 5 print(u) 6 7 报错信息: 8 Traceback (most recent call last): 9 File "E:/Python_Programs/S13/day3/function.py", line 9, in <module> 10 u = display(1, 2, c=3) 11 TypeError: display() takes 0 positional arguments but 2 were given
1 def display(x, *args): 2 return args 3 4 u = display(0, 1, 2, 3) 5 print(u) 6 7 输出: 8 (1, 2, 3)
这里需要注意的是,参数组参数传入方式与其他参数传入方式混合使用时,参数组传入的参数需要尽量放在后面,位置调用参数也不能跟在关键字参数后面。1 def display(x, **args): 2 return args 3 4 u = display(3, a=0, b=1, c=2) 5 print(u) 6 7 输出: 8 {'a': 0, 'c': 2, 'b': 1}
- 混合参数传入
混合参数传入即将上述的几种参数传入方式混合使用,此时位置调用不在遵循位置的严格对应关系,或者说传入参数的方式更符合上述哪种传入方式就按照哪种方式来传入参数。
这里举一例说明:
上述例子中我们混合使用了位置参数、参数组和关键字参数三种参数传入方式,且参数的传入位置没有严格对应,程序仍然能正常执行,原因是参数的传入符合规则,并且不存在二义性,参数在传入解释器会选择最优最能符合参数调用特征的那种来进行参数传递。1 def display(x, y, **args): 2 print(x) 3 print(y) 4 return args 5 print(display(3, a=0, b=1, c=2, y=4 )) 6 7 8 输出: 9 3 10 4 11 {'b': 1, 'a': 0, 'c': 2}
另外这里也可以看出字典元素的无序性。
三、函数的作用域
先脑补下基本概念, 作用域是指对某一变量和方法具有访问权限的代码空间。这里限于学习的局限性暂时只探讨变量。
根据函数变量作用域的不同,我们可以把变量分为全局变量和局部变量:
全局变量:在函数外部(子程序)定义,定义后在程序的全局范围内均有效且可被访问到,即它的作用域是程序全局。
局部变量:在函数内部定义,仅在函数内部效且可被访问到,即作用域是函数内部,除非通过global关键字将变量的作用域修改为全局。
总结:
全局变量和局部变量各自的作用域决定了默认情况下在函数内部可任意调用访问全局变量,但反之在函数外部范围内不可调用访问在该函数内部定义的局
部变量,除非在函数内部定义局部变量时加上global关键字。
实际应用中全局变量和局部变量的界限并不是那么明晰,因为二者存在转换和被对方调用的可能性,具体的规则如下:
(1)默认情况下可在函数内部调用全局变量并对其重新定义后赋值(注意不包括增删改等修改操作),但该值仅在函数内部有效,不影响函数外部该全局变量
的属性和值。即局部变量和全局变量的属性不变,在函数(子程序)内部局部变量生效,函数外部全局变量生效,唯一的联系是全局变量和局部变量共用
了一个变量名称,但本质上是两码事。
(2)函数内部定义局部变量时,可通过global关键字将变量的作用域提升扩大到全局范围,即把局部变量转换为全局变量。但建议尽量避免使用global,原因
是一旦使用,对稍微复杂些的程序而言会引起变量的混乱,给程序调试带来干扰。
(3)字符串、整数数据类型在函数内部修改(重新赋值)不覆盖全局变量的值,除此之外复杂的数据类型局部(如列表、字典、集合)变量的修改(请一定注
意是修改,增删改等操作,不包括重新定义后赋值)会覆盖全局变量的值,具体原因还有待深究。
下面通过程序示例来加深理解:
函数内部对全局变量重新定义赋值的情况:
1 flag = 0 2 name = 'Tom' 3 list1 = ['a', 'b', 'c'] 4 dict1 = {1: 'a', 2: 'b', 3: 'c'} 5 set1 = {'a', 'b', 'c'} 6 tuple1 = ('a', 'b', 'c') 7 print('Before executing function:') 8 print(flag) 9 print(name) 10 print(list1) 11 print(dict1) 12 print(set1) 13 print(tuple1) 14 print('==========Break line==========') 15 16 17 def display(): 18 flag = 1 19 name = 'Jack' 20 list1 = ['d', 'e', 'f'] 21 dict1 = {4: 'a', 5: 'b', 6: 'c'} 22 set1 = {'a1', 'b2', 'c3'} 23 tuple1 = ('a1', 'b2', 'c3') 24 print(flag) 25 print(name) 26 print(list1) 27 print(dict1) 28 print(set1) 29 print(tuple1) 30 print('Executing function:') 31 print(display()) 32 print('============Break line===============') 33 print('After executing function:') 34 print(flag) 35 print(name) 36 print(list1) 37 print(dict1) 38 print(set1) 39 print(tuple1) 40 41 输出: 42 Before executing function: 43 0 44 Tom 45 ['a', 'b', 'c'] 46 {1: 'a', 2: 'b', 3: 'c'} 47 {'b', 'c', 'a'} 48 ('a', 'b', 'c') 49 ==========Break line========== 50 Executing function: 51 1 52 Jack 53 ['d', 'e', 'f'] 54 {4: 'a', 5: 'b', 6: 'c'} 55 {'c3', 'b2', 'a1'} 56 ('a1', 'b2', 'c3') 57 None 58 ============Break line=============== 59 After executing function: 60 0 61 Tom 62 ['a', 'b', 'c'] 63 {1: 'a', 2: 'b', 3: 'c'} 64 {'b', 'c', 'a'} 65 ('a', 'b', 'c')
从上面示例程序来看,在函数内部对已经定义过的全局变量进行重新定义并赋值,丝毫不影响函数外部的全局变量。事实上这里只是变量名称的重复而已,本质上二者没有任何关联。
但如果换作是在函数内部对外部已经定义过的变量进行修改(增删改操作)呢?
1 flag = 0 2 name = 'Tom' 3 4 5 def display(): 6 flag += 1 7 name = name[:1] 8 print(flag) 9 #print(name) 10 print(display()) 11 print('============Break line===============') 12 print(flag) 13 print(name) 14 15 输出: 16 Traceback (most recent call last): 17 File "D:/python/S13/Day3/function.py", line 14, in <module> 18 print(display()) 19 File "D:/python/S13/Day3/function.py", line 10, in display 20 flag -= 1 21 UnboundLocalError: local variable 'flag' referenced before assignment
对字符串或int型(程序中已注释,不在此赘述过程)全局变量在函数内部直接修改值会报错,提示局部变量在定义之前就开始引用了。这点需要注意,当然如果通过global关键字声明是可以避免错误的。
与此相反,除字符串和int型以外的可以修改的复杂数据类型(如列表、字典、元组)的全局变量,可直接在函数内部修改,并且是修改后全局生效。
1 list1 = ['a', 'b', 'c'] 2 dict1 = {1: 'a', 2: 'b', 3: 'c'} 3 set1 = {'a', 'b', 'c'} 4 print('Before executing function:') 5 print(list1) 6 print(dict1) 7 print(set1) 8 print('==========Break line==========') 9 10 11 def display(): 12 list1[0] = 0 13 list1.append('d') 14 print(list1) 15 dict1.pop(3) 16 print(dict1) 17 set1.pop() 18 print(set1) 19 print('Executing function:') 20 print(display()) 21 print('============Break line===============') 22 print('After executing function:') 23 print(list1) 24 print(dict1) 25 print(set1) 26 27 28 输出: 29 Before executing function: 30 ['a', 'b', 'c'] 31 {1: 'a', 2: 'b', 3: 'c'} 32 {'b', 'c', 'a'} 33 ==========Break line========== 34 Executing function: 35 [0, 'b', 'c', 'd'] 36 {1: 'a', 2: 'b'} 37 {'c', 'a'} 38 None 39 ============Break line=============== 40 After executing function: 41 [0, 'b', 'c', 'd'] #函数内部修改后全局有效 42 {1: 'a', 2: 'b'} 43 {'c', 'a'}
四、递归函数
递归函数即在函数内部继续循环调用自身,直到满足条件后结束循环。但需要注意的是这个循环递归不是无限继续下去,Python中最大递归深度999。
使用递归函数需注意以下特性:
- 必须有一个明确可以结束的条件
- 每次进入更深一次递归时(即每递归循环一次),递归的问题规模相对于上一次应有所减少
- 如果递归的效率不高,递归的层数过多会导致栈溢出
这是因为在计算机中函数的调用是通过栈stack这种数据结构来实现的,调用函数时栈会加一层栈帧,结束调用时则减一层栈帧(入栈出栈处理),而栈的大小是有限的,过多次数的递归调用可能会导致栈溢出。
还是来一个递归函数的实际案例:
1 def enlarge(n): 2 if n < 100: #定义明确的递归结束条件 3 print(n) 4 return enlarge(n+1) #n每递归一次自增一次,循环的次数就减少一次 5 enlarge(10) 6 7 输出: 8 ... #省略前面的输出 9 97 10 98 11 99
从上面的示例程序可看出,递归函数类似于for循环的逻辑:
1 for i in range(200): 2 if i < 100: 3 print(i) 4 i += 1
五、函数式编程
函数式编程了解非常有限,这里就贴下Alex老师对函数式编程的全部内容,引用自http://www.cnblogs.com/alex3714/articles/5740985.html
函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。
函数式编程中的函数这个术语不是指计算机中的函数(实际上是Subroutine),而是指数学中的函数,即自变量的映射。也就是说一个函数的值仅决定于函数参数的值,不依赖其他状态。比如sqrt(x)函数计算x的平方根,只要x不变,不论什么时候调用,调用几次,值都是不变的。
Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言。
定义:
简单说,"函数式编程"是一种"编程范式"(programming paradigm),也就是如何编写程序的方法论。
主要思想是把运算过程尽量写成一系列嵌套的函数调用。举例来说,现在有这样一个数学表达式:
(1 + 2) * 3 - 4
传统的过程式编程,可能这样写:
1 var a = 1 + 2; 2 var b = a * 3; 3 var c = b - 4;
var result = subtract(multiply(add(1,2), 3), 4);
这段代码再演进以下,可以变成这样:
add(1,2).multiply(3).subtract(4)
这基本就是自然语言的表达了。再看下面的代码,大家应该一眼就能明白它的意思吧:
merge([1,2],[3,4]).sort().search("2")
因此,函数式编程的代码更容易理解。
要想学好函数式编程,不要玩py,玩Erlang,Haskell, 好了,我只会这么多了。。。
六、高阶函数
既然我们可以根据需要自由定义函数的参数,那么一个函数定义为另外一个函数的参数也是OK的。接收另外一个函数并将其作为参数的函数,称之为高阶函数。目前还不明确其应用场景,就先举例加深理解吧:
1 def func2(a, b, f): 2 return f(a)+f(b) 3 4 5 print(func2('c', 'd', str)) 6 7 输出: 8 cd
实验发现,作为参数传入的那个函数,需要是Python的内置函数,因为如果是自定义函数,作为参数传入时还需要为这个函数传入它需要接收的参数,普通的自定义函数在调用时会提示诸如字符串、整型的对象不可调用。具体原因以后再深究吧。