陷阱!python参数默认值

在stackoverflow上看到这样一个程序:

class demo_list:
    def __init__(self, l=[]):
        self.l = l
    def add(self, ele):
        self.l.append(ele)
def appender(ele):
    obj = demo_list()
    obj.add(ele)
    print obj.l
if __name__ == "__main__":
    for i in range(5):
        appender(i)

输出结果是

[0] [0, 1] [0, 1, 2] [0, 1, 2, 3] [0, 1, 2, 3, 4]

有点奇怪,难道输出不应该是像下面这样吗?

[0] [1] [2] [3] [4]

其实想要得到上面的输出,只需要将obj = intlist()替换为obj = intlist(l=[])。

默认参数工作机制

上面怪异的输出简单来说是因为:

Default values are computed once, then re-used.

因此每次调用init(),返回的是同一个list。为了验证这一点,下面在init函数中添加一条语句,如下:

def __init__(self, l=[]):
    print id(l),
    self.l = l

输出结果为:

4346933688 [0] 4346933688 [0, 1] 4346933688 [0, 1, 2] 4346933688 [0, 1, 2, 3] 4346933688 [0, 1, 2, 3, 4]

可以清晰看出每次调用init函数时,默认参数l都是同一个对象,其id为4346933688。

关于默认参数,文档中是这样说的:

Default parameter values are evaluated when the function definition is executed. This means that the expression is evaluated once, when the function is defined, and that the same “pre-computed” value is used for each call.

为了能够更好地理解文档内容,再来看一个例子:

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 "-" * 15, "Call b()", "-" * 15
    b()
    print b.__defaults__
    print "id(b.__defaults__[0]): ", id(b.__defaults__[0])
for i in range(2):
    print "-" * 15, "Call b(list())", "-" * 15
    b(list())
    print b.__defaults__
    print "id(b.__defaults__[0]): ", id(b.__defaults__[0])

注意,当python执行def语句时,它会根据编译好的函数体字节码和命名空间等信息新建一个函数对象,并且会计算默认参数的值。函数的所有构成要素均可通过它的属性来访问,比如可以用funcname属性来查看函数的名称。所有默认参数值则存储在函数对象的_defaults属性中,它的值为一个列表,列表中每一个元素均为一个默认参数的值。

好了,你应该已经知道上面程序的输出内容了吧,一个可能的输出如下(id值可能为不同):

a executed ————— Call b() ————— id(x): 4316528512 x: [5] ([5],) id(b.defaults[0]): 4316528512 ————— Call b() ————— id(x): 4316528512 x: [5, 5] ([5, 5],) id(b.defaults[0]): 4316528512 ————— Call b(list()) ————— id(x): 4316684872 x: [5] ([5, 5],) id(b.defaults[0]): 4316528512 ————— Call b(list()) ————— id(x): 4316684944 x: [5] ([5, 5],) id(b.defaults[0]): 4316528512

我们看到,在定义函数b(也就是执行def语句)时,已经计算出默认参数x的值,也就是执行了a函数,因此才会打印出a executed。之后,对b进行了4次调用,下面简单分析一下:

  • 第一次不提供默认参数x的值进行调用,此时使用函数b定义时计算出来的值作为x的值。所以id(x)和id(b.defaults[0])相等,x追加数字后,函数属性中的默认参数值也变为[5];
  • 第二次仍然没有提供参数值,x的值为经过第一次调用后的默认参数值[5],然后对x进行追加,同时也对函数属性中的默认参数值追加;
  • 传递参数list()来调用b,此时新建一个列表作为x的值,所以id(x)不同于函数属性中默认参数的id值,追加5后x的值为[5];
  • 再一次传递参数list()来调用b,仍然是新建列表作为x的值。

如果上面的内容你已经搞明白了,那么你可能会觉得默认参数值的这种设计是python的设计缺陷,毕竟这也太不符合我们对默认参数的认知了。然而事实可能并非如此,更可能是因为:

Functions in Python are first-class objects, and not only a piece of code.

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

可变对象作为参数默认值?

参数的默认值为可变对象时,多次调用将返回同一个可变对象,更改对象值可能会造成意外结果。参数的默认值为不可变对象时,虽然多次调用返回同一个对象,但更改对象值并不会造成意外结果。

因此,在代码中我们应该避免将参数的默认值设为可变对象,上面例子中的初始化函数可以更改如下:

def __init__(self, l=None):
    if not l:
        self.l = []
    else:
        self.l = l

在这里将None用作占位符来控制参数l的默认值。不过,有时候参数值可能是任意对象(包括None),这时候就不能将None作为占位符。你可以定义一个object对象作为占位符,如下面例子:

