Python中的函数参数与参数传递

注:本篇文章使用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) # 输出[1, 2, 3, 4, 5]

内存中有一个list对象([1, 2, 3]),该对象有两个别名:arg1param。由于list对象是可变的(mutable),所以可以通过param这个别名修改这个list对象的内容。

tuple.webp

1
2
3
4
5
6
def foo2(param: tuple):
param += (4, 5)

arg2 = (1, 2, 3)
foo2(arg2)
print(arg2) # 输出(1, 2, 3)

内存中有一个tuple对象((1, 2, 3)),该对象也有两个别名:arg2param。但由于tuple对象是不可变的(immutable),当执行param += (4, 5)时,解释器创建了一个新的tuple对象((1, 2, 3, 4, 5)),并让param指向这个新的对象,而原来的对象没有被改变。

tuple.webp

在Python中,参数传递本质上是为已有的对象取了一个函数作用域级别的别名。如果该对象是可变的,那么就可以在函数内修改该对象,这种修改也可以被其它的别名所感知。弄清楚对象、别名的关系,就不会对值传递引用传递这种说法感到困惑了。


Python中的函数参数与参数传递
https://www.yooo.ltd/2020/07/04/python-parameters-and-arguments/
作者
OrangeWolf
发布于
2020年7月4日
许可协议