Python3从入门到实战
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

3.3 函数的参数

3.3.1 默认形参

函数的形参可以有默认值,称为“默认形参”,调用函数时如果没有为默认形参提供实际参数,则该形参就取默认值。例如:

def date(year, month='01', day='01'):
  print(year, month, day)

date(2018)
date(2018, '07')
date(2018, '07', '25')

输出:

2018 01 01
2018 07 01
2018 07 25

如果一个函数的形参中既有默认形参也有非默认形参,则默认形参必须都在非默认形参的后面,默认形参后面不能再有非默认形参。例如:

def f(a, b=2, c):
    pass

执行后产生语法错误(“默认形参b的后面又出现了非默认形参c”):

  File "<ipython-input-1-dcdf3a460ecb>", line 1
    def f(a, b=2, c):
          ^
SyntaxError: non-default argument follows default argument

默认形参的默认值是函数定义时就计算好的。例如:

i=5
def f(arg=i):
    print(arg)

i=6
f()  # 将输出:5

输出:

5

因为定义函数f()时,默认形参arg的值等于i(i=5),尽管i变量之后修改了,但并不会改变最初的函数f()的默认值,所以输出是5。

由于默认形参的默认值只在函数定义时计算一次,所以每次函数调用这个默认形参时,始终指向的都是初始化的那个对象。如果这个对象是一个可变对象(mutable object),则当每次函数调用时,如果对这个默认形参引用的这个对象进行修改,则修改的将都是同一个对象。例如:

def f(var, arr=[]):
    arr.append(var)
    return arr

print(f(1))
print(f(2))

输出:

[1]
[1, 2]

上例中函数定义时,arr引用的是空的list对象,函数调用f(1)、f(2)中的arr也始终引用的是同一个list对象,在f(1)中将1追加到这个list对象(arr.append(1)),在f(2)中将2追加到这个list对象(arr.append(2))。

如果希望每次调用函数时默认形参指向的是不同的对象,则可以采用下面的技巧:

def f(var, arr=None):
    if arr==None:
      arr=[]
    arr.append(var)
    return arr

print(f(1))
print(f(2))

输出:

[1]
[2]

默认形参arr初始化为None对象,每次调用函数f()时默认形参arr都是引用这个对象,但函数体开头因为满足条件“arr==None”,所以对arr的重新赋值“arr=[]”使得这个arr指向了一个新的空的list对象[],而不再是原先参数arr引用的那个None对象了。实际上,Python可以通过赋值运算符重新定义同名的变量以引用不同的对象,即每个函数f()中的arr执行过if语句后,就引用一个新的空的list对象[]。

也可以去掉这个默认形参。例如:

def f(var):
    arr=[]
    arr.append(var)
    return arr

print(f(1))
print(f(2))

输出:

[1]
[2]

当然,还可以为默认形参传递一个实参,此时,形参就与这个实参引用同一个对象。

3.3.2 位置实参和关键字实参

函数定义中的形参是有顺序的,调用函数时传递的实参是按照顺序为对应位置的形参赋值的。这种按照位置顺序传递的实参称为“位置实参”。例如:

def hello(name, msg="Good morning! "):
  print("哈罗!", name + ', ' + msg)
#调用函数:传递位置参数
hello("小白")             #"小白"作为第1个位置实参传递给形参name,形参msg有默认值
hello("老张", "你好吗?")   #"老张"作为第1个位置实参传递给形参name
                          #"你好吗?"作为第2个位置实参传递给形参msg

输出:

哈罗!小白,Good morning!
哈罗!老张,你好吗?

另外还有一种称为“关键字实参”的参数传递方式,该传递方式在传递实参时指明这个实参传递给哪个形参。其语法格式是:

形参名=实参

也可以通过关键字实参传递实参给被调用函数。例如:

hello(name="小白", msg="你好吗?")  #形参name=实参"小白",形参msg=实参"你好吗"
hello(msg="你好吗?", name="老张")  #形参msg=实参"你好吗",形参name=实参"老张"
# 1个位置参数,1个关键字参数
hello("李平", msg="你好吗?")

输出:

哈罗!小白,你好吗?
哈罗!老张,你好吗?
哈罗!李平,你好吗?

无论函数的形参是否有默认值,都可以采用“位置实参”或“关键字实参”的方式将实参传递给形参。

3.3.3 任意形参(可变形参)

如果在定义函数时不知道将来使用者调用这个函数时传递的实际参数个数,则可以在定义函数时,在这个形参名前加一个星号*。函数定义中的这种形参称为“任意形参”或“可变形参”。传递给可变形参的多个实参被组装成一个tuple对象并传递给这个可变形参。

例如:

def hello(*names):        #可变形参names可以接收任意多的实参
  print("哈罗:")
  for name in names:      #对比可变形参names元组(tuple)的每个元素name
    print(name)
  print();

如果调用这个函数:

hello("小白", "老张")      #给names形参传递两个实参
hello("小白", "老张", "老王")#给names形参传递三个实参

输出:

哈罗:
小白
老张

哈罗:
小白
老张
老王