sentinel = object()
def func(var=sentinel):
    if var is sentinel:
        pass
    else:
        print var

虽然应该避免默认参数值为可变对象,不过有时候使用可变对象作为默认值会收到不错的效果。比如我们可以用可变对象作为参数默认值来统计函数调用次数,下面例子中使用collections.Counter()作为参数的默认值来统计斐波那契数列中每一个值计算的次数。

def fib_direct(n, count=collections.Counter()):
    assert n > 0, 'invalid n'
    count[n] += 1
    if n < 3:
        return n
    else:
        return fib_direct(n - 1) + fib_direct(n - 2)
print fib_direct(10)
print fib_direct.__defaults__[0]

运行结果如下:

89 Counter({2: 34, 1: 21, 3: 21, 4: 13, 5: 8, 6: 5, 7: 3, 8: 2, 9: 1, 10: 1})

我们还可以用默认参数来做简单的缓存,仍然以斐波那契数列作为例子,如下:

def fib_direct(n, count=collections.Counter(), cache={}):
    assert n > 0, 'invalid n'
    count[n] += 1
    if n in cache:
        return cache[n]
    if n < 3:
        value = n
    else:
        value = fib_direct(n - 1) + fib_direct(n - 2)
    cache[n] = value
    return value
print fib_direct(10)
print fib_direct.__defaults__[0]

结果为:

89 Counter({2: 2, 3: 2, 4: 2, 5: 2, 6: 2, 7: 2, 8: 2, 1: 1, 9: 1, 10: 1})

这样就快了太多了,fib_direct(n)调用次数为o(n),这里也可以用装饰器来实现计数和缓存功能

原文链接:http://selfboot.cn/2014/10/27/python_default_values/

原文发布于微信公众号 - CDA数据分析师(cdacdacda)

原文发表时间:2016-10-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏java学习

工程师笔试题2(答案解析)

一、单项选择题 1.二进制数 11101 转化为十进制数是( )。 A.23 B.17 C.26 D.29 2.以下可以对对象加互斥锁的关键字是( )。 ...

3236
来自专栏Golang语言社区

[基础篇]Go语言变量

变量来源于数学,是计算机语言中能储存计算结果或能表示值抽象概念。变量可以通过变量名访问。 Go 语言变量名由字母、数字、下划线组成,其中首个字母不能为数字。 声...

3727
来自专栏Python自动化测试

Python的元组学习(五)

本节来学习python的元组,在python语言中,元组的关键字是tuple同时元组是不可变的,列表与字典是可变的,元组的定义是一个(),下面通过代码...

1084
来自专栏racaljk

关于C++函数返回局部对象的详细分析

以前一直挺好奇的,C++是怎么在函数内返回一个局部对象的。因为按照我之前的想法,函数返回一个基本类型的值是通过存放到ecx实现的(关于浮点不了解),但是局部对象...

3441
来自专栏GreenLeaves

JS框架设计之对象扩展一种子模块

对象扩展 说完了,对象的创建(框架的命名空间的创建)以及如何解决多库之间的命名空间冲突问题之后,接下来,就是要扩展我们的对象,来对框架进行扩展,我们需要一种新功...

2239
来自专栏从流域到海域

《笨办法学Python》 第32课手记

《笨办法学Python》 第32课手记 本节课讲for循环和list,list里类似于c中的数组,但有区别很大。C语言中的数组是数据类型相同的值的集合,list...

2029
来自专栏抠抠空间

集合 (set) 的增删改查及 copy()方法

简介: 集合是无序的,不重复的数据集合,它里面的元素是可哈希的(不可变类型),但是集合本身是不可哈希(所以集合做不了字典的键)的。以下是集合最重要的两点: 1、...

29611
来自专栏张首富-小白的成长历程

每日一题--统计字符串出现的次数

使用awk统计出来指定字符串中重复出现的字符并重复出现了几次,现在只考虑有数字和字母,先区分大小写 eg: aaabbc------> a 重复出现3次,b重复...

2234
来自专栏lgp20151222

Class.forName()用法详解

主要功能 Class.forName(xxx.xx.xx)返回的是一个类 Class.forName(xxx.xx.xx)的作用是要求JVM查找并加载指定的类,...

1381
来自专栏大闲人柴毛毛

Java基础深入解析——类与对象

成员变量与局部变量的区别 1.成员变量定义在类中,整个类中都能够访问。   局部变量定义在局部代码块中,只能在局部代码块中访问。 2.成员变量存在于堆内存中,有...

3297

扫码关注云+社区