master
/ 4.2 函数的参数传递.ipynb

4.2 函数的参数传递.ipynb @masterview markup · raw · history · blame

Notebook

4.3 函数的参数传递

当函数定义里有多个参数时,其参数的传递形式主要有以下五种:__位置传递____关键字____默认值传递____包裹传递____解包裹传递__

Python 中一切皆为__对象__,数字是对象,列表是对象,函数也是对象。而变量是对象的一个__引用__,对象的操作都是通过引用来完成的。

函数调用过程中,__传递的是对象__。参数的传递本质上是名字到对象的__绑定过程__

默认情况下,参数可以按位置或显式关键字传递给 Python 函数。为了让代码易读、高效,最好限制参数的传递方式,这样,开发者只需查看函数定义,即可确定参数项是仅按位置、按位置或关键字,还是仅按关键字传递。

函数定义如下:下:下:

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2): ----------- ---------- ---------- | | | | 位置或关键字参数 | | - 仅关键字参数 -- 仅位置参数

/ 和 是可选的。这些符号表明形参如何把参数值传递给函数:位置、位置或关键字、关键字。
关键字形参也叫作命名形参。函数定义中未使用 / 和
时,参数可以按位置或关键字传递给函数。

4.3.1 位置传递

位置固定,参数传递时按照形式参数__定义的顺序__提供实际参数。 其优点是使用方便,缺点是当参数数目较多时,参数对应容易混淆。且要求传入参数的数量必须和定义函数时参数的数量相同。

仅限位置 时,形参的顺序很重要,且这些形参不能用关键字传递。仅限位置形参应放在 / (正斜杠)前。/ 用于在逻辑上分割仅限位置形参与其它形参。如果函数定义中没有 /,则表示没有仅限位置形参。

In [ ]:
def fun(name, city, hobby):
    '''接收三个字符串作为参数,返回一个进行自我介绍的字符串'''
    return '我的名字是{},来自{},我的爱好是{}。 '.format(name, city, hobby) 


n,c,h = input().split()  # 根据空格切分输入字符串,分别赋值给n,c,h 
print(fun(n,c,h))       # 根据实参变量n,c,h的顺序传递参数或值给形参name, city, hobby

调用fun()函数时,将实际参数n、c、h这三个字符串对象按顺序赋值给形式参数name、city、hobby,或者说将三个变量标签name、city、hobby按出现的顺序分别贴在"夏琦"、"武汉"、"羽毛球"这三个字符串对象上。

在定义fun()函数时,参数name、city、hobby尚未绑定对象,没有值。

只有经过函数调用,才会把调用时的参数值赋值给形式参数,函数体中的变量才有了值。

4.3.2 关键字传递

关键字传递是指,在函数调用时__提供__实际参数所对应的__形式参数__的名称,根据__参数名称来传递参数__

关键字并不需要遵守位置的对应关系。其优点是明确标示实际参数和形式参数的对应关系,参数的书写顺序更灵活。缺点是增加了函数调用时的代码书写量。

In [ ]:
def fun(name, city, hobby):
    return '我的名字是{},来自{},我的爱好是{}'.format(name, city, hobby) 


n,c,h = input().split()  # 切分字符串,分别赋值给n,c,h 
print(fun(hobby = h, city = c,name = n))      # 根据关键字name, city, hobby来传递参数,关键字传递时顺序无关 

位置传递和关键字传递__可以混用__。但要注意的是,混用时,按__位置传递__的参数要出现在按__关键字传递__的参数__之前__。否则,编译器无法明确知道除关键字以外的参数出现的顺序。

In [ ]:
def fun(name, city, hobby):
    return '我的名字是{},来自{},我的爱好是{}'.format(name, city, hobby)

n,c,h = input().split()   # 切分字符串,分别赋值给n,c,h
print(fun(n, hobby = h, city = c))      # 位置参数n出现在关键字参数h和c之前 

在定义函数时,我们可以在参数列表中用“/”设置强制位置参数(positional-only arguments)。用“*”设置命名关键字参数。
所谓强制位置参数,就是调用函数时只能按照参数位置来接收参数值的参数;而命名关键字参数只能通过“参数名=参数值”的方式来传递和接收参数,大家可以看看下面的例子。

In [ ]:
def fun(name, city, hobby, /):  # /前面的参数是强制位置参数
    """接收三个字符串作为位置参数,返回一个进行自我介绍的字符串"""
    return '我的名字是{},来自{},我的爱好是{}。 '.format(name, city, hobby)


