day10-12 函数语法铺垫&函数的参数

发布时间 2023-05-20 14:47:27作者: Chimengmeng

第八节 函数

【一】函数初识

【一】函数的基本使用

  • 基于前一部分的学习,我们已经能开发一些功能简单的小程序了,但随着程序功能的增多,代码量随之增大,此时仍不加区分地把所有功能的实现代码放到一起,将会使得程序的组织结构不清晰,可读性变差,且程序中需要频繁使用同一功能时,只能重复编写该功能的实现代码,日积月累,程序将变得冗长,并且当某一功能需要修改时,又不得不找出所有定义及使用这段功能的地方修改之,管理维护的难度极大,好吧,装了半天逼,到底该如何解决提出的这些问题呢?

  • 我们完全可以从现实生活中找到简化程序设计的方案:比如一个修理工会事先准备好螺丝刀、锤子等工具,这样在进行修理的过程中,需要用到拧螺丝的功能时就直接拿来螺丝刀使用,需要用到锤击物体的功能时就直接拿来锤子使用,而无需临时制造。这个例子的核心在于’事先准备好工具’,遇到应用场景时’拿来就用’,。

  • 在程序中,具备某一功能的‘工具’指的就是函数,‘事先准备工具’的过程即函数的定义,‘拿来就用’即函数的调用。

【二】函数的定义

  • 函数的使用必须遵循’先定义,后调用’的原则。

  • 函数的定义就相当于事先将函数体代码保存起来,然后将内存地址赋值给函数名,函数名就是对这段代码的引用,这和变量的定义是相似的。

  • 没有事先定义函数而直接调用,就相当于在引用一个不存在的’变量名’。

  • 定义函数的语法

def 函数名(参数1,参数2,...):
	"""文档描述"""
	函数体
	return 值
  1. def: 定义函数的关键字;
  2. 函数名:函数名指向函数内存地址,是对函数体代码的引用。函数的命名应该反映出函数的功能;
  3. 括号:括号内定义参数,参数是可有可无的,且无需指定参数的类型;
  4. 冒号:括号后要加冒号,然后在下一行开始缩进编写函数体的代码;
  5. """文档描述""": 描述函数功能,参数介绍等信息的文档,非必要,但是建议加上,从而增强函数的可读性;
  6. 函数体:由语句和表达式组成;
  7. return 值:定义函数的返回值,return是可有可无的。
  • 参数是函数的调用者向函数体传值的媒介

    • 若函数体代码逻辑依赖外部传来的参数时则需要定义为参函数

      def my_min(x,y):
          res=x if x < y else y
          return res
      
    • 否则定义为无参函数

    def interactive():
        user=input('user>>: ').strip()
        pwd=input('password>>: ').strip()
        return (user,pwd)
    
  • 函数体为pass代表什么都不做,称之为空函数。

  • 定义空函数通常是有用的,因为在程序设计的开始,往往是先想好程序都需要完成什么功能,然后把所有功能都列举出来用pass充当函数体“占位符”,这将使得程序的体系结构立见,清晰且可读性强。

  • 例如要编写一个ftp程序

    • 我们可能想到的功能有用户认证,下载,上传,浏览,切换目录等功能
    • 可以先做出如下定义:
def auth_user():
    """user authentication function"""
    pass

def download_file():
    """download file function"""
    pass

def upload_file():
    """upload file function"""
    pass

def ls():
    """list contents function"""
    pass

def cd():
    """change directory"""
    pass
  • 之后我们便可以统筹安排编程任务
    • 有选择性的去实现上述功能来替换掉pass
    • 从而提高开发效率。

【三】函数的调用

  • 函数的使用分为定义阶段与调用阶段,定义函数时只检测语法,不执行函数体代码
    • 函数名加括号即函数调用
    • 只有调用函数时才会执行函数体代码
# 定义阶段
def foo():
    print('in the foo')
    bar()

def bar():
    print('in the bar')

# 调用阶段
foo()
  • 执行结果:
in the foo
in the bar
  • 定义阶段函数foo与bar均无语法错误

    • 而在调用阶段调用foo()时,函数foo与bar都早已经存在于内存中了
    • 所以不会有任何问题。
  • 按照在程序出现的形式和位置

    • 可将函数的调用形式分为三种:
