注:本篇文章使用CPython3.6
定位参数、关键字参数
在Python中,当定义一个函数时,函数接收的参数叫做为形式参数(parameters
),以下简称形参;当调用一个函数时,调用者传递给该函数的值叫做实际参数(arguments
),以下简称实参。
根据inspect模块的描述,Python的形参可以分成如下五类:
- POSITIONAL_OR_KEYWORD,默认类型,可通过定位/关键字实参传递;
- VAR_POSITIONAL,定位形参元祖,如
*args
,捕获剩下的定位实参;
- KEYWORD_ONLY,在
*
或*args
之后的形参,只能通过关键字实参传递;
- VAR_KEYWORD,关键字形参字典,如
**kwargs
,捕获剩下的关键字实参;
- POSITIONAL_ONLY,只能通过定位实参传递,Python语法暂不支持,只有一些C函数(如
divmod
)使用。
比如定义如下函数:
1 2
| def foo(a, *args): print(a, args)
|
其中形参a
属于POSITIONAL_OR_KEYWORD,可通过定位/关键字实参传递:
1 2 3 4
| >>> foo(1) 1 () >>> foo(a=1) 1 ()
|
满足形参a
之后,剩余的定位实参将被*args
以元组的形式捕获:
1 2
| >>> foo(1, 2, 3) 1 (2, 3)
|
再比如定义如下函数:
1 2
| def foo(a, *args, b, **kwargs): print(a, args, b, kwargs)
|
形参b
属于KEYWORD_ONLY,因为它在*args
之后定义:
1 2
| >>> foo(1, b=2) 1 () 2 {}
|
满足形参b
之后,剩余的关键字实参将被**kwargs
以字典的形式捕获:
1 2
| >>> foo(1, b=2, c=3) 1 () 2 {'c': 3}
|
如果想定义KEYWORD_ONLY形参,但不想使用VAR_POSITIONAL形参(即*args
),则可以在定义函数时单独的*
号:
1 2 3 4 5 6 7 8 9
| >>> def foo(a, *, b): ... print(a, b) ... >>> foo(1, b=2) 1 2 >>> foo(1, 2) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: foo() takes 1 positional argument but 2 were given
|
参数默认值
在定义函数时,我们可以给形参指定默认值,比如:
1 2 3 4 5
| >>> def foo(a=1, *args, b=2, **kwargs): ... print(a, args, b, kwargs) ... >>> foo() 1 () 2 {}
|
需要注意的是,形参的默认值存储在函数对象的__defaults__
和__kwdefaults__
属性里,而不是每次调用函数时动态生成,所以最好不要用可变对象充当形参的默认值。下面的例子就是反面教材:
1 2 3 4 5 6 7 8 9 10 11
| >>> def foo(param=[]): ... param.append(1) ... print(id(param), param) ... >>> >>> print(id(foo.__defaults__[0]), foo.__defaults__[0]) 140009169940232 [] >>> foo() 140009169940232 [1] >>> foo() 140009169940232 [1, 1]
|
获取关于参数的信息
内省指程序在运行时检查对象类型的一种能力,本节介绍的内容就属于函数内省的范围。假设有如下函数:
1 2 3
| def foo(a=1, *args, b=2, **kwargs): c = a print(c, args, b, kwargs)
|
就像上一节中提到的,foo
函数有__defaults__
、__kwdefaults__
属性,用于记录定位参数和关键字参数的默认值;有__code__
属性,存储函数编译后的字节码信息,其中就包括参数的名称。通过这些属性,我们可以获取关于函数参数的信息:
1 2 3 4 5 6 7 8 9 10
| >>> foo.__defaults__ (1,) >>> foo.__kwdefaults__ {'b': 2} >>> foo.__code__.co_varnames ('a', 'b', 'args', 'kwargs', 'c') >>> foo.__code__.co_argcount 1 >>> foo.__code__.co_kwonlyargcount 1
|
但这样还是太原始、太不方便了。幸好,我们有更好的选择:Python内置的inspect
模块。下面这个例子就提取了foo
函数的签名,然后获取函数的参数信息:
1 2 3 4 5 6 7 8 9 10 11
| >>> from inspect import signature >>> sig = signature(foo) >>> sig <Signature (a=1, *args, b=2, **kwargs)> >>> for name, param in sig.parameters.items(): ... print(f'{str(param.kind):<21} : {param.name:<6} = {param.default}') ... POSITIONAL_OR_KEYWORD : a = 1 VAR_POSITIONAL : args = <class 'inspect._empty'> KEYWORD_ONLY : b = 2 VAR_KEYWORD : kwargs = <class 'inspect._empty'>
|
同时,inspect.Signature
对象还有一个bind
方法,该方法可以将一些对象绑定到函数的形参上,就像Python解释器在调用函数时做的那样。通过这种方法,框架可以在真正执行函数前验证参数,就像下面这个例子:
1 2 3 4 5 6 7 8 9 10 11
| >>> bound = sig.bind(1, 2, 3, c=3) >>> for name, value in bound.arguments.items(): ... print(f'{name:<6} = {value}') ... a = 1 args = (2, 3) kwargs = {'c': 3} >>> bound = sig.bind(1, 2, 3, a=4) Traceback (most recent call last): File ... TypeError: multiple values for argument 'a'
|
函数参数传递
说起函数参数传递,可能就有人想起了引用传递
、值传递
……忘掉这两个概念,来看看下面两个例子:
1 2 3 4 5 6
| def foo1(param: list): param += [4, 5]
arg1 = [1, 2, 3] foo1(arg1) print(arg1)
|
内存中有一个list
对象([1, 2, 3]
),该对象有两个别名:arg1
和param
。由于list
对象是可变的(mutable
),所以可以通过param
这个别名修改这个list
对象的内容。
1 2 3 4 5 6
| def foo2(param: tuple): param += (4, 5)
arg2 = (1, 2, 3) foo2(arg2) print(arg2)
|
内存中有一个tuple
对象((1, 2, 3)
),该对象也有两个别名:arg2
和param
。但由于tuple
对象是不可变的(immutable)
,当执行param += (4, 5)
时,解释器创建了一个新的tuple
对象((1, 2, 3, 4, 5)
),并让param
指向这个新的对象,而原来的对象没有被改变。
在Python中,参数传递本质上是为已有的对象取了一个函数作用域级别的别名。如果该对象是可变的,那么就可以在函数内修改该对象,这种修改也可以被其它的别名所感知。弄清楚对象、别名的关系,就不会对值传递
、引用传递
这种说法感到困惑了。