函数一词起源于数学,但是在编程中的函数和数学中的有很大不同。编程中的函数式组织好的,可重复使用的,用于实现单一功能或相关联功能的代码块。
我们在学习过程中已经使用过一些python内建的函数,如print()。但我们也可以自己创建函数,这被叫做用户自定义函数。
函数的特点:
1 代码重用
2 保持一致性
3 便与修改,易扩展
1. 定义一个函数
1.1 python定义函数用def关键字,一般格式如下
1 def 函数名(参数列表): 2 函数体
1.2 函数名的命名规则
- 函数名必须以下划线或字母开头,可以包含任意字母、数字或下划线的组合。不能使用任何的标点符号;
- 函数名是区分大小写的。
- 函数名不能是保留字。
1 def hello(): 2 print('Hello World') 3 4 hello() 5 >>>: 6 Hello World
1.3 形参和实参
形参:形式参数,不是实际存在,是虚拟变量。在定义函数和函数体的时候使用形参,目的是在函数调用时接收实参(实参个数,类型应与实参一一对应)
实参:实际参数,调用函数时传给函数的参数,可以是常量,变量,表达式,函数,传给形参
区别:形参是虚拟的,不占用内存空间,.形参变量只有在被调用时才分配内存单元,实参是一个变量,占用内存空间,数据传送单向,实参传给形参,不能形参传给实参
1 def area(width, height): 2 return width * height 3 w = 4 4 h = 5 5 print(area(w, h)) 6 >>>: 7 20
width和height是形参,w=4和h=5是实参。
我们就用一个例子来说明函数的三个特性:
1 def action1(n): 2 print ('starting action1...') 3 4 with open('日志记录','a') as f: 5 f.write('end action%s '%n) 6 7 def action2(n): 8 print ('starting action2...') 9 10 with open('日志记录','a') as f: 11 f.write('end action%s '%n) 12 13 def action3(n): 14 print ('starting action3...') 15 16 with open('日志记录','a') as f: 17 f.write('end action%s '%n) 18 19 action1(1) 20 action2(2) 21 action3(3) 22 23 24 ##***************代码重用 25 26 def logger(n): 27 with open('日志记录','a') as f: 28 f.write('end action%s '%n) 29 30 def action1(): 31 print ('starting action1...') 32 logger(1) 33 34 35 def action2(): 36 print ('starting action2...') 37 logger(2) 38 39 40 def action3(): 41 print ('starting action3...') 42 logger(3) 43 44 45 action1() 46 action2() 47 action3() 48 49 ##***************可扩展和保持一致 50 ##为日志加上时间 51 import time 52 53 def logger(n): 54 time_format='%Y-%m-%d %X' 55 time_current=time.strftime(time_format) 56 57 with open('日志记录','a') as f: 58 f.write('%s end action%s '%(time_current,n)) 59 60 def action1(): 61 print ('starting action1...') 62 logger(1) 63 64 65 def action2(): 66 print ('starting action2...') 67 logger(2) 68 69 70 def action3(): 71 print ('starting action3...') 72 logger(3) 73 74 action1() 75 action2() 76 action3()
2 函数的参数
- 必备参数
- 关键字参数
- 默认参数
- 不定长参数
必需的参数:
1 def f(name,age): 2 3 print('I am %s,I am %d'%(name,age)) 4 5 f('alex',18) 6 f('alvin',16)
关键字参数:
关键字参数和函数调用关系紧密,函数调用使用关键字参数来确定传入的参数值。使用关键字参数允许函数调用时参数的顺序与声明时不一致,因为 Python 解释器能够用参数名匹配参数值。
1 def f(name,age): 2 3 print('I am %s,I am %d'%(name,age)) 4 5 # f(16,'alvin') #报错 6 f(age=16,name='alvin')
默认参数:
调用函数时,缺省参数的值如果没有传入,则被认为是默认值。下例会打印默认的age,如果age没有被传入:
1 def print_info(name,age,sex='male'): 2 3 print('Name:%s'%name) 4 print('age:%s'%age) 5 print('Sex:%s'%sex) 6 return 7 8 print_info('alex',18) 9 print_info('铁锤',40,'female')
不定长参数
你可能需要一个函数能处理比当初声明时更多的参数。这些参数叫做不定长参数,和上述2种参数不同,声明时不会命名。
def add(*tuples): sum=0 for v in tuples: sum+=v return sum print(add(1,4,6,9)) print(add(1,4,6,9,5))
加了星号(*)的变量名会存放所有未命名的变量参数。而加(**)的变量名会存放命名的变量参数
1 def print_info(**kwargs): 2 3 print(kwargs) 4 for i in kwargs: 5 print('%s:%s'%(i,kwargs[i]))#根据参数可以打印任意相关信息了 6 7 return 8 9 print_info(name='alex',age=18,sex='female',hobby='girl',nationality='Chinese',ability='Python') 10 11 ###########################位置 12 13 def print_info(name,*args,**kwargs):#def print_info(name,**kwargs,*args):报错 14 15 print('Name:%s'%name) 16 17 print('args:',args) 18 print('kwargs:',kwargs) 19 20 return 21 22 print_info('alex',18,hobby='girl',nationality='Chinese',ability='Python') 23 # print_info(hobby='girl','alex',18,nationality='Chinese',ability='Python') #报错 24 #print_info('alex',hobby='girl',18,nationality='Chinese',ability='Python') #报错
注意,还可以这样传参:
1 def f(*args): 2 print(args) 3 4 f(*[1,2,5]) 5 6 def f(**kargs): 7 print(kargs) 8 9 f(**{'name':'alex'})
高阶函数:
高阶函数是至少满足下列一个条件的函数:
- 接受一个或多个函数作为输入
- 输出一个函数
1 def add(x,y,f): 2 return f(x) + f(y) 3 4 res = add(3,-6,abs) 5 print(res) 6 ############### 7 def foo(): 8 x=3 9 def bar(): 10 return x 11 return bar
3 函数的返回值
要想获取函数的执行结果,就可以用return语句把结果返回
注意:
- 函数在执行过程中只要遇到return语句,就会停止执行并返回结果,so 也可以理解为 return 语句代表着函数的结束
- 如果未在函数中指定return,那这个函数的返回值为None
- return多个对象,解释器会把这多个对象组装成一个元组作为一个一个整体结果输出。
4 作用域
4.1 作用域介绍
python中的作用域分4种情况:
- L:local,局部作用域,即函数中定义的变量;
- E:enclosing,嵌套的父级函数的局部作用域,即包含此函数的上级函数的局部作用域,但不是全局的;
- G:globa,全局变量,就是模块级别定义的变量;
- B:built-in,系统固定模块里面的变量,比如int, bytearray等。 搜索变量的优先级顺序依次是:作用域局部>外层作用域>当前模块中的全局>python内置作用域,也就是LEGB。
1 x = int(2.9) # int built-in 2 3 g_count = 0 # global 4 def outer(): 5 o_count = 1 # enclosing 6 def inner(): 7 i_count = 2 # local 8 print(o_count) 9 # print(i_count) 找不到 10 inner() 11 outer() 12 13 # print(o_count) #找不到
当然,local和enclosing是相对的,enclosing变量相对上层来说也是local。
4.2 作用域产生
在Python中,只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域,其它的代码块(如if、try、for等)是不会引入新的作用域的,如下代码:
1 if 2>1: 2 x = 1 3 print(x) # 1
这个是没有问题的,if并没有引入一个新的作用域,x仍处在当前作用域中,后面代码可以使用。
1 def test(): 2 x = 2 3 print(x) # NameError: name 'x2' is not defined
def、class、lambda是可以引入新作用域的。
4.3 变量的修改
1 x=6 2 def f2(): 3 print(x) 4 x=5 5 f2() 6 7 # 错误的原因在于print(x)时,解释器会在局部作用域找,会找到x=5(函数已经加载到内存),但x使用在声明前了,所以报错: 8 # local variable 'x' referenced before assignment.如何证明找到了x=5呢?简单:注释掉x=5,x=6 9 # 报错为:name 'x' is not defined 10 #同理 11 x=6 12 def f2(): 13 x+=1 #local variable 'x' referenced before assignment. 14 f2()
4.4 global关键字
当内部作用域想修改外部作用域的变量时,就要用到global和nonlocal关键字了,当修改的变量是在全局作用域(global作用域)上的,就要使用global先声明一下,代码如下:
1 count = 10 2 def outer(): 3 global count 4 print(count) 5 count = 100 6 print(count) 7 outer() 8 #10 9 #100
4.5 nonlocal关键字
global关键字声明的变量必须在全局作用域上,不能嵌套作用域上,当要修改嵌套作用域(enclosing作用域,外层非全局作用域)中的变量怎么办呢,这时就需要nonlocal关键字了
1 def outer(): 2 count = 10 3 def inner(): 4 nonlocal count 5 count = 20 6 print(count) 7 inner() 8 print(count) 9 outer() 10 #20 11 #20
4.6 小结
(1)变量查找顺序:LEGB,作用域局部>外层作用域>当前模块中的全局>python内置作用域;
(2)只有模块、类、及函数才能引入新作用域;
(3)对于一个变量,内部作用域先声明就会覆盖外部变量,不声明直接使用,就会使用外部作用域的变量;
(4)内部作用域要修改外部作用域变量的值时,全局变量要使用global关键字,嵌套作用域变量要使用nonlocal关键字。nonlocal是python3新增的关键字,有了这个 关键字,就能完美的实现闭包了。
5 递归函数
定义:在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。
例1(阶乘)
1 def factorial(n): 2 3 result=n 4 for i in range(1,n): 5 result*=i 6 7 return result 8 9 print(factorial(4)) 10 11 12 #**********递归********* 13 def factorial_new(n): 14 15 if n==1: 16 return 1 17 return n*factorial_new(n-1) 18 19 print(factorial_new(3))
例2(斐波那契数列)
1 def fibo(n): 2 3 before=0 4 after=1 5 for i in range(n-1): 6 ret=before+after 7 before=after 8 after=ret 9 10 return ret 11 12 print(fibo(3)) 13 14 #**************递归********************* 15 def fibo_new(n):#n可以为零,数列有[0] 16 17 if n <= 1: 18 return n 19 return(fibo_new(n-1) + fibo_new(n-2)) 20 21 print(fibo_new(3))
1 print(fibo_new(30000))#maximum recursion depth exceeded in comparison
递归函数的优点: 是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。
递归特性:
1. 必须有一个明确的结束条件
2. 每次进入更深一层递归时,问题规模相比上次递归都应有所减少
3. 递归效率不高,递归层次过多会导致栈溢出(在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返 回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。)
6 内置函数
重要的内置函数:
1 filter(function, sequence)
对sequence中的item依次执行function(item),将执行结果为True的item做成一个filter object的迭代器返回。可以看作是过滤函数。
1 str = ['a', 'b','c', 'd'] 2 3 def fun1(s): 4 if s != 'a': 5 return s 6 7 8 ret = filter(fun1, str) 9 10 print(list(ret))# ret是一个迭代器对象
2 map(function, sequence)
对sequence中的item依次执行function(item),将执行结果组成一个map object迭代器返回。
1 str = ['a', 'b','c','d'] 2 3 def fun2(s): 4 5 return s + "alvin" 6 7 ret = map(fun2, str) 8 9 print(ret) # map object的迭代器 10 print(list(ret))# ['aalvin', 'balvin', 'calvin', 'dalvin']
map也支持多个sequence,这就要求func也支持相应数量的参数输入:
1 ef add(x,y): 2 return x+y 3 print (list(map(add, range(10), range(10))))##[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
3 reduce(function, sequence, starting_value)
对sequence中的item顺序迭代调用function,如果有starting_value,还可以作为初始值调用。
4 lambda匿名函数
普通函数与匿名函数的对比:
1 #普通函数 2 def add(a,b): 3 return a + b 4 5 print add(2,3) 6 7 8 #匿名函数 9 add = lambda a,b : a + b 10 print add(2,3) 11 12 13 #========输出=========== 14 5 15 5
匿名函数的命名规则,用lamdba 关键字标识,冒号(:)左侧表示函数接收的参数(a,b) ,冒号(:)右侧表示函数的返回值(a+b)。
因为lamdba在创建时不需要命名,所以,叫匿名函数
7 函数式编程
学会了上面几个重要的函数后,我们就可以来聊一聊函数式编程到底是个什么鬼
一 概念(函数式编程)
函数式编程是一种编程范式,我们常见的编程范式有命令式编程(Imperativeprogramming),函数式编程,常见的面向对象编程是也是一种命令式编程。
命令式编程是面向计算机硬件的抽象,有变量(对应着存储单元),赋值语句(获取,存储指令),表达式(内存引用和算术运算)和控制语句(跳转指令),一句话,命令式程序就是一个冯诺依曼机的指令序列。
而函数式编程是面向数学的抽象,将计算描述为一种表达式求值,一句话,函数式程序就是一个表达式。
函数式编程的本质
函数式编程中的函数这个术语不是指计算机中的函数,而是指数学中的函数,即自变量的映射。也就是说一个函数的值仅决定于函数参数的值,不依赖其他状态。比如y=x*x函数计算x的平方根,只要x的平方,不论什么时候调用,调用几次,值都是不变的。
纯函数式编程语言中的变量也不是命令式编程语言中的变量,即存储状态的单元,而是代数中的变量,即一个值的名称。变量的值是不可变的(immutable),也就是说不允许像命令式编程语言中那样多次给一个变量赋值。比如说在命令式编程语言我们写“x = x + 1”,这依赖可变状态的事实,拿给程序员看说是对的,但拿给数学家看,却被认为这个等式为假。
函数式语言的如条件语句,循环语句也不是命令式编程语言中的控制语句,而是函数的语法糖,比如在Scala语言中,if else不是语句而是三元运算符,是有返回值的。
严格意义上的函数式编程意味着不使用可变的变量,赋值,循环和其他命令式控制结构进行编程。
函数式编程关心数据的映射,命令式编程关心解决问题的步骤,这也是为什么“函数式编程”叫做“函数式编程”。
二 实例
现在,让你把number =[2, -5, 9, -7, 2, 5, 4, -1, 0, -3, 8]中的正数的平均值,你肯定可以写出:
1 #计算数组中正整数的平均值 2 3 number =[2, -5, 9, -7, 2, 5, 4, -1, 0, -3, 8] 4 count = 0 5 sum = 0 6 7 for i in range(len(number)): 8 if number[i]>0: 9 count += 1 10 sum += number[i] 11 12 print sum,count 13 14 if count>0: 15 average = sum/count 16 17 print average 18 19 #========输出=========== 20 30 6 21 5
首先循环列表中的值,累计次数,并对大于0的数进行累加,最后求取平均值。
这就是命令式编程——你要做什么事情,你得把达到目的的步骤详细的描述出来,然后交给机器去运行。
这也正是命令式编程的理论模型——图灵机的特点。一条写满数据的纸带,一条根据纸带内容运动的机器,机器每动一步都需要纸带上写着如何达到。
那么,不用这种方式如何做到呢?
1 number =[2, -5, 9, -7, 2, 5, 4, -1, 0, -3, 8] 2 3 positive = filter(lambda x: x>0, number) 4 5 average = reduce(lambda x,y: x+y, positive)/len(positive) 6 7 print average 8 9 #========输出=========== 10 5
这段代码最终达到的目的同样是求取正数平均值,但是它得到结果的方式和 之前有着本质的差别:通过描述一个列表->正数平均值 的映射,而不是描述“从列表得到正数平均值应该怎样做”来达到目的。
再比如,求阶乘
通过Reduce函数加lambda表达式式实现阶乘是如何简单:
1 from functools import reduce 2 print (reduce(lambda x,y: x*y, range(1,6)))
又比如,map()函数加上lambda表达式(匿名函数)可以实现更强大的功能:
1 squares = map(lambda x : x*x ,range(9)) 2 print (squares)# <map object at 0x10115f7f0>迭代器 3 print (list(squares))#[0, 1, 4, 9, 16, 25, 36, 49, 64]
三 函数式编程的好处
1)代码简洁,易懂。
2)无副作用
由于命令式编程语言也可以通过类似函数指针的方式来实现高阶函数,函数式的最主要的好处主要是不可变性带来的。没有可变的状态,函数就是引用透明(Referential transparency)的和没有副作用(No Side Effect)。