参数类型

大多数函数在定义时会接收一定数量的参数,然后根据调用时提供的参数值的不同来产生不同的结果。前面我们已经提到,不建议在函数内部使用与外部变量相同的参数名称,因为这样容易引起混淆和错误。通常情况下,函数的定义和参数传递如下所示:

x, y, z = 1, 2, 3

def add(a, b, c):

    return a+b+c

add(x, y, x)        # 使用变量,传递参数
add(4, 5, 6)        # 直接传递值也是可以的。

在上述示例中,a、b、c被称为形式参数,简称形参。而x、y、z和4、5、6被称为实际参数,简称实参,即实际传递的值。通常我们讨论的参数指的是形参。

在定义函数时,参数的名称和位置确定下来后,函数的接口就被固定了。对于函数的调用者来说,只需要知道如何传递正确的参数以及函数将返回什么样的值就足够了。函数内部的复杂逻辑被封装起来,调用者无需了解其中的细节。Python函数的参数定义非常灵活。除了常规的位置参数,还可以使用默认参数、可变参数和关键字参数,这些都是形参的不同类型。

一、位置参数

也称为必传参数、顺序参数,是最重要的参数类型,调用函数时必须明确提供,并按照顺序一一对应传递。位置参数必须按照定义时的顺序依次传递,数量必须匹配。

在上面的例子中,a、b、c就是位置参数。当我们使用add(4, 5, 6)进行调用时,将4传递给a,5传递给b,6传递给c,实现了一一对应的传递。类似于add(4, 5, 6, 7)add(4)add(5, 4, 6)这种多余或不足的调用都是错误的。其中,add(5, 4, 6)在语法上是合法的,但输出结果可能与预期不符。

注意:Python在进行函数参数传递时不会对数据类型进行检查,理论上你可以传递任何类型的参数!

def add(a, b, c):
    return a+b+c

result = add("haha", 2,  3)

但是,上面的add函数,如果你传递了一个字符串和两个数字,结果是弹出异常,因为字符串无法和数字相加。这就是Python的弱数据类型和动态语言的特点。在简单、方便的时候,需要你自己去实现数据类型检查。

Traceback (most recent call last):
  File "F:/Python/pycharm/201705/func.py", line 33, in <module>
    result = add("haha", 2,  3)
  File "F:/Python/pycharm/201705/func.py", line 31, in add
    return a+b+c
TypeError: must be str, not int

二、 默认参数

在函数定义时,如果给某个参数提供一个默认值,这个参数就变成了默认参数,不再是位置参数了。在调用函数的时候,我们可以给默认参数传递一个自定义的值,也可以使用默认值。

def power(x, n = 2):
    return x**n

ret1 = power(10)   # 使用默认的参数值n=2
ret2 = power(10, 4)  # 将4传给n,实际计算10**4的值

上面例子中的n就是个默认参数。默认参数可以简化函数的调用,在为最常用的情况提供简便调用的同时,还可以在特殊情况时传递新的值。但是在设置默认参数时,有几点要注意:

  • 默认参数必须在位置参数后面!

如果你违反了这点,在语法层面直接是通不过的。

# 这是一个错误的例子
def power(n = 2,x):
    return x**n
  • 当有多个默认参数的时候,通常将更常用的放在前面,变化较少的放后面。
def student(name, sex, age, classroom="101", tel="88880000", address="..."):
    pass
  • 在调用函数的时候,尽量给实际参数提供默认参数名。
def student(name, sex, age, classroom="101", tel="88880000", address="..."):
    pass

student('jack','male',17)       # 其它全部使用默认值
student('tom','male',18,'102','666666','beijing')    # 全部指定默认参数的值
student('mary','female',18,'102',tel='666666')  # 挑着来
student('mary','female',18,tel='666666','beijing')   #  这是错误的参数传递方式
student("mary","female",18,tel="666666",address="beijing")

注意最后两种调用方式,倒数第二种是错误的,而最后一种是正确的。为什么会这样呢?因为在调用函数时,如果没有提供参数名,实参会按照位置参数的顺序从左到右进行匹配。

  • 使用参数名传递参数

通常情况下,我们在调用函数时,按照位置参数的顺序依次传入参数,并且必须在默认参数之前。但是,如果在位置参数传递时,为实际参数指定参数名,那么位置参数的顺序就可以改变,例如:

