首页 > 技术文章 > 函数的参数详解

themost 2017-07-27 09:05 原文

# 函数的参数

定义函数的时候,我们把参数的名字和位置确定下来,函数的接口定义就算完成了。
对于函数的调用者来说,只需要知道如何传递正确的参数,以及函数将返回什么样的值就够了
函数内政部的复杂逻辑被封装起来,调用者无需了解。

python的函数定义非常简单,单灵活度却非常大。除了正常定义的必选参数外,
还可以使用默认参数、可变参数和关键字参数,使得函数定义出来的接口,不但能处理复杂的参数
还可以简化调用者的代码。

一、位置参数
我们先写一个计算x的平方的函数
def power(x):
    return x * x
对于power(x)函数,参数想就是一个位置参数。当我们调用power函数时,必须传入有且仅有的一个参数x
>>> power(5)
25
>>> power(15)
225
>>> power(125)
15625

那么,如果我们现在要计算x的立方怎么办?可以再定义一个power3函数,但是如果要计算x的四次方,五次方怎么办?
假设x的n次方,可以把x和n都设为参数

def powerful(x, n):
    result = 1
    if n == 0:
        return result
    elif n > 0:
        while n >= 1:
            result = result * x
            n-=1
        return result
    elif n < 0:
        while n >= 1:
            result = result * x
            result = 1 / result
        return result

对于这个修改后的powerful(x, n)函数,可以计算任意N次方
修改后的powerful(x, n)函数有两个参数,x和n,这两个参数都是位置参数,调用函数时,传入的两个值
按照位置顺序一次赋给参数x和n



二、默认参数
如果我们经常计算x的平方,那么我们完全可以把第二个参数的默认值设定为2

def powerful(x, n = 2):
    result = 1
    if n == 0:
        return result
    elif n > 0:
        while n >= 1:
            result = result * x
            n-=1
        return result
    elif n < 0:
        while n >= 1:
            result = result * x
            result = 1 / result
        return result

>>> powerful(5)
25
>>> powerful(5,3)
125

这样如果我们给n赋值,n就会成为被赋的值,如果不给n赋值,默认就是n=2,默认计算平方。

从以上的例子可以看出,默认参数可以简化函数的调用。

设置默认参数时,有几点问题需要注意:

一是必选参数在前,默认参数在后,否则python的解释器会报错。
二是如何设置默认参数。

当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。
变化小的参数就可以作为默认参数。使用默认参数最大的好处就是能够降低使用函数的难度。

举个例子,我们写一个小学一年级学生注册的函数,需要传入name和gender两个参数
def enroll(name, gender):
    print("name:", name)
    print("gender:", gender)

这样,调用enroll()函数只需要传入两个参数
>>> enroll("Sarah", "F")
name: Sarah
gender: F

如果要继续传入年龄,城市信息怎么办? 我们如果规定一大堆位置参数,会使得调用函数的难度大大增加
因为位置参数需要严格按照位置传入,python默认从左到右给参数赋值,而位置参数一旦增多,
必然会要求函数调用者必须严格按照一定的顺序来,这样会导致出错的概率大大增加。
在本例中,由于年龄、城市信息绝大多数学生是一致的,因此可以设定为默认值
这样,大多数学生注册时不需要提供年龄和城市,只需提供必须的两个参数
只有与默认参数不符的学生才需要提供额外的信息

def enroll(name, gender, age = 6, city = "BeiJing"):
    print("name:",name)
    print("gender:",gender)
    print("age:",age)
    print("city:",city)

>>> enroll("xiaowang", "M")
name: xiaowang
gender: M
age: 6
city: BeiJing

>>> enroll("xiaoshitou", "F", 8, "shanghai")
name: xiaoshitou
gender: F
age: 8
city: shanghai

>>> enroll("xiaojuzi","F", 12)
name: xiaojuzi
gender: F
age: 12
city: BeiJing

>>> enroll("ligoudan","M", city="LA")
name: ligoudan
gender: M
age: 6
city: LA

可见,默认参数降低了函数调用的难度,而一旦需要更复杂的调用时,又可以传递更多的参数来实现
无论是简单调用还是复杂调用,函数只需要定义一个。

