好冷的Python~默认参数、可变对象和不可变对象

原文链接:http://www.juzicode.com/archives/3402

昨天有个小伙伴给桔子菌留言发来一段代码,疑惑为什么没有得到他预期的结果,桔子菌把代码简化之后是这个样子的:

def foo(x,l=[]):
    l.append(x)
    return l

a = foo(1)
b = foo(2)
print('a:',a)
print('b:',b)

==========运行结果:
a: [1, 2]
b: [1, 2]

这段代码的本意是想两次调用foo()函数之后得到不同的结果,看代码应该会返回2个不同的列表,但是实际上得到的结果却是一样的。难道是因为Python中的小整数设计导致的,Python中-5~256的整数所指向的数值保存在内存中,用不同的变量赋值为小整数,如果数值一样,实际是指向同一个整数实例。但是上面这个函数返回的类型已经是列表而不是整数数值了,为了排除这个疑惑我们把数值加大到256以上看看:

#微信公众号:桔子code ; #juzicode.com 
def foo(x,l=[]):
    l.append(x)
    return l

a = foo(1000)#加大到1000
b = foo(2000)
print('a:',a)
print('b:',b)

==========运行结果:
a: [1000, 2000]
b: [1000, 2000]

结果还是一样的,说明并不是小整数问题。既然两次调用结果一样,我们将foo()返回的值用id()函数看看变量所指向的地址是不是同一个:

#微信公众号:桔子code ; #juzicode.com 
def foo(x,l=[]):
    l.append(x)
    return l

a = foo(1000)
b = foo(2000)
print('a:',a)
print('b:',b)
print('id(a):',id(a))
print('id(b):',id(b))

==========运行结果:
a: [1000, 2000]
b: [1000, 2000]
id(a): 2200119105792
id(b): 2200119105792

果然如此,既然二者的id是一样的,说明第二次调用确实影响了第一次调用返回变量的内容,这就能解释调用函数两次后二者内容为什么是一样的了。

既然两次调用都指向了同一个对象,在函数体外面并没有定义一个新的对象,极有可能用到了函数内部定义的同一个对象,我们在函数内部用id()显示下变量l的地址:

#微信公众号:桔子code ; #juzicode.com 
def foo(x,l=[]):
    l.append(x)
    print('id(l):',id(l))
    return l

a = foo(1000)
b = foo(2000)
print('a:',a)
print('b:',b)
print('id(a):',id(a))
print('id(b):',id(b))

==========运行结果:
id(l): 2200119098432
id(l): 2200119098432
a: [1000, 2000]
b: [1000, 2000]
id(a): 2200119098432
id(b): 2200119098432

从运行结果看确实如此,foo()函数内部变量l和两次调用foo()函数后得到的变量a和b都是指向了同一个地址。

在Python中一切皆对象,当调用foo()函数时,没有传递默认参数l的值,这个时候foo()函数内部要操作变量l如果没有一个合适的对象,就会变成“无源之水”。实际上解释器在遇到def语句时就会对函数的默认参数自动构造对象,而且只构造这么一次,所以多次调用foo()函数没有给默认参数传值时,在函数内部实际使用的对象都是同一个。

我们接下来变化下使用场景,试下如果传递一个参数给l,看结果会如何:

#微信公众号:桔子code ; #juzicode.com 
def foo(x,l=[]):
    l.append(x)
    print('id(l):',id(l))
    return l

la = [1]
lb = [2]
print('id(la):',id(la))
print('id(lb):',id(lb))
a = foo(1000,l=la)#传值给默认参数
b = foo(2000,l=lb)
print('a:',a)
print('b:',b)
print('id(a):',id(a))
print('id(b):',id(b))
print('la:',la) #打印原始变量la的值
print('lb:',lb)

==========运行结果:
id(la): 2200118797568
id(lb): 2200119113472
id(l): 2200118797568
id(l): 2200119113472
a: [1, 1000]
b: [2, 2000]
id(a): 2200118797568
id(b): 2200119113472
la: [1, 1000]   #原始变量la的值发生了变化
lb: [2, 2000]   #原始变量lb的值也发送了变化

从上面的代码可以看到对默认参数传值时,这个时候foo()函数没有再使用函数默认构造的实例,而是使用传入的la和lb作为操作对象,返回的结果a和b也是分别指向了la和lb,所以经过函数foo()后,返回结果不再相互影响,但是注意原始变量la和lb的内容也发生了改变。

到这里已经弄清楚了用list类型的变量作为默认参数时,如果不传值调用函数,因为用到了为函数构造默认参数生成的对象,所以多次调用会相互影响。如果要用list型的变量作为默认参数,必须传入不同的参数才能保证函数的2次调用不会相互影响。

