在定义函数的时候, 我们将参数的名称和对应的位置先给确定好, 这样函数的接口定义就算完成了。
对于那个要调用函数的对象来说呢, 它只需要知道应该怎样去传递正确的参数, 以及这个函数最后会返回一个什么样的值就可以了。至于函数内部那些复杂得很的逻辑, 全都给它封装了起来, 叫那个调用者完全不需要去费心了解。
这个函数的定义看起来是极为简单的, 然而所具备的灵活程度却是相当巨大的。
除了采用通常意义上规定的必须要提供的参数之外, 还能够通过使用有着默认值的参数、能够接受不定数量参数的可变参数以及使用类似键值对那样的关键字参数这样的方式, 这样一来就使得通过这种方式定义出来的函数接口, 不单单具备了处理十分复杂的参数的能力, 同时还能够起到简化那些需要调用该函数的人的代码的作用。
位置参数
def power(x):return x * x
提到power(x)函数的时候, 参数x它就是一个典型的位置参数。
我们在调用权力函数这个操作的时候, 需要确保所输入的参数数量是有且仅有一个的情况, 同时这个唯一的参数名称必须是字母x, 这一点是不可省略的条件。
>>> power(5)25
>>> power(15)
225
def power(x, n):s = 1
while n > 0:
n = n - 1
s = s * x
return s
对于这个修改后的power(x, n)函数, 我们可以进行任意的n次方的计算工作。
>>> power(5, 2)25
>>> power(5, 3)
125
修改后的power(x, n)函数, 这个函数有两个参数, 一个是x, 另一个是n, 这两个参数都是位置参数, 在调用这个函数的时候, 传进去的两个值, 会按照位置的顺序, 依次赋值给参数x和参数n。
默认参数
咱们给这个叫做power的东西定义了一个新的函数, 它的写法本身其实是没有什么逻辑漏洞的, 可是那些老早之前写好的调用语句却全都出了故障, 搞砸了。为啥会这样? 原因是我们在后面硬塞进去多了一个参数, 结果? 旧的那些代码片段因为拿不出第二个必要的参数给凑数, 就彻底没法顺畅地动起来执行啦:
>>> power(5)Traceback (most recent call last):
File "", line 1, in
TypeError: power() missing 1 required positional argument: 'n'
这个错误信息已经十分清晰了。它明确指出了在调用函数power()的时候, 少传递了一个位置参数n。
在这种情况之下, 默认参数这个概念就派上了用场。因为我们在日常生活中需要经常地去计算那个x的平方是多少, 所以呢我们是可以完全把第二个参数的那个n的这个默认值给设定为2这个数值的。
def power(x, n=2):s = 1
while n > 0:
n = n - 1
s = s * x
return s
通过这种方式, 在调用函数power(5)的时候, 其实际执行效果等同于去调用带有两个参数的那个版本, 也就是去调用power(5, 2), 这一过程就这样展开了。
>>> power(5)25
>>> power(5, 2)
25
那么关于那些比2还大的, 也就是n大于2的其它状况, 这就非得把那个n清清楚楚地传过去不可, 就像power(5, 3)这么个传法一样。
从上述那个例子那儿看得出来, 使用默认参数这个东西的话, 就能把函数调用这个动作给简化了。然后, 在去设置那些默认参数的过程当中, 是有这么几个地方值得注意的。
第一个必须遵守的情况是, 要把那些必选的参数安排在前面, 而将那些带有默认值的参数放置在后面, 这样一来如果位置交换了的话, 程序的运行解释器就会报错, 建议大家去仔细思考一下为什么默认参数不应当被放到必选参数的前头。
第二个问题是, 该怎么去设定那些初始的或者基础的参数值。
当函数具有不止一个参数的时候, 应该把那些容易发生变动的参数放置在比较靠前的位置处, 与此同时呢, 将那些不太容易发生变更的参数安排在后边去, 对于那些相对固定的参数而言, 则可以直接赋予它们某种默认的设置值以便于调用。
使用默认参数, 它有一个最大的好处, 那就是能够有效地降低调用函数的难度。
打个比方来讲, 我们如果要编写那个针对一年级小学生进行的用户注册操作所使用的函数代码的话, 这个函数是需要被传入姓名名称以及另外两个额外参数的。
def enroll(name, gender):print('name:', name)
print('gender:', gender)
在这种情况下, 当需要去调用该函数的时候, 操作者只需提供两个参数即可。
>>> enroll('Sarah', 'F')name: Sarah
gender: F
如果咱们想要接着再把年龄还有城市这类信息传进去的话该怎么办呢, 这样子一来会使得调用这个函数的复杂度变得非常大。
我们可以把年龄这个因素以及城市这个因素设定为默认的那些参数, 这样一来操作起来就更为方便了。
def enroll(name, gender, age=6, city='Beijing'):print('name:', name)
print('gender:', gender)
print('age:', age)
print('city:', city)
基于这样的规定, 绝大多数的学生在进行注册操作的时候, 是不需要提供关于年龄以及所居住城市方面的信息的, 仅仅只需要提供那两一个是必须的参数。
>>> enroll('Sarah', 'F')name: Sarah
gender: F
age: 6
city: Beijing
只有那些情况与默认设定的参数不一致的学生, 才需要补充提供额外的相关信息。
enroll('Bob', 'M', 7)enroll('Adam', 'M', city='Tianjin')
由此可见, 默认参数的存在使得调用函数的门槛降低了。与此同时, 一旦未来需要执行更为复杂的调用操作, 依旧能够通过传输更多的参数方式来实现最终目的。正因为无论是执行简单层面的调用任务, 还是完成复杂层面的调用任务, 程序在函数层面上仅仅只需要保持单一的定义就可以了。
当存在多个默认参数之时, 进行函数调用之际, 操作者既可以把这些默认依据传递顺序来予以提供, 就比如说对名为('Bob', 'M', 7)的情形作出调用, 该种说法所表达的意思是指, 除开名称、第二个标识之外, 最后提及的那一个数值会被应用在规定为年龄的位置之上, 至于城市一项是因为并未给出相应的值, 所以其依旧将维持既有的预设状态。
也可以不按照固定的顺序来提供那部分默认参数, 在这种情况下必须把参数的名称明确地写出来, 例如在函数调用中写成('Adam', 'M', city='')的形式, 这样的做法代表的意思是让city这个参数使用已经传进去的具体值, 而对于那些没有被指定的其他所有默认参数就都继续进行原本设定的默认值。
那个默认的参数设置其实是有它的好处的, 但是一旦使用的场合不对, 就很容易遇到麻烦状况。这个默认参数存在的一个最大的陷阱问题, 具体的演示情况如下所示:
首先我们要去定义出一个函数来, 这个函数是接收一个列表作为参数的, 接着我们就在这个列表的末端再去添加上一个END这样的东西, 最后把结果给返回出去:
def add_end(L=[]):L.append('END')
return L
当你在正常使用的情形下对其进行调用时, 最终的展现出来的那个结果, 看来表现得还挺不错的。
>>> add_end([1, 2, 3])[1, 2, 3, 'END']
>>> add_end(['x', 'y', 'z'])
['x', 'y', 'z', 'END']
当大家在调用那个环节之中使用了系统预先设定好的标准条件之时, 最初的输出反馈情况也同样是处于准确无误的状态之中的。
>>> add_end()['END']
不过, 在进行第二次调用的时候, 发现得到的结果变得不正确了:
>>> add_end()['END', 'END']
>>> add_end()
['END', 'END', 'END']
许多初学者感到十分困惑, 因为默认参数的值虽然看起来是这样设定, 但函数的行为似乎总是能够记录上次列表里面添加了这个元素之后的状态。
这个现象背后的原因, 可以作如上解释。
当我们为函数定义参数的时候, 系统会把默认参数L的具体数值先计算并确定下来。这是因为, 默认参数L本身也是一个变量, 它会指向某个特定的对象。如果你在每个调用该函数的过程中改变了L里面的内容, 那么当下一次再调用这个函数的时侯, 你拿到的默认参数的内容就不再是当初定义函数时的那个样子了。
想要对前述示例进行更改, 我们能够运用 None 这个不可变对象来达成这一目的。
def add_end(L=None):if L is None:
L = []
L.append('END')
return L
眼下, 不管执行调用功能的次数有多少次, 都不存在任何麻烦:
>>> add_end() //每次调用这个无参函数的时候,都不会改变默认参数L的值(因为在def add_end(L=None):已经设置为None)['END']
>>> add_end()
['END']
为什么要设计像是str、None这样不可变的对象呢? 原因是不当对象一旦创建之后, 其内部的数据就不能再进行修改了, 这种情形能够起到减少由数据修改所引发的错误的作用。
另外, 由于对象的状态是保持不变的, 所以在多任务的环境中实现对对象的同时读取的时候, 是不需要添加锁的, 这样做的话在同时读取的时候不会存在任何的问题。我们大家在编写程序的过程之中, 如果能够进行一个不变对象的设计工作, 那么就应当尽量将其设计成不可变的形式。
把参数前面加上星号, 这就是可变参数的意思。
def calc(numbers):sum = 0
for n in numbers:
sum = sum + n * n
return sum
不过在执行调用操作的时候, 前提步骤是需要先人工组合创建一个列表或者元组出来的。
>>> calc([1, 2, 3])14
>>> calc((1, 3, 5, 7))
84
当采取使用可变参数这种技术手段, 对于调用函数这一行为进行操作的话, 就可以将其简化成为如下所示的形式。
>>> calc(1, 2, 3) //就是我想传多少个值进去,就传多少个值进去14
>>> calc(1, 3, 5, 7)
84
因此, 我们就把函数的参数这一块内容给改成可变参数的形式了:
def calc(*numbers):sum = 0
for n in numbers:
sum = sum + n * n
return sum
与定义一个list或者tuple参数进行比较, 定义可变参数的不同之处仅仅是在参数的最前面多添加了一个*号。在函数的内部实现中, 程序接收到的这个参数其实是一个tuple, 也就是我们说的元组。
这样一来, 函数内部的代码部分可以保持着完全不变的状态。然而, 当我们去调用这个函数的时候, 就具备了能够传入任意数量参数能力, 这个数量里面自然也就包括了零个参数的情况:
>>> calc(1, 2)5
>>> calc()
0
当情况是已经存在了一个列表或者元组, 此时如果想要调用一个能够接受可变数量参数的函数, 应当采取什么样的做法呢? 针对这个问题, 可以采用以下方式进行操作:
>>> nums = [1, 2, 3]>>> calc(nums[0], nums[1], nums[2]) //把list的各个元素值分别传进去
14
这种写法毫无疑问是完全可以的, 然而其不足之处在于过程过于繁杂, 因此允许你在list或者tuple的前面添加一个*号, 以此来把list或者tuple的元素变成可变参数的方式传进去:
>>> nums = [1, 2, 3]>>> calc(*nums)
14
nums代表着将nums这个列表里面所有的元素统统提取出来, 并且作为那些数量不固定的参数传递给对应的函数去使用, 这样一种书写方式显得非常关键, 而且在实际的代码编写工作里面是非常普遍的一种情况。
在参数的前面加上两个星号, 这种方法就是关键字参数。
可变参数这个东西, 它是允许你传入0个或者任意个数的参数的, 这些可变参数在函数调用的时候, 会自动被组装成一个tuple。而关键字参数呢, 也是允许你传入0个或者任意个带有参数名的参数的, 这些关键字参数在函数内部会自然而然地被组装成一个dict类型的对象。请你查看以下的示例代码内容:
在那个kw参数的前头加上两个星号, , 那么它就代表这个参数是一个关键字参数。
def person(name, age, **kw):print('name:', name, 'age:', age, 'other:', kw)
这个函数除了接受一个叫name的必选参数, 还有另一个叫age的必选参数之外, 它还接受一个通过关键字kw的形式来传入的参数。当用户在对上述这个函数进行调用的时候, 可以选择只将那些必选参数进行传入:
>>> 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'}
关键字参数到底起到了什么作用, 其实它能把函数的功能给扩展开来。举例来说, 在函数内部, 我们要确保能拿到 name 和 age 这两个参数, 可是, 如果调用的人愿意多给一些参数, 我们照样也能接得住。
你不妨想象一下, 正当你在开发用户注册这个功能的时候, 用户名跟年龄属于非得填不可的项目, 至于别的字段, 全都可以随便选是不是要填, 这样的话, 要是你用关键字参数的写法去定这个函数, 那自然就能满足关于注册方面的那些个需求了。
这和可变参数的情况是一样的, 也就是可以先组装出一个字典, 然后将这个字典转换成关键字参数传递进去。
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}>>> person('Jack', 24, city=extra['city'], job=extra['job'])
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}
其实, 那个上面所说的非常复杂的调用方式, 大家是可以用那种相对简化之后的写法来进行表示的。
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}
extra这个东西的意思是, 把extra这个字典里面的所有的键值对都当作关键字参数送到那个叫kw的函数参数里面去。然后, 这个kw会拿到一个字典出来的结果。
大家要特别注意一点, 就是kw这里拿到的这个字典, 其实是extra的一份拷贝版本来的。所以如果你对kw里面的东西进行修改或者改动的操作是绝对不会影响到函数外面那个原始的extra里面的内容的。
命名关键字参数
关于关键字参数这一块, 其实函数的调用方是完全能够在调用的时候往里传入一些没有受到具体限制的关键字参数的。那么至于最终究竟有哪些具体的关键字参数被传进来了呢, 这个需要让函数自己在其内部的执行过程里去通过那个叫做kw的对象进行相关的检查和确认。
如果想要把关键字参数的名字给限住, 那么就可以去使用命名关键字参数这个办法。就拿只允许接收 city 以及 job 这两个内容作为关键字参数的情况来看吧。按照这种逻辑去定义出来的函数, 它的样子是这样的。
def person(name, age, *, city, job):print(name, age, city, job)
这一点和关键字参数kw是不一样的, 它不一样在于命名关键字参数是需要一个特殊的分隔符*的, 而在*后面的那些参数就会被视为是命名关键字参数。
具体的调用方法是这样子的。
>>> person('Jack', 24, city='Beijing', job='Engineer')Jack 24 Beijing Engineer
若是函数的定义里面已经存在一个可变参数了, 那么后面跟着的那些命名关键字参数, 就无须再使用特殊分隔符星号来进行分隔了。
def person(name, age, *args, city, job):print(name, age, args, city, job)
那个被定义为命名关键字参数的东西, 是必须在调用的时候把参数名给传进去的, 这一点是和位置参数有着不一样的规则的, 如果大家在调用的过程中并没有把对应的参数名给传过去的话, 那么这次调用行为就将会导致报错结果的产生:
>>> person('Jack', 24, 'Beijing', 'Engineer')Traceback (most recent call last):
File "", line 1, in
TypeError: person() takes 2 positional arguments but 4 were given
因为调用的时候没给参数命名, 把city和job这两个参数漏掉了, 所以解释器就把这4个参数全都当成位置参数来处理了, 可是括号里面的这个函数仅仅只允许接收2个位置参数。
你可以给那些通过指定名字来传递的参数设置一个默认的数值, 这样一来, 在实际调用的过程中的操作就被简化了一大截:
def person(name, age, *, city='Beijing', job):print(name, age, city, job)
因为那个用在了命名关键字里面的名字叫做city的参数, 它本身是带有一个预设好的默认值的, 所以在进行函数调用的时候, 完全可以选择不去给它专门传入一个名为city的参数。
>>> person('Jack', 24, job='Engineer')Jack 24 Beijing Engineer
在使用命名关键字参数的时候, 是需要去特别留意相关情况的, 如果不存在可变参数的话, 那就必须加上一个星号来充当特殊类型的分隔符, 一旦发现缺失了这个星号, 那么程序解释器就会没有办法去正确识别出哪些是位置参数, 以及哪些是属于命名关键字的参数部分。
def person(name, age, city, job):# 缺少 *,city和job被视为位置参数(位置参数的定义见本文前半部分)
pass
参数组合
在定义函数的时候, 可以选着使用必选参数、默认参数、可变参数、关键字参数和命名关键字参数, 这五类参数能够进行组合搭配。
可是要留意的一点是, 参数的定义顺序必须得按照这个规矩来排才行, 也就是必须先摆上必选参数, 然后接着放默认参数, 再往后才是可变参数, 随后才轮到命名关键字参数登场, 到最后面才是放置关键字参数的位置, 整个次序绝不能搞乱, 必须严格遵守这样一个排列逻辑。
打个比方,我们来定义一个函数, 这个函数里面, 要包含上面说到的那几种参数。
def f1(a, b, c=0, *args, **kw): //args是可变参数,kw是关键字参数print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)
def f2(a, b, c=0, *, d, **kw): //d是命名关键字参数,kw是关键字参数
print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)
当发生函数调用的行为的时候, 解释器会自动地依据参数的位置以及参数的名称来把相匹配的参数传递进入。
>>> f1(1, 2)a = 1 b = 2 c = 0 args = () kw = {}
>>> f1(1, 2, c=3)
a = 1 b = 2 c = 3 args = () kw = {}
>>> f1(1, 2, 3, 'a', 'b')
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {}
>>> f1(1, 2, 3, 'a', 'b', x=99)
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {'x': 99}
>>> f2(1, 2, d=99, ext=None)
a = 1 b = 2 c = 0 d = 99 kw = {'ext': None}
其中最令人感到奇妙的一点在于, 即便使用的是元组这一数据结构以及字典这一数据结构, 你也是能够执行前面所提到的那些函数操作的。
>>> args = (1, 2, 3, 4)>>> kw = {'d': 99, 'x': '#'}
>>> f1(*args, **kw)
a = 1 b = 2 c = 3 args = (4,) kw = {'d': 99, 'x': '#'}
>>> args = (1, 2, 3)
>>> kw = {'d': 88, 'x': '#'}
>>> f2(*args, **kw)
a = 1 b = 2 c = 3 d = 88 kw = {'x': '#'}
所以, 针对任何一个函数而言, 都可以采取类似 func(*args, kw) 这种方式去调用它。这种情况的发生并不取决于该函数的参数具体是如何进行定义的。
请注意, 尽管功能上可以将多达5种参数进行组合, 但是大家在使用的时候千万不要一次性把太多组合全都用上, 因为如果那样的话函数接口的可读性就会变得非常差, 也就是让人很难看懂接口到底是想干嘛。
小结
该函数具备极高的灵活性, 在参数形态方面表现得十分多变。你既可以直接采用最简单的方式进行调用, 也可以选择为其输入结构相当复杂的各类参数。
默认参数必须要用不可变对象, 如果使用的是可变对象, 那么在程序运行的时候就会存在逻辑上的错误。
要注意定义可变参数和关键字参数的语法:
*args这个玩意儿, 它是一个能够变得多可以变少的参数, 它会接收到一个东西, 这个东西呢, 是元组, 也有人叫它数组的。
kw这个名词指的是关键字参数, 而kw实际上接收的是一个字典类型的对象, 也就是通常所说的键值对集合。
另外, 还涉及到当函数被调用的时刻, 应该怎么样去提交那些可以变化的参数以及带有关键字标志的参数所对应的语法规则内容:
可变参数, 这玩意儿, 既可以, 像是func(1, 2, 3)这样, 直接就把参数给传进去。当然, 你也可以先, 把那些参数, 给组装到一个list或者tuple里面去。然后呢, 再通过args, 这样间接的方式, 把它给传进去。比如, 就像是func((1, 2, 3))这样的情况。
关键字参数这种调用方式既可以直接将像 a=1, b=2 这样的内容直接传入到函数 func( a=1, b=2) 里面去, 同时呢, 也可以先通过一个叫做组装 dict 的操作手段, 先把相关的值给准备好, 然后再借助于一种名为 kw 的东西作为传递的手段, 最终把它传给那个名为 func({'a': 1, 'b': 2})的函数去执行操作。
在编程里, *args和kw是那种大家习惯了就用的写法, 说白了就是常规操作, 虽然你也可以换几个别的参数名称出来用, 但是还是按照老规矩来吧, 这样别人看起来更顺畅一些。
通过设置关键字参数中的命名标识这种方式, 目的是为了去对调用方人员所能进行传参的名称范围做出约束和限制, 与此同时还可以为其赋予一个预设置的默认数值。
在定义了名称关键字参数的时候, 如果没有使用可变参数的情形下, 千万不可以忘记了去书写那个代表位置的分隔符星号。因为要是忘记写了的话, 那么你所定义的那个东西, 就将不再是我们所期望的关键字参数了, 而会变成一个单纯的位置参数。
本文参考自: