您的位置:首页 > 编程语言 > Python开发

深入讲解Python函数中参数的使用及默认参数的陷阱

2016-03-13 00:00 981 查看
C++里函数可以设置缺省参数,Java不可以,只能通过重载的方式来实现,python里也可以设置默认参数,最大的好处就是降低函数难度,函数的定义只有一个,并且python是动态语言,在同一名称空间里不能有想多名称的函数,如果出现了,那么后出现的会覆盖前面的函数。

def power(x, n=2):
  s = 1
  while n > 0:
    n = n - 1
    s = s * x
  return s


看看结果:

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


注意: 必选参数在前,默认参数在后,否则Python的解释器会报错。

建议:*当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数。

默认参数也有坑,看看下面的代码,先定义一个list,添加一个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']


这里需要解释一下,Python函数在定义的时候,默认参数L的值就被计算出来了,即[]。此时L指向[]。所以如果L中的内容改变了,下次调用引用的内容也就不再是[]了。所以要牢记一点定义默认参数必须指向不可变对象!。

可变参数

第一种方法,传入的参数为一个list或者tuple。

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


第二种方式,直接传入多个参数,函数内部会自动用一个tuple接收。

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


调用方式:

>>> calc(1, 2)
5
>>> calc()
0


这个时候如果还想把一个list或者tuple里的数据传进去,可以这样:

>>> nums = [1, 2, 3]
>>> calc(*nums)
14


关键字参数

关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。

def person(name, age, **kw):
  print 'name:', name, 'age:', age, 'other:', 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'}


参数组合

在Python中定义函数,可以用必选参数、默认参数、可变参数和关键字参数,这4种参数都可以一起使用,或者只用其中某些,但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数和关键字参数。

递归函数

基本的也没什么可讲的,和Java/C++里一样,就是调用本身的一种。这里重点介绍一下尾递归优化。事实上尾递归和循环效果是一样的,很显然的一个优点那就是可以防止递归调用栈溢出。

定义:在函数返回的时候调用自身,并且,return语句不能包含表达式。编译器或者解释器可以对其做优化,无论调用多少次,只占用一个栈帧,不会出现溢出的情况。

举个简单的例子,以阶乘函数为例:

def fact(n):
  if n==1:
    return 1
  return n * fact(n - 1)


如果传入的n很大,就可能会溢出,这是由于return n * fact(n - 1)引入了乘法表达式,就不是尾递归了。把代码改一下:

def fact(n):
  return fact_iter(n, 1)

def fact_iter(num, product):
  if num == 1:
    return product
  return fact_iter(num - 1, num * product)


默认参数陷阱

Python的函数定义提供了默认参数这个选择,使得函数的定义和使用更加的灵活,但是也会带来一些坑,例如之前的一个例子:

函数定义:

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']


很明显这个与函数的定义初衷不符,用一句话解释就是:

Default values are computed once, then re-used.

为了深入研究这个问题,我们来看看另一个例子:

# coding=utf-8

def a():
  print "a executed"
  return []

def b(x=a()):
  print "id(x):", id(x)
  x.append(5)
  print "x:", x

for i in range(2):
  print "不带参数调用,使用默认参数"
  b()
  print b.__defaults__
  print "id(b.__defaults__[0]):", id(b.__defaults__[0])

for i in range(2):
  print "带参数调用,传入一个list"
  b(list())
  print b.__defaults__
  print "id(b.__defaults__[0]):", id(b.__defaults__[0])


NOTE:稍微解释一下,所有默认值都存储在函数对象的__defaults__属性中,这是一个列表,每一个元素均为一个默认参数值。

来看看输出结果:

a executed
不带参数调用,使用默认参数
id(x): 140038854650552
x: [5]
([5],)
id(b.__defaults__[0]): 140038854650552
不带参数调用,使用默认参数
id(x): 140038854650552
x: [5, 5]
([5, 5],)
id(b.__defaults__[0]): 140038854650552
带参数调用,传入一个list
id(x): 140038854732400
x: [5]
([5, 5],)
id(b.__defaults__[0]): 140038854650552
带参数调用,传入一个list
id(x): 140038854732472
x: [5]
([5, 5],)
id(b.__defaults__[0]): 140038854650552


简单分析一下输出结果:

第1行

在定义函数b(),即执行def语句,代码第7行def b(x=a()):的时候,这句话使用了默认参数,所以在定义的时候会计算默认参数x的值,这个时候会调用a(),所以打印出了a executed。

第2~6行

第一次执行循环,代码第14行调用b()没有传递参数,使用默认参数,此时x=[],所以调用一次之后

print b.__defaults__


输出结果为

([5],)


第7~11行

第二次循环,代码第14行调用b()没有传递参数,使用默认参数。

注意:默认参数只会计算一次,也就是说那个内存区域就固定了,但是这个地址所指向的是一个list,内容可以改变,此时由于上一次调用x: [5],所以

print b.__defaults__


输出结果为

([5, 5],)


第12~16行

第二个循环语句,第一次循环,代码第20行传入一个空的list,所以不使用默认参数,此时x=[],所以

print b.__defaults__


输出结果为

([5],)


第18~21行

第二个循环语句,第二次循环,代码第20行传入一个空的list,所以也不使用默认参数,此时仍然是x=[],所以

print b.__defaults__


输出结果依然为

([5],)


函数也是对象,因此定义的时候就被执行,默认参数是函数的属性,它的值可能会随着函数被调用而改变。其他对象不都是如此吗?

牢记: 默认参数必须指向不变对象!代码改一下如下:

# coding=utf-8

def a():
  print "a executed"
  return None

def b(x=a()):
  print "id(x):", id(x)
  if x is None:
    x = []
  x.append(5)
  print "x:", x

for i in range(2):
  print "不带参数调用,使用默认参数"
  b()
  print b.__defaults__
  print "id(b.__defaults__[0]):", id(b.__defaults__[0])

for i in range(2):
  print "带参数调用,传入一个list"
  b(list())
  print b.__defaults__
  print "id(b.__defaults__[0]):", id(b.__defaults__[0])


此时的输出结果看看是什么:

a executed
不带参数调用,使用默认参数
id(x): 9568656
x: [5]
(None,)
id(b.__defaults__[0]): 9568656
不带参数调用,使用默认参数
id(x): 9568656
x: [5]
(None,)
id(b.__defaults__[0]): 9568656
带参数调用,传入一个list
id(x): 140725126699632
x: [5]
(None,)
id(b.__defaults__[0]): 9568656
带参数调用,传入一个list
id(x): 140725126699704
x: [5]
(None,)
id(b.__defaults__[0]): 9568656


您可能感兴趣的文章:

Python函数中*args和**kwargs来传递变长参数的用法
Python可变参数函数用法实例
Python中函数的参数传递与可变长参数介绍
Python中type的构造函数参数含义说明
Python中函数的参数定义和可变参数用法实例分析
Python函数可变参数定义及其参数传递方式实例详解
理解Python中函数的参数
深入讨论Python函数的参数的默认值所引发的问题的原因
python通过装饰器检查函数参数数据类型的方法
Python自定义函数的创建、调用和函数的参数详解
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: