吉吉:
python函数
函数的设计和使用
函数的设计和使用
- 函数是为实现一个操作而集合在一起的语句集,可以用来定义可重用代码、组织和简化代码。
- 将可能需要反复执行的代码封装为函数,并在需要该功能的地方进行调用,不仅可以实现代码复用,更重要的是可以保证代码的一致性,只需要修改该函数代码则所有调用均受到影响。
- 在实际项目开发中,往往会把一些通用的函数封装到一个模块中,并把这个通用模块文件放到顶层文件夹中,这样更方便管理。
1、函数定义语法:
def 函数名([参数列表]):
'''注释'''
函数体
注意事项:
函数名是标识符,命名必须符合Python标识符的规定(字母、数字、下划线,但是不能以数字打头) ;
形式参数,简称为形参,写在一对圆括号里面;
括号后面的冒号必不可少;
函数体相对于def关键字必须保持一定的空格缩进
- 在定义函数时,开头部分的注释并不是必需的,但如果为函数的定义加上注释的话,可以为用户提供友好的提示。
- 三个引号能包含多行字符串,常常出现在函数的声明的下一行,来注释函数的功能,python中这个注释成为函数的一个默认属性,还可以通过 函数名.doc 来访问(有点意思)
函数的返回值
-
如果函数不返回任何值,在主程序中被当作一个语句调用。
而另一些函数可能有返回值。 -
如果函数有返回值,则可称为带返回值的函数,使用关键字return来返回值;
-
不论return语句出现在函数的什么位置,一旦得到执行将直接结束函数的执行,即执行return语句意味着函数的终止。
-
Python中不需要指定函数返回值类型,函数返回值类型与return语句返回表达式的类型一致。如例6-4所示。
-
如果函数没有return语句,或者有return语句但是没有执行到,或者执行了不返回任何值的return语句,解释器都会认为该函数以return None结束,即返回空值。如下例所示。
>>> def fib(n):
a,b=1,1
while n>=a:
print(a,end=" ")
a,b=b,a+b
>>> type(fib(100))
1 1 2 3 5 8 13 21 34 55 89 <class 'NoneType'>
>>>
- Python中return语句还可以从函数中返回多个值
- 格式如下:
def 函数名(形式参数):
……
return <表达式1>,…,<表达式n> - 当函数具有多个返回值的时候,我们如果只用一个变量来接收返回值,函数返回的“多个值”实际上构成了一个元组,所以也可以按如下元组的方式输出。
#主程序
r=3
re=circle (r) #函数调用,并赋值给变量re
print "半径为",r,"的圆面积为:",re[0]
print "半径为",r,"的圆周长为:",re[1]
>>> =================RESTART=========================
半径为 3 的圆面积为: 28.26
半径为 3 的圆周长为: 18.84
- 一般来说,函数执行完所有步骤之后才得出计算结果并返回,return语句通常出现在函数的末尾。但是,有时我们希望改变函数的正常流程,在函数到达末尾之前就终止并返回,例如当函数检查到错误的数据时就没有必要继续执行
函数的调用
- 函数的定义是定义函数做什么。而函数一旦被定义,就可以在程序的任何地方调用这个函数。
- 当调用一个函数时,程序控制权就会转移到被调用的函数上;当执行完函数,被调用的函数就会将程序控制权交还给调用者。
函数的递归调用
-
函数的递归调用是函数调用的一种特殊情况,函数调用自己,自己再调用自己,自己再调用自己,…,当某个条件得到满足的时候就不再调用了,然后再一层一层地返回直到该函数的第一次调用。
-
递归是一种非常实用的程序设计技术。许多问题具有递归的特性,在某些情况下,用其它方法很难解决的问题,利用递归可以轻松解决。
-
编写递归函数的时候必须满足:
(1)递归终止条件及终止时的值;
(2)能用递归形式表示,并且向终止条件的方向发展。
思考,如果这样编写fac函数会出现什么问题?
def fac(n):
s=n*fac(n-1)
return s
-
以上示例既可以用递归函数实现,也可以用非递归方法实现。
-
但有些问题使用递归很容易解决,不使用递归很难解决,如经典的汉诺塔问题。
-
汉诺塔问题
n个标记1、2、3、…、n的大小互不相同的盘子,三个标记A、B、C的塔。借助塔C把所有盘子从塔A移动到塔B。初始状态时所有盘子都放在塔A,任何时候盘子都不能放在比它小的盘子的上方,每次只能移动一个盘子,并且这个盘子必须在塔顶位置。 -
分析:
当只有1个盘子,即n=1时,我们可以简单的把这个盘子直接从塔A移动到塔B。这就是我们所说的终止条件。当n>1时,我们依次解决以下三个子问题即可(具体分析过程请大家参阅相关书籍):
(1)借助塔B将前n-1个盘子从A移到C;
(2)将盘子n从塔A移到塔B;
(3)借助塔A将前n-1个盘子从C移到B。 -
使用递归解决汉诺塔问题
#eg5_24.py
#ftower,表示原始盘
#ttower,表示目标盘
#atower,表示过渡盘
def han(n,ftower,ttower,atower): #函数定义
if n==1:
print(n,"from",ftower,"to",ttower)
else:
han(n-1,ftower,atower,ttower)
print (n,"from",ftower,"to",ttower)
han(n-1,atower,ttower,ftower)
#主程序
a=input("please enter n:")
han(a,"A","B","C")
程序运行结果:
>>> ==========RESTART===========
please enter n:4
1 from A to C
2 from A to B
1 from C to B
3 from A to C
1 from B to A
2 from B to C
1 from A to C
4 from A to B
1 from C to B
2 from C to A
1 from B to A
3 from C to B
1 from A to C
2 from A to B
1 from C to B
函数定义要点总结:
-
函数名是标识符,命名必须符合Python标识符的规定;
-
形式参数,简称为形参,写在一对圆括号里面,形参是可选的,即函数可以包含参数,也可以不包含参数;即使该函数不需要接收任何参数,也必须保留一对空的圆括号
-
函数形参不需要声明类型
-
括号后面的冒号必不可少
-
函数体相对于def关键字必须保持一定的空格缩进
-
不论return语句出现在函数的什么位置,一旦得到执行将直接结束函数的执行。
-
不需要指定函数返回值类型,函数返回值类型与return语句返回表达式的类型一致。
-
如果函数没有return语句、有return语句但是没有执行到或者执行了不返回任何值的return语句,解释器都会认为该函数以return None结束,即返回空值。
-
Python允许嵌套定义函数
-
设计函数时,应注意提高模块的内聚性,同时降低模块之间的隐式耦合。
-
在编写函数时,应尽量减少副作用,尽量不要修改参数本身,不要修改除返回值以外的其他内容。
-
应充分利用Python函数式编程的特点,让自己定义的函数尽量符合纯函数式编程的要求,例如保证线程安全、可以并行运行等等。
形参与实参
- 函数定义时()内为形参,如果有多个形参,需要使用逗号进行分割。
- 函数调用时向其传递实参,将实参的值或引用传递给形参。
- ,在函数内部直接修改形参的值不会影响实参。例如:
>>> def addOne(a):
print(a)
a += 1
print(a)
>>> a = 3
>>> addOne(a)
3
4
>>> a
3
- 在有些情况下,可以通过特殊的方式在函数内部修改实参的值,例如下面的代码。
>>> def modify(v): #修改列表元素值
v[0] = v[0]+1
>>> a = [2]
>>> modify(a)
>>> a
[3]
>>> def modify(v, item):#为列表增加元素
v.append(item)
>>> a = [2]
>>> modify(a,3)
>>> a
[2, 3]
-
也就是说,如果传递给函数的是可变序列,并且在函数内部使用下标或可变序列自身的方法增加、删除元素或修改元素时,修改后的结果是可以反映到函数之外的,实参也得到相应的修改。
def modify(d): #修改字典元素值或为字典增加元素
d[‘age’] = 38a = {‘name’:‘Dong’, ‘age’:37, ‘sex’:‘Male’}
modify(a)
a
{‘age’: 38, ‘name’: ‘Dong’, ‘sex’: ‘Male’} -
Python在定义函数时不需要指定形参的类型,完全由调用者传递的实参类型以及Python解释器的理解和推断来决定,类似于重载和泛型。
-
但是Python支持对函数参数和返回值类型的标注,但实际上并不起任何作用,只是看起来方便。
>>> def test(x:int, y:int) -> int:
'''x and y must be integers, return an integer x+y'''
#用assert断言来测试表示式,其返回值为假,就会触发异常
assert isinstance(x, int), 'x must be integer '
assert isinstance(y, int), 'y must be integer '
z = x+y
assert isinstance(z, int), 'must return an integer'
return z
>>> test(1, 2)
3
>>> test(2, 3.0)#参数类型不符合要求,抛出异常
AssertionError: y must be integer
位置参数
- 在Python中,函数的实参(即调用函数时的参数传递方式)有2种形式:位置参数和关键参数。
- 位置参数(positional arguments)是比较常用的形式,调用函数时实参和形参的顺序必须严格一致,并且实参和形参的数量必须相同。
>>> def demo(a, b, c):
print(a, b, c)
>>> demo(3, 4, 5) #按位置传递参数
3 4 5
>>> demo(3, 5, 4)
3 5 4
>>> demo(1, 2, 3, 4)#实参与形参数量必须相同
TypeError: demo() takes 3 positional arguments but 4 were given
关键参数
- 实参作为关键参数被传递是指通过name=value的形式传递每个参数
- 通过关键参数,实参顺序可以和形参顺序不一致,但不影响传递结果,避免了用户需要牢记位置参数顺序的麻烦。
>>> def demo(a,b,c):
print(a,b,c)
>>> demo(3,7,5)
3 7 5
>>> demo(a=7,b=3,c=6)
7 3 6
>>> demo(c=8,a=9,b=0)
9 0 8
默认值参数
- 在Python中,函数的形参也可以有多种形式。
- 可以为其设置默认值,Python允许定义带默认参数值的函数
- 如果在调用函数时不为这些函数提供值,这些参数就使用默认值;
- 如果在调用的时候有实参,则将实参的值传递给形参。
- 设置默认参数值的格式如下:
def 函数名(形参名=默认值,……)
>>> def say( message, times =1 ):
print(message * times)
>>> say('hello')
hello
>>> say('hello',3)
hello hello hello
>>> say('hi',7)
hi hi hi hi hi hi hi
-
调用带有默认值参数的函数时,可以不对默认值参数进行赋值,也可以赋值,具有较大的灵活性。
-
默认值参数必须出现在函数参数列表中非默认值参数的右端。
-
默认值参数如果使用不当,会导致很难发现的逻辑错误,例如:
def demo(newitem,old_list=[]):
old_list.append(newitem)
return old_list
print(demo('5',[1,2,3,4])) #right
print(demo('aaa',['a','b'])) #right
print(demo('a')) #right
print(demo('b')) #wrong
- 试着想一想,这段代码会输出什么呢?
- 上面的代码输出结果如下,最后一个结果是错的。
[1, 2, 3, 4, ‘5’]
[‘a’, ‘b’, ‘aaa’]
[‘a’]
[‘a’, ‘b’]
继续想:为什么会这样呢?
-
原因在于默认值参数的赋值只会在函数定义时被解释一次。当使用可变序列作为参数默认值时,一定要谨慎操作。
-
最后一个问题来了:正确的代码该怎么写呢?
-
终极解决方案:改成下面的样子就不会有问题了
def demo(newitem,old_list=None):
if old_list is None:
old_list=[]
old_list.append(newitem)
return old_list
print(demo('5',[1,2,3,4]))
print(demo('aaa',['a','b']))
print(demo('a'))
print(demo('b') )
注意:
默认值参数只在函数定义时被解释一次
可以使用“函数名.defaults”查看所有默认参数的当前值
>>> i = 3
>>> def f(n=i): #参数n的值仅取决于i的当前值
print(n)
>>> f()
3
>>> i = 5 #函数定义后修改i的值不影响参数n的默认值
>>> f()
3
可变长度参数
- 在前面的函数介绍中,我们知道一个实参只能接收一个形参,其实在Python中,函数可以接收不定个数的参数,即用户可以给函数提供可变长度的参数,这可以通过在参数前面使用标识符*来实现。
- 可变长度参数主要有两种形式:
*parameter用来接受多个位置实参并将其放在一个元组中
**parameter接受多个关键参数并存放到字典中
(1)parameter的用法:
形参p前面有一个标识符,表明形参p可以接收不定个数的参数,并以元组的形式传递进函数。
>>> def demo(*p): #定义函数demo
print(p)
>>> demo(1,2,3) #调用函数
(1, 2, 3)
>>> demo(1,2)
(1, 2)
>>> demo(1,2,3,4,5,6,7)
(1, 2, 3, 4, 5, 6, 7)
>>> demo(1)
(1,) #即使实参只有一个,也以元组的形式传递进函数中
- 用标识符*实现的可变长度的参数也可以和其它普通参数混合使用,这时一般将可变长度参数放在形参列表的最后。
>>> def all_print(brgs,*args): #函数定义
print (brgs)
print (args)
>>> all_print(“abc”,“a”,2,3,“b”) #函数调用
abc#位置参数原样输出
(‘a’, 2, 3, ‘b’) #可变长度参数结合成元组
>>>
- (2)parameter的用法
形参p前面有标识符,表明形参p可以接收多个关键字参数,并以一个字典的形式传递进函数。
>>> def demo(**p):
for item in p.items():
print(item)
>>> demo(x=1,y=2,z=3)
('y', 2)
('x', 1)
('z', 3)
参数传递的序列解包
传递参数时,可以通过在实参序列前加星号*将其解包,然后传递给多个单变量形参。
>>> def demo(a, b, c):
print(a+b+c)
>>> seq = [1, 2, 3]
>>> demo(*seq)
6
>>> tup = (1, 2, 3)
>>> demo(*tup)
6
>>> dic = {1:'a', 2:'b', 3:'c'}
>>> demo(*dic)
6
>>> Set = {1, 2, 3}
>>> demo(*Set)
6
>>> demo(*dic.values())
abc
- 注意:调用函数时如果对实参使用一个星号*进行序列解包,这么这些解包后的实参将会被当做普通位置参数对待,并且会在关键参数和使用两个星号**进行序列解包的参数之前进行处理。
>>> def demo(a, b, c):#定义函数
print(a, b, c)
>>> demo(*(1, 2, 3)) #调用,序列解包
1 2 3
>>> demo(1, *(2, 3)) #位置参数和序列解包同时使用
1 2 3
>>> demo(1, *(2,), 3)
1 2 3
>>> demo(a=1, *(2, 3)) #序列解包相当于位置参数,优先于关键字参数处理
Traceback (most recent call last):
File "<pyshell#26>", line 1, in <module>
demo(a=1, *(2, 3))
TypeError: demo() got multiple values for argument 'a'
>>> demo(b=1, *(2, 3))
Traceback (most recent call last):
File "<pyshell#27>", line 1, in <module>
demo(b=1, *(2, 3))
TypeError: demo() got multiple values for argument 'b'
>>> demo(c=1, *(2, 3))
2 3 1
- 如果函数实参是字典,可以在前面加两个星号**进行解包,等价于关键参数。
>>> def demo(a, b, c):
print(a+b+c)
>>> dic = {'a':1, 'b':2, 'c':3}
>>> demo(**dic)
6
>>> demo(a=1, b=2, c=3)
6
>>> demo(*dic.values())
6
>>> demo(**{'a':1, 'b':2}, *(3,)) #序列解包不能在关键参数解包之后
SyntaxError: iterable argument unpacking follows keyword argument unpacking
>>> demo(*(3,), **{'a':1, 'b':2})
Traceback (most recent call last):
File "<pyshell#30>", line 1, in <module>
demo(*(3,), **{'a':1, 'b':2})
TypeError: demo() got multiple values for argument 'a'
>>> demo(*(3,), **{'c':1, 'b':2})
3 2 1
变量作用域
-
变量起作用的代码范围称为变量的作用域,不同作用域内变量名可以相同,互不影响。
-
一个变量在函数外部定义和在函数内部定义,其作用域是不同的。在函数内部定义的普通变量只在函数内部起作用,称为局部变量。当函数执行结束后,局部变量自动删除,不再可以使用。
-
局部变量的引用比全局变量速度快,应优先考虑使用。
-
如果想要在函数内部给一个定义在函数外的变量赋值,那么这个变量就不能是局部的,其作用域必须为全局的,能够同时作用于函数内外,称为全局变量,可以通过global来定义。这分为两种情况:
-
一个变量已在函数外定义,如果在函数内需要为这个变量赋值,并要将这个赋值结果反映到函数外,可以在函数内用global声明这个变量,将其声明为全局变量。
-
在函数内部直接将一个变量声明为全局变量,在函数外没有声明,该函数执行后,将增加为新的全局变量。
def demo():
global x #声明x是全局变量
x = 3
y =4
print(x,y)
>>> x = 5
>>> demo() #本次调用demo中, global x 为声明使用外部变量x
3 4
>>> x
3
>>> y
NameError: name 'y' is not defined
>>> del x
>>> x
NameError: name 'x' is not defined
>>> demo()#本次调用demo中, global x 为定义(新增)全局变量
3 4
>>> x
3
>>> y
NameError: name 'y' is not defined
也可以这么理解:
-
在函数内如果只引用某个变量的值而没有为其赋新值,该变量为(隐式的)全局变量;
-
如果在函数内任意位置有为变量赋新值的操作,该变量即被认为是(隐式的)局部变量,除非在函数内显式地用关键字global进行声明。
-
注意:在某个作用域内只要有为变量赋值的操作,该变量在这个作用域内就是局部变量,除非使用global进行了声明。
>>> x = 3
>>> def f():
print(x) #本意是先输出全局变量x的值,但是不允许这样做
x = 5 #有赋值操作,因此在整个作用域内x都是局部变量
print(x)
>>> f()
Traceback (most recent call last):
File "<pyshell#10>", line 1, in <module>
f()
File "<pyshell#9>", line 2, in f
print(x)
UnboundLocalError: local variable 'x' referenced before assignment
- 如果局部变量与全局变量具有相同的名字,那么该局部变量会在自己的作用域内隐藏同名的全局变量。
>>> x = 3
>>> def demo():
x = 5 #创建了局部变量,并自动隐藏了同名的全局变量
>>> demo()
>>> x #函数执行不影响外面全局变量的值
3
- 如果需要在同一个程序的不同模块之间共享全局变量的话,可以编写一个专门的模块来实现这一目的。例如,假设在模块A.py中有如下变量定义:
global_variable = 0
- 可以在模块B.py中包含以下用来修改全局变量的语句:
import A
A.global_variable = 1
- 也可以在模块C.py中有以下语句来访问全局变量的值:
import A
print(A.global_variable)
lambda表达式
- lambda表达式可以用来声明匿名函数,也就是没有函数名字的临时使用的小函数,尤其适合需要一个函数作为另一个函数参数的场合。
- lambda表达式只包含一个表达式,该表达式的计算结果可以看作是函数的返回值,不允许包含其他复杂的语句,但在表达式中可以调用其他函数。
>>> f = lambda x,y,z:x+y+z #可以给lambda表达式起名字
>>> f(1,2,3) #像函数一样调用
6
>>> g = lambda x, y=2,z=3: x+y+z #参数默认值
>>> g(1)
6
>>> g(2, z=4, y=5) #关键参数
11
>>> def demo(n):
return n*n
>>> demo(5)
25
>>> a_list = [1,2,3,4,5]
>>> list(map(lambda x: demo(x), a_list)) #在lambda表达式中调用函数
[1, 4, 9, 16, 25]
>>> data = list(range(20)) #创建列表
>>> data
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
>>> import random
>>> random.shuffle(data) #打乱顺序
>>> data
[4, 3, 11, 13, 12, 15, 9, 2, 10, 6, 19, 18, 14, 8, 0, 7, 5, 17, 1, 16]
>>> data.sort(key=lambda x: x) #和不指定规则效果一样
>>> data
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
>>> data.sort(key=lambda x: len(str(x))) #按转换成字符串以后的长度排序
>>> data
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
>>> data.sort(key=lambda x: len(str(x)), reverse=True)
#降序排序
>>> data
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> import random
>>> x = [[random.randint(1,10) for j in range(5)] for i in range(5)]
#使用列表推导式创建列表
#包含5个子列表的列表
#每个子列表中包含5个1到10之间的随机数
>>> for item in x:
print(item)
[5, 6, 8, 7, 4]
[1, 5, 3, 9, 4]
[9, 6, 10, 7, 6]
[8, 2, 7, 1, 6]
[1, 7, 5, 3, 5]
>>> y = sorted(x, key=lambda item: (item[1], item[4]))
#按子列表中第2个元素升序、第5个元素升序排序
>>> for item in y:
print(item)
[8, 2, 7, 1, 6]
[1, 5, 3, 9, 4]
[5, 6, 8, 7, 4]
[9, 6, 10, 7, 6]
[1, 7, 5, 3, 5]
__name__属性
- 每个Python模块脚本在运行时都有一个“name”属性。如果脚本作为模块被导入,则其“name”属性的值被自动设置为模块名;如果脚本独立运行,则其“name”属性值被自动设置为“main”。例如,假设文件nametest.py中只包含下面一行代码:print(name)
- 在IDLE中直接运行该程序时,或者在命令行提示符环境中运行该程序文件时,运行结果如下:main
- 而将该文件作为模块导入时得到如下执行结果:
>>> import nametest
nametest
- 在导入模块的时候,首先,会去程序的当前主目录下寻找;如果找不到,就去PYTHONPATH目录下寻找;如果还找不到,就去标准链接库目录寻找。
- 可以将要被导入的模块所在的路径- 导入到path中。
- 我们先通过import sys导入sys模块。
- 然后通过sys.path.append(r“F:\python\moddm”)将模块所在的路径插入path中
- 然后再通过import dm导入dm模块
- 在很多Python代码中,__name__属性被用来区分上述module文件被使用的两种方式。一种常用的做法是将module文件自己的单测代码,放到__name__属性为"main"的情形中去。
- 这样一来,程序直接执行时将会得到测试代码,而使用import语句将其作为模块导入后可以使用其中的类、方法、常量或其他成员。
def fib(n):
a,b=1,1
while n>=a:
print(a,end=" ")
a,b=b,a+b
#以下为检测的代码
if __name__=='__main__':
fib(100)
map()
- 内置函数map()可以将一个函数作用到一个序列或迭代器对象上。
>>> list(map(str,range(5)))
['0', '1', '2', '3', '4']
>>> def add5(v):
return v+5
>>> list(map(add5,range(10)))
[5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
>>> def add(x, y):return x+y
>>> list(map(add, range(5), range(5)))
[0, 2, 4, 6, 8]