函数

1,定义和调用函数

1,定义函数

在python中,使用def定义函数。语法格式如下:

1
2
def 函数名([参数列表]):
函数体

无参函数:当函数体内代码不需要外部传入参数时,也能够独立运行,那么就可以定义无参数的函数;

1
2
def hi():
print("Hi,python.")

当函数没有参数时,也必须添加一对小括号,否则抛出语法异常

空函数:就是不执行任何操作的函数,在函数体内使用pass空语句填充函数体。如果缺少了pass,就会抛出语法错误。

1
2
def no():
pass

空函数的作用:可以作为占位符备用,在函数的具体功能还没有实现前,可以先定义空函数,让代码能运行起来,事后再编写函数体代码

有参函数:当函数体内代码必须依赖外部传入参数时,那么就可以定义有参数的函数

1
2
3
4
5
def abs(x,y):
if x >= y:
return x
else:
return y

函数的参数放在函数名后面的小括号内,可以设置一个或多个参数,以逗号分隔。函数的返回值通过return语句设置。

在函数体内,一旦执行return语句,函数将返回结果,并立即停止函数的运行。如果没有return语句,执行完函数体内所有代码后,也会返回特殊值(None)。如果明确让函数返回None。可以按如下方式编写。

1
2
3
return None
简写为:
return

【拓展】

再定义函数时,如果在函数体第一行使用”””或””添加多行注释时,则在调用函数时,会自动提示注释信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def abs(x):
"""
abs(float x)
功能:求绝对值
参数:x,为数字
返回值:x的绝对值
"""

if x >= 0:
return x
else:
return -x

print(abs(13))

因此,可以在函数体第一行添加函数的帮助信息,如函数的功能、参数类型和作用、返回值类型等信息

2,调用函数

定义函数后,函数内的代码不会自动执行,只用调用函数时,函数体的代码才被执行。使用小括号可以直接调用一个函数。语法格式如下:

1
函数名([参数列表])

如果在定义函数时没有设置参数,则调用函数时不用传入参数。

如果在定义函数时设置了多个参数,则调用函数时必须传入同等数量的参数,否则抛出TypeError错误。如果传入的参数数量是对的,但参数类型不能被函数所接收,也会抛出TypeError错误,并且给出错误信息。

1
2
3
hi()
no()
abs(-45)

调用函数的3种形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
1,语句:
例如:一下三个函数都是以语句形式调用。
hi()
no()
abs(-45)

2,表达式:
函数都有返回值,因此调用函数之后,实际上得到的就是一个值,值可以参与表达式运算。例如,下面代码将函数返回值(45)乘以2,然后把表达式的运算结果(90)再赋值给变量num
num = abs(-45) * 2

3,参数
把函数调用作为参数传递给另一个函数。例如:
print(abs(-45))

函数都是先定义,后调用。在定义阶段发现语法错误,将会提示错误,但是不会判断逻辑错误,只有在调用函数时,才会判断逻辑错误。

3,定义嵌套函数

python允许创建多级嵌套的函数,就允许在函数内部定义函数,这些内部函数都是遵循各自的作用域和生命周期等规则。

嵌套函数:内层函数可以访问外层函数作用域中的变量,但是外层函数不允许访问内层函数作用域中的变量。

1
2
3
4
5
6
def outer():
x = 1
def inner():
print(x)
inner()
outer()

2,函数的参数和返回值

1,形参与实参

函数的参数分为两种:

形参:在定义函数时声明的参数变量,尽在函数内部可见

实参:在调用函数时实际传入的值。

1
2
3
4
5
6
def r(a,b):
return a+b

x=1
x=2
print(f(x,y))

上面的示例中。a,b为形参,x,y为实参。在执行函数时,Python把实参变量的值赋值给形参变量,实现参数的传递。

根据实参的类型不同,可以把实参分为两种类型:

固定值:实参为不可变对象,如数字,布尔值,字符串,元组

可变值:实参为可变对象,如列表,字典和集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def fun(obj):
obj += obj
return obj

#传递固定值
a = 1
b = fun(a)
print(a)
print(b) #输出2

#传递可变值
a = [1]
b = fun(a)
print(a)
print(b) #输出[1,1]

2,位置参数

位置参数就是根据位置关系有序把实参的值有序传递给形参。在一般情况下,实参和形参应该是一一对应的,不能够错位传递,否则会引发异常或者运行错误。

【提示】

1
2
3
在调用函数时,实参和形参必须保持一致,具体说明如下:
在没有设置默认参数和可变参数的情况下,实参和形参的个数必须相同
在一般情况下,实参和形参的类型必须保持一致。

在python中,内置函数会自动检查传入值的个数和类型,如果个数或类型不一致,则会引发异常。例如,调用内置函数abs(),并传入字符串,则抛出TypeError错误。

对于自定义函数,Python会自动检查实参个数,如果实参和形参个数不一致,将抛出TypeError错误。但是Python不会检查传入值的类型和形参类型是否一致。

如果实参于形参的位置顺序不对应,虽然Python不会自动检查,但是容易引发异常或逻辑错误

3,关键字参数

在调用函数时,实参一般是按顺序传递给形参的。

1
2
3
4
5
6
def test(a,b,c):
print("a=",a)
print("b=",b)
print("c=",c)

test(1,2,3)

实参1,2,3按顺序传入函数,函数能够按顺序把他们分配给形参变量a,b,c。

关键字参数能够打破参数的位置关系,根据关键字映射实现给形参赋值。例如:

1
2
3
4
5
6
def test(a,b,c):
print("a=",a)
print("b=",b)
print("c=",c)

test(c=3,a=1,b=2)

关键字参数是针对调用函数时传递的实参而言,而位置参数是针对定义函数时设置的形参而言。

位置参数和关键字参数可以混用,一般位置参数在前,关键字参数在后。

一旦使用关键字参数后,其后不能使用位置参数。因为这样会重复为一个形参赋值,应确保形参和实参个数相同。

例如:test(c = 3,b=2,1)

4,默认参数

调用Python内置函数int(),可以传递一个参数,也可以传递两个参数。

int()函数的第2个参数用来设置转换进制,默认是十进制(base=10)。因此,默认参数的作用是能够简化函数调用,当需要的时侯,也允许传入额外的参数来覆盖默认参数值。

1
2
def 函数值(参数1,参数2,...,参数n=默认值n,参数n+1=默认值n+1,...):
函数体

当定义函数时,如果某个参数的值不经常变动,就可以考虑将这个参数设置为默认参数。使用默认参数之后,位置参数必须放到前面,默认参数放到后面。位置参数和默认参数没有个数限制。

1
2
3
4
5
6
7
8
def power(x,n):
s=1
while n > 0:
n = n - 1
s = s * x
return s

power(34)

默认参数是在定义函数时赋值的,且仅赋值一次。当调用函数时,如果没有传入实参值,函数就会使用默认值。

应避免使用可变参数作为参数的默认值

1
2
3
4
5
6
7
8
def sum(num,scores=[]):
scores.append(num)
return scores

result=sum(12)
print(result)
result=sum(24)
print(result)

预设程序运行的结果是[12]、[24],但是实际结果是[12]、[12,24]。出现问题的原因在于scores的默认值是一个列表对象,而列表对象,而列表对象是一个可变类型,那么使用append()方法添加列表元素时,不会为score重新创建一个新的列表,而是在原来的对象的基础上只想操作。

因此,对于默认参数,如果默认值是不可变类型,那么多次调用函数是不会相互干扰的,如果默认值是可变参数,那么在调用函数时就要重新初始化可变参数,避免多次调用的相互干扰。

5,可变参数