从前面的例子中,我们看到list型的变量传入到函数内部被修改了,在函数外部是可见的。接下来看下如果默认参数是int类型会是什么情况:

#微信公众号:桔子code ; #juzicode.com 
def foo(x,y=0):  #默认参数用整型变量
    y = x+y
    print('id(y):',id(y))
    return y

ya = 1
yb = 2
print('id(ya):',id(ya))
print('id(yb):',id(yb))
a = foo(1000,y=ya)
b = foo(2000,y=yb)
print('a:',a)
print('b:',b)
print('id(a):',id(a))
print('id(b):',id(b))

==========运行结果:
id(ya): 140709146793632  -----原始变量ya的地址
id(yb): 140709146793664
id(y): 2200118758320  -----函数内部变量y的地址,不同于原始变量ya的地址
id(y): 2200119136880
a: 1001
b: 2002
id(a): 2200118758320  -----函数返回值变量a的地址,等于函数内部临时变量y的地址
id(b): 2200119136880
ya: 1                 -----原始变量的值没有被改变
yb: 2

从上面的运行结果看,如果是int型变量,在函数内部会构造一个新的int对象,在这个对象上操作后返回给变量a或者b,a或b指向的是函数内部新构造的int对象的地址,原来的ya和yb所指向的内容并没有发生变化。

接下来回到再往前一步的实验,如果不对默认参数y进行传值会发生什么:

#微信公众号:桔子code ; #juzicode.com  
def foo(x,y=0): #默认参数用整型变量
    y = x+y
    print('id(y):',id(y))
    return y

ya = 1
yb = 2
print('id(ya):',id(ya))
print('id(yb):',id(yb))
a = foo(1000 ) #不对默认参数进行传值
b = foo(2000 )
print('a:',a)
print('b:',b)
print('id(a):',id(a))
print('id(b):',id(b))
print('ya:',ya)
print('yb:',yb)

==========运行结果:
id(ya): 140709146793632
id(yb): 140709146793664
id(y): 2200119136528
id(y): 2200119136848
a: 1000
b: 2000
id(a): 2200119136528
id(b): 2200119136848
ya: 1
yb: 2

从这里可以看到如果用的是int型变量,不对默认参数进行传值,得到的还是不一样的结果,两次调用函数并不会相互影响。

这里可以得出结论,如果默认参数用的是int类型的变量,不管传值与否,多次的函数调用不会相互影响。

接下来看看float类型的变量,默认参数不传值:

#微信公众号:桔子code ; #juzicode.com 
def foo(x,y=0.0): #默认参数用float型变量
    y = x+y
    print('id(y):',id(y))
    return y

ya = 1.0
yb = 2.0
print('id(ya):',id(ya))
print('id(yb):',id(yb))
a = foo(1000.0 ) #不对默认参数进行传值
b = foo(2000.0 )
print('a:',a)
print('b:',b)
print('id(a):',id(a))
print('id(b):',id(b))
print('ya:',ya)
print('yb:',yb)

==========运行结果:
id(ya): 2200119137392
id(yb): 2200118313040
id(y): 2200119137744
id(y): 2200118759184
a: 1000.0
b: 2000.0
id(a): 2200119137744
id(b): 2200118759184
ya: 1.0
yb: 2.0

float类型的变量,默认参数传值:

#微信公众号:桔子code ; #juzicode.com 
def foo(x,y=0.0): #默认参数用float型变量
    y = x+y
    print('id(y):',id(y))
    return y

ya = 1.0
yb = 2.0
print('id(ya):',id(ya))
print('id(yb):',id(yb))
a = foo(1000.0,y=ya) #对默认参数进行传值
b = foo(2000.0,y=yb)
print('a:',a)
print('b:',b)
print('id(a):',id(a))
print('id(b):',id(b))
print('ya:',ya)
print('yb:',yb)

==========运行结果:
id(ya): 2200119137392
id(yb): 2200119136304
id(y): 2200118758032
id(y): 2200118313040
a: 1001.0
b: 2002.0
id(a): 2200118758032
id(b): 2200118313040
ya: 1.0
yb: 2.0

再看下dict类型的变量,默认参数不传值:

#微信公众号:桔子code ; #juzicode.com 
def foo(x,l={}):#默认参数用dict型变量
    l['x'] = x
    print('id(l):',id(l))
    return l