print(fun('夏琦', '武汉', '羽毛球'))  # 可以按位置传递参数
# 输出 我的名字是夏琦,来自武汉,我的爱好是羽毛球。 
In [ ]:
def fun(name, city, hobby, /):  # /前面的参数是强制位置参数
    """接收三个字符串作为位置参数,返回一个进行自我介绍的字符串"""
    return '我的名字是{},来自{},我的爱好是{}。 '.format(name, city, hobby)


# 下面的代码会产生TypeError错误,错误信息提示“强制位置参数是不允许给出参数名的”
# TypeError: fun() got some positional-only arguments passed as keyword arguments: 'city, hobby'
n, c, h = '夏琦', '武汉', '羽毛球'
print(fun(n, hobby=h, city=c))  # 传入一个位置参数和两个关键字参数
In [ ]:
def fun(name, *, city, hobby):  # *后面的参数是强制关键字参数
    """接收三个字符串,一个位置参数两个关键字参数,返回一个进行自我介绍的字符串"""
    return '我的名字是{},来自{},我的爱好是{}。 '.format(name, city, hobby)


n, c, h = '夏琦', '武汉', '羽毛球'
print(fun(n, hobby=h, city=c))  # 传入一个位置参数和两个关键字参数
# 输出 我的名字是夏琦,来自武汉,我的爱好是羽毛球。
In [ ]:
def fun(name, *, city, hobby):  # *后面的参数是强制关键字参数
    """接收三个字符串,一个位置参数两个关键字参数,返回一个进行自我介绍的字符串"""
    return '我的名字是{},来自{},我的爱好是{}。 '.format(name, city, hobby)


# 下面的代码会产生TypeError错误,错误信息提示“函数只能接收一个位置参数,但传递了3个位置参数”
# TypeError: fun() takes 1 positional argument but 3 were given
print(fun('夏琦', '武汉', '羽毛球'))  # 传入三个位置参数

4.3.3 默认值传递

在定义函数的时候,使用形如city='武汉'的方式,可以给形式参数赋予_默认值(default)__。在函数调用时,如果该参数得到传入值,按传入值进行计算,否则使用默认值。

In [ ]:
def fun(name, city, hobby='唱歌'):
    return '我的名字是{},来自{},我的爱好是{}。'.format(name, city, hobby)

n,c = input().split()   # 切分字符串,分别赋值给n,c,例如输入:夏琪 武汉
print(fun(n, c))        # 默认值参数可以不传值 
# 输出: 我的名字是夏琪,来自武汉,我的爱好是唱歌。

print(fun(n, c, '游泳')) # 传值时,默认值会被传入的值替换 
# 输出: 我的名字是夏琪,来自武汉,我的爱好是游泳。

可以用列表存储输入,再用星号*解包传递给函数,可做到传递任意数量的参数。

In [ ]:
def fun(name, city, hobby='唱歌'):
    return '我的名字是{},来自{},我的爱好是{}。'.format(name, city, hobby)

ls = input().split()   # 切分字符串得到列表,例如输入:夏琪 武汉,得到['夏琪', '武汉']
print(fun(*ls))        # ['夏琪', '武汉']解包为:'夏琪', '武汉'
# 输出: 我的名字是夏琪,来自武汉,我的爱好是唱歌。


ls = input().split()   # 切分字符串得到列表,例如输入:夏琪 武汉 游泳,得到['夏琪', '武汉', '游泳']
print(fun(*ls))        # ['夏琪', '武汉', '游泳']解包为:'夏琪', '武汉', '游泳'
# 输出: 我的名字是夏琪,来自武汉,我的爱好是游泳。

需要注意的是,__默认值参数__必须放在必选参数__之后__。当函数的参数有多个时,默认值参数必须在后面,非默认值参数在前面。

也就是说,__一旦__出现了带默认值的参数,__后面的其他参数都必须带默认值了__

In [ ]:
def fun( city,name='夏琪', hobby='唱歌'):    #语法错误,city 是非默认值参数,出现在默认值参数hobby的前面了
    return '我的名字是{},来自{},我的爱好是{}。'.format(name, city, hobby)


n,c,h = input().split()  # 切分字符串,分别赋值给n,c,h 
print(fun(n,c,h))        # 调用函数

练一练

利用Python中支持默认值参数的特性,修改上一节里定义的幂函数power(x,n),使其默认计算x的平方。

In [ ]:
#修改后的power(x,n)函数

需要注意的是,默认值参数可以指向不可变对象,如整型(int)、字符串(string)、浮点型(float)、元组(tuple)等,不能指向字典(dict)和列表(list)等可变对象。