可变参数就是允许定义能和多个实参相匹配的形参。当无法确定实参个数时,使用可变参数是最佳选择。定义可变参数有两种形式,具体说明如下。

1,单星号形参

当定义参数时,在形参名称前添加一个星号前缀,就可以定义一个可变的位置参数,其语法格式如下:

1
2
def 函数名(*param):
pass

声明一个类似*param的可变参数时,从此处开始直到结束的所有位置参数都将被收集,并汇集成一个名为param的元组中。在函数体内可以使用for语句循环遍历param对象,读取每个元素,实现对可变位置参数的读取。

示例一:定义一个求和函数,能够把参数中所有数字进行相加并返回。

1
2
3
4
5
6
7
8
9
def num(*sums):
i = 0
for i in nums:
if(isinstance(n,(int,float))):
i += n
return i
print(sum(1,2,3,4)) #输出为10
print(sum(1,2,3,4,"a","b")) #输出为10
print(sum(1,2,3,4.4,5.67)) #输出为13.3700000000001

使用可变参数进行参数传递,设计求和函数会显得非常的方便。

python也允许传入单星号实参。在调用函数时,当在实参前添加星号(*)前缀,python会自动遍历该参数对像,提取所有元素,并按顺序转换为位置参数。因此,要确保被添加星号的实参是可迭代对像。这个过程也被称为解包位置参数。

示例二:调用函数sum(),并传入可遍历数据对像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def num(*nums):
i = 0
for n in nums:
if(isinstance(n,(int,float))):
i += n

return i