有多个默认参数时,调用的时候,既可以按顺序提供默认参数
比如enroll("Bob","M",7)
意思是, 除了name和gender这两个参数外,最后一个参数应用在age上,
city参数由于没有提供,仍然使用默认值。

也可以不按顺序提供部分默认参数。当不按顺序提供部分默认参数时,需要把参数名写上,相当于指名道姓的给某某赋值
比如调用enroll("Adam", "M", city= "tianjin")
意思是, city参数用穿进去的值,其他默认参数继续使用默认值。

注意:默认参数很有用,但使用不当也会掉坑里。默认参数有个最大的坑,演示如下:
先定义一个函数,传入一个list,添加一个END再返回。

def add_end(L=[]):
    L.append("END")
    return L

当你正常调用时,结果似乎不错
>>> add_end([1,2,3])
[1, 2, 3, 'END']
>>> add_end(["anything you want","shuzu","data",1985])
['anything you want', 'shuzu', 'data', 1985, 'END']

当时使用默认参数时,一开始的结果也是对的:
>>> add_end()
['END']

但是,当你再次调用时,结果就不对了
>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']

很多人开始疑惑,默认参数是一个空列表[], 但是函数似乎每次都记住了上次添加了“END”后的list
原因解释如下:
python函数在定义的时候,默认参数L的值就被计算出来了,即[]
因为默认参数 L 也是一个变量,他指向对象[], 每次调用该函数,如果改变了 L 的内容,
则下次调用时, 默认参数的内容就变了,不再是函数定义时的[]了。

也就是说,如果你的默认参数是一个变量,而这个变量在函数执行的过程中会发生改变,那么
当你下一次重新调用这个函数意图使用函数定义时的默认参数时,就不可能了,因为默认参数已经变了

所以说,默认参数应该是一个固定的值,是大多数情况下不需要修改的值,必须指向不变对象!

要修改上面的例子,可以用None这个不变对象来实现:

def add_end(L = None):
    if L == None:
        L = []
        L.append("END")
        return L
    else:
        L.append("END")
        return L

这样,无论调用多少次,都不会有问题,这样传入的参数会先进行判断,如果为空,则创建一个空列表,添加一个END返回
>>> add_end([1,2,3])
[1, 2, 3, 'END']
>>> add_end()
['END']
>>> add_end()
['END']
>>> add_end()
['END']

为什么要设计str、None这样的不可变对象呢? 因为不可变对象一旦创建,对象内部的数据就不能修改,
这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取不需要加锁。
同时读取一点问题都没有。
我们在编写程序时,如果可以设计一个不可变对象,那就尽量设计成不变对象。

什么是不可变对象?
比如你创建一个字符串"李狗蛋",李狗蛋就是李狗蛋,不能变成其他的,至于你说,可以拆分成"李狗""李蛋"
这不是发生了对字符串的更改,而是重新生成了两个字符串"李狗""李蛋",他们与原来字符串"李狗蛋"的关系
仅仅是在原来字符串基础上衍生的新字符串,并没有对原字符串发生更改。



三、可变参数

在python函数中,还可以定义可变参数。
顾名思义,可变参数就是传入的参数个数是可变的,可以是1个,2个,任意个。也可以0个。
我们以数学题为例子:
给定一组数字a, b, c....,请计算a的平方 + b的平方 + c的平方

要定义出这个函数,我们必须确定输入的参数。然而,很悲伤,我们竟不知道有多少参数,
即使知道有多少个,但是总不能在函数定义时全部写出来吧!

我们可以显而易见的想到一个比较好的方法:把参数用一个列表或者元组传进来,然后遍历,再做运算,听起来不错。

def sum(numbers):
    sum = 0
    for n in numbers:
        sum = sum + n
    return sum

但是调用的时候,需要先组装出一个list或者tuple
>>> sum([1,2,3,4,5])
15

还有更好的方法:函数定义时,设置可变参数。同
样可以遍历传入的*numbers

def sum(*numbers):
    sum = 0
    for n in numbers:
        sum += n
    return sum

>>> sum(1,3,5,7)
16
>>> sum()
0

定义可变参数和定义一个list或者tuple参数相比,仅仅在参数前面加了一个 * 号。
在函数内部, 参数number接受到的是一个tuple,因此,函数的代码完全不变。
但是调用该函数时,额可以传入任意个参数,包括0个参数。
着重理解一下:*numbers 在函数内部是一个元组形式,具备元组的一些功能。

