3、函数式编程:

我们在用Python程序处理实际问题时,有些代码可能需要重复使用,如果每次使用都要编写一遍代码会耗费不少工作量,我们可以把这部分代码编写成函数,每次调用函数就能完成工作,不用再重复编写代码了,函数使编程效率大大提高,也使程序代码更为简洁。

我们在前文已经介绍过两个Python内置函数:input()和print()。

3.1、函数的声明和调用:

声明和定义的含义虽有区别,但本教程不做特别区分,后续内容会混合使用声明或定义,按同样的含义理解不影响量化学习。

函数声明时不会执行,只是告诉Python声明了一个函数,函数被调用时才会执行函数体的语句。(函数是可调用对象,在被调用时才会执行)

因为Python代码的执行顺序是从上到下,存在先后顺序,所以在Python中必须先声明函数然后再调用函数,否则在调用函数时会提示找不到函数。调用函数时,只要按照函数声明的形式传递参数,就可以使用函数完成相应的功能,并可以获取函数执行完后的返回值。

声明函数的关键字是def,在函数中以缩进表示各语句属于函数体。声明函数的形式如下:

def 函数名(参数):

语句块

return 返回值

参数是函数需要处理的数据,可以有多个,也可以没有,返回值是当函数执行完后抛出的值,返回值以关键字return引导,return后也可以没有返回值,return语句也可以没有,当未指定返回值时,函数默认返回None值。若函数中有多个return语句,当一个return语句被执行后,其后的语句将不再执行,函数抛出返回值并结束。

前文介绍了数据对象、表达式和流程控制语句,函数可看做是数据对象、表达式和流程控制语句的结合。

示例:

>>> def func(a,b,c):

... print(a,b,c)

... return '执行完成'

...

>>> x=func(1,2,3)

1 2 3

>>> print(x)

执行完成

>>>

示例声明了一个名称为func的函数,有三个参数a,b,c,函数语句块是调用输出函数print()打印a,b,c,函数执行完后抛出返回值'执行完成',返回值是一个字符串。调用函数时传入了三个实参1,2,3,并把函数返回值赋值给了x,所以打印x的值便输出字符串'执行完成'。

声明函数时定义的参数称为形参,在调用函数时具体传给函数的参数称为实参。

函数若需要抛出多个返回值,多个返回值可用逗号“,”隔开,多个返回值会以元组类型抛出,例如:

>>> def func(a,b,c):

... print(a,b,c)

... return (a,b,c)

...

>>> x=func(1,2,3)

1 2 3

>>> print(x)

(1, 2, 3)

>>>

多个返回值可以不用小括号括起来,在前文介绍元组时可知,Python会把用逗号“,”隔开的多个对象创建为元组,因此小括号可以省略,当返回值数量非常多的情况下用小括号会使语句结构更为清晰。

Python3允许定义函数时给参数和返回值增加注释,以便调用者知道应该传给函数什么类型的参数及返回值类型。参数的注释以:value的形式放在参数名后“=”前,返回值以-> value的形式放在小括号后冒号前,例如:

def func(a:str,b:list,c:int=8) ->tuple:

print(a,b,c)

return (a,b,c)

注释会被收集在函数的__annotations__属性中,例如:

>>> func.__annotations__

{'a': , 'b': , 'c': , 'return': }

>>>

有了注释,调用者在调用func时会知道应该给a传入字符串,给b传入列表,给c传入整数,并且函数的返回值是元组。

3.2、函数的参数传递:

3.2.1、无默认值参数:

声明函数时,诸如def func(a,b,c):,参数a,b,c的值未知,此类参数称为位置参数,当调用函数时,可以按位置传递实参,例如,func(1,2,3),实参按位置顺序1传给a、2传给b、3传给c。

也可以按关键字(参数名)传递,例如,func(a=1,b=2,c=3),此时直接给a、b、c赋值,清晰明了的知道了a,b,c的值,不会出现传参错误,传参的顺序便无所谓了,func(a=1,c=3,b=2)也是正确的。

如果传递实参时既有位置参数也有关键字参数,Python传递参数的规则是先传位置参数,后传关键字参数,因此,func(1,b=2,c=3)正确,func(a=1,2,c=3)错误,所以在传递实参时,要先按位置顺序传递参数,再传递关键字参数,顺序不能颠倒。

