3-3-参数传递

1. 参数传递

给函数传递参数时的一些简要的关键点:

  • **参数的传递是通过自动将对象赋值给本地变量名来实现的。**因为引用是以指针的形式实现的,所有的参数实际上都是通过指针进行传递的。作为参数,被传递的对象从来不自动拷贝。
  • **在函数内部的参数名的赋值不会影响调用者。**在函数运行时,在函数头部的参数名是一个新的、本地的变量名,这个变量名是在函数的本地作用域内的。
  • **改变函数的可变对象参数的值也许会对调用者有影响。**因为参数是简单地赋值给传入的对象,函数能够就地改变传入的可变对象,其结果会影响调用者。
  • 不可变参数“通过值”进行传递。
  • 可变对象通过“指针”进行传递。

1.1 参数和共享引用

1
2
3
4
5
6
def f(a):                          # 传递的对象赋值给 a
a = 99 # 只改变本地变量 a

b = 88
f(b) # 初始 a 和 b 的引用都为 88
print(b) # b 没有改变
88

对函数中的一个参数名的赋值(例如,a = 99)不会影响到函数调用作用域中的 b 这样的一个变量。参数名称可能最初共享传递的对象,但只是临时的,即当函数第一次调用的时候,只要对参数名进行重新赋值,这种关系就结束了。

当参数传递像列表和字典这样的可修改对象的时候,对这样的对象的原处修改可能在函数退出后依然有效,并由此影响到调用者。

1
2
3
4
5
6
7
8
def changer(a, b):
a = 2
b[0] = 'spam' # 在原处改变共享对象

X = 1
L = [1, 2] # 调用者
changer(X, L) # 传递不可变和可变对象
X, L # X 不改变,L 改变
(1, ['spam', 2])

1.2 避免可变参数的修改

在 Python 中,默认通过引用进行函数的参数传递,这意味着不需要创建多个拷贝就可以在程序中传递很大的对象,并且能够按照需要方便地更新这些对象。

如果不想要函数内部在原处的修改影响传递给它的对象,可以简单地创建一个明确的可变对象的拷贝。

1
2
L = [1, 2]
changer(X, L[:])

如果不想改变传入的对象,也可以在函数内部进行拷贝。

1
2
3
4
def changer(a, b):
b = b[:] # 拷贝输入列表,不影响调用者
a = 2
b[0] = 'spam' # 仅改变列表拷贝

为了真正意义上防止这些改变,可以将可变对象转换为不可变对象来杜绝这种问题。例如,元组,在试图改变时会抛出一个异常。

1
2
L = [1, 2]
changer(X, tuple(L))
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-4-384c8df03f94> in <module>()
      1 L = [1, 2]
----> 2 changer(X, tuple(L))

<ipython-input-3-eee0d865cbfa> in changer(a, b)
      2     b = b[:]
      3     a = 2
----> 4     b[0] = 'spam'

TypeError: 'tuple' object does not support item assignment

这种方法强制函数写成绝不改变传入参数的样子,强制对函数比原本应该的进行了更多的限制,通常意义下应该避免出现。

1.3 对参数输出和多重结果进行模拟

return 能够返回任意种类的对象,所以它也能够返回多个值,如果这些值封装进一个元组或其他的集合类型。

1
2
3
4
5
6
7
8
9
def multiple(x, y):
x = 2
y = [3, 4]
return x, y

X = 1
L = [1, 2]
X, L = multiple(X, L) # 将结果赋值给调用者的名称
X, L
(2, [3, 4])

2. 特定的参数匹配模型

参数在 Python 中总是通过赋值进行传递的。传入的对象赋值给了在 def 头部的变量名。

默认情况下,参数是通过其位置进行匹配的,从左至右,而且必须精确地传递和函数头部参数名一样多的参数。

2.1 参数匹配基础知识

位置:从左至右进行匹配
        一般情况下,是通过位置进行匹配,把参数值传递给函数头部的参数名称,匹配顺序从左到右。

关键字参数:通过参数名进行匹配
        调用者可以定义哪一个函数接受这个值,通过在调用时使用参数的变量名,使用 name=value 这种语法。

默认参数:为没有传入值的参数定义参数值
        如果调用时传入的值过少的话,函数能够为参数定义接受的默认值,再一次使用 name=value 语法。

可变参数:收集任意多基于位置或关键字的参数
        函数能够使用特定的参数,它们是以一个或两个字符 * 开头,收集任意多的额外参数。

可变参数解包:传递任意多的基于位置或关键字的参数
        调用者能够再使用 * 语法去将参数集合打散,分成参数。* 在函数头部意味着收集任意多的参数,而在调用者中意味着传递任意多的参数。

keyword-only 参数:参数必须按照名称传递
        函数可以指定参数,参数必须用带有关键参数的名字(而不是位置)来传递。这样的参数通常用来定义实际参数以外的配置选项。

2.2 匹配语法

语法 位置 解释
func(value) 调用者 常规参数:通过位置进行匹配
func(name=value) 调用者 关键字参数:通过变量名匹配
func(*iterable) 调用者 传递可迭代对象中的所有对象,并作为独立的基于位置的参数
func(**dict) 调用者 成对的传递字典中所有的关键字/值,并作为独立的关键字参数
def func(name) 函数 常规参数:通过位置或变量名进行匹配
def func(name=value) 函数 默认参数值,如果没有在调用中传递的话
def func(*name) 函数 匹配并收集(在元组中)所有包含位置的参数
def func(**name) 函数 匹配并收集(在字典中)所有包含位置的参数
def func(*other, name)
def func(*, name=value) 函数 参数必须在调用中按照关键字传递

2.3 细节

如果决定使用并混合特定的参数匹配模型,Python 将会遵循下面有关顺序的法则:

  • 正在函数调用中,参数必须以此顺序出现:任何位置参数(value),后面跟着任何关键字参数(name=value)和 *iterable 形式的组合,后面跟着 **dict 形式。
  • 在函数头部,参数必须以此顺序出现:任何一般参数(name),紧跟着任何默认参数(name=value),如果有的话,后面是 *name(或者在 Python 3.X 中是 *)的形式,后面跟着任何 name 或 name=value keyword-only参数,后面跟着 **name 形式。

Python 内部使用以下步骤在赋值前进行参数匹配:

  1. 通过位置分配非关键字参数。
  2. 通过匹配变量名分配关键字参数。
  3. 其他额外的非关键字参数分配到 *name 元组中。
  4. 其他额外的关键字参数分配到 **name 字典中。
  5. 用默认值分配给在头部未得到分配的参数。

2.4 关键字参数和默认参数实例

常规参数

1
2
3
def f(a, b, c): print(a, b, c)

f(1, 2, 3)
1 2 3

关键字参数
关键字参数允许通过变量名进行匹配,而不是通过位置。

1
f(c=3, b=2, a=1)
1 2 3
1
f(1, c=3, b=2)
1 2 3

默认参数
默认参数允许创建函数可选的参数,如果没有传入值的话,在函数运行前,参数就被赋了默认值:

1
2
3
def f(a, b=2, c=3): print(a, b, c)

f(1) # 不给 b 和 c 传递值,会默认分别赋值为 2 和 3
1 2 3
1
f(a=1)
1 2 3
1
f(1, 4)                          # 当给函数传递两个值的时候,只有 c 得到默认值
1 4 3
1
f(1, 4, 5)                       # 当有三个值传递时,不会使用默认值
1 4 5
1
f(1, c=6)                        # 关键字和默认参数一起使用,关键字参数从本质上允许我们跳过有默认值的参数
1 2 6

关键字参数和默认参数的混合

1
2
3
4
def func(spam, eggs, toast=0, ham=0):
print((spam, eggs, toast, ham))

func(1, 2)
(1, 2, 0, 0)
1
func(1, ham=1, eggs=0)
(1, 0, 0, 1)
1
func(spam=1, eggs=0)
(1, 0, 0, 0)
1
func(toast=1, eggs=2, spam=3)
(3, 2, 1, 0)
1
func(1, 2, 3, 4)
(1, 2, 3, 4)

2.5 任意参数实例

*** 让函数支持接收任意数目的参数,它们都可以出现在函数定义或是函数调用中。

头部:收集参数
在函数定义中,在元组中收集不匹配的位置参数。

1
2
3
def f(*args): print(args)

f()
()
1
f(1)
(1,)
1
f(1, 2, 3, 4)
(1, 2, 3, 4)

** 特性类似,但只对关键字参数有效。

1
2
3
def f(**args): print(args)

f()
{}
1
f(a=1, b=2)
{'a': 1, 'b': 2}

函数头部能够混合一般参数、* 参数以及 ** 实现更加灵活的调用方式:

1
2
3
def f(a, *pargs, **kargs): print(a, pargs, kargs)

f(1, 2, 3, x=1, y=2)
1 (2, 3) {'x': 1, 'y': 2}

调用:解包参数
在调用函数时能够使用 * 语法,它与函数定义的意思相反,它会解包参数的集合,而不是创建参数的集合。