In [ ]:
def fun(x, ls = []):   # 这里默认参数ls指向了空列表,列表是可变对象
    ls.append(x)       # 在列表ls的末尾追加一个元素
    return ls          # 返回ls

print(fun(1))  # 第一次调用,输出 [1] 
print(fun(3))  # 第二次调用,输出 [1, 3] 
print(fun(5))  # 第三次调用,输出 [1, 3, 5] 

按函数调用规则,每次函数调用时,形参都会被重新赋值。即在三次调用中,形参x分别被赋值为1,3,5,带缺省值的形参ls每次应该被重新赋值为空列表。

但程序的运行结果表明,在多次调用过程中,__ls并没有被重新赋值为空列表__,导致ls中的元素累积下来。 分析其原因,ls是可变对象,在函数定义时被创建,__其后所有函数调用都引用这个列表对象__。Python规定参数传递都是传递的引用,也就是传递给函数的是原变量实际所指向的内存空间。而append()方法并不会改变列表的内存空间,也就是说不会重新创建列表对象,只是向其中增加元素。所以每次调用该函数时,一直引用函数定义时创建的列表对象ls,__导致元素累积__

如果一定要用这种方法的话,可以做如下修改:

In [ ]:
def fun(x, ls = None):  # ls为默认值参数,设为不可变对象None
    if ls is None:      # 若ls值为None,说明是重新调用,将列表置为空
        ls = []
    ls.append(x)
    return ls 


print(fun(1))          # 第一次调用,输出 [1] 
print(fun(3))          # 第二次调用,输出 [3] 
print(fun(5))          # 第三次调用,输出 [5]

4.3.4 包裹传递

包裹传递也称为不定参数传递,用于在定义函数时不能确定函数调用时会传递多少个参数时使用。函数每次调用时,传递的参数数量可以不同。

  1. 当传入参数是位置传递时,所有传入参数被收集打包合并成一个元组,再传递给函数。
In [9]:
def add(*number):
    """接收不确定数量位置传递的参数,合并为一个元组number在函数体内使用"""
    print(number)  # 查看参数类型,元组
    result = 0
    for i in number:
        if type(i) in (int, float):  # 对参数进行了类型检查(数值型的才能求和)
            result = result + i
    return result


if __name__ == '__main__':
    print(add(1, 3, 5, 7, 9))
    print(add(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
    print(add(1, 2, 'hello', 3.45, 6))  # 12.45
(1, 3, 5, 7, 9)
25
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
55
(1, 2, 'hello', 3.45, 6)
12.45
  1. 当传入参数是关键字传递时,所有传入的关键字参数被收集打包合并成一个字典,再传递给函数。
In [10]:
def add(**number):
    """接收不确定数量的关键字参数,合并为一个字典number在函数体内使用"""
    print(number)
    return sum(number.values())


if __name__ == '__main__':
    print(add(a=1, b=3, c=5))
    print(add(m=1, n=2, o=3, p=4, q=5, i=6, j=7, k=8))
{'a': 1, 'b': 3, 'c': 5}
9
{'m': 1, 'n': 2, 'o': 3, 'p': 4, 'q': 5, 'i': 6, 'j': 7, 'k': 8}
36

参数列表中的**kwargs可以接收0个或任意多个关键字参数
调用函数时传入的关键字参数会组装成一个字典(参数名是字典中的键,参数值是字典中的值)
如果一个关键字参数都没有传入,那么kwargs会是一个空字典

In [ ]:
def foo(*args, **kwargs):
    print(args)
    print(kwargs)


foo(3, 2.1, True, name='骆昊', age=43, gpa=4.95)

4.3.5 解包裹传递

“*”“**”也可以以函数调用时使用,此时称为解包裹传递。

In [ ]:
def add(a, b, c):
    """接收三个位置传递参数,返回其加和"""
    return a + b + c


if __name__ == '__main__':
    num1 = (1, 3, 5)
    print(add(*num1))
    num2 = {'a': 1, 'b': 2, 'c': 3}
    print(add(**num2))

函数调用总是会给形参列表中列出的所有形参赋值,或是用位置参数,或是用关键字参数,或是用默认值。 如果存在 "identifier" 这样的形式,它会被初始化为一个元组来接收任何额外的位置参数,默认为一个空元组。 如果存在 "**identifier" 这样的形式,它会被初始化为一个新的有序映射来接收任何额外的关键字参数,默认为一个相同类型的空映射。
在 "
" 或 "*identifier" 之后的形参都是仅限关键字形参因而只能通过关键字参数传入。
在 "/" 之前的形参都是仅限位置形参因而只能通过位置参数传入。