3.2.2、默认值参数:

在声明函数时,也可以给参数赋值默认值,在调用函数时如果不给有默认值的形参传递实参,函数体就会以形参的默认值执行。在声明函数时,默认值参数也要放在无默认值参数后面。

例如:

>>> def func(a,b,c=6):

... print(a,b,c)

... return a,b,c

...

>>> x=func(1,b=2)

1 2 6

>>> print(x)

(1, 2, 6)

>>>

函数func有默认值c=6,在调用函数时,实参1按位置传给了a,b=2按关键字传递,参数c没有传递实参,取得默认值6。

3.2.3、可变参数:

在有些情形下,我们需要传给函数的参数数量不是固定的,可以按需传递。

可变无默认值参数

声明函数时,在参数前加一个星号“*”,该参数便是可变数量无默认值参数,传给该参数的实参会被收集到一个元组里。例如:

>>> def func(*a):

... print(a)

...

>>> func(1,2,3,4,5,6)

(1, 2, 3, 4, 5, 6)

参数*a是不定量参数,调用函数时,传入的参数1,2,3,4,5,6以元组类型传递(赋值)给a,即a=(1,2,3,4,5,6)。

若函数中既有不定量无默认值参数又有定量无默认值参数,则不定量参数应放在定量参数之后。例如:

>>> def func(b,c,*a):

... print(b,c,a)

...

>>> func(7,8,1,2,3,4,5,6)

7 8 (1, 2, 3, 4, 5, 6)

>>>

调用函数时,实参7传给参数b,实参8传给参数c,剩下的1,2,3,4,5,6以元组类型传给a。

实际上,b、c、*a都可以看做无默认值的位置参数,Python在传递实参时按位置依次给b、c传参,b、c传参完,剩下的再都传给位置*a。

调用函数时,也可以按关键字传递参数,传递参数时仍然遵循位置参数在前关键字参数在后,例如:

>>> def func(*a,b,c):

... print(b,c,a)

...

>>> func(1,2,3,4,5,6,b=7,c=8)

7 8 (1, 2, 3, 4, 5, 6)

>>>

调用函数时,实参1,2,3,4,5,6先按位置传给*a,接着遇到关键字传参,则按关键字传递:b=7,c=8。若b、c有一个没有按关键字传参会报错,例如,func(1,2,3,4,5,6,7,c=8)错误,因为b没有被传参。为避免按位置传参出错,声明函数时不定量参数最好放在定量参数之后。

既然可变无默认值参数传参时会被收集到元组里,可不可以直接把元组传递给可变参数呢?当然可以的,不仅是元组,列表、字符串都可以传递给可变参数,例如:

>>> def func(*a):

... print(a)

...

>>> b=(1,2,3,4);c=[5,6,7,8];d='abcde'

>>> func(*b);func(*c);func(*d)

(1, 2, 3, 4)

(5, 6, 7, 8)

('a', 'b', 'c', 'd', 'e')

>>>

调用函数时分别传入了元组、列表和字符串,其中的元素被作为可变参数又以元组类型收集到a中,传参的时候b、c、d前都要有一个星号“*”,星号“*”在此处是“拆解”的意思,表示将元组、列表和字符串的元素拆解出来。

可变有默认值参数

除了无默认值参数可变,有默认值参数也可变,可变有默认值参数也称为可变关键字参数。

声明函数时,在参数前加两个星号“**”,该参数便是可变数量有默认值参数,传给该参数的关键字实参会按“'参数名':参数值”收集到一个字典里。例如:

>>> def func(**a):

... print(a)

...

>>> func(a=1,b=2,c=3,d=4)

{'a': 1, 'b': 2, 'c': 3, 'd': 4}

>>>

除了以关键字向可变关键字参数传参,还可以把字典传给可变关键字参数,传参时在字典前加两个星号“**”,例如:

>>> def func(**a):

... print(a)

...

>>> b={'a': 1, 'b': 2, 'c': 3, 'd': 4}

>>> func(**b)

{'a': 1, 'b': 2, 'c': 3, 'd': 4}

>>>

同样,两个星号“**”在这里是“拆解”的意思,表示把字典的“键:值”对拆解成关键字参数。