1
2
3
4
5
def func(a, b, c, d): print(a, b, c, d)

args = (1, 2)
args += (3, 4)
func(*args)
1 2 3 4

** 会以键/值对的形式解包一个字典,使其成为独立地关键字参数。

1
2
3
args = {'a':1, 'b':2, 'c':3}
args['d'] = 4
func(**args)
1 2 3 4
1
func(*(1, 2), **{'d':4, 'c':4})
1 2 4 4
1
func(1, *(2, 3), **{'d':4})
1 2 3 4
1
func(1, c=3, *(2,), **{'d':4})
1 2 3 4
1
func(1, *(2, 3), d=4)
1 2 3 4
1
func(1, *(2,), c=3, **{'d':4})
1 2 3 4

2.6 Python 3.X 的 Keyword-Only 参数

keyword-only 参数即必须只按照关键字传递并且不会由一个位置参数来填充的参数。

keyword-only 参数编码为命名的参数,出现在参数列表中的 *args 之后。

1
2
3
4
def kwonly(a, *b, c):
print(a, b, c)

kwonly(1, 2, c=3)
1 (2,) 3
1
kwonly(a=1, c=3)
1 () 3
1
kwonly(1, 2, 3)
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-30-62465c70825a> in <module>()
----> 1 kwonly(1, 2, 3)

TypeError: kwonly() missing 1 required keyword-only argument: 'c'

也可以在参数列表中使用一个 * 字符,来表示一个函数不会接受一个变量长度的参数列表,而是仍然期待跟在 * 后面的所有参数都作为关键字传递。

1
2
3
4
def kwonly(a, *, b, c):
print(a, b, c)

kwonly(1, c=3, b=2)
1 2 3
1
kwonly(c=3, b=2, a=1)
1 2 3
1
kwonly(1, 2, 3)
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-33-62465c70825a> in <module>()
----> 1 kwonly(1, 2, 3)

TypeError: kwonly() takes 1 positional argument but 3 were given

带有默认值的 keyword-only 参数都是可选的,那些没有默认值的 keyword-only 参数真正地变成了函数必需的 keyword-only 参数:

1
2
3
4
def kwonly(a, *, b, c='spam'):
print(a, b, c)

kwonly(1, b='eggs')
1 eggs spam
1
kwonly(1, c='eggs')
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-35-ee26104a7cdb> in <module>()
----> 1 kwonly(1, c='eggs')

TypeError: kwonly() missing 1 required keyword-only argument: 'b'
1
kwonly(1, 2)
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-36-fec93b2f48aa> in <module>()
----> 1 kwonly(1, 2)

TypeError: kwonly() takes 1 positional argument but 2 were given

排序规则

keyword-only 参数必须在一个单个星号后面指定,而不是两个星号。即在一个函数头部,keyword-only 参数必须编写在 **args 任意关键字形式之前,且在 *args 任意位置形式之后。

1
2
3
def f(a, *b, c=6, **d): print(a, b, c, d)

f(1, 2, 3, x=4, y=5) # 默认使用方法
1 (2, 3) 6 {'x': 4, 'y': 5}
1
f(1, 2, 3, x=4, y=5, c=7)                     # 重载默认值
1 (2, 3) 7 {'x': 4, 'y': 5}

在调用函数中,传递 keyword-only 参数必须出现在一个 **args 形式之前。keyword-only 参数可以编写在 *args 之前或者之后,并且可能包含在 **args 中:

1
2
3
def f(a, *b, c=6, **d): print(a, b, c, d)

f(1, *(2, 3), **dict(x=4, y=5))
1 (2, 3) 6 {'x': 4, 'y': 5}
1
f(1, c=7, *(2, 3), **dict(x=4, y=5))           # keyword-only 参数在 * 之前
1 (2, 3) 7 {'x': 4, 'y': 5}
1
f(1, *(2, 3), **dict(x=4, y=5, c=7))           # keyword-only 参数在 ** 中
1 (2, 3) 7 {'x': 4, 'y': 5}

为何使用 keyword-only 参数
它们使得很容易允许一个函数既接受任意多个要处理的位置参数,也接受作为关键字传递的配置选项。没有 keyword-only 参数的话,要为这样的选项提供默认值并验证没有传递多余的关键字则需要额外的工作。

假设一个函数处理一组传入的对象,并且允许传递一个跟踪标志:

1
2
process(X, Y, Z)
process(X, Y, notify=True)

没有 keyword-only 参数的话,必须使用 *args**args,并且手动检查关键字,有了 keyword-only 参数,需要较少的代码。

1
def process(*args, notify=False): ...