如果已经有一个现成的list或者tuple,要调用一个可变参数怎么办?
可以这样做:
>>> nums= [1, 2, 3]
>>> sum(nums[0], nums[1], nums[2])
6

这种写法是可行的,但是相当繁琐。
真正好的方法是在list或者tuple前面加一个 * ,好像一个反运算,又把list或者tuple的元素拆解出来传进去了

>>> sum(*nums)
6

*nums表示把nums这个list的所有元素作为可变参数穿进去。这种写法相当有用,而且很常见。



四、关键字参数
可变参数允许你传入0个或者任意个参数,这些可变参数在函数调用时自动组装成为一个tuple。
但是,你传入的这些不确定个数的参数仅仅是一个值,至于这个值是干什么,什么属性,一切都无从得知
比如传入可变参数[1,3,5,7] 这是可以的,但是你想传入一个x,并且准备让x=9
也就是说你想传成这样的形式[1,3,5,7,x = 9]
这样该怎么办?

使用关键字参数。
关键字参数允许你传入0个或者任意个含参数名的参数,这些参数可以说有名有姓,有键有值
这些关键字参数在函数内部自动组装为一个dict

还是举小学生注册的例子:
def person(name, age, **kwargs):
    print("name:",name, "age:",age, "other:",kwargs)

函数person出了必选参数name和age外, 还接受关键字参数kwargs
在调用该函数时,可以只传入必选参数

>>> person("stephen curry", 29)
name: stephen curry age: 29 other: {}

当然咯,作为一个信息录入的函数,我们也十分喜欢用户传的信息详细些,比如这样
>>> person("stephen curry", 29, gender = "M", tags="the famous basketball star")
name: stephen curry age: 29 other: {'gender': 'M', 'tags': 'the famous basketball star'}


所以, 关键字参数有什么用呢?
它可以扩展函数的功能。
比如,在person函数里,我们保证能接受到name和age这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。
试想,你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数
就能满足注册的需求。

当然,也可以先组装一个dict,然后用 ** 分解开传进去
>>> extra = {"reward":"twice mvp of NBA", "hobby":"golf"}
>>> person("stephen curry", 29 **extra)
name: stephen curry age: 29 other: {'reward': 'twice mvp of NBA', 'hobby': 'golf'}
** extra表示把extra这个dict的所有key-value用关键字参数传入到函数的 **kwargs
kwargs将获得一个dict
注意kwargs获得的dict是extra的一份拷贝,对kwargs的改动不会影响函数外的extra。

五、命名关键字参数

对于关键字参数,函数的调用者可以传入任意不收限制的关键字参数,至于到底传入了哪些
就需要在函数内部通过kwargs检查
仍以person为例,我们希望检查时候有city和job参数


def person(name, gender, **kwargs):

    if "city" in kwargs:
        print("有city参数传入")
    if "job" in kwargs:
        print("有job参数传入")

    print("name:",name, "gender:",gender, "other:",kwargs)

但是调用者仍然可以传入不受限制的关键字参数

>>> person("stephen curry", "M", hobby="basketball", city="Golden States")
有city参数传入
name: stephen curry gender: M other: {'hobby': 'basketball', 'city': 'Golden States'}

如果要限制关键字参数的名字,就可以用命名关键字参数, 例如,只接收city和job作为关键字参数。
这种方式定义的函数如下:
def person(name, age, *, city, job):
    print(name, age, city, job)

命名关键字参数必须传入参数名,这和位置参数不同。
>>> person("stephen curry",29, city="Colden States",job="basketball")
stephen curry 29 Colden States basketball

如果没有传入参数名,调用将报错
>>> person("stephen curry",29,"GS","basketball")
Traceback (most recent call last):
  File "<pyshell#85>", line 1, in <module>
    person("stephen curry",29,"GS","basketball")
TypeError: person() takes 2 positional arguments but 4 were given


由于调用时缺少参数名city和job,python解释器把这4个参数均视为位置参数,
但是person()函数仅接受了2个位置参数。