# 1、语句形式:
foo()

# 2、表达式形式:
m=my_min(1,2) #将调用函数的返回值赋值给x
n=10*my_min(1,2) #将调用函数的返回值乘以10的结果赋值给n

# 3、函数调用作为参数的形式:
# my_min(2,3)作为函数my_min的第二个参数,实现了取1,2,3中的较小者赋值给m
m=my_min(1,my_min(2,3))
  • 若需要将函数体代码执行的结果返回给调用者,则需要用到return。
    • return后无值或直接省略return,则默认返回None,return的返回值无类型限制
    • 且可以将多个返回值放到一个元组内。
>>> def test(x,y,z):
...     return x,y,z #等同于return (x,y,z)
... 
>>> res=test(1,2,3)
>>> print(res)
(1, 2, 3)
  • return是一个函数结束的标志,函数内可以有多个return
    • 但只执行一次函数就结束了,并把return后定义的值作为本次调用的结果返回。

【二】函数的参数

【1】形参和实参引入

  • 函数的参数分为形式参数和实际参数,简称形参和实参:
    • 形参即在定义函数时,括号内声明的参数。
      • 形参本质就是一个变量名,用来接收外部传来的值。
    • 实参即在调用函数时,括号内传入的值
      • 值可以是常量、变量、表达式或三者的组合:
# 1:实参是常量
res=my_min(1,2)

# 2:实参是变量
a=1
b=2
res=my_min(a,b)

# 3:实参是表达式
res=my_min(10*2,10*my_min(3,4))

# 4:实参可以是常量、变量、表达式的任意组合
a=2
my_min(1,a,10*my_min(3,4))
  • 在调用有参函数时,实参(值)会赋值给形参(变量名)。
    • 在Python中,变量名与值只是单纯的绑定关系,而对于函数来说,这种绑定关系只在函数调用时生效,在调用结束后解除。

【2】形参和实参的具体应用

(1)位置参数
  • 位置即顺序,位置参数指的是按顺序定义的参数

    • 需要从两个角度去看:

      • 在定义函数时,按照从左到右的顺序依次定义形参,称为位置形参
        • 凡是按照这种形式定义的形参都必须被传值
      >>> def register(name,age,sex): #定义位置形参:name,age,sex,三者都必须被传值
      ...     print('Name:%s Age:%s Sex:%s' %(name,age,sex))
      ... 
      >>> register() #TypeError:缺少3个位置参数
      
      • 在调用函数时,按照从左到右的顺序依次定义实参,称为位置实参
      • 凡是按照这种形式定义的实参会按照从左到右的顺序与形参一一对应
      >>> register('lili',18,'male') #对应关系为:name=’lili’,age=18,sex=’male’
      Name:lili Age:18 Sex:male
      
(2)关键字参数
  • 在调用函数时,实参可以是key=value的形式,称为关键字参数

    • 凡是按照这种形式定义的实参,可以完全不按照从左到右的顺序定义,但仍能为指定的形参赋值

      >>> register(sex='male',name='lili',age=18)
      Name:lili Age:18 Sex:male
      
    • 需要注意在调用函数时,实参也可以是按位置或按关键字的混合使用,但必须保证关键字参数在位置参数后面,且不可以对一个形参重复赋值

      >>> register('lili',sex='male',age=18) #正确使用
      >>> register(name='lili',18,sex='male') #SyntaxError:关键字参数name=‘lili’在位置参数18之前
      >>> register('lili',sex='male',age=18,name='jack') #TypeError:形参name被重复赋值
      
(3)默认参数
  • 在定义函数时,就已经为形参赋值,这类形参称之为默认参数
    • 当函数有多个参数时,需要将值经常改变的参数定义成位置参数,而将值改变较少的参数定义成默认参数。
      • 例如编写一个注册学生信息的函数
        • 如果大多数学生的性别都为男,那完全可以将形参sex定义成默认参数
>>> def register(name,age,sex='male'): #默认sex的值为male
...     print('Name:%s Age:%s Sex:%s' %(name,age,sex))
...
  • 定义时就已经为参数sex赋值,意味着调用时可以不对sex赋值,这降低了函数调用的复杂度

    >>> register('tom',17) #大多数情况,无需为sex传值,默认为male
    Name:tom Age:17 Sex:male
    >>> register('Lili',18,'female') #少数情况,可以为sex传值female
    Name:Lili Age:18 Sex:female
    