在函数调用时,可以传递多个实参给可变形参,这些实参(如“小白”“老张”)都被打包为一个tuple对象传递给函数的可变形参,函数内部可以用“for...in”或下标运算符[]访问该可变形参引用的tuple对象中的元素。

如果函数的形参中既有默认形参又有可变形参,则默认形参必须位于可变形参的后面。例如:

def date(*args, sep="/"):
  return sep.join(args)

print(date("2018", "07", "25"))    #给args可变形参传递了3个实参
date("2018", "07", "25", sep=".")

输出:

2018/07/25
'2018.07.25'

注意

函数定义中的可变形参最多只能有一个,不能有两个可变形参,否则,如果给这些形参传递实参,就无法区分哪个实参是传给哪个形参了。

3.3.4 字典形参

和可变形参类似,还有一个字典形参。字典形参名前面有2个**,这个形参指向的是一个dict对象,调用函数时必须以“key=value”的形式传递可变数量的实参,这些实参被组装成一个dict对象,并赋值给字典形参。

如果函数定义中既有可变形参又有字典形参,则字典形参必须位于可变形参的后面。例如:

def f(x, *y, **z):
    print("x:", x)
    #访问任意形参中的参数
    for e in y:
      print(e)
    print()
    #访问字典形参中的参数
    for key in z:
      print(key, ":", z[key])

如果调用这个函数:

f("hello", "li ping",60.5, year="2018", month=7, day=25)

则输出:

x: hello
li ping
60.5

year : 2018
month : 7
day : 25

在调用函数时,通过“key=value”(“键-值”)的形式(“year="2018", month=7, day=25”)将实参传给字典形参。而在函数体中,则是通过字典形参获取传来的这些实参“键-值”。也就是说,这些实参被打包到了字典形参中。

注意

函数定义中的字典形参最多只能有一个,不能有两个字典形参,否则,如果给这些形参传递字典实参,则无法区分哪个字典实参是传给哪个字典形参了。

3.3.5 解封参数列表

假设有一个如下的函数用于计算两个对象(数)的和:

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

调用这个函数时,就必须传递两个实参给它:

print(add(3,5))

输出:

8

假设需要求和的两个数放在一个tuple或list对象中,能否将这两个数直接传递给这个函数add()呢?例如:

ab=[3,5]
print(add(ab))

执行后产生TypeError(类型错误)(“缺少一个位置参数”)的错误:

----------------------------------------------------------------------

TypeError                    Traceback(most recent call last)

<ipython-input-14-a219a64a643d> in <module>()
      1 ab=[3,5]
----> 2 print(add(ab))
TypeError: add()missing 1 required positional argument: 'y'

以上错误是因为函数需要两个实参,而这里只传递了一个实参ab。解决方法是,通过下标运算符从这个ab对象中获得两个实参。例如:

print(add(ab[0], ab[1]))

输出:

8

有没有一种更方便的方法呢?答案是:有。将这个list或tuple变量名前用一个*作为实参传给被调用函数。Python解释器会自动从这个list或tuple对象中解析出每个实参并传递给被调用函数。这种传递实参的方式称为“解封实参列表”。例如:

print(add(*ab))

输出:

8

再如,生成“两个整数之间的一系列整数的可迭代对象”的函数range()需要单独的start和end参数,而start和end的值如果在一个list或tuple对象中,就可以通过这种解封实参列表方法将list或tuple对象的数据元素作为实参传递给函数range()。例如:

#函数range接收两个参数:range(start, end)
s=list(range(3,7))
print(s)
args=[3, 7]
s=list(range(*args))         #*args将args列表中的元素3和7分离出来
print(s)

输出:

[3, 4, 5, 6]
[3, 4, 5, 6]

类似地,假设参数在一个字典中,则要用两个星号**将它们分离出来。例如:

def f(name, score=0.0):
  print('the name: ', name)
  print('the score:', score)
d={"name": "li ping", "score": 60.5}
f(**d)  #**d将字典中的参数分离出来

输出:

the name:  li ping
the score: 60.5

总结

● 函数的形参可以有默认值,称为默认形参,形参名前有一个*的称为可变形参,形参名前有两个**的称为字典形参。可变形参必须在非默认形参的后面,默认形参必须在非默认形参和可变形参的后面,字典形参必须放在最后面。

● 函数定义中的形参是有顺序的,实参可以按照位置传递给形参,称为位置实参,也可以按照形参名=实参的方式将实参传递给形参,称为关键字实参。关键字实参可以任意顺序排列。

● 可以给可变形参传递多个实参,这些实参被打包成一个tuple对象传递给可变形参。函数可以像普通tuple对象一样访问可变形参中的实参。

● 可以采用键-值的方式将字典实参传递给字典形参。这些键-值实参被打包成一个字典对象传给字典形参。函数可以像普通字典对象一样访问字典形参中的每个键-值实参。

● 假如要传递给函数的实参放在一个tuple或list对象中,则可以通过在指向这个对象的变量名前加*的解封实参列表方式传递给被调用函数,list或tuple中的这些实参将被解封传递给被调用函数的形参。假如要传给函数的实参放在一个dict对象中,则可以通过在指向这个对象的变量名前加**的解封实参列表方式将字典实参传递给形参。