关于python2与python3中的位置参数,可变参数,默认参数,关键字参数

参考:
https://blog.csdn.net/Alian_W/article/details/105131376
https://www.jianshu.com/p/98f7e34845b5

1. 位置参数 (Positional)

位置参数顾名思义就是与位置有关的参数,在调用函数的时候需要根据传入参数的位置来一一对应参数。并且,位置参数是不可缺省的,即不传入完整的参数是会报错的。位置参数是最为常见的一种参数,例如:

1
2
3
4
def add(num1, num2):
return num1 + num2

add(1,2)

add 函数的两个参数即为位置参数,调用时第几个位置传入,则对应函数定义中该位置的参数。

小结:位置参数就是与函数参数位置有关的参数,且函数的位置参数不可缺省,必须传满

2. 默认参数 (Default)

默认参数顾名思义就是与默认值有关的参数,为了增加函数的鲁棒性,也为了传参时在可以使用默认设置的情况下不用传那么多参数,参数可以设置默认值,这样即使缺省了该参数也不会报错。例如:

1
2
3
4
def minus(num1, num2=0):
return num1 - num2

minus(1) #1传给num1,此时num2缺省不会报错

注意:定义函数时默认参数一定要在位置参数之后。其实很好理解,若默认参数可以放在位置参数之前,那当传参的个数少于参数个数,解释器到底是将参数给位置参数还是给默认参数?如下所示:

1
2
3
4
5
6
7
def minus2(num1=1, num2):
return num1 - num2
# 定义时会报错:SyntaxError: non-default argument follows default argument

# 若给num1,则num2缺省了,不就报错了?
# 若给num2,我怎么知道你是不是要给num1赋值?
minus2(1)

小结:默认参数就是需要设置默认值的参数,默认参数必须放在位置参数之后,当需要使用默认值时可以不传默认参数

3. 可变参数

在定义函数时有可能出现不确定需要传递的参数的个数的情况,这时可变参数将会给我们带来极大的便利。顾名思义,可变参数就是传入的参数个数是可变的,可以是1个、2个到任意个,还可以是0个。

示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
def fun(num1, num2=1, *args):
print(args)
print(*(args)) # 等价于写成 print(*args)

fun(1, 2, 3, 4, 5)
# 结果:
# (3, 4, 5)
# 3 4 5

# 1和2 分别对应位置参数与关键字参数
# 剩余的3 4 5都传递给了 *args
# 通过print(args)可以看到这个参数装的是一个元组,里面都是剩余的没对应上的参数
# 通过 *(args)拆包可以直接将里面的参数获取

函数定义时, *args 表示把传进来的位置参数都装在元组 args 里面;函数内部的*args 表示拆包。

