个人总结难免疏漏,请多包涵。更多内容请查看原文。本文以及学习笔记系列仅用于个人学习、研究交流。
本文主要介绍参数(对象如何传递给函数)。参数通过赋值传递到函数中,赋值方式是通过对象引用,实际上是通过指针传递到函数中。以及参数的一些更加高级的扩展,包括默认参数和关键字参数、使用任意的多个参数的工具,以及收集参数(*args,**args)、解包参数(*args,**args)。最后模拟编写min、max、set、print函数。
目录
传递参数
参数和共享引用
避免可变参数的修改
对参数输出进行模拟
参数匹配模型
基础知识
匹配语法
细节
关键字参数和默认参数的实例
位置传递
关键字参数
默认参数
为什么要在意:关键字参数
关键字参数和默认参数的混合
任意参数的实例
收集参数
解包参数
应用函数通用性
Keyword-Only参数
排序规则
为何使用keyword-only参数
模拟编写
min函数
满分示例
加分点
更有用的示例-模拟set
模拟print函数
使用Keyword-Only参数
17笔记介绍了Python的作用域背后的细节——即定义和查找变量的位置,在代码中定义一个名称的位置决定了它大部分的含义。
本文学习Python中的参数传递的概念,即对象作为输入发送给函数的方式。学习Python中的参数传递的概念,即对象作为输入发送给函数的方式。参数(argument,也叫做parameter)赋值给一个函数中的名称,但是它们更多地与对象引用相关,而不是与变量作用域相关。还将介绍Python所提供的额外工具,如关键字、默认值和任意参数收集器,它们为参数发送给函数的方式提供了广泛的灵活性。
传递参数
前面介绍过参数是通过赋值来传递的,可能介绍不是很清楚,会在这一部分进行详尽的阐述。下面是给函数传递参数时的一些简要的关键点。
- 参数的传递是通过自动将对象赋值给本地变量名来实现的。函数参数[调用者发送的(可能的)的共享对象引用值]在实际中只是Python赋值的另一个实例而已。因为引用是以指针的形式实现的,所有的参数实际上都是通过指针进行传递的。作为参数被传递的对象从来不自动拷贝。
- 在函数内部的参数名的赋值不会影响调用者。在函数运行时,在函数头部的参数名是一个新的、本地的变量名,这个变量名是在函数的本地作用域内的。函数参数名和调用者作用域中的变量名是没有别名的。
- 改变函数的可变对象参数的值也许会对调用者有影响。换句话说,因为参数是简单地赋值给传入的对象,函数能够就地改变传入的可变对象,因此其结果会影响调用者。可变参数对于函数来说是可以做输入和输出的。
这里所学的对于函数的参数来说也适用引用细节,尽管对参数名的赋值是自动并且隐式的。
Python的通过赋值进行传递的机制与C++的引用参数选项并不完全相同,但是在实际中,它与C语言的参数传递模型相当相似。
- 不可变参数“通过值”进行传递。像整数和字符串这样的对象是通过对象引用而不是拷贝进行传递的,但是因为你无论怎样都不可能在原处改变不可变对象,实际的效果就很像创建了一份拷贝。
- 可变对象是通过“指针”进行传递的。例如,列表和字典这样的对象也是通过对象引用进行传递的,这一点与C语言使用指针传递数组很相似:可变对象能够在函数内部进行原处的改变,这一点和C数组很像。
如果从来没有使用过C,Python的参数传递模型看起来也会比较简单:它仅仅是将对象赋值给变量名,并且无论对可变对象或不可变对象都是这样的。
参数和共享引用
为了说明参数传递属性的工作方式,看如下的代码:
def f(a): #a被分配/引用给传递的对象a = 99 #仅仅只改变了局部变量b = 88
f(b) #a b这里都引用了相同的88
print(b) #b并没有改变
88
在这个例子中,在使用f(b)调用函数的时候,变量a赋值了对象88,但是,a只是存在于调用的函数之中。在函数中修改a对于调用函数的地方没有任何影响,它直接把本地变量a重置为一个完全不同的对象。
这就是没有名称冲突的含义——对函数中的一个参数名的赋值(例如,a=99)不会神奇地影响到函数调用作用域中的b这样的一个变量。参数名称可能最初共享传递的对象(它们实质上是指向这些对象的指针),但只是临时的,即当函数第一次调用的时候。只要对参数名进行重新赋值,这种关系就结束了。这是对参数名称自身赋值的情况。
当参数传递像列表和字典这样的可修改对象的时候,还需要注意,对这样的对象的原处修改可能在函数退出后依然有效,并由此影响到调用者。
这是一个在实际情况下能够展示以上一些特性的例子。
def changer(a,b):a = 2b[0] = 'spam'X = 1
L = [1,2]
changer(X,L)X,L
(1, ['spam', 2])
在这段代码中,changer函数给参数a赋值,并给参数b所引用的一个对象元素赋值。这两个函数内的赋值从语法上仅有一点不同,但是从结构上看却大相径庭。
- 因为a是在函数作用域内的本地变量名,第一个赋值对函数调用者没有影响,它仅仅把本地变量a修改为引用一个完全不同的对象,并没有改变调用者作用域中的名称X的绑定。这和前面的例子中的情况是相同的。
- b也是一个本地变量名,但是它被传给了一个可变对象(在调用者作用域中叫做L的列表)。因为第二个赋值是一个在原处发生的对象改变,对函数中b[0]进行赋值的结果会在函数返回后影响L的值。
实际上,changer中的第二条赋值语句没有修改b,我们修改的是b当前所引用的对象的一部分。这种原处修改,只有在修改的对象比函数调用生命更长的时候,才会影响到调用者。名称L也没有改变——它仍然引用同样的、修改后的对象,但是就好像L在调用后变化了一样,因为它引用的值已经在函数中修改过了。
图18-1表明了在函数被调用后,函数代码在运行前,变量名/对象所存在的绑定关系。
引用:参数。因为参数是通过赋值传递的,函数中的参数名可以在调用时通过变量实现共享对象。因此,函数中对可变对象参数的在原处的修改能够影响调用者。这里,函数中的a和b在函数一开始调用时最初通过变量X和L进行了对象引用。通过变量b对列表的改变在函数调用返回后,L也会发生改变
如果这个例子还是令人困惑,下面的提醒或许有些帮助。也就是说,自动对传入的参数进行赋值的效果与运行一系列简单的赋值语句是相同的。对于第一个参数,参数赋值对于调用者来说没有什么影响:
X = 1
a = X
a = 2print(X)
1
但是,对第二个参数的赋值就会影响调用的变量,因为它对对象进行了在原处的修改:
L = [1,2]
b = L
b[0] = 'spam'print(L)
['spam', 2]
在第6和第9学习笔记中对于共享可变对象的争论可以说明这种现象:对可变对象的在原处的修改会影响其他引用了该对象的变量。这里,实际效果就是使其中的一个参数表现得就像函数的一个输入和一个输出。
避免可变参数的修改
对可变参数的原处修改的行为不是一个bug——它只是参数传递在Python中工作的方式。在Python中,默认通过引用(也就是指针)进行函数的参数传递,是因为这通常是我们所想要的:这意味着不需要创建多个拷贝就可以在我们的程序中传递很大的对象,并且能够按照需要方便地更新这些对象。实际上,后面你将在第六部分(第25篇笔记后)看到的,Python的类模型依赖于原处修改一个传入的"self"参数来更新对象状态。
如果不想要函数内部在原处的修改影响传递给它的对象,那么可以简单地创建一个明确的可变对象的拷贝,正如在第6个笔记学到的那样。
对于函数参数,总是能够在调用时对列表进行拷贝。
L = [1,2]
changer(X,L[:])
如果不想改变传入的对象,无论函数是如何调用的,同样可以在函数内部进行拷贝。
def changer(a,b):b = b[:]a = 2
b[0] = 'spam'
这两种拷贝的机制都不会阻止函数改变对象:这样做仅仅是防止了这些改变会影响调用者。为了真正意义上防止这些改变,我们总是能够将可变对象转换为不可变对象来杜绝这种问题。
例如,元组,在试图改变时会抛出一个异常。
L = [1,2]changer(X,tuple(L))
TypeError: 'tuple' object does not support item assignment
这种原理会使用到内置tuple函数,将会以一个序列(任意可迭代对象)中的所用元素为基础创建一个新的元组。这种方法从某种意义上来说有些过于极端:因为这种方法强制函数写成绝不改变传入参数的样子,这种办法强制对函数比原本应该的进行了更多的限制,所以通常意义下应该避免出现。
或许将来你会发现对于一些调用来说改变参数是有用的一件事。使用这种技术会让函数失去一种参数能够调用任意列表特定方法的能力,包括不会在原处改变对象的那些方法都不再能够使用。
需要记住的就是,函数能够升级为传入可变对象(例如,列表和字典)的形式。这不是一个问题,并且有时候这对于有些用途很有用处。此外,原处修改传入的可变对象的函数,可能是为此而设计并有意而为之——修改可能是一个定义良好的API的一部分,而我们不应该通过产生副本来违反该API。
但是必须要意识到这个属性:如果在你没有预期的情况下对象在外部发生了改变,检查一下是不是一个调用了的函数引起的,并且有必要的话当传入对象时进行拷贝。
对参数输出进行模拟
之前已经讨论了return语句并在例子中使用了它。这里有一个较为纯粹的技巧:因为return能够返回任意种类的对象,所以它也能够返回多个值,如果这些值封装进一个元组或其他的集合类型。
实际上,尽管Python不支持一些其他语言所谓的“通过引用进行调用”的参数传递,我们通常能够通过返回元组并将结果赋值给最初的调用者的参数变量名来进行模拟。
def multiple(a,b):a = 2b = [3,4]return a,b #返回了一个元组x = 1
l = [1,2]
x,l = multiple(x,l)
x,l
(2, [3, 4])
看起来这里的代码好像返回了两个值,但是实际上只有一个:一个包含有2个元素的元组,它的圆括号是可选的,这里省略了。在调用返回之后,我们能够使用元组赋值去分解这个返回元组的组成部分。(忘了回去翻第9、11笔记)。这段代码的实际效果就是通过明确的赋值模拟了其他语言中的输出参数。X和L在调用后发生了改变,但是这仅仅是因为代码编写而已。
参数匹配模型
正如上面看到的,参数在Python中总是通过赋值进行传递的。传入的对象赋值给了在def头部的变量名。尽管这样,在模型的上层,Python提供了额外的工具,该工具改变了调用过程中赋值时参数对象匹配在头部的参数名的优先级。这些工具都是可选的,但是允许编写支持更复杂的调用模式的函数,并且你可能会遇到需要这些工具的一些库。
在默认情况下,参数是通过其位置进行匹配的,从左至右,而且必须精确地传递和函数头部参数名一样多的参数。还能够通过定义变量名进行匹配,默认参数值,以及对于额外参数的容器。
基础知识
在学习语法的细节之前,需要强调一点,这些特定的模型是可选的,并且必须要根据变量名匹配对象,匹配完成后在传递机制的底层依然是赋值。实际上,这些工具对于编写库文件的人来说,要对比应用程序开发者更有用。但是因为尽管你不会自己动手编写这些模型,很有可能在这儿犯错,这里是一些关于匹配模型的大纲。
位置:从左至右进行匹配
一般情况下,也是我们迄今为止最常使用的那种方法,是通过位置进行匹配把参数值传递给函数头部的参数名称,匹配顺序为从左到右。
关键字参数:通过参数名进行匹配
调用者可以定义哪一个函数接受这个值,通过在调用时使用参数的变量名,使用name=value这种语法。
默认参数:为没有传入值的参数定义参数值
如果调用时传入的值过于少的话,函数能够为参数定义接受的默认值,再一次使用语法name=value。
可变参数:收集任意多基于位置或关键字的参数
函数能够使用特定的参数,它们是以字符*开头,收集任意多的额外参数(这个特性常常叫做可变参数,类似C语言中的可变参数特性,也能够支持可变长度参数的列表)。
可变参数解包:传递任意多的基于位置或关键字的参数
调用者能够再使用*语法去将参数集合打散,分成参数。这个“*”与在函数头部的“*”恰恰相反:在函数头部它意味着收集任意多的参数,而在调用者中意味着传递任意多的参数。
Keyword-only参数:参数必须按照名称传递
函数也可以指定参数,参数必须用带有关键参数的名字(而不是位置)来传递。这样的参数通常用来定义实际参数以外的配置选项。
匹配语法
表18-1总结了与特定参数匹配模式有关的语法。
这些特殊的匹配模式分解到如下的函数调用和定义中:
- 在函数的调用中(在表中的前4行),简单的通过变量名位置进行匹配,但是使用name=value的形式告诉Python依照变量名进行匹配,这些叫做关键字参数。
- 在调用中使用*sequence或者**dict允许我们在一个序列或字典中相应地封装任意多的位置相关或者关键字的对象,并且在将它们传递给函数的时候,将它们解包为分开的、单个的参数。
- 在函数的头部,一个简单的变量名是通过位置或变量名进行匹配的(取决于调用者是如何传递给它参数的),但是name=value的形式定义了默认的参数值。
- *name的形式收集了任意的额外不匹配的参数到元组中,并且**name的形式将会收集额外的关键字参数到字典之中。在Python 3.0及其以后的版本中,跟在*name或一个单独的*之后的、任何正式的或默认的参数名称,都是keyword-only参数,并且必须在调用中按照关键字传递。
在这其中,关键字参数和默认参数也许是在Python代码中最常见的了。在前面,已经非正式地使用过这两种形式了:
- 已经使用关键字来指定Python 3.0的print函数的选项,但是,它们有更广泛的用途——关键字允许使用其变量名去标记参数,让调用变得更有意义。
- 之前见过默认参数,作为一种从内嵌函数作用域传递值的办法,但是它们实际上比这更通用:它们允许创建任意可选的参数,并在函数定义中提供了默认值。
正如下面将看到的,函数头部的默认参数和调用中的关键字的这些组合,进一步允许我们挑选要覆盖哪些默认参数。
简而言之,特定的参数匹配模式可以自由地确认有多少参数是必须传递给函数的。
如果函数定义了默认参数,你传递太少的参数它们就会被使用。如果一个函数使用*可变参数列表的形式,你能够传入任意多的参数;*变量名会将额外的参数收集到一个数据结构中去。
细节
如果决定使用并混合特定的参数匹配模型,Python将会遵循下面有关顺序的法则。
- 在函数调用中,参数必须以此顺序出现:任何位置参数(value),后面跟着任何关键字参数(name=value)和*sequence形式的组合,后面跟着**dict形式。
- 在函数头部,参数必须以此顺序出现:任何一般参数(name),紧跟着任何默认参数(name=value),如果有的话,后面是*name(或者在Python 3.0中是*)的形式,后面跟着任何name或name=value keyword-only参数(在Python 3.0中),后面跟着**name形式。
在调用和函数头部中,如果出现**arg形式的话,都必须出现在最后。如果你使用任何其他的顺序混合了参数,将会得到一个语法错误,因为其他顺序的混合会产生歧义。
Python内部是使用以下的步骤来在赋值前进行参数匹配的:
- 1.通过位置分配非关键字参数。
- 2.通过匹配变量名分配关键字参数。
- 3.其他额外的非关键字参数分配到*name元组中。
- 4.其他额外的关键字参数分配到**name字典中。
- 5.用默认值分配给在头部未得到分配的参数。
在这之后,Python检测来确保每个参数只传入了一个值。如果不是这样的话,将会发生错误。当所有的匹配都完成了,Python把传递给参数名的对象赋值给它们。
Python使用的真正的匹配算法更复杂一些(例如,它必须考虑Python 3.0中的keyword-only参数),因此,要了解更为详细的介绍,请参考Python的标准语言手册。这不是必须阅读的材料,但是它所介绍的Python匹配算法能够帮助你理解一些令人费解的情况,特别是当模式混合的时候。
注意:函数头部中的参数名称也可以有一个注解值,特定形式如name:value(或者要给出默认值的话是name:value=default形式)。这只是参数的一个额外语法,不会增加或修改这里所介绍的参数顺序规则。函数自身也可以有一个注解值,以def f()->value的形式给出。
关键字参数和默认参数的实例
位置传递
使用代码来解释要比前文描述所暗含的意思更简单。如果没有使用过任何特殊的匹配语法,Python默认会通过位置从左至右匹配变量名。
例如,如果定义了一个需要三个参数的函数,必须使用三个参数对它进行调用。
def f(a,b,c): print(a,b,c)
这里依照位置传递值:a匹配到1,b匹配到2,依次类推:
f(1,2,3)
1 2 3
关键字参数
在Python中,调用函数的时候,能够更详尽的定义内容传递的位置。关键字参数允许通过变量名进行匹配,而不是通过位置。
f(c=3,b=2,a=1)
1 2 3
这个调用中c=3,意味着将3传递给参数c。更准确地讲,Python将调用中的变量名c匹配给在函数定义头部的名为c的参数,并将值3传递给了那个参数。
实际的效果就是这个调用与上一个调用的效果一样,但是注意到,当关键字参数使用时参数从左至右的关系不再重要了,因为参数是通过变量名进行传递的,而不是根据其位置。
甚至在一个调用中混合使用基于位置的参数和基于关键字的参数都可以。在这种情况下,所有基于位置的参数首先按照从左至右的顺序匹配头部的参数,之后再进行基于变量名进行关键字的匹配。
f(1,c=3,b=2)
1 2 3
关键字在Python中扮演了两个典型的角色。首先,使调用显得更文档化一些(假设使用了比a、b和c更好的参数名)。
例如,下面这种形式的调用:
这种形式的调用要比直接进行一个由逗号分隔的三个值的调用明了得多:关键字参数在调用中起到了数据标签的作用。第二个主要的角色就是与使用的默认参数进行配对。
默认参数
之前讨论嵌套作用域时,涉及了一些默认参数的内容。简而言之,默认参数允许创建函数可选的参数。如果没有传入值的话,在函数运行前,参数就被赋了默认值。
例如,这里有个函数需要一个参数和两个默认参数。
def f(a,b=2,c=3): print(a,b,c)
当调用这个函数的时候,我们必须为a提供值,无论是通过位置参数还是关键字参数来实现。然而,为b和c提供值是可选的。如果我们不给b和c传递值,它们会默认分别赋值为2和3:
f(1)
1 2 3f(a=1)
1 2 3
当给函数传递两个值的时候,只有c得到默认值,并且当有三个值传递时,不会使用默认值:
f(1,4)
1 4 3f(1,4,5)
1 4 5
最后,这是关键字和默认参数一起使用后的情况。因为它们都破坏了通常的从左至右的位置映射,关键字参数从本质上允许我们跳过有默认值的参数:
f(1,c=5)
1 2 5
这里,a通过位置得到了1,c通过关键字得到了5,而b,在两者之间,通过默认值获得2。
小心不要被在一个函数头部和一个函数调用中的特定的name=value语法搞糊涂。在调用中,这意味着通过变量名进行匹配的关键字,而在函数头部,它为一个可选的参数定义了默认值。无论是哪种情况,这都不是一个赋值语句。它是在这两种情况下的特定语法,改变了默认的参数匹配机制。
为什么要在意:关键字参数
推荐最后阅读
你可能已经知道,高级参数匹配模式可能更复杂。它们也完全是可选的,可以只使用简单的位置匹配,并且当你刚开始编程的时候,这可能是一个好办法。
然而,由于一些Python工具使用它们,了解一些关于这些模式的常识是很重要的。
例如,关键字参数在tkinter中扮演很重要的角色,Tkinter是Python中的标准GUI API。例如,一种调用形式:
创建了一个新的按钮并定义了它的文字以及回调函数,使用了text和command关键字参数。因为对于一个部件的设置选项的数目可能很多,关键字参数能够从中进行选择。如果不是这样的话,也许必须根据位置列举出所有可能的选项,要么期待一个明智的基于位置的参数的默认协议来处理每一个可能选项的设置。
Python中的很多内置函数期待我们对使用模式的选项也用关键字参数,这可能有默认值也可能没有。例如,在第8笔记所学习过的sorted内置函数:
期待传入一个可迭代对象来进行排序,但是也允许传递可选的关键字参数来指定一个字典排序键和一个反向排序标志,其默认值分别为None和False。因为我们通常不会使用这些选项,它们可能会被省略而使用默认值。
关键字参数和默认参数的混合
下面是一个介绍关键字和默认参数在实际应用中稍复杂的例子。
在这个的例子中,调用者必须至少传递两个参数(去匹配spam和eggs),其他的是可选的。如果忽略它们,Python将会分配头部定义的默认值给toast和ham。
def func(spam,eggs,toast=0,ham=0):print(spam,eggs,toast,ham)func(1,2)
1 2 0 0func(1,ham=1,eggs=0)
1 0 0 1func(spam=1,eggs=0)
1 0 0 0func(toast=1,eggs=2,spam=3)
3 2 1 0func(1,2,3,4)
1 2 3 4
再次强调当关键字参数在调用过程中使用时,参数排列的位置并没有关系,Python通过变量名进行匹配,而不是位置。调用者必须提供spam和eggs的值,而它们可以通过位置或变量名进行匹配。
另外注意:name=value的形式在调用时和def中有两种不同的含义(在调用时代表关键字参数,而在函数头部代表默认值参数)。
任意参数的实例
最后两种匹配扩展,*和**,是让函数支持接受任意数目的参数的。它们都可以出现在函数定义或是函数调用中,并且它们在两种场合下有着相关的目的。
收集参数
第一种用法:在函数定义中,在元组中收集不匹配的位置参数。
def f(*args): print(args)
当这个函数调用时,Python将所有位置相关的参数收集到一个新的元组中,并将这个元组赋值给变量args。因为它是一个一般的元组对象,能够索引或在一个for循环中进行步进。
f()
()
f(1)
(1,)
f(1,2,3,4)
(1, 2, 3, 4)
**特性类似,但是它只对关键字参数有效。将这些关键字参数传递给一个新的字典,这个字典之后将能够通过一般的字典工具进行处理。在这种情况下,**允许将关键字参数转换为字典,你能够在之后使用键调用进行步进或字典迭代,如下段程序所示。
def f(**args): print(args)f()
{}f(a=1,b=2)
{'a': 1, 'b': 2}
最后,函数头部能够混合一般参数、*参数以及**去实现更加灵活的调用方式。
例如,在下面的代码中,1按照位置传递给a,2和3收集到pargs位置元组中,x和y放入kargs关键字词典中:
def f(a,*pargs,**kargs): print(a,pargs,kargs)f(1,2,3,x=1,y=2)
1 (2, 3) {'x': 1, 'y': 2}
实际上,这种特性能够混合成更复杂的形式。看一下在函数调用时而不是定义时使用*和**发生了什么。
解包参数
我们在调用函数时能够使用*或**语法,在这种情况下,它与函数定义的意思相反。
调用函数时能够使用*语法会解包参数的集合,而不是创建参数的集合。
例如,能够通过一个元组给一个函数传递四个参数,并且让Python将它们解包成不同的参数。
def func(a,b,c,d): print(a,b,c,d)args = (1,2)
args += (3,4)func(*args)
1 2 3 4
相似地,在函数调用时,**会以键/值对的形式解包一个字典,使其成为独立的关键字参数。
kargs = {'a':1,'b':2,'c':3}
kargs['d'] = 4func(**kargs)
1 2 3 4
另外,在调用中能够以非常灵活的方式混合普通的参数、基于位置的参数以及关键字参数。
func(*(1,2),**{'c':1,'d':2})
1 2 1 2func(1,*(2,3),**{'d':4})
1 2 3 4func(1,c=3,*(2,),**{'d':4})
1 2 3 4func(1,*(2,3),d=4)
1 2 3 4
注意这里不能用func(*(1,2),**{'a':1,'b':2}),会出现多余的a参数,再出现会报错,已经被解包了的1赋值了,后面字典解包相当于再赋值一次。可以把a换成别的。
func(*(1,2),**{'a':1,'b':2})
TypeError: func() got multiple values for argument 'a'
在编写脚本时,当我们不能预测将要传入函数的参数的数量的时候,这种代码是很方便的。
f(1,*(2,),c=3,**{'d':4})
1 2 3 4
作为替代方法,能够在运行时创建一个参数的集合,并且可以统一使用这种方法进行函数的调用。另外,别混淆函数头部或函数调用时*/**的语法:在头部,它意味着收集任意数量的参数,而在调用时,它解包任意数量的参数。
注意:这里调用中的*pargs形式是一个迭代环境,因此技术上它接受任何可迭代对象,而不仅是像这里的示例所示的元组或其他序列。例如,一个文件对象在*之后工作,并且将其行解包为单个的参数(例如,func(*open('fname'))。
应用函数通用性
前面的示例可能有些笨拙,但它们比我们想象的更常用。很多程序需要以通用的形式调用任意函数,提前并不知道函数的名称和参数。(如果你看其他人封装的代码,你会发现很多人都在使用)
实际上,特殊的"varargs"调用的真正强大之处是,在编写一段脚本之前不需要知道一个函数调用需要多少参数。例如,可以使用if逻辑来从一系列函数和参数列表中选择,并且通用地调用其中任何一个:
更广泛地说,当你无法预计参数列表的任何时候,这种varargs调用语法很有用。
例如,如果用户通过一个用户界面选择任意一个函数,你可能在编写自己的脚本的时候无法直接编写一个函数调用。要解决这个问题,直接用序列操作构建一个参数列表,并且用带星号的名称解包参数以调用它:
args = (2,3)
args += (4,)
args
(2, 3, 4)func(*args)
由于这里的参数列表时作为元组传入的,程序可以在运行时构建它。这种技术对于那些测试和计时其他函数的函数来说很方便。
例如,在下面的代码中,通过传递任何发送进来的参数来支持具有任意参数的任意函数:
def tracer(func,*pargs,**kargs):print('calling:',func.__name__)return func(*pargs,**kargs) #这里返回了func函数解包def func1(a,b,c,d): return a+b+c+dprint(tracer(func1,1,2,c=3,d=4))
当这段代码运行的时候,tracer收集参数,然后以varargs调用语法来传递它;
calling: func1
10
后面将看到这种用法的更大的例子,特别参见第20笔记的序列计时示例,以及我们将在第38笔记编写的装饰器工具。
Keyword-Only参数
Python 3.0后把函数头部的排序规则通用化了,允许我们指定keyword-only参数——即必须只按照关键字传递并且不会由一个位置参数来填充的参数。如果想要一个函数既处理任意多个参数,也接受可能的配置选项的话,这是很有用的。
从语法上讲,keyword-only参数编码为命名的参数,出现在参数列表中的*args之后。所有这些参数都必须在调用中使用关键字语法来传递。
例如,在如下的代码中,a可能按照名称或位置传递,b收集任何额外的位置参数,并且c必须只按照关键字传递:
def kwonly(a,*b,c): print(a,b,c)kwonly(1,2,c=3)
1 (2,) 3kwonly(a=1,c=3)
1 () 3kwonly(1,2,3)
TypeError: kwonly() missing 1 required keyword-only argument: 'c'
也可以在参数列表中使用一个*字符,来表示一个函数不会接受一个变量长度的参数列表,而是仍然期待跟在*后面的所有参数都作为关键字传递。
在下一个中,a可能再次按照位置或名称传递,但b和c必须按照关键字传递,不允许其他额外的位置传递:
def kwonly(a,*,b,c): print(a,b,c)kwonly(1,c=3,b=2)
1 2 3
kwonly(c=3,b=2,a=1)
1 2 3kwonly(1,2,3)
TypeError: kwonly() takes 1 positional argument but 3 were given
kwonly(1)
TypeError: kwonly() missing 2 required keyword-only arguments: 'b' and 'c'
仍然可以对keyword-only参数使用默认值,即便它们出现在函数头部中的*的后面。在下面的代码中,a可能按照名称或位置传递,而b和c是可选的,但是如果使用的话必须按照关键字传递:
def kwonly(a,*,b='spam',c='ham'): print(a,b,c)kwonly(1)
1 spam ham
kwonly(1,c=3)
1 spam 3
kwonly(a=1)
1 spam ham
kwonly(c=3,b=2,a=1)
1 2 3kwonly(1,2)
TypeError: kwonly() takes 1 positional argument but 2 were given
实际上,带有默认值的keyword-only参数都是可选的,但是那些没有默认值的keyword-only参数真正地变成了函数必需的keyword-only参数:
排序规则
注意keyword-only参数必须在一个单个星号后面指定,而不是两个星号——命名的参数不能出现在**args任意关键字形式的后面,并且一个**不能独自出现在参数列表中。这两种做法都将产生语法错误:
这意味着,在一个函数头部,keyword-only参数必须编写在**args任意关键字形式之前,且在*args任意位置形式之后,当二者都有的时候。
无论何时,一个参数名称出现在*args之前,它可能是默认位置参数,而不是keyword-only参数:
实际上,在函数调用中,类似的排序规则也是成立的:当传递keyword-only参数的时候,它们必须出现在一个**args形式之前。keyword-only参数可以编写在*args之前或者之后,并且可能包含在**args中:
在这些人为编写的例子中,它们可能是最糟糕的情况,但是,在实际中有可能会遇到它们,特别是那些编写库和工具供其他Python程序员使用的人。
为何使用keyword-only参数
为什么要关心keyword-only参数?
简而言之,它们使得很容易允许一个函数既接受任意多个要处理的位置参数,也接受作为关键字传递的配置选项。尽管它们的使用是可选的,没有keyword-only参数的话,要为这样的选项提供默认值并验证没有传递多余的关键字则需要额外的工作。
假设一个函数处理一组传入的对象,并且允许传递一个跟踪标志:
没有keyword-only参数的话,必须使用*args和**args,并且手动地检查关键字,但是有了keyword-only参数,需要较少的代码。
下面的语句通过notify保证不会有位置参数错误匹配,并且要求它如果传递则作为一个关键字传递:
后面在 模拟Python print函数 将继续看到一些更实用的例子
模拟编写
min函数
下面的内容更为更为实际。通过一个练习来说明实际应用中的一个参数匹配工具。
假设你想要编写一个函数,这个函数能够计算任意参数集合和任意对象数据类型集合中的最小值。也就是说,这个函数应该接受零个或多个参数:希望传递多少就可以传递多少。此外,这个函数应该能够使用所有的Python对象类型:数字、字符串、列表、字典的列表、文件甚至None。
第一个要求提供了一个能够充分展示*的特性的自然示例:我们能够将参数收集到一个元组中,并且可以通过简单的loop依次步进处理每一个参数。
第二部分的问题定义很简单:因为每个对象类型支持对比,没有必要对每种类型都创建一个函数(一个多态的应用)。我们能够不论其类型进行简单地比较,并且让Python执行正确的比较。
满分示例
下面文件介绍了编写这个操作的三种方法,从某种程序上讲其中至少一个是学生在学习的过程中提出来的:
·第一个函数获取了第一个参数(args是一个元组),并且使用分片去掉第一个得到了剩余的参数(一个对象同自己比较是没有意义的,特别是这个对象是一个较大的结构时)。
·第二个版本让Python自动获取第一个参数以及其余的参数,因此避免了进行一次索引和分片。
·第三个版本通过对内置函数list的调用让一个元组转换为一个列表,之后调用list内置的sort方法来实现比较。
Sort方法是用C语言进行编写的,所以有时它要比其他的程序运行的快,而头两种办法的线性搜索将会让它们在绝大多数时间都要更快(Python sort例程是以C写成,使用高度优化的算法,试着利用被排序元素间的部分次序。)。
文件mins.py包含了所有三种解决办法的代码。
所有的这三种解决办法在文件运行时都产生了相同的结果。
注意:上边这三种方法都没有做没有参数传入时的测试。它们可以做这样的测试,但是在这里做是没有意义的。所有的这三种解决办法,如果没有参数传入的话,Python都会自动地抛出一个异常。当尝试获取元素0时,第一种方案会发生异常;当Python检测到参数列表不匹配时,第二种方案会发生异常;在尝试最后返回元素0时,第三种方案会发生异常。
这就是我们所希望得到的结果:因为函数支持任何数据类型,其中没有有效的信号值能够传回标记一个错误。有异常来做这种规则(例如,在运行到错误发生时,不得不有很复杂的运行开销),通常来说,最好假设参数在函数代码中有效,并且当它们不是这样时可以让Python来抛出一个错误。
加分点
如果能够使用这些函数来计算最大值,而不是最小值的话,那么他们能够在这里得到加分。
这算是简单的:头两个函数只需要改为<t o>,而第三个只需要返回tmp[-1]而不是tmp[0]。对于加分点,请确认函数名也修改成了max(尽管这从严格意义上讲是可选的)。
通用化单个的函数计算无论最小值还是最大值都可以,也是可能的,这样的函数需要使用到评估对比表达式。例如,内置函数eval(参考库手册)或者传入一个任意的比较函数。
下面是示例:
和这里一样,函数作为另一种参数对象可以传入一个函数。例如,为了创建max(或者其他)函数,能够简单地传入正确种类的比较函数。这看起来像是附加的工作,但是这种通用化函数(而不是剪切和粘贴来改变一个字符)核心的一点就是在未来我们只需要修改一个版本就可以了,而不是两个。
最后:
所有这些不过是编写代码练习而已。没有理由编写min或max函数,因为这两个都是Python内置的函数。内置版本的函数工作起来基本上很像我们自己编写的函数,不过它们是用C语言编写的,目的是为了优化运行速度,并接受一个单个的可迭代对象或多个参数。然而,尽管它在这一环境下是多余的,但我们在这里使用的通用编码模式可能在其他情况下有用。
更有用的示例-模拟set
看一个实际中常用的使用特定参数匹配模式的例子。前面的末尾,编写了一个函数返回了两个序列的公共部分(它将挑选出在两个序列中都出现的元素)。
这里是一个能够对任意数目的序列(一个或多个)进行公共部分挑选的函数,通过使用可变参数的匹配形式*args去收集传入的参数。因为参数是作为一个元组传入的,能够通过一个简单的for循环对它们进行处理。
我们编写一个union函数,来从任意多的参数中收集所有曾经在任意操作对象中出现过的元素。
因为这些工具是值得重用的,将会把这些函数保存为一个名为inter2.py的模块文件。
无论是哪个函数,参数在调用时都是作为元组args传入的。就像原始的intersect函数一样,这些函数也都对任意类型的序列有效。在这里,他们处理了字符串、混合类型以及两个以上的序列。
注意:因为Python有一个的set对象类型,这里所有关于集合处理的例子都没有严格存在的必要。之所以介绍,不过是用来说明如何编写函数。
模拟print函数
看参数匹配用法的最后一个例子。
这里看到的代码专门用于Python 2.6或更早的版本(它在Python 3.0下也能工作,但是没有什么意义):它使用*args任意位置元组以及**args任意关键字参数字典来模拟Python 3.0 print函数所做的大多数工作。
为了说明一般性的参数匹配,如下的文件print30.py,用少量可重用的代码做了同样的工作:
测试它,将其导入到另一个文件或交互提示模式中:
执行结果:
尽管在Python 3.0中没有意义,但运行的时候,结果是相同的。与通常情况一样,Python的通用性设计允许我们在Python语言自身中原型化或开发概念。在这个例子中,参数匹配工具在Python代码中与在Python的内部实现中一样的灵活。
使用Keyword-Only参数
下面例子使用前面所介绍的keyword-only参数来编写,从而来自动验证配置参数:
这个版本与最初的版本一样有效,并且它是说明keyword-only参数如何方便好用的基本例子。最初的版本假设所有的位置参数都要打印,并且所有的keyword-only参数都只是可选的。大多数情况下这样就够了,但是,任何额外的keyword-only参数都默默地忽略掉了。
例如,如下的一个调用将会对keyword-only参数形式产生一个异常:
但是,会默默地忽略最初版本中的name参数。要手动检测多余的关键字,我们可以使用dict.pop()删除收到的条目,并检查字典是否为空。这里是keyword-only参数版本的一个对等形式:
它和前面一样有效,但是现在它也会捕获外部的关键字参数:
keyword-only参数可以简化一类既接受参数又接受选项的函数。