因为Python先按位置再按关键字传参,所以,当函数中定量无默认值参数、可变无默认值参数、定量关键字参数及可变关键字参数同时存在时,排列顺序应为:定量无默认值参数,可变无默认值参数,定量关键字参数,可变关键字参数。例如:

>>> def func(a,*b,c=7,**d):

... print(a,b,c,d)

...

>>> func(1,2,3,4,c=5,e=6,f=7,g=8)

1 (2, 3, 4) 5 {'e': 6, 'f': 7, 'g': 8}

>>>

调用函数时,先按位置依次传参,例如1传参给位置a,定量无默认值参数传参完后,剩下的位置参数2,3,4以元组传给*b,接着按关键字一一对应传参,例如c=5,关键字对应传参完后,剩下的关键字参数e=6,f=7,g=8没有了对应关系,便以字典传给**d。

在函数式编程中,有时参数的名称有着特定含义,给参数一一对应赋值可使编程逻辑更为清晰,此时即便在声明函数时没有给参数赋默认值,但参数仍具有关键字参数的意义,也可以把这些参数放在可变无默认值参数之后,只要在调用函数时以关键字形式传参即可,整体规则还是位置参数在前,关键字参数在后,例如:

>>> def func(*b,a,c=7,**d):

... print(a,b,c,d)

...

>>> func(2,3,4,a=1,c=5,e=6,f=7,g=8)

1 (2, 3, 4) 5 {'e': 6, 'f': 7, 'g': 8}

>>>

3.2.4、以函数作为参数:

Python中函数也是对象,因此也可以将函数作为实参传递给另一个函数的形参,传参时只需要传入实参函数的名称,如果实参函数也有参数可能需要再传入其参数,在调用方函数的语句块中调用实参函数。例如:

>>> def func1(c,d):

... print(c+d)

... return '函数f执行完成'

...

>>> def func2(f,a,b):

... x=f(c=a,d=b)

... print(x)

...

>>> func2(f=func1,a=2,b=3)

5

函数f执行完成

>>>

上例声明了两个函数,func1和func2,func1有参数c,d,函数语句块是打印参数之和c+d并抛出返回值'函数f执行完成';func2有参数f,a,b,函数语句块是调用函数f(c=a,d=b),并把返回值赋值给变量x,(f的参数名和函数func1的参数名要保持一致),最后打印x的值。

声明了函数func1和func2之后,调用函数func2(f=func1,a=2,b=3),函数名func1传递给f,2传递给a,3传递给b,由于func1也有参数,从func2的语句块可知func1会使用a和b的值。执行时func2便调用func1打印5,最后再打印x的值'函数f执行完成'。

函数也可以调用其本身,函数调用自身常用在递归问题中,例如求解斐波那契数列:

>>> def fib(n):

... if n <= 1:

... return n

... return fib(n-1) + fib(n-2)

...

>>> for n in range(11):

... print(fib(n),end=',')

...

0,1,1,2,3,5,8,13,21,34,55,

函数fib有两个返回值,当n <=1返回n,当n>1时返回fib(n-1)+ fib(n-2),return语句中调用了函数fib自身。通过for循环求解fib(0)···fib(10)并将结果输出。range( )是一个生成器,每次循环生成一个整数,range( )本身是一个类,且经常被用到。

3.3、变量的作用域:

Python的作用域可分为:内置作用域:Python预先定义的

全局作用域:所编写的整个程序

局部作用域:某个函数的内部范围

声明函数时,Python会检查函数是否存在语法错误,创建函数的名称,但在调用函数时Python才会执行函数体,为函数对象创建一个命名空间,该命名空间就是局部作用域。

不同函数的作用域是相互独立的,同一函数在不同时间调用,其作用域也是相互独立的。作用域相互独立,在函数内声明的名字相同的变量便也是相互独立的,各函数对其内部变量处理时互不影响。例如:

def func1():

c=345

print(c)

def func2():

c=567

print(c)

函数func1和func2有同名变量c,但在调用时互不影响。

Python会默认函数内部声明的变量为局部变量,但若函数内没有声明变量,Python便会从全局作用域中(向前)查找同名变量。例如:

>>> c=456

>>> def func1():

... c=345

... print(c)

...

>>> def func2():

... print(c)

...

>>> def func3():

... print(c)

... c=567

...

>>> func1()

345

>>> func2()

456

>>> func3()

Traceback (most recent call last):

File "", line 1, in

File "", line 2, in func3

UnboundLocalError: local variable 'c' referenced before assignment

>>>

上面例子声明了全局变量c=456和函数func1、func2、func3,函数func1内部声明了变量c=345,调用func1便输出345,函数func2内部没有声明变量,调用func2便(向上)查找同名全局变量c输出456,函数func3内部声明了变量c=567,变量c便是局部变量,但因print(c)先使用变量c后声明变量c,便提示了错误。

实际上,是函数内部声明变量时覆盖掉了同名的全局变量,并使函数内部声明的变量成为了局部变量。Python会先从函数内部查找声明的变量,若函数内没有声明变量,Python便会从全局作用域中(向前)查找同名变量。

但如果把全局变量作为实参传给函数,函数会先使用全局变量,直到遇到同名变量声明再转换为局部变量,这一点需要注意。例如:

>>> a=3;b=[456]

>>> def func3(a,b):

... print(a,b)

... a=5 #声明

... b.append(789) #修改

... print(a,b)

... b=[3,4,5] #声明

... print(a,b)

... b.pop() #修改

... print(a,b)

...

>>> func3(a=a,b=b)

3 [456]

5 [456, 789]

5 [3, 4, 5]

5 [3, 4]

>>> print(a,b)

3 [456, 789]

>>>

上面例子定义了全局变量a=3;b=[456]和函数func3(a,b),调用func3时把全局变量a、b的值传给func3,所以第一句print(a,b)先使用全局变量值输出了3 [456],接着声明了局部变量a=5,并给b添加一个元素789,第二句print(a,b)输出局部变量a的值5和b的新值[456,789],接着再给b赋值b=[3,4,5],b此时变成了局部变量,第三句print(a,b)输出局部变量a、b的值5 [3,4,5],接着再删除局部变量b最后一个元素,第四句print(a,b)输出5 [3,4]。

函数执行完成之后,再用print(a,b)输出全局变量a、b的值,a的值还是3,但b的值变成了第一次修改的值[456,789],第二次修改的b值是局部变量,没有影响全局变量b。

因此,函数调用全局变量时要注意,是否会与内部声明的变量存在值的覆盖,以及函数是否会修改全局变量。函数内部声明的变量最好不要与全局变量同名,直接调用全局变量时也需要明晰是否有修改全局变量的行为,以免造成难以预料的结果。

如果明确要修改全局变量,最好以关键字global声明,关键字global表示该变量是全局变量,对该变量修改便会直接修改全局变量,以global声明需要修改的全局变量会使代码逻辑更加清晰,便于对复杂的代码维护。例如:

>>> a=3;b=[456]

>>> def func4():

... global a,b,c

... a=567

... b=[3,4,5]

... c=89

... print(a,b)

...

>>> func4()

567 [3, 4, 5]

>>> print(a,b,c)

567 [3, 4, 5] 89

>>>

上例声明了全局变量a=3;b=[456]和函数func4,函数内把a、b声明为全局变量,所以函数内对a、b赋值之后也修改了全局变量a、b的值,global关键字同时也声明了全局变量c,因此c也可以在全局使用了,函数内print(a,b)输出了值567 [3,4,5],函数外print(a,b,c)输出了值567 [3,4,5] 89。

3.4、匿名函数 lambda:

关键字lambda用来创建匿名函数,其语法形式为:

func = lambda 参数:(表达式1,表达式2) #声明匿名函数

func(参数) #调用函数

参数是表达式要处理的数据,多个参数以逗号隔开,表达式的结果是函数的返回值,多个表达式用小括号括起来,会以元组类型返回,匿名函数赋值给变量func,之后像调用其他函数那样调用匿名函数func(参数),例如:

>>> func = lambda x:(x**2,x**3)

>>> print(func(2))

(4, 8)

>>>

匿名函数func有一个参数x,两个表达式分别求x的平方和x的立方,调用func并传入参数2,输出元组(4,8)。

匿名函数的声明只有一行,在有些情形下采用匿名函数会使代码更加简洁。

3.5、Python常用内置函数:dir() :列出对象的相关信息

help() :列出对象的帮助信息

type() :查看对象的数据类型(本身是类)

isinstance(obj, str):判断obj是否是str类型

Logo

加入社区!打开量化的大门,首批课程上线啦!

更多推荐