从函数定义中参数的位置来看,从左到右依次是位置参数、默认参数、可变参数(*args)。(Python2中必须按这个顺序传,Python3中默认参数可以在可变参数之后,具体示例见后文

可以注意到,按上面位置参数、默认参数、可变参数的顺序来定义函数时,在调用时,如果想传入可变参数,必须先传入默认参数。如上面的示例,2被传给num2,(3,4,5)被传给args,就算我们想使用num2的默认值1,也必须在调用时写明。例如我们想给num2传入1,给args传入(2,3,4,5),需写作 fun(1,1,2,3,4,5),如果省略,写作fun(1,2,3,4,5),则会导致2被传给num2。猜测这也是为什么Python3中允许函数定义时将可变参数写在默认参数前,如此默认参数如果使用默认值则无需传入。


示例2:以一道数学题为例,给定一组数字 \(a,b,c\), …,请计算 \(a^2 + b^2 + c^2 +\)

要定义出这个函数,我们必须确定输入的参数。由于参数个数不确定,我们首先想到可以把a,b,c……作为一个list或tuple传进来,这样,函数可以定义如下:

1
2
3
4
5
def calc(numbers):
sum = 0
for n in numbers:
sum += n * n
return sum

调用的时候,需要传入一个list或tuple:

1
2
3
4
>>> calc([1, 2, 3])
14
>>> calc((1, 3, 5, 7))
84

如果利用可变参数,函数的定义和调用方式如下:

1
2
3
4
5
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
1
2
3
4
>>> calc(1, 2, 3)
14
>>> calc(1, 3, 5, 7)
84

函数定义的差别,仅仅在参数前面加了一个*号,函数内部,numbers接收到的是一个tuple,函数代码完全不变。函数调用的差别,由原来传入一个tuple或list变为传入单个数。调用该函数时,可以传入任意个参数,包括0个参数:

1
2
3
4
>>> calc(1, 2)
5
>>> calc()
0

如果已经有一个list或者tuple,要调用一个可变参数怎么办?可以这样做:

1
2
3
>>> nums = [1, 2, 3]
>>> calc(nums[0], nums[1], nums[2])
14

这种写法当然是可行的,但是太繁琐,所以Python允许你在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去:

1
2
3
>>> nums = [1, 2, 3]
>>> calc(*nums)
14

*nums表示把nums这个list的所有元素作为可变参数传进去(拆包)。这种写法相当有用,而且很常见。

小结:*args传递参数的时候会被封装成一个元组形式,可以通过拆包的方式获取里面的参数。

4. 关键字参数 (Keyword)

可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple,而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。请看示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def fun(num1, num2=1, *args, **kwargs):
print(args)
print(*args)
print(kwargs)
print(*kwargs)
print(kwargs['name'], kwargs['age'])

fun(1, 2, 3, 4, 5, name='ali', age=100)
# 结果:
# (3, 4, 5)
# 3 4 5
# {'name': 'ali', 'age': 100}
# name age
# ali 100

直接打印kwargs可以看到传来的是一个字典,即前面的3 4 5 都丢进了*args里,而**kwargs装的是没有对应上的关键字参数。通过拆包我们看到里面包含的关键字有name, age,通过字典的方式可以获取关键字对应的值。


示例2:

1
2
def person(name, age, **kwargs):
print('name:', name, 'age:', age, 'other:', kwargs)

函数person除了必选参数nameage外,还接受关键字参数kw。在调用该函数时,可以只传入必选参数,也可以传入任意个数的关键字参数:

1
2
3
4
5
6
>>> person('Michael', 30)
name: Michael age: 30 other: {}
>>> person('Bob', 35, city='Beijing')
name: Bob age: 35 other: {'city': 'Beijing'}
>>> person('Adam', 45, gender='M', job='Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}

关键字参数有什么用?它可以扩展函数的功能。比如,在person函数里,我们保证能接收到name和age这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。

和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:

1
2
3
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, city=extra['city'], job=extra['job'])
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

当然,上面复杂的调用可以用简化的写法:

1
2
3
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

**extra表示把extra这个dict的所有key-value用关键字参数传入到函数的**kwargs参数,kwargs将获得一个dict。注意kwargs获得的dict是extra的一份拷贝,对kwargs的改动不会影响到函数外的extra。

5. Python2 与 Python3 中参数定义顺序的差异

5.1 函数定义中的*与**

参考
python2和python3中的可变参数有什么不同?
https://www.zhihu.com/question/21099657

python2中参数定义的顺序必须是:位置参数、默认参数、可变参数和关键字参数。python3中不一定要如此。

例:

1
2
3
4
5
6
7
#Python2中只能按如下方式定义
def func1(a, b=0, *args, **kwargs):
print(a,b,args,kwargs)

#Python2中如果按以下方式定义会报错,Python3中则没问题(并且Python3中更推荐使用如下顺序)
def func2(a, *args, b=0, **kwargs):
print(a,b,args,kwargs)

调用:

1
2
3
4
5
6
7
8
9
10
11
# Python2
>>>func1(1, 2, 3, 4, 5, x=6, y=7)
1 2 (3, 4, 5) {'x': 6, 'y': 7}

# 注意在Python3中,两种定义方式调用结果的区别:
>>>func1(1, 2, 3, 4, 5, x=6, y=7)
1 2 (3, 4, 5) {'x': 6, 'y': 7}

>>>func2(1, 2, 3, 4, 5, x=6, y=7)
1 0 (2, 3, 4, 5) {'x': 6, 'y': 7}
# 也即2,3,4,5都传入了*args,参数b没有传入,为默认取值0

值得注意的是在调用的时候,positional argument cannot appear after keyword arguments(python2, python3 中均如此)

1
2
3
4
5
6
7
8
func1(1,b=2,3,4,x=5,y=6)
结果会报错:
File "<ipython-input-7-12fe2b3ef8e0>", line 1
f(1,b=2,3,4,x=5,y=6)
SyntaxError: non-keyword arg after keyword arg

func1(1,b=2,x=5,y=6)
#1 2 () {'x': 5, 'y': 6}

↑在python2与python3中运行结果一致,都会报错,即调用时,出现了某参数=某值时,后面的参数均要加上参数名。(也即func1若想改变默认参数的取值,不能使用b=2,而只能在对应位置写上2)

这也是为什么在Python3中更推荐使用函数参数定义顺序:位置参数,可变参数,默认参数,关键字参数(也即上面func2的写法)。调用时:

1
2
3
func2(1, 3, 4, b=2, x=6, y=7)
#1 2 (3, 4) {'x': 6, 'y': 7}
# ↑对默认参数的传入更清晰

另一个例子:

1
2
def concat1(*args, sep="/"):
print(sep.join(args))

↑此定义在python2中会报错(默认参数应在可变参数前),在python3中不会报错。在python2中的正确写法:

1
2
def concat2(sep="/", *args):
print(sep.join(args))

现在,想通过".""aaa","bbb","ccc"连接起来。调用时:

1
2
3
4
5
concat1("aaa","bbb","ccc",sep=".")
#aaa.bbb.ccc

concat2(".","aaa","bbb","ccc")
#aaa.bbb.ccc
5.2 函数调用中的*与**

参考 Python函数传参中的*与**

假设有函数:

1
2
def test(a,b,c):
print("a =", a, ", b =", b, ", c =", c)

test(*args)*的作用其实就是把args中的每个元素,作为位置参数传进去。 test(**kwargs)** 的作用则是把 kwargs 变成关键字参数传递。

例1:

1
2
3
4
5
6
args = (1,2,3)
test(*args)
# 结果:a = 1 , b = 2 , c = 3

1. 写成 args = [1,2,3] 也可以,结果一样
2. 若令 args = (1,2,3,4),报错:TypeError: test() takes 3 positional arguments but 4 were given

例2:

1
2
3
kwargs = {'a':1, 'b':2, 'c':3}
test(**kwargs)
# 结果:a = 1 , b = 2 , c = 3

例3:

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
def test(*args):
for item in args:
print(item)

args = (1,2,3)
test(*args)
# 结果:
1
2
3
# ↑ 1,2,3 被看作多个参数,分别被print出来

test(args)
# 结果:
(1,2,3)
# ↑ (1,2,3) 被看作一个整体

test(1,2,3)
# 结果:
1
2
3

test((1,2,3))
# 结果:
(1,2,3)

另一个例子:

1
2
3
4
5
6
7
8
9
def super_func(*args):
return sum(args)

# ↑ by adding a star to args, we're saying,
# hey, this can accept any number of positional arguments

print(super_func(1, 2, 3, 4, 5))
# 15
# 实际上执行的是:sum((1,2,3,4,5)), 对一个tuple执行求和操作
1
2
3
4
5
6
7
8
9
10
def super_func(*args, **kwargs):
print(kwargs)
total = 0
for items in kwargs.values():
total+=items
return sum(args)+total

print(super_func(1,2,3,4,5, num1=5, num2=10))
#{'num1': 5, 'num2': 10}
#30

关于,可变参数与默认参数,再来看一个更复杂的例子,先来看下面这段源码,来自于pyplot的barbs函数:

1
2
3
@docstring.copy(Axes.barbs)
def barbs(*args, data=None, **kw):
return gca().barbs(*args, **({"data": data} if data is not None else {}), **kw)

data=None作为默认参数是可以放在*args后面的,那么我们仿一下这个函数,看看它传进来的值都是怎么赋值的。

1
2
3
4
5
6
7
8
9
def fun2(*args, data=None, **kwargs):
print(args)
print(data)
print(kwargs)

fun2(1, 2, 3, 4, data=123, name='ali', age=100)
# (1, 2, 3, 4)
# 123
# {'name': 'ali', 'age': 100}

1 2 3 4都传给了*args;data=123 作为关键字参数传给了data=None 从而后面的kwargs就不能再有data这个关键字了,否则会报重复关键字错误;在确定后面没有默认参数后,将接下来的关键字参数都丢给了**kwargs

接着上面的源码,它调用了gca().barbs并返回这个函数的返回值,来看看barbs这个函数是怎么接收参数的:

1
2
def barbs(self, *args, **kw):
# 可以看到这个函数只有*args和**kwargs

那么再来仿一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def fun2(*args, data=None, **kwargs):
print(args)
print(data)
print(kwargs)

fun3(*args, **({'data': data} if data is not None else {}), **kwargs)


def fun3(*args, **kwargs):
print(args)
print(kwargs)


# 调用
fun2(1, 2, 3, 4, data=123, name='ali', age=100)
# (1, 2, 3, 4)
# 123
# {'name': 'ali', 'age': 100}
# (1, 2, 3, 4)
# {'data': 123, 'name': 'ali', 'age': 100}

可以看到*args的参数直接丢给fun3了,但是data做了一个简单的判断封装成了字典,跟着**kwargs一起丢给fun3**kwargs

从这个例子能学到什么呢?

首先默认参数也是可以放在*args后面,因为编译器可以识别出后面的关键字参数已经不是*args的内容了,接下来遇上了默认参数,一一赋值给关键字参数,剩下的再给**kwargs就可以了。

其次,**kwargs是可以接收多个字典并且封装成一个大字典的,他的过程就是将所有关键字参数、字典都拆解然后再重新封装成字典的过程,最终在fun3中接收到的就是一个大字典。

6. 题外话:parameter和argument的区别

参考 https://blog.csdn.net/it1988888/article/details/8871895

  1. parameter是指函数定义中参数,而argument指的是函数调用时的实际参数。
  2. 简略描述为:parameter=形参(formal parameter), argument=实参(actual parameter)。
  3. 在不很严格的情况下,二者可以混用,一般用argument,而parameter则比较少用。

A parameter is an intrinsic property of the procedure, included in its definition. By contrast, the arguments are the values actually supplied to the procedure when it is called. Unlike the parameters, which form an unchanging part of the procedure’s definition, the arguments can, and often do, vary from call to call.