正所谓不传不行,不指名也不行。
>>> person("xiaowang",24)
Traceback (most recent call last):
  File "<pyshell#92>", line 1, in <module>
    person("xiaowang",24)
TypeError: person() missing 2 required keyword-only arguments: 'city' and 'job'
>>> person("xiaowang",24, "beijing","program")
Traceback (most recent call last):
  File "<pyshell#93>", line 1, in <module>
    person("xiaowang",24, "beijing","program")
TypeError: person() missing 2 required keyword-only arguments: 'city' and 'job'


命名关键字参数可以有缺省值,从而简化调用:
def person(name, age, *, city="BeiJing", job):
    print(name, age, city, job)
由于命名关键字参数city具有默认值,调用时,可不传入city参数。
>>> person("Jack",24,job="Engineer")
Jack 24 BeiJing Engineer


使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个 * ,如果缺少 * ,
python解释器将无法是被位置参数和命名关键字参数
def person(name, age, city, job):
    # 缺少 * city和job被视为位置参数
    pass

命名关键字参数表示,只接收命名的关键字,多余的不再接受

六、参数组合

在python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。
但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。

比如定义一个函数,包含上述若干种参数:
def f1(a, b, c=0, *args, **kwargs):
    print("a = ", a, "b = ", b, "c = ", c, "args = ", args, "kwargs = ", kwargs)

def f2(a, b, c=0, *, d, **kwargs):
    print("a = ", a, "b = ", b, "c = ", c, "d = ", d, "kwargs = ", kwargs)

在函数调用的时候,python解释器会自动按照参数位置和参数名把对应的参数传进去。

>>> f1(1,2)
a =  1 b =  2 c =  0 args =  () kwargs =  {}
>>> f1(1,2,c=3)
a =  1 b =  2 c =  3 args =  () kwargs =  {}
>>> f1(1,2,3)
a =  1 b =  2 c =  3 args =  () kwargs =  {}
>>> f1(1,2,3,4)
a =  1 b =  2 c =  3 args =  (4,) kwargs =  {}
>>> f1(1,2,3,4,5)
a =  1 b =  2 c =  3 args =  (4, 5) kwargs =  {}
>>> f1(1,2,3,4,5,6,7)
a =  1 b =  2 c =  3 args =  (4, 5, 6, 7) kwargs =  {}
>>> f1(1,2,3,4,5,6,x=10)
a =  1 b =  2 c =  3 args =  (4, 5, 6) kwargs =  {'x': 10}


>>> f2(1,2,3, d= 108, extra=None)
a =  1 b =  2 c =  3 d =  108 kwargs =  {'extra': None}

最神奇的是通过一个tuple和dict,你也可以调用上述函数:

>>> f1(*args, **kwargs)
a =  1 b =  2 c =  3 args =  (4,) kwargs =  {'d': 99, 'x': None}

>>> f2(*args,**kwargs)
a =  1 b =  2 c =  3 d =  99 kwargs =  {'tags': 'basketball'}

所以,对于任意函数都可以通过类似func(*args, **kwargs)的形式调用它,因为加入星号*作为参数传入进函数时
就相当于把参数拆解成一个个,如果传入的参数正确,函数会自动进行一一赋值。


总结:

python的函数具有非常灵活的参数形态,既可以实现简单的调用,又可以传入非常复杂的参数。

注意:默认参数一定要用不可变对象,如果是可变对象,程序运行时会有逻辑错误。

注意:定义可变参数和关键字参数的语法
*args是可变参数, args接收的是一个tuple
**kwargs 是关键字参数,kwargs接收的是一个dict

注意:调用函数时如何传入可变参数和关键字参数的语法
可变参数既可以直接传入:func(1,2,3),又可以先组装成list或者tuple通过*args传入:func(*(1,2,3))
关键字参数既可以直接传入:func(a = 1, b = 2),又可以先组装dict,再通过**kwargs传入:func(**{"a":1,"b":2})

使用*args 和 **kwargs 是python的习惯写法,当然也可用其他的写法,不过最好用习惯写法。

命名的关键字参数是为了限制调用者可以传入的参数,同时可以提供默认值。

注意:定义命名关键字参数在没有可变参数的情况下不要忘了写分隔符 * ,否则定义的将是位置参数。

 

推荐阅读