a = (2,4,6,8,10)
print(sum(*a,2,3,4,1)) #输出为10
b = [(2,4,6,8,10)]
print(sum(*b,2,3,4,1)) #输出为10
c = {2,4,6,8,10}
print(sum((*c,2,3,4,1)) #输出为13.3700000000001
d = {2:"a",4:"b",6:"c",8:"d",10:"e"}
print(sum(*d,2,3,4,1))

2,双星号形参

当定义函数时,在形参名称前面添加两个星号前缀,就可以定义一个可变的关键字参数,其语法格式如下:

1
2
def 函数名(**param):
pass

声明一个类似*param的可变参数时,从此处开始直到结束的所有位置参数都将被收集,并汇集成一个名为param的字典中。在函数体内可以使用for语句循环遍历param对象,读取每个元素,实现对可变位置参数的读取。

示例三:定义一个求和函数,能够接受关键字传递的参数,并把所有键,值进行汇总,如果值为数字,则叠加并记录,最后反回一个元组,包含可汇总的键的列表,以及汇总值的和。

1
2
3
4
5
6
7
8
9
10
def sum(**nums):
i = 0
temp = []
for keys,value in nums.items():
if (isinstance(value,(int,float))):
i += value
temp.append(keys)
return (temp,i)
a = sum(a=1,b=2,c=3,d=4)
print(" + ".join(a[0]),"=",a[1])

python也允许传入双星号实参。在调用函数时,当在实参前添加双星号(**)前缀,python会自动遍历该参数对像,提取所有元素,并按顺序转换为关键字参数。因此,要确保被添加双星号的实参为字典对像。这个过程也被称为解包关键字参数。

示例四:针对示例三,先定义一个字典对象,然后再调用函数sum(),并传入字典对象作为可变参数,则也可以得到相同的结果。

1
2
3
4
5
6
7
8
9
10
11
12
def sum(**nums):
i = 0
temp = []
for keys,value in nums.items():
if (isinstance(value,(int,float))):
i += value
temp.append(keys)
return (temp,i)

d = {"a":1,"b":2,"c":3,"d":4}
a = sum(**d)
print(" + ".join(a[0]),"=",a[1])

示例五:利用可变参数可以定义创建字典对象的函数

1
2
3
4
5
def dict(**kwargs):
return kwargs

d = dict(a=1,b=2,c=3,d=4,e=5)
print(d)

6,混合使用参数

位置参数、默认参数,关键字参数和可变参数可以混合使用。混用时的位置顺序如下:

在定义函数时,形参位置顺序:位置参数在前,默认参数在后

1
2
(位置参数,默认参数,可变位置参数,可变关键字参数)     #默认参数会被重置
(位置参数,可变位置参数,默认参数,可变关键字参数) #默认参数保持默认

在调用函数时,实参位置顺序:位置参数在前,关键字参数在后。

1
2
3
4
5
6
7
8
#推荐顺序
(位置参数,关键字参数,可变位置参数,可变关键字参数)
(位置参数,可变位置参数,关键字参数,可变关键字参数)

#可选顺序
(可变位置参数,位置参数,关键字参数,可变关键字参数)
(位置参数,可变位置参数,可变关键字参数,关键字参数)
(可变位置参数,位置参数,可变关键字参数,关键字参数)

示例一:定义一个函数,包含位置参数和默认参数,在调用函数时,使用位置参数和关键字参数。

1
2
3
4
5
6
7
8
9
def f(name,age,sex=1):
print("name=",name,end=" ")
print("age=",age,end=" ")
print("sex=",sex)

f('zhangsan',25,0)
f(age=25,name= 'zhangsan')
f(age=25,sex=0,name='zhangsan')

示例二:可变位置参数和可变关键字参数混用:可变参数在前,可变关键字参数在后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def f(*args,**kwargs):
print("args=",args,end=" ")
print("kwargs=",kwargs)

if __name__ == '__main__':
f(1,2,3,4)
f(a=1,b=2,c=3)
f(1,2,3,4,a=1,b=2,c=3)
f('a',1,None,a=1,b='2',c=3)

运行结果:
args= (1, 2, 3, 4) kwargs= {}
args= () kwargs= {'a': 1, 'b': 2, 'c': 3}
args= (1, 2, 3, 4) kwargs= {'a': 1, 'b': 2, 'c': 3}
args= ('a', 1, None) kwargs= {'a': 1, 'b': '2', 'c': 3}

示例三:可变位置参数与位置参数和默认参数混合使用

1
2
3
4
5
6
7
8
9
10
11
12
#可变位置参数放在位置参数的后面,默认参数放在所有参数的最后
def f(x,*args,a=4):
print("x=",x,end=" ")
print("a=",a,end=" ")
print("args=",args)
#调用函数
f(1,2,3,4,5,6,7,8,9,10,a=100)
f(1,2,3,4,5,6,7,8,9,10)

运行结果:
x= 1 a= 100 args= (2, 3, 4, 5, 6, 7, 8, 9, 10)
x= 1 a= 4 args= (2, 3, 4, 5, 6, 7, 8, 9, 10)
1
2
3
4
5
6
7
8
9
10
11
12
13
#可变位置参数放在所有的参数的最后,默认参数放在位置参数的后面
def f(x,a=4,*args):
print("x=",x,end=" ")
print("a=",a,end=" ")
print("args=",args)

f(1,2,3,4,5,6,7,8,9,10)
f(1,*(2,3,4,5,6,7,8,9,10))


运行结果:
x= 1 a= 2 args= (3, 4, 5, 6, 7, 8, 9, 10)
x= 1 a= 2 args= (3, 4, 5, 6, 7, 8, 9, 10)

示例4:可变关键字参数与位置参数和默认参数混合使用。

1
2
3
4
5
6
7
8
#默认参数要放在位置参数的后面,可变关键字参数放在最后。
def f(x,a=4,**kwargs):
print("x=",x,end=" ")
print("a=",a,end=" ")
print("kwargs=",kwargs)

f(1,y=2,z=3)
f(1,5,y=2,z=3)

【注意】默认参数不能放在可变关键字参数的后面,否则将抛弃语法错误。

示例五:位置参数,默认参数,可变位置参数和可变关键字参数混用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#如果要保持使用默认参数的默认值时,默认参数的位置应该位于可变位置参数之后。
def f(x,*args,a=4,**kwargs):
print("x=",x,end=" ")
print("a=",a,end=" ")
print("args=",args,end=" ")
print("kwargs",kwargs)

#直接传递值
f(1,5,6,7,8,y=2,z=3)
#传递可变参数
f(1,*(5,6,7,8),**("y":2,"z":3))

#当需要修改默认参数的默认值时,默认参数应该放在可变位置参数之前,位置参数之后。
def f(x,a=4,*args,**kwargs):
print("x=",x,end=" ")
print("a=",a,end=" ")
print("args=",args,end=" ")
print("kwargs=",kwargs)

#直接传递值
f(1,5,6,7,8,y=2,z=3)
#传递可变参数
f(1,5,6,*(7,8),**("y":2,"z":3))

7,函数的返回值

在python函数体内,使用return语句可以设置函数的放回值,语法格式如下:

1
return [表达式]

【注意】如果一个函数没有return语句,其是他有一个隐含的return语句,返回值是False,类型是NoneType。与return或则return none等价,都是返回none。

示例一:以下三个返回值返回none。

1
2
3
4
5
6
7
8
9
10
11
12
13
def f1():
pass

def f2():
return

def f3():
return none


print(f1())
print(f2())
print(f3())

retun语句还结束函数调用的功能。

示例2:把return语句放在函数体内第二行,这将导致return语句后免得语句无法被执行,因此仅看到1语句被执行,而3语句无法被执行。

1
2
3
4
5
def f():
print(1)
return
print(3)
f()

在函数体内可以设置多条return语句,但是只有一条语句被执行。如果没有任一一条被执行,那么就会隐式调用的return None语句作为返回值。

示例三:在排序函数中,经常会用到以下设置,通过比较两个函数的大小,确定他们排序顺序。

1
2
3
4
5
6
7
8
9
10
def f(x,y):
if x < y:
return 1
if x >= y:
return -1
else:
return 0

print(1,2)
print(3,2)

函数的返回值可以是任意类型,但是返回值只能是单值,值可以是包含多个元素的对象,如列表,字典等,因此要返回多个值,可以考虑把多个值放到列表中,元组,字典等对象中再放回。

示例4:在本示例中,虽然return语句后面跟随多个值,但是python会把他们隐式封装起来成为一个元组对象返回。

1
2
3
def f():
return 1,2,3,4
print(f())

3,匿名函数

1,定义匿名函数

在python中,使用lamda运算符可以定义匿名函数,语法格式如下:

1
fn = lamda [arg1 [arg2,...,argn]]:expression

具体说明:

[arg1 [arg2,…,argn]]:可选参数,表示匿名函数的参数,参数的个数不限,参数之间通过逗号分隔;

expression:表示必选参数,为一个表达式定义函数体,并能够访问冒号左侧的参数

fn:表示一个变量,用来接收lamda表达式的返回值,返回值为一个匿名函数对象,通过fn变量可以调用lamda函数。

lamda是一个表达式,而不是一个语句块,它具有以下的几个特点:

1
2
#与def语句的语法相比较,lamda不需要小括号,冒号(:)左侧的值表示函数体的参数,函数不需要return语句,冒号右侧表达式的运算结果就是返回值。
#与def语句的功能相比较,lamda的结构单一,功能有限。lamda的主体是一个表达式,而不是一个语句块,因此不能包含各种指令,如for,if,while等结构化语句,仅能使用在lamda表达式中封装有限的运算逻辑。

lamda表达式也会产生一个新的局部作用域,拥有独立的命名空间。在def定义的函数中嵌套lamda表达式,lamda表达式可以访问外层的def定义的函数中可用的变量。

示例一:定义一个无参匿名函数,直接返回一个固定值

1
2
t = lamda :True
t()

等价于def func():return True

示例二:定义一个带参数的匿名函数,用来求两个数字之和。

1
2
3
4
5
#求和匿名函数
sum = lamda a,b: a + b #直接赋值给变量,然后像普通函数一样调用。
#调用匿名函数
print(sum(10,20))
print(sum(20,20))

其中,Sum=lambda a,b:a+b就等效于def sum(a,b):return a+b

示例三:通过一行代码把字符串中的各种空字符转换为空格

1
print((lambda s:' '.join(s.split()))("this is\na\ttest"))

输出为:

1
this is a test

上面一行代码等价于:

1
2
3
s = "this is\na\ttest"
s=' '.join(s.split())
print(s)

示例四:在匿名函数中设置默认值

1
2
c= lambda x,y=2:x+y
print(c(10))

示例五:

快速转换为字典对象

1
2
3
c = lambda **arg:arg
d = c(a=1,b=2,c=3)
print(d)

示例六:通过匿名函数设置字典排序的主键

1
2
3
4
5
6
7
8
9
10
infors = [
{"name":"a","age":15},
{"name":"b","age":20},
{"name":"c","age":10}
]
infors.sort(key=lambda x:x['age'])
print(infors)

运行结果:
[{'name': 'c', 'age': 10}, {'name': 'a', 'age': 15}, {'name': 'b', 'age': 20}]

示例七:通过匿名函数设置一个高阶函数

1
2
3
4
5
def test(a,b,func):
return func(a,b)

num =test(34,26,lambda x,y:x-y)
print(num)

示例八:通过匿名函数过滤出能够被3整除的元素

1
2
3
d=[1,2,4,67,85,34,45,100,456,34]
d = filter(lambda x:x%3==0,d)
print(list(d))

【提示】

过滤器函数fliter(function,iterable)包含两个参数,参数function是筛选函数,参数iterable是可迭代对象。filter()可以从序列中过滤出符合条件的元素,d=filter(lambda x:x%3==0,d)的含义就是从序列中筛选出符合函数lambda x:x%3==0的新序列。

2,序列处理函数和lambda表达式

使用序列处理函数来操作序列对象,包括filter(),map(),reduce()和sorted()。

1
2
3
4
filter(function,iterable):根据函数过滤函数
map(function,iterable,...):根据函数映射一个新的序列
reduce(function,iterable[,initalizer]):根据函数对元素执行汇总计算,并返回汇总的值。
sorted(iterable,key=funtion,reverse=False):根据函数对元素进行排序

在python2中reduce()是内置函数,而在python3中归为functools模块,因此需要导入该模块。

案例完整代码如下所示,演示效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from functools import reduce
d = [1,-2,4,67,-85,34,45,-100,456,-34]

#map映射一个序列
print(list(map(lambda x:x*2+5,d)))
print([x*2+5 for x in d])

#map映射两个序列
map_list1=map(lambda x,y:x * y + 1,[10,9,8,7,6],[5,4,3,2,1])
print(list[map_list1])

#当两个序列长度不一样时,以最短的长度序列为准
map_list2 = map(lambda x,y:x * y + 1,[10,9,8,7,6],[5,4,3,2])
print(map_list2)

#filter过滤序列
print(list(filter(lambda x:x%3==0,d)))
print([x for x in d if x%3 ==0])

#reduce汇总序列
print(reduce(lambda x,y:x+y,[1,2,3]))
print(reduce(lambda x,y:x+y,d))
print(reduce(lambda x,y:x+y,d,4))

#sorted排序序列
print(sorted(d,key=lambda x:abs(x)))

1
2
3
4
5
6
7
8
9
10
11
运算结果:
[7, 1, 13, 139, -165, 73, 95, -195, 917, -63]
[7, 1, 13, 139, -165, 73, 95, -195, 917, -63]
list[<map object at 0x00000284A74946A0>]
<map object at 0x00000284A7496020>
[45, 456]
[45, 456]
6
386
390
[1, -2, 4, 34, -34, 45, 67, -85, -100, 456]

4,变量作用域

变量作用域是指变量(scop)是指变量在程序中可以被访问的有效范围,也成为命名空间、变量的可见性。python的作用域是静态的,在源代码中定义变量的位置决定了该变量能够被访问的范围,即python变量的作用域由变量所在源代码中的位置决定。

1,定义作用域

在python中,并不是所有的语句块中都会产生作用域。只有模块、类、函数才会产生作用域。

【示例】分别使用class和def创建作用域,并演示访问作用域内变量的方法

1
2
3
4
5
6
7
8
9
10
class C():
n=1

def f():
n=2
print(n)

print(C.n)
f()
print(n)

在作用域中定义的变量,一般只能在作用域内可见,不允许在作用域外直接访问

【提示】

在条件,循环,异常处理,上下文管理器等语句块中不会创建作用域。

2,作用于类型

在python中,作用域可以分为四种类型,简单的说就是

1,L(local)级:局部作用域

每当函数被调用时都会创建一个新的局部作用域,包括def函数和lambda表达式函数。如果是递归的函数,每次被调用都会创建一个新的局部变量域。在函数体内,除非使用global关键字声明变量的作用域为全局作用域,否则默认都为局部变量。局部作用于不会持续存在,存在的时间依赖与函数的生命周期。所以,一般建议都尽量避免定义全局变量,因为全局变量在模块运行的过程中会一直存在,占据着内存。

2,E(enclosing)级:嵌套作用域

嵌套作用域也是函数作用域,与局部作用域是相对关系。相对于上一层的函数而言,嵌套作用域也是局部作用域。对于一个函数而言,L表示定义在该函数内部的局部作用域,而E表示是定义在此函数的上一层父级函数中的局部作用域。

3,G(global)级:全局变量

每一个模块都是一个全局作用域,在模块中声明的变量都具有全局作用域。从外部来看,全局变量就是一个模块对象中的属性。

【注意】全局作用域的作用范围仅限于单个模块文件内

4,B(bulit-in)级:内置作用域

在系统内置模块里定义的变量,如预定义在bulit-in模块内的变量。

3,LEGB解析规则

变量名的LEGB解析规则:当在函数中使用未确定的变量名时,Python会按照优先级依次搜索4个作用域,以此来确定该变量名的意义。

1
局部作用域>嵌套作用域>全局作用域>内置作用域

具体解析步骤如下:

第1步,在局部作用域(L)中搜索变量。

第2步,如果在局部作用域中没有找到变量,则跳转到上一层嵌套结构中,访问def或lambda函数的嵌套作用域(E)。

第3步,如果在函数作用域中没有找到同名变量,则向上访问全局作用域(G)

第4步,如果在全局作用域中也没有找到同名变量,最后访问内置函数作用域(B)

根据上述顺序,在第一处找到的位置停止搜索,并读取变量的值。如果在整个作用域链上都没有找到,则会抛出NameError异常。

示例一:比较全局作用域和局部作用域的优先级关系。

1
2
3
4
5
6
n = 1
def f():
n=2
print(n)
f()
print(n)

输出为

1
2
2
1

在上面的代码中,有一个全局变量n,值为1,在函数f()中定义布局变量n,值为2,在函数内部输出n变量时,会优先搜索局部作用域,所以打印输出2,在全局作用域打印n为1。

示例二:在示例1的基础上再添加一个嵌套作用域,然后比较局部作用域、嵌套作用域和全局作用域之间的优先级关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
n=1
def f():
n=2
print(n)
def sub():
print(n)
sub()
f()
print(n)

运行结果:
2
2
1

对于sub()函数来说,当前局部作用域中没有变量n,所以在L层找不到,然后在E层搜索,当在嵌套函数f()中找到变量后,就直接读取并打印输出,不再进一步搜索G层全局作用域。

示例三:在示例一基础上调整print(n)和n=2两条语句的顺序。

1
2
3
4
5
6
7
n=1
def f():
print(n)
n=2

f()
print(n)

输出为:

1
2
发生异常: UnboundLocalError
local variable 'n' referenced before assignment

在Python预编译期,局部变量n被定义,但没有赋值。当执行程序时,不会到全局作用域去搜索变量n。当使用print()打印变量n时,局部变量n并没有绑定对象,既没有被赋值,抛出变量在分配前被引用的错误。所以,在引用一个变量之前,一定要先赋值。

为什么本例会触发UnboundLocalError异常,而不是NameError:name ‘n’ is not dedfind。Python模块代码在执行之前,并不会经过预编译,但是函数体代码在运行前会经过预编译,因此不管变量名绑定在哪个函数作用域上,都能被编译器知道。Python虽然是一个静态作用域语言,当变量访问是动态的,直到在程序运行时,才会发现变量引用问题。

示例4,在示例一基础上删除局部变量n=2

1
2
3
4
5
n=1
def f():
print(n)
f()
print(n)

输出为:

1
2
1
1

在上面的示例中,先访问局部作用域,没有找到变量n,所以在打印时直接找到了全局变量n,然后的读取并输出。

4,跨越修改变量

一个非L层的变量相对于L层变量而言,默认是只读,而不能修改。如果希望在L层中修改定义在非L层的变量,可以为其绑定一个新的值,Python会认为是在当前的L中引入了一个新的变量,即便内外两个变量重名,却也有着不同的意义,而且在L层中修改新变量不会影响到非L层的变量。如果希望在L层中修改非L层中的变量,则可以使用global、nonlocal关键字。

1,global关键字

示例一:如果希望在L层中修改G层中的变量,可以使用global关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
n = 1
def f():
def sub():
global n
print(n)
n=2
return sub
f()()
print(n)

输出为:
1
2

在上面代码中,使用global关键字之后,在sub()函数中使用的n变量就是全局作用域中的n变量,而不会新生成一个局部作用域中的n变量。

2,nonlocal关键字

使用nonlocal关键字可以实现在L层中修改E层中的变量。这时Python3新增的特性。

示例二:针对示例一,修改其中代码,把全局变量移到嵌套作用域中,然后使用nonlocal n命令在本地作用域中修改嵌套作用域中变量n的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
def f():
n = 1
def sub():
nonlocal n
n = 2
print(n)
sub()
print(n)
f()

输出为:
2
2

在上面的代码中,由于声明了nonlocal,这样在sub()函数中使用的n变量就是E层(即f()函数中)声明的n变量,所以输出两个2。

5,修改嵌套作用域变量

除了使用nonlocal关键字外,也可以使用列表等可变类型的容器间接修改嵌套作用域变量。本例比较演示这两种嵌套作用域变量的修改方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#使用列表等可变容器包含待修改的值
def demol():
x = 2
list1 = [x,]
def demo2():
list1[0] = list[0] + 1
return list1[0]
return demo2
print(demo1()())

#使用nonlocal关键字声明嵌套变量
def func1():
x = 2
def func():
nonlocal x
x += 1
return x
return func2
print(func1()())

6,比较全局变量和局部变量

使用下面两个Python内置函数可以访问全局变量和局部变量。

globals():以字典类型返回当前位置的全部全局变量。

locals():以字典类型返回当前位置的全部局部变量。

【注意】

1
通过globals()和locals()返回的字典对象可以读,写全局变量和局部变量。

下面案例简单比较了全局变量和局部变量的不同用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
d = 4
def test1(a,b):
c = 3
print('全局变量 d:',globals() ['d'])
print('局部变量集={0}'.format(locals()))
print('全局变量集={0}'.format(globals()))
d = 5
test(1,2)
print('globals variable d:%d'%d)

d = 4
def test2(a,b):
c = 3
global d
print('全局变量d:%d'%d)
print("局部变量集={0}".format(locals()))
print("全局变量集={0}".format(globals()))
d = 5
test2(1,2)
print('全局变量 d:%d'%d)

运行结果:

1
2
3
4
5
6
7
8
全局变量 d: 4
局部变量集={'a': 1, 'b': 2, 'c': 3}
全局变量集={'__name__': '__main__', '__doc__': '\ndef abs(x):\n """\n abs(float x)\n 功能:求绝对值\n 参数:x,为数字\n 返
回值:x的绝对值\n """\n\n if x >= 0:\n return x\n else:\n return -x\n \nprint(abs(13))\n', '__package__': '',
'__loader__': None, '__spec__': None, '__file__': 'E:\\新建文件夹 (2)\\大三上学期\\网络应用开发python\\函数.py', '__cached__': None, '__builtins__': {'__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil

>, 'runfile': <function runfile at 0x000001EFDACBDAB0>}, 'd': 4, 'test1': <function test1 at 0x000001EFDAF8AD40>, 'test2': <function test2 at 0x000001EFDAF8B490>}
全局变量 d:5

5,闭包和装饰器

Python支持函数闭包和装饰器的特性,它们都是特殊结构的嵌套函数,具有特殊功能和用途。

1,定义闭包

闭包就是一个在函数调用时所产生的,持续存在的上下文活动对象。

1,形成原理

函数被调用时,会产生一个临时的上下文活动对象(namespace),他是函数作用域的顶级对象,作用域内所有局部变量,参数,内层函数等都将作为上下文活动对象的属性而存在。

在默认情况下,当调用函数后,上下文活动对象会被立即释放,避免占用系统资源,但是,当函数内的局部变量,参数,内层函数被外界引用时,则这个上下文活动对象会继续存在,直到所有外部引用全部被注销。

但是,函数作用域是封闭的,外界无法访问,那么如何外界才能访问到函数内的私有成员呢?

根据作用域链,内层函数可以访问外层函数的私有成员。如果内层函数引用了外层函数的私有成员,同时内层函数又被传送给外部变量,那么闭包就形成了,这个函数就是一个闭包体,当它被调用时后,这个函数的上下文活动对象就暂时不被注销,其上下文环境会一直存在,通过内层函数,可以不断读,写外层函数的私有成员。

2,闭包结构

典型的闭包体就是一个嵌套结构的函数。内层函数引用外层函数的私有成员,同时内层函数又被外界引用,当外层函数被调用后,就形成了闭包,这个函数也称为闭包函数。

下面是一个典型的闭包结构。

1
2
3
4
5
6
def outer(x):
def inner(y):
return x + y
return inner
f = outer(5)
print(f(6))

【示例一】在嵌套结构中的函数中,外层函数使用return语句返回内层函数,在内层函数中包含了对外层函数作用域中变量的引用,一旦形成闭包体,就可以利用返回的内层函数的closure内置属性访问外层函数闭包体。

1
2
3
4
5
6
7
8
def outer(x):
def inner():
print(x)
return inner

func = outer(1)
print(func._closure_)
func()

输出为:

1
2
(<cell at 0x00000145A6777E80: int object at 0x00000145A3BA00F0>,)
1

【示例二】使用闭包实现优雅的打包,定义临时寄存器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def f():
a = 0
def sub(x):
nonlocal a
a = a + x
return sub
return a

add = f()
add(1)
add(12)
add(23)
sum = add(34)
print(sum)

在上面的示例中,通过外层函数设计一个闭包体,定义一个持久的寄存器。当调用外层函数生成上下文对象之后,就可以利用返回的内层函数不断地向闭包体内的局部变量a递加值,该值会一直存在。

2,函数装饰器

装饰器是Py的一个重要特性,本质上他就是一个以函数作为参数,并放回一个函数的函数。装饰器可以作为一个函数在不需要修改代码的前提情况下增加其他功能,常用于有切面请求的场景,通常是,插入日志,增加计时逻辑,性能测试,事务处理,缓存,权限校验等场景。有了装饰器,开发人员就可以抽离出大量与函数功能无关的雷同的代码并不断重用。

装饰器的语法以”@”开头,接着是装饰器函数的名称和可选的参数,然后是被装饰的函数,具体语法:

1
2
3
@decorator(dec_opt_args):
def func_decotator(dec_opt_args):
pass

其中decorator表示装饰器,dec_opt_args表示装饰器可选的参数类型,func_decorator表示装饰器函数名称,func_decorator_args表示装饰的函数的参数。

结合实例介绍装饰器的用法:

【示例一】:定义一个简单的函数

1
2
def foo():
print('I am foo')

为增加函数的新功能:记录函数的执行日志

1
2
3
def foo():
print('I am foo')
print('foo is running')

为了减少重复代码,可以定义一个函数:专门处理日志,处理完日志后再执行业务代码。

1
2
3
4
5
6
7
8
def logging(func):
print('%s is running' % func.__name__)
func()

def foo():
print('I am foo')

logging(foo)

P使用’@’作为装饰器的语法操作符,方便应用装饰函数。针对示例一,下面使用”@”语法应用一下装饰器函数。

1
2
3
4
5
6
7
8
9
10
def logging(func):
def sub():
print("%s is running" % func.__name__)
func()
return sub
@logging
def foo():
print('I am foo')

foo()

装饰器相当于执行了装饰器logging函数后,又返回被装饰函数foo,因此foo()被调用时相当于执行了两个函数,等价于logging(foo())

3,案例,为装饰器设置参数

1,对带参数的函数进行装饰

【示例一】:设计业务函数需要传入的两个参数并计算值,因此需要对装饰器函数内部的嵌套函数进行改动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def logging(func):
def sub(a,b):
print("% is running" % func.__name__)
return func(a,b)
return sub
@logging
def foo(a,b):
return a + b

sum = foo(2,5)
print(sum)

运行结果:
foo is running
7

2,解决函数参数数量不确定的问题

【示例二】示例一展示了参数个数固定的应用场景,不过可以使用Python的可变参数args和*kwargs来解决参数数量不确定的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def logging(func):
def sub(*args,**kwargs):
print("%s is running" % func.__name__)
return func(*args,**kwargs)
return sub

@logging
def bar(a,b):
print(a+b)

@logging
def foo(a,b,c):
print(a+b+c)

bar(1,2)
foo(1,2,3)

运行结果:

1
2
bar is running
1 2

3,装饰器带参数

【示例三】在某些情况下,装饰器可能也需要参数,这时就需要使用高价函数来进行设计。针对实例2,为logging装饰器再嵌套一层函数,然后在外层函数中定义一个标志参数lock,默认参数值为True,表示打开日志打印功能:如果为False,则关闭日志功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def logging(lock = True):
def _logging(func):
def sub(*args,**kwargs):
if lock:
print("%s is running" % func.__name__)
return func(*args,**kwargs)
return sub
return _logging

@logging
def bar(a,b):
print(a+b)

@logging(False)
def foo(a,b,c):
print(a+b+c)

bar(1,2)
foo(1,2,3)

运行结果:

1
2
3
bar is running
3
6

4,案例:解决装饰器的副作用

使用装饰器可以简化代码的编写,但是它也有一个缺点:被装饰的原函数的原信息被覆盖了,如函数的doc(文档字符串)、name(函数名称)、code.co.varnames(参数列表)等。

例如针对上一节示例3,输入如下代码。

1
print(bar.__name__)

将打印sub(),而不是bar,这种情况在使用反射函数时间就会带来问题,不过使用functools.wraps()函数可以解决这个问题。

【示例】导入functools模块,然后使用functools.wrap()函数恢复参数函数的元信息,这样当调用装饰器后,被装饰的函数的元信息重新被恢复为原来的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import functools
def logging(lock=True):
def _logging(func):
@functools.wraps(func)
def sub(*args,**kwargs):
if lock:
print("%s is running" % func.__name__)
return func(*args,**kwargs)
return sub
return _logging

@logging()
def bar(a,b):
print(a+b)

@logging(False)
def foo(a,b,c):
print(a+b+c)


print(bar.__name__)

5,案例:设计函数调用日志装饰器

​ 下面的案例将完善装饰器的功能,使其能够适应带参数和不带参数等不同的应用需求。在装饰函数时,可以按默认设置把日志信息写入当前目录下的out.log文件中,也可以指定一个文件,日志信息主要包含函数调用的时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from functools import wraps		#调用functools模块中的wrap函数
import time #导入time模块
from os import path #导入os模块中的path子模块

def logging(arg='out.log'): #日志处理函数
if callable(arg): #判断参数是否为函数,不带参数的装饰器将调用这个分支
@wraps(arg) #恢复参数函数的元信息
def sub(*args,**kwargs):#嵌套函数
log_string = arg.__name__ + " was called " + \
time.strftime("%Y-%m-%d %H:%M:%S",time.localtime()) #设计日志信息字符串
print(log_string) #打印信息
logfile = 'out.log' #指定存储的文件
with open(logfile,'a') as opened_file: # 打开logfile,并写入内容
opened_file.write(log_string + '\n')
return arg(*args,**kwargs) #调用参数函数
return sub #返回嵌套函数
else: #带参数的装饰器调用这个分支
def _logging(func): #2层嵌套函数
@wraps(func) #恢复参数函数的元信息
def sub(*args,**kwargs): #三层嵌套函数
if isinstance(arg,str): #如果指定文件路径
if path.splitext(arg)[1] == ".log": #筛选log文件
logfile = arg #自定义文件名
else:
logfile = 'out.log' #默认文件名
log_string = func.__name__ + " was called " + \
time.strftime("%Y-%m-%d %H:%M:%S",time.localtime()) #设计日志信息字符串
print(log_string) #打印信息
with open(logfile,'a') as opened_file: #打开logfile,并写入内容
opened_file.write(log_string + '\n') #将日志打到指定的logfile
return func(*args, **kwargs) #调用参数函数
return sub #返回3层嵌套函数
return _logging #返回2层嵌套函数

@logging #应用装饰函数,将默认文件记录日志信息
def bar(a,b): #业务参数
print(a+b)


@logging("foo.log") #应用装饰函数,指定日志文件
def foo(a,b,c): #业务函数
print(a+b+c)

bar(2,3)
foo(2,3,3)

运行结果:

1
2
3
4
bar was called 2023-10-27 13:06:08
5
foo was called 2023-10-27 13:06:08
8

6,案例:使用lambda表达式定义闭包

使用lambda表达式定义的匿名函数与def函数一样,也拥有独立的作用域。当在嵌套结构中的闭包体中使用lambda表达式替代def函数,有时会使代码更加简洁、易读。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1.不使用lambda表达式
def demo(n):
def fun(s):
return s ** n
return fun

a1 = demo(2)
a = a1(8)
print(a)
# 2.使用lambda表达式
def demo(n):
return lambda s : s ** n
a = demo(2)
print(a(8))
# 3.完全使用lambda表达式定义闭包结构
demo = lambda n : lambda s : s ** n
a = demo(2)
print(a(8))

运行结果:

1
2
3
64
64
64

7,案例:解决闭包的副作用

闭包的优点:

1
2
3
#实现在外部访问函数内的变量。函数是独立的作用域,它可以访问外部变量,但外部无法访问内部变量。
#避免变量污染。使用global、nonlocal关键字,可以开放函数内部变量,但是容易造成内部变量被外部污染。
#使函数变量常驻内存,为可持续保存数据提供便利

闭包的缺点:

1
2
3
4
5
6
#常驻内存会增大内存负担。无节制的滥用闭包,容易造成内存泄露。
解决方法:如果没有必要,就不要使用闭包,特别是在循环体内无限生成闭包;在退出函数之前,将不用的局部变量全部删除。
#破坏函数作用域。闭包函数能够改变外层函数内变量的值。所以,如果把外层函数当作对象使用,把闭包函数当作公用方法,把内部变量当作私有属性,就一定要小心。
解决方法:不要随便改变外层函数内变量的值。
#闭包函数所引用的外层函数的变量是延迟绑定的,只有当内层函数被调用时,才会搜索、绑定变量的值,这会带来不确定性。
解决方法:生成闭包函数的时候就立即绑定变量。

下面结合示例介绍如何解决闭包的第3个缺点。

【示例一】下面示例分别使用lambda表达式和嵌套结构 的函数定义闭包,返回两个闭包函数,在闭包函数内引用了外层函数的变量i,计算参数的i次方。

1
2
3
4
5
6
7
#使用lambda表达式
def demo():
for i in range(1,3):
yield lambda x : x ** i
demo1,demo2 = demo()
print(demo1(2),demo2(3))

1
2
3
4
5
6
7
8
9
10
# 使用嵌套结构函数
def demo():
result = list()
for i in range(1,3):
def func(x):
return x ** i
result.append(func)
return result
demo1,demo2 = demo()
print(demo1(2),demo2(3))

由于延迟绑定的缘故,当调用闭包函数时,两个函数所搜索的外层变量i的值此时都为2,打印结果如下

1
4 9

【示例二】针对示例1存在的问题,可以使用形参的默认值立即绑定变量

1
2
3
4
5
6
#使用lambda变量
def demo():
for i in range(1,3):
yield lambda x, i = i : x ** i
demo1, demo2 = demo()
print(demo1(2),demo2(3))
1
2
3
4
5
6
7
8
9
10
#使用嵌套结构函数
def demo():
result = list()
for i in range(1,3):
def func(x,i=i):
return x ** i
result.append(func)
return result
demo1,demo2 = demo()
print(demo1(2),demo2(3))

运行结果:

1
2 9

在生成内层函数时,把外层变量i的值赋值给内层函数的形参i,立即绑定变量,这样在内层函数中

访问时,就是形参变量i,而不是外层函数的变量i。此时两个闭包函数引用i的值就与循环过程中i的值保持一致,分别为1和2,打印结果如下:

1
2
2 9
2 9

6,递归函数

递归就是调用自身的一种方法,是循环计算的一种算法模式,在程序中广泛应用。

1,定义递归函数

​ 递归函数就是一个函数直接或间接地调用自身。一般来说,递归需要有边界条件、递归前进段和递归返回段、当边界条件不满足时,递归前进;当边界条件满足时,递归就返回。在递归调用的过程中,系统会每一层的调用进行缓存,因此递归运算会占用大量的系统资源,过多的递归次数容易导致内存溢出。

​ 递归必须由以下两部分组成。

1
2
#递归调用的过程
#递归调用的条件

在没有限制的情况下,递归运算会无终止地调用自身。因此,在递归运算中要结合if语句进行控制,只有在某个条件成立时才允许执行递归,否则不允许调用自身。

递归运算的应用场景如下:

1,求解递归问题

主要解决一些数学运算,如阶乘函数,幂函数和斐波那契数列。

【示例1】使用递归运算来设计阶乘函数

1
2
3
4
5
6
7
def f(x):
if (x < 2):
return 1
else:
return x * f(x - 1)
print(f(5))

在这个过程中,利用if语句把递归结束的条件和递归运算分开。

2,解析递归型数据结构

​ 很多数据结构都具有递归性,如DOM文档树、多级目录结构、多级导航菜单、家族谱系结构等。

对于这类数据结构,使用递归算法进行遍历比较合适

【示例2】Python递归遍历目录下所有文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
#遍历filepath下所有文件,包括子目录
import os
def gci(filepath):
files = os.listdir(filepath)
for fi in files:
fi_d = os.path.join(filepath,fi)
if os.path.isdir(fi_d):
gci(fi_d)
else:
print(fi_d)

#递归遍历test目录下所有文件
gci('test')

3,适合使用递归法解决问题

有些问题最适合采用递归的方法求解,如汉诺塔问题。

【示例3】使用递归运算设计汉诺塔演示函数。参数说明:n表示金片数;a,b,c表示柱子,注意排序顺序。返回说明:当指定金片数和柱子名称时,将输出整个移动的过程。

1
2
3
4
5
6
7
8
9
10
def f(n,a,b,c):
if(n == 1):
print("移动【盘子%s】 从 【%s柱】 到 【%s柱】" % (n,a,c))
else:
f(n - 1,a,c,b)
print("移动【盘子%s】 从 【%s柱】 到 【%s柱】" % (n,a,c))
f(n - 1,b,a,c)

f(3,"A","B","C")

运行结果:

1
2
3
4
5
6
7
移动【盘子1】 从 【A柱】 到 【C柱】
移动【盘子2】 从 【A柱】 到 【B柱】
移动【盘子1】 从 【C柱】 到 【B柱】
移动【盘子3】 从 【A柱】 到 【C柱】
移动【盘子1】 从 【B柱】 到 【A柱】
移动【盘子2】 从 【B柱】 到 【C柱】
移动【盘子1】 从 【A柱】 到 【C柱】

2,尾递归

尾递归是递归的一种优化算法,他从最后开始计算,每递归一次就算出相应的结果,并把当前的运算结果(或路径)放在参数里传给下一层函数。在递归的尾部,立即返回最后的结果,不需要递归返回,因此不用缓存中间调用对象。

【示例】下面是阶乘的一种普通线性递归运算

1
2
3
def f(n):
return 1 if (n==1) else n * f(n-1)
print(f(5))

使用尾递归算法后,则可以使用如下的方法。

1
2
3
def f(n,a):
return a if (n==1) else f(n-1,a*n)
print(f(5,1))

当n=5时,线性递归的递归过程如下:

1
2
3
4
5
6
7
8
9
f(5) = {5 * f(4)}
= {5 * {4 * f{3}}}
= {5 * {4 * {3 * f{2}}}}
= {5 * {4 * {3 * {2 * f{1}}}}}
= {5 * {4 * {3 * {2 * 1}}}}
= {5 * {4 * {3 * {2}}}}
= {5 * {4 * 6}}
= {5 * 24}
= 120

而尾递归的递归过程如下:

1
2
3
4
5
6
f(5) = f(5,1)
= f(4,5)
= f(3,20)
= f(2,60)
= f(1,120)
= 120

很容易看出,普通递归比尾递归更加消耗资源,每次重复的过程调用都使得调用链条不断加长,使系统不得不使用栈进行数据保存与恢复,而尾递归就不存在这样的问题,因为他的状态完全由变量n和a保存。

【提示】

1
2
3
4
5
6
从理论上分析,尾递归也是递归的一种类型,不过他的算法具有迭代算法的特征。上面的阶乘尾递归可以改写为下面的迭代循环。
n = 5
w = 1
for i in range(1,n+1):
w = w * 1
print(w)

3,递归与迭代

​ 递归和迭代都是循环运算的一种方法。简单比较如下。

1
2
3
4
在程序结构上,递归是重复调用函数自身实现循环,迭代是通过循环语句实现循环。
在结束方式上,递归遇到终止条件时会递归返回后结束,迭代则直接结束循环。
在执行效率上,迭代的效率明显高于递归,因为递归需要占用大量系统资源,如果递归次数很大,系统资源可能会不够用。
编程实现:递归可以很方便地把数学公式转换为程序,易理解、易编程。迭代虽然效率高,不需要系统开销,但不容易理解,编写复杂问题时比较麻烦。

【注意】

在实际应用中,能不用递归就不用递归,递归都可以用迭代来代替

【示例】下面拿斐波那契数列为例进行说明。

斐波那契数列就是一组数字,从第3项开始,每一项都等于前两项之和。例如:

1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181

使用递归函数计算斐波那契数列,其中最前面的两个数字是0和1。

1
2
3
def fibonacii (n):
return n if (n < 2) else fibonacii(n - 1) + fibonacii(n - 2)
print(fibonacii(19))

尝试传入更大的数字,会发现递归运算的次数加倍递增,速度加倍递减,返回值加倍放大。如果尝试计算100的斐波那契数列,则需要等待很长的时间。

下面使用迭代算法来设计斐波那契数列,代码如下,基本没有任何延迟。

1
2
3
4
5
6
def fibonacii(n):
a = [0,1]
for i in range(2,n+1):
append(a[i-2] + a[i-1])
return a[n]
print(fibonacii(19))

下面使用高阶函数来进行设计,把斐波那契数列函数封装在一个闭包体内,然后返回斐波那契数列函数,在闭包内使用memo字典持久记录每级斐波那契数列函数的求值结果,在下一次求值之前,先在字典中检索是否存在同级(数列的个数,字典的键)计算结果,如果存在,则直接返回,避免重复行计算;如果没有找到结果,则调用斐波那契数列函数进行求和。实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
def fibonacii():
memo = {0:0,1:1}
def fib(n):
result = memo.get(n,None)
if not isinstance(result,(int,float)):
result = fib(n-1) + fib(n-2)
memo[n] = result
return result
return fib

fibonacii = fibonacii()
print(fibonacii(100))

运行结果:

1
354224848179261915075

4,案例:猴子吃桃

有一只猴子,第1天摘下若干个桃子,当即吃了一半,不接馋,又多吃了一个。第2天早上又将剩下的桃子吃掉了一半,又多吃了一个。以后每天早上都吃了前一天剩下的一半零一个。到第10天早上想再吃时,见只剩下一个桃子了。求第一天共摘了多少个桃子。

【设计思路】

第10天时,还剩下一个桃子,所以peach(10)=1.

第1-9天时,peach(day - 1) =peach(day)/2-1,由此可见,peach(day) = (peach(day+1)+1)*2,采用递归思路即可求解。

【实现代码】

1
2
3
4
5
6
7
def peach(day:int) -> int:
if day == 10:
return 1
else:
return (peach(day + 1) + 1)*2
print(peach(1))

【提示】

1
2
3
4
5
6
7
Python从3.5版本开始,引入了类型注解 ,用于类型检查,防止运行时出现参数和返回值类型、变量类型不符合。
注解方法:
变量:类型 = 初始值
def 函数名(参数:类型) -> 返回值类型:
pass

注解不会影响程序的运行,不会正式报错误,只有提醒。

5,案例:角谷定理

角谷定理:任意输入一个大于1的自然数,如果为偶数,则将它除以2;如果为奇数,则将它乘以3加1.经过如此有限次计算后,总可以得到自然数值1,求经过多少此可以得到自然数1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
num = int(input("请输入一个整数:"))
times = 0
def func(num):
global times
if num == 1:
return times

if num % 2 == 0:
times += 1
num /= 2
print("第{}步,得到结果{}".format(times,num))
func(num)
else:
times += 1
num = num * 3 + 1
print("第{}步,得到结果{}".format(times,num))
func(num)

func(num)
print('总共经过了%d得到了自然数1'% times)

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
请输入一个整数:7
第1步,得到结果22
第2步,得到结果11.0
第3步,得到结果34.0
第4步,得到结果17.0
第5步,得到结果52.0
第6步,得到结果26.0
第7步,得到结果13.0
第8步,得到结果40.0
第9步,得到结果20.0
第10步,得到结果10.0
第11步,得到结果5.0
第12步,得到结果16.0
第13步,得到结果8.0
第14步,得到结果4.0
第15步,得到结果2.0
第16步,得到结果1.0
总共经过了16得到了自然数1

7,案例实战

1,函数合成

高阶函数是函数式编程最显示的特征,起形式应至少满足下列条件之一。

1
2
#函数可以作为参数被传入(即回调函数),如函数合成运算
#可以返回函数作为输出,如函数柯里化运算

compose(函数合成)和(柯里化)是函数式编程两种最基本的运算,他们都利用了函数闭包的特性和思路来进行设计。

【问题提出】

在函数式编程中,经常见到如下表达式运算。

1
a(b(c(x)));

这是“包菜式”多层函数调用,不是很优雅。为了解决函数多层调用的嵌套问题,需要用到函数合成。合成语法如下:

1
2
f = compose(a,b,c)
f(x)

例如:

1
2
3
4
5
6
7
8
9
def compose(f,g):
def sub(x):
return f(g(x))
return sub

def add(x):return x + 1
def mul(x):return x * 5
f = compose(mul,add)
print(f(2))

上面的代码中,compose()函数的作用就是组合函数,将函数串联起来执行。将多个函数组合起来,函数的输出结果是另一个函数的输入参数,一旦第1个函数开始执行,就会像多米诺骨牌一样开始执行了。

【注意】

1
2
3
4
使用compose()函数需要注意以下三点。
#compose()的参数是函数,返回的也是一个函数。
#除了初始函数(最右侧的一个)外,其他函数的接收参数都是上一个函数的返回值,即初始函数的参数可以是多元的,而其他函数的接收值是一元的。
#compose()函数可以接收任意的参数,所有的参数都是函数,且执行方向是自右向左的,初始函数一定放到参数的最右侧。

【实现代码】

下面来完善compose()实现,实现无线函数合成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#函数合成,从右到左合成函数
def compose(*argumenta):
_arguments = arguments #缓存外层函数
length = len(_length) #缓存长度
index = length #定义游标长度
#检测参数,如果存在非函数参数,则抛出异常
while (index):
index = index - 1
if not callable (_arguments[index]):
raise NameError('参数必须为函数!')

#在返回的内存函数中执行运算
def sub(*args,**kwargs):
index = length-1 #定位到最后一个参数下标
#调用最后一个参数函数,并传入内层参数
result = _arguments[index](*args,**kwargs)
#迭代参数函数
while (index):
index = index-1
#把右侧函数的执行效果作为参数传给左侧参数函数,并调用
result = _arguments[index](result)
return result #返回最左侧参数函数的执行效果
return sub #返回内层函数

#反向函数合成,即从左到右合成函数
def composeLeft(*arguments):
list = [] #定义临时列表
for i in arguments: #遍历参数列表
list.insert(0,i) #倒序排列,因为元素为函数,无法调用内置函数
return compose(*list) #调用compose()函数




在上面的代码中,compose()实现从右到左进行合成,也提供了从左到右的合成,即composeLeft(),同时在compose()内添加了一层函数的校验,允许传递一个或多个参数。

【应用代码】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
add = lambda x : x + 5 
mul = lambda x : x * 5
sub = lambda x : x - 5
div = lambda x : x / 5
fn = compose(div,sub,mul,add);
print(fn(50));
fn = composeLeft(mul,div,sub,add);
print(fn(50));
fn = compose(add,mul,sub,div);
print(fn(50));
fn = compose(add,compose(mul,sub,div));
print(fn(50));
fn = compose(compose(add,mul),sub,div);
print(fn(50));

最后3种组合方式都返回30。注意,排序顺序要保持一致

2,函数柯里化

【问题提出】

函数合成是把多个单一参数的函数合成为一个多参数的函数运算。例如,a(x)和b(x)组合为a(b(x)),则组合为f(a,b,x)。

【注意】

1
这里的a(x)和b(x)都只能接收一个参数。如果接收多个参数,如a(x,y)和b(a,b,c),那么函数合成就比较麻烦。

与函数合成相反的运算就是函数柯里化。所谓柯里化,就是把一个多参数的函数转化为一个单一参数的函数。有了柯里化运算之后,就能让所有函数只接收一个参数,实现分布运算。

【设计思路】

定义一个闭包体,先记录函数部分所需要的参数,然后异步接收剩余参数。也就是说,把多参数的函数分解为多步运算的函数,以实现每次调用函数时,仅需要传递更少或单个参数。例如,下面是一个简单的求和函数add()。

1
2
def add(x,y):
return x+y

每次调用add(),需要同时传入2个参数,如果希望每次仅传入1个参数,可以这样实现柯里化:

1
2
3
4
5
6
7
8
def add(x):
def sub(y):
return x + y
return sub

print(add(2)(6)) #输出为8,连续调用
add1 = add(200)
print(add1(2)) #输出为202,分布调用

函数add()接收一个参数,并返回一个参数,这个返回的参数可以再接收一个参数,最后返回两个参数数之和。从某种意义上讲,这是一种对参数的“缓存”,是一种非常高效的函数式运算方法。

【代码实现】

设计curry可以接收一个函数,即原始函数,返回的也是一个函数,及柯里化函数。这个返回的柯里化函数在执行过程中会不断的返回一个存储了传入参数的函数,直到触发了原始函数执行的条件。例如,设计一个add()函数,计算连个参数之和。

1
2
def add(x,y):
return x * y

柯里化函数

1
curryAdd = curry(add)

这个add()需要两个参数,但是执行curryAdd()时可以传入一个参数,当传入的参数少于add()需要的参数时,add()函数并不会执行,curryAdd()就会将这个参数记录下来,并且返回林外一个函数,这个函数可以继续接收传入参数。如果传入参数的总数等于add()需要参数的总数,就执行原始参数,返回想要的结果。或则没有参数限制,最后根据空的小括号调用作为执行原始参数的条件,返回运算结果。

【封装代码】

1
#函数柯里化

【应用代码】

应用函数无形参限制

【示例一】设计求和函数没有形参限制,函数柯里化将根据空小括号作为最后调用原始函数的条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
%求和函数,参数不限
def add(*arguments):
#迭代所有参数值,返回最后汇总的值
sum = 0
for i in arguments:
if isinstance(i,(int,float)):
sum = sum + i
return sum

#柯里化函数
curried = curry(add)
print(curried(1)(2)(3)())
curried = curry(add)
print(curried(1,2,3)(4)())
curried = curry(add,1)
print(curried(1,2)(3)(3)())
curried = curry(add,1,5)
print(curried(1,2,3,4)(5)())

应用函数有形参限制

【限制2】设计求和函数,返回3个参数之和。

1
2
3
4
5
6
7
8
9
10
11
12
def add(a,b,c):
return a + b + c

#柯里化函数
curried = curry(add,2)
print(curried(1)(2))
curried = curry(add,2,1)
print(curried(2))
curried = curry(add)
print(curried(1)(2)(6))
curried = curry(add)
print(curried(1,2,6))

curry函数的设计不是固定的,可以根据具体应用场景灵活定制。curry主要有3个作用:缓存参数、暂缓函数执行、分解执行任务。