需要注意:

  • 默认参数必须在位置参数之后
  • 默认参数的值仅在函数定义阶段被赋值一次
>>> x=1
>>> def foo(arg=x):
...     print(arg)
... 
>>> x=5 #定义阶段arg已被赋值为1,此处的修改与默认参数arg无任何关系
>>> foo()
1
  • 默认参数的值通常应设为不可变类型
  • 每次调用是在上一次的基础上向同一列表增加值,修改如下
>>> def foo(n,arg=[]):
...     arg.append(n)
...     return arg
... 
>>> foo(1)
[1]
>>> foo(2)
[1, 2]
>>> foo(3)
[1, 2, 3]
>>> def foo(n,arg=None):
...     if arg is None:
...         arg=[]
...     arg.append(n)
...     return arg
... 
>>> foo(1)
[1]
>>> foo(2)
[2]
>>> foo(3)
[3]
(4)可变长参数((*与**的用法))
  • 参数的长度可变指的是在调用函数时,实参的个数可以不固定
    • 而在调用函数时,实参的定义无非是按位置或者按关键字两种形式
    • 这就要求形参提供两种解决方案来分别处理两种形式的可变长度的参数
(4.1)可变长参数的位置参数
  • 如果在最后一个形参名前加 * 号,那么在调用函数时,溢出的位置实参,都会被 * 接收,以元组的形式保存下来赋值给该形参
>>> def foo(x,y,z=1,*args): #在最后一个形参名args前加*号
...     print(x)
...     print(y)
...     print(z)
...     print(args)
... 
>>> foo(1,2,3,4,5,6,7)  #实参1、2、3按位置为形参x、y、z赋值,多余的位置实参4、5、6、7都被*接收,以元组的形式保存下来,赋值给args,即args=(4, 5, 6,7)

1
2
3
(4, 5, 6, 7)
  • 如果我们事先生成了一个列表,仍然是可以传值给 *args
>>> def foo(x,y,*args):
...     print(x)
...     print(y)
...     print(args)
... 
>>> L=[3,4,5]
>>> foo(1,2,*L) # *L就相当于位置参数3,4,5, foo(1,2,*L)就等同于foo(1,2,3,4,5)
1
2
(3, 4, 5)

注意:如果在传入 L 时没有加 * ,那 L 就只是一个普通的位置参数了

>>> foo(1,2,L) #仅多出一个位置实参L
1
2
([1, 2, 3],)

如果形参为常规的参数(位置或默认),实参仍可以是 * 的形式

>>> def foo(x,y,z=3):
...     print(x)
...     print(y)
...     print(z)
... 
>>> foo(*[1,2]) #等同于foo(1,2)
1
2
3

如果我们想要求多个值的和,*args 就派上用场了

>>> def add(*args):
...     res=0
...     for i in args:
...         res+=i
...     return res
... 
>>> add(1,2,3,4,5)
15
(4.2)可变长度的关键字参数
  • 如果在最后一个形参名前加 **
    • 那么在调用函数时,溢出的关键字参数,都会被 ** 接收,以字典的形式保存下来赋值给该形参
>>> def foo(x,**kwargs): #在最后一个参数kwargs前加**
...     print(x)        
...     print(kwargs)   
... 
>>> foo(y=2,x=1,z=3) #溢出的关键字实参y=2,z=3都被**接收,以字典的形式保存下来,赋值给kwargs
1
{'z': 3, 'y': 2}
  • 如果我们事先生成了一个字典,仍然是可以传值给 **kwargs
>>> def foo(x,y,**kwargs):
...     print(x)
...     print(y)
...     print(kwargs)
... 
>>> dic={'a':1,'b':2} 
>>> foo(1,2,**dic) #**dic就相当于关键字参数a=1,b=2,foo(1,2,**dic)等同foo(1,2,a=1,b=2)
1
2
{'a': 1, 'b': 2}

注意:如果在传入 dic 时没有加 ** ,那 dic 就只是一个普通的位置参数了