la = {'a':1}
lb = {'b':2}
print('id(la):',id(la))
print('id(lb):',id(lb))
a = foo(1000)#默认参数不传值
b = foo(2000)
print('a:',a)
print('b:',b)
print('id(a):',id(a))
print('id(b):',id(b))
print('la:',la) #打印原始变量la的值
print('lb:',lb)  
==========运行结果:
id(la): 2200119271232
id(lb): 2200119271360
id(l): 2200119271296
id(l): 2200119271296
a: {'x': 2000}
b: {'x': 2000}
id(a): 2200119271296
id(b): 2200119271296
la: {'a': 1}
lb: {'b': 2}

dict类型的变量,默认参数传值:

#微信公众号:桔子code ; #juzicode.com 
def foo(x,l={}):#默认参数用dict型变量
    l['x'] = x
    print('id(l):',id(l))
    return l

la = {'a':1}
lb = {'b':2}
print('id(la):',id(la))
print('id(lb):',id(lb))
a = foo(1000,l=la)#默认参数传值
b = foo(2000,l=lb)
print('a:',a)
print('b:',b)
print('id(a):',id(a))
print('id(b):',id(b))
print('la:',la) #打印原始变量la的值
print('lb:',lb)  

==========运行结果:
id(la): 2200119268800
id(lb): 2200119268864
id(l): 2200119268800
id(l): 2200119268864
a: {'a': 1, 'x': 1000}
b: {'b': 2, 'x': 2000}
id(a): 2200119268800
id(b): 2200119268864
la: {'a': 1, 'x': 1000}
lb: {'b': 2, 'x': 2000}

从上面的实验可以看到,dict和list表现一致,而float和int表现一样。实际上在Python中,函数的参数传递要注意可变对象和不可变对象的差异,Python中的list,dict,set都是可变对象,tuple,str,数值类型的int,float等都是不可变对象,可变对象在函数内部的修改在函数外部是可见的,而不可变对象如果在函数内部有修改,在函数外部是不可见的。

其实前面的问题纠缠着2个事情,一个是默认参数的构造问题,一个是可变对象的问题,接下来我们去掉默认参数这个”研究变量”,只看可变对象和不可变对象的问题,这样可以看的更清楚些。下面的例子中定义了2个函数,函数的入参一个是使用int型,一个使用list型,在函数内部都进行了修改,看下他们在函数内外部的表现。

#微信公众号:桔子code ; #juzicode.com 
#int型变量
def add(x):
    print('id(x)-1:',id(x))
    x = x + 5
    print('id(x)-2:',id(x))
    return x

a = 2
print('id(a)-1:',id(a))
a = add(a)
print('a:',a)
print('id(a)-2:',id(a))

==========运行结果:
id(a)-1: 140709146793664
id(x)-1: 140709146793664  --传入函数内仍然一样
id(x)-2: 140709146793824  --修改后已发生改变
a: 7
id(a)-2: 140709146793824
#微信公众号:桔子code ; #juzicode.com 
#list型变量
def apd(x):
    print('id(x)-1:',id(x))
    x.append(5)
    print('id(x)-2:',id(x))
    return x

a = [2]
print('id(a)-1:',id(a))
a = apd(a)
print('a:',a)
print('id(a)-2:',id(a))

==========运行结果:
id(a)-1: 1895168790912 
id(x)-1: 1895168790912 
id(x)-2: 1895168790912 --函数内修改前后地址一样
a: [2, 5]
id(a)-2: 1895168790912 --函数内外地址一样

从这个例子可以看到list型的参数在函数内外、函数内修改前后都是同一个对象;而int型变量在传入函数时还是同一个对象,但是当函数内部修改这个变量后就不是同一个了。

小结:

1、如果用可变对象进行传值,特别是默认参数类型,需要当心多次函数调用后的结果。

2、可变对象的传值可以和C语言中的指针、C++中的指针、引用类比,在函数体内部对入参的修改在函数体外部是可见的。

3、函数也是对象,函数的默认参数也是对象,默认参数在函数定义时构造一次,且只构造一次。

4、前面提到的问题实际包含了2个事情,一个是函数默认参数的问题,一个是可变对象的问题,将二者分开研究更容易看到问题的本质。

最后抛个小问题,下面的这段代码中2次执行foo()函数得到的变量a和b,它们的地址是同一个,看起来好像和int型变量是不可变对象冲突了,这是为什么?

#vx:桔子code ; #juzicode.com 
def foo(x=0):
    x = 5**x
    return x

a=foo()
b=foo()
print('a:',a)
print('b:',b)
print('id(a):',id(a))
print('id(b):',id(b))

==========运行结果:
a: 1
b: 1
id(a): 140709146793632
id(b): 140709146793632

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注