def student(name, age, classroom, tel, address="..."):
    pass

student(classroom=101, name="Jack", tel=66666666, age=20)

注意指定的参数名必须和位置参数的名字一样。

  • 默认参数尽量指向不变的对象!

下面是国内某上市互联网公司Python面试真题:

def func(a=[]):
    a.append("A")
    return a

print(func())
print(func())
print(func())

不要上机测试,仅凭代码,你能说出打印的结果吗?

很多同学可能会说,这还不简单,肯定是下面的结果啊:

['A']
['A']
['A']

真的是这样吗?错了!真正的结果是:

['A']
['A', 'A']
['A', 'A', 'A']

Why?为什么会这样?

因为Python函数体在被读入内存的时候,默认参数a指向的空列表对象就会被创建,并放在内存里了。因为默认参数a本身也是一个变量,保存了指向对象[]的地址。每次调用该函数,往a指向的列表里添加一个A。a没有变,始终保存的是指向列表的地址,变的是列表内的数据!我们可以测试一下:

def func(a=[]):
    print("函数内部a的地址为:%s" % id(a))
    a.append("A")
    return a

b = func()
print('此时b的值为:%s' % b)
print("函数外部b的地址为:%s" % id(b))
print("-------------")

c = func()
print('此时c的值为:%s' % c)
print("函数外部c的地址为:%s" % id(c))
print("-------------")

d = func()
print('此时d的值为:%s' % d)
print("函数外部d的地址为:%s" % id(d))

打印结果是:

函数内部a的地址为:39287880
此时b的值为:['A']
函数外部b的地址为:39287880
-------------
函数内部a的地址为:39287880
此时c的值为:['A', 'A']
函数外部c的地址为:39287880
-------------
函数内部a的地址为:39287880
此时d的值为:['A', 'A', 'A']
函数外部d的地址为:39287880

那么如何避免这个问题呢?

使用不可变的数据类型作为默认值!

def func(a=None):
    # 注意下面的if语句
    if a is None:
        a = []
    a.append("A")
    return a

print(func())
print(func())
print(func())

将默认参数a设置为一个类似None、数字或字符串等不可变对象。在函数内部将其转换为可变类型,例如空列表。这样无论调用多少次函数,运行结果都将是['A']。

三、动态参数

动态参数是指传入函数的参数个数是可变的,可以是1个、2个,甚至是任意个数,也可以是0个。当不需要传递参数时,你可以完全忽略动态参数,不传入任何值。

Python中有两种动态参数,分别是*args**kwargs,这里关键是星号的个数区别,而不是参数名的区别,实际上你可以使用*any**whatever等方式,但通常习惯使用*args**kwargs

注意:动态参数必须放在所有位置参数和默认参数之后!

def func(name, age, sex='male', *args, **kwargs):
    pass

1.*args

一个星号表示接收任意个参数。调用时,会将实际参数打包成一个元组传入形式参数。如果参数是个列表,会将整个列表当做一个参数传入。例如:

def func(*args):
    for arg in args:
        print(arg)

func('a', 'b', 'c')

li = [1, 2, 3]
func(li)

运行结果是:

a
b
c
[1, 2, 3]

通过循环args,我们可以获取传递的每个参数。但是对于列表li,我们本意是将其中的1、2、3作为单独的参数传递进函数,但实际情况是列表本身作为一个整体进行传递。那该怎么办呢?我们可以使用一个星号(*)!在调用函数并传递实参时,在列表前面添加一个星号,就可以达到我们的目的。这样,不仅适用于列表,还适用于任何序列类型的数据对象,例如字符串、元组,都可以通过这种方式将内部元素逐一作为参数传递给函数。而对于字典,会将所有的键(key)逐一传递进函数。

def func(*args):
    for arg in args:
        print(arg)

li = [1, 2, 3]
func(*li)

2.**kwargs

两个星表示接受键值对的动态参数,数量任意。调用的时候会将实际参数打包成字典。例如:

def func(**kwargs):
    for kwg in kwargs:
        print(kwg, kwargs[kwg])
        print(type(kwg))

func(k1='v1', k2=[0, 1, 2])

运行结果是:

k1 v1
<class 'str'>
k2 [0, 1, 2]
<class 'str'>

而如果我们这样传递一个字典dic呢?我们希望字典内的键值对能够像上面一样被逐一传入。