>>> foo(1,2,dic) #TypeError:函数foo只需要2个位置参数,但是传了3个
  • 如果形参为常规参数(位置或默认),实参仍可以是 ** 的形式
>>> def foo(x,y,z=3):
...     print(x)
...     print(y)
...     print(z)
... 
>>> foo(**{'x':1,'y':2}) #等同于foo(y=2,x=1)
1
2
3
  • 如果我们要编写一个用户认证的函数,起初可能只基于用户名密码的验证就可以了
    • 可以使用 **kwargs 为日后的扩展供良好的环境,同时保持了函数的简洁性。
>>> def auth(user,password,**kwargs): 
...     pass 
...
(5)命名关键字参数
  • 在定义了 ** kwargs参数后
    • 函数调用者就可以传入任意的关键字参数key=value
      • 如果函数体代码的执行需要依赖某个key,必须在函数内进行判断
>>> def register(name,age,**kwargs):
...     if 'sex' in kwargs:
...         #有sex参数
...         pass
...     if 'height' in kwargs:
...         #有height参数
...         pass
...
  • 想要限定函数的调用者必须以key=value的形式传值
    • Python3提供了专门的语法:+
      • 需要在定义形参时,用 * 作为一个分隔符号,* 号之后的形参称为命名关键字参数。
    • 对于这类参数,在函数调用时,必须按照key=value的形式为其传值,且必须被传值
>>> def register(name,age,*,sex,height): #sex,height为命名关键字参数
...     pass
... 
>>> register('lili',18,sex='male',height='1.8m') #正确使用
>>> register('lili',18,'male','1.8m') # TypeError:未使用关键字的形式为sex和height传值
>>> register('lili',18,height='1.8m') # TypeError没有为命名关键字参数height传值。
  • 命名关键字参数也可以有默认值,从而简化调用
>>> def register(name,age,*,sex='male',height):
...     print('Name:%s,Age:%s,Sex:%s,Height:%s' %(name,age,sex,height))
... 
>>> register('lili',18,height='1.8m')
Name:lili,Age:18,Sex:male,Height:1.8m
  • 需要强调的是:
    • sex不是默认参数,height也不是位置参数
    • 因为二者均在 * 后,所以都是命名关键字参数,形参sex=’male’属于命名关键字参数的默认值
      • 因而即便是放到形参height之前也不会有问题。
        • 另外,如果形参中已经有一个*args了,命名关键字参数就不再需要一个单独的 * 作为分隔符号了
>>> def register(name,age,*args,sex='male',height):
...   print('Name:%s,Age:%s,Args:%s,Sex:%s,Height:%s' %(name,age,args,sex,height))
... 
>>> register('lili',18,1,2,3,height='1.8m') #sex与height仍为命名关键字参数
Name:lili,Age:18,Args:(1, 2, 3),Sex:male,Height:1.8m

(6)*args**kwargs 的组合使用

  • 综上所述所有参数可任意组合使用,但定义顺序必须是:
    • 位置参数默认参数*args命名关键字参数**kwargs
  • 可变参数*args 与关键字参数**kwargs通常是组合在一起使用的
  • 如果一个函数的形参为 **args**kwargs,那么代表该函数可以接收任何形式、任意长度的参数
>>> def wrapper(*args,**kwargs):
...     pass
...
  • 在该函数内部还可以把接收到的参数传给另外一个函数(这在4.6小节装饰器的实现中大有用处)
>>> def func(x,y,z):
...     print(x,y,z)
... 
>>> def wrapper(*args,**kwargs):
...     func(*args,**kwargs)
...
>>> wrapper(1,z=3,y=2)
1 2 3

提示: *args**kwargs 中的args和kwargs被替换成其他名字并无语法错误,但使用args、kwargs是约定俗成的。

按照上述写法,在为函数wrapper传参时,其实遵循的是函数func的参数规则,调用函数wrapper的过程分析如下:

  1. 位置实参1被*接收,以元组的形式保存下来,赋值给args,即args=(1,),关键字实参z=3,y=2被**接收,以字典的形式保存下来,赋值给kwargs,即kwargs=
  2. 执行func(args,kwargs),即func(*(1,),** {'y': 2, 'z': 3}),等同于func(1,z=3,y=2)