def func(**kwargs):
    for kwg in kwargs:
        print(kwg, kwargs[kwg])

dic = {
    'k1': 'v1',
    'k2': 'v2'
}

func(dic)

实际结果却是弹出错误,为什么?

Traceback (most recent call last):
  File "F:/Python/pycharm/201705/func.py", line 10, in <module>
    func(dic)
TypeError: func() takes 0 positional arguments but 1 was given

因为这时候,我们其实是把dic当做一个位置参数传递给了func函数。而func函数并不接收任何位置函数。那怎么办呢?使用两个星号

def func(**kwargs):
    for kwg in kwargs:
        print(kwg, kwargs[kwg])

dic = {
    'k1': 'v1',
    'k2': 'v2'
}

func(**dic)

有了前面一个星号的基础,这里我们应该很好理解了。两个星号能将字典内部的键值对逐一传入**kwargs

3.“万能”参数

*args**kwargs组合起来使用,理论上能接受任何形式和任意数量的参数,在很多代码中我们都能见到这种定义方式。需要注意的是,*args必须出现在**kwargs之前。

def func(*args, **kwargs):

    for arg in args:
        print(arg)

    for kwg in kwargs:
        print(kwg, kwargs[kwg])


lis = [1, 2, 3]
dic = {
    'k1': 'v1',
    'k2': 'v2'
}

func(*lis, **dic)

现在我们结合一下普通参数和万能参数,看看会有什么情况发生:

def func(a, b, c=1, *args, **kwargs):
    for arg in args:
        print(arg)

    for kwg in kwargs:
        print(kwg, kwargs[kwg])


lis = ['aaa', 'bbb', 'ccc']
dic = {
    'k1': 'v1',
    'k2': 'v2'
}

func(1, 2, *lis, **dic)

打印结果是:

bbb
ccc
k1 v1
k2 v2

列表lis中的第一个元素‘aaa’怎么没有打印出来?

我们改一下代码,打印一下参数c的结果就知道了:

def func(a, b, c=1, *args, **kwargs):
    print('c的值是:', c)
    for arg in args:
        print(arg)

    for kwg in kwargs:
        print(kwg, kwargs[kwg])


lis = ['aaa', 'bbb', 'ccc']
dic = {
    'k1': 'v1',
    'k2': 'v2'
}

func(1, 2, *lis, **dic)

打印结果为:

c的值是: aaa
bbb
ccc
k1 v1
k2 v2

原来,lis的第一个元素被传递给参数c了!这就是Python的参数传递规则之一。

4.关键字参数

对于*args**kwargs参数,函数的调用者可以传入任意不受限制的参数。比如:

def func(*args):
    pass

func("haha", 1, [], {})
func(1,2,3,4,5,6)

对于这样的参数传递方式,虽然灵活性很大,但是风险也很大,可控性差,必须自己对参数进行过滤和判定。例如下面我只想要姓名、年龄和性别,就要自己写代码检查:

def student(name, age, **kwargs):
    if 'sex' in kwargs:
        student_sex = kwargs['sex']

但是实际上,用户任然可以随意调用函数,比如student("jack", 18, xxx='male'),并且不会有任何错误发生。而我们实际期望的是类似student("jack", 18, sex='male')的调用。那么如何实现这种想法呢?

可以用关键字参数!关键字参数前面需要一个特殊分隔符*和位置参数及默认参数分隔开来,*后面的参数被视为关键字参数。在函数调用时,关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错。不同于默认参数,关键字参数必须传递,但是关键字参数也可以有缺省值,这时就可以不传递了,从而简化调用。

我们把前面的函数改写一下:

def student(name, age, *, sex):
    pass

student(name="jack", age=18, sex='male')

注意函数的定义体首行。

如果函数定义中已经有了一个*args参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了。

def student(name, age=10, *args, sex, classroom, **kwargs):
    pass

student(name="jack", age=18, sex='male', classroom="202", k1="v1")

Python的函数参数种类多样、形态多变,既可以实现简单的调用,又可以传入非常复杂的参数。需要我们多下功夫,多写实际代码,多做测试,逐步理清并熟练地使用参数。

标签: python, python下载, Python教程, Python技术, Python学习, Python学习教程, Python语言, Python开发, Python入门教程, Python进阶教程, Python高级教程, Python面试题, Python笔试题, Python编程思想