接着上篇的内容,本篇文章为大家介绍一下函数参数传递的一些问题和参数类型注解功能。
1. 值传递还是引用传递
熟悉C/C++的同学一定对这两个词语非常清楚。值传递表示参数传递时会拷贝一份实参副本给函数执行,而引用传递则传递的是对某个对象的内存地址。这里对Python中的引用多做一些解释。Python中每一个对象(一切皆对象)都有一个唯一的值来表示它的身份,这个值可以用函数获取到。在CPython实现(CPython是官方标准采用C语言实现的Python)中,这个身份是由对象的内存地址来表示。在Python中,引用表示用一个标识符(其实就是变量名)来指向某个对象。操作符表示引用操作,而非传统意义的赋值运算符。所以当你写了时,Python做了这样的操作:
在内存某个位置存储了整数,是实实在在的内存空间;
令标识符指向上面的内存地址。
为了证明这一点,来看下面的说明:可以看到,,,的值均一样,即三者均是同一个对象(数字)的引用。为什么和一样呢?因为Python为了降低内存的申请频率,对于相同的字面量值,只会在内存中存储一次:列表元素也是对对象的引用:其他类型的对象,则会多次分配内存:你可以用关键字判断两个标识符是否指向了同一个对象(即是否一样):
a=1
b=1
print(aisb)# True
a= [1,2,3]
b= [1,2,3]
print(aisb)# False如果我改变的值,会变吗?
a=1
c=a
print(cisa)# True
a=2
print(c)# 1
print(cisa)# False总结起来:
字面量常量在内存中永远只有一份存储,不论在哪里创建了它;
普通不可变对象每次创建都会在内存中存储一份,即使它们的内容完全一致,但是对象中的内容不可被修改;
可变对象每次创建都会在内存中存储一份,但是对象中的内容可以被修改;
回到正题,那Python中函数参数传递究竟是值传递还是引用传递呢?
如果是值传递,那么变量在函数局部会得到一份拷贝,修改拷贝不会影响到函数外的值:
deffunc1(a):
a[] =1
a= [forxinrange(3)]
print(a)# [0, 0, 0]
func1(a)
print(a)# [1, 0, 0]看到了吗?变量被修改了,所以值传递的说法是不准确的
如果是引用传递,那么变量在函数局部做修改后会影响外部的值,因为传递的是实际对象的地址,经过地址索引后读取到的是原始值,做的修改也是对原始值进行修改。在Python中是这样的吗?其实上面的和已经给出答案了:
如果你理解了我前面说的Python引用,你应该能理解上面的结果
有人说,Python中存在可变对象和不可变对象,对可变对象采用的引用传递,对不可变对象采用值传递,这个说法准确吗?
其实这个说法和上一个存在的问题类似,都是没有真正理解Python引用和传统的引用是有区别的。来看一个例子解释这个说法的不准确之处,这里参数传递一个可变对象列表:
总结起来,Python中的参数传递方式是对象引用的方式,由于变量(标识符)本身并不是对象,而是对指向对象的指针的绑定,所以在参数传递时,实际上是用了一个新的标识符绑定到调用参数的标识符所绑定的对象上,就像前面一样。对新的标识符做修改(可变对象才能修改),自然可以反映到调用者,因为它们绑定的是同一个对象;令新标识符引用一个新的对象(等号操作符),自然不会反映到调用者,因为它是一个新的标识符。(好像人名和人的关系一样,Lucy有个外号叫Jane(),后来它把这个外号给了另一个人做名字()。如果生病了,会生病吗?)
2. 默认参数陷阱前一篇文章(→传送门)中讲到了Python参数默认值,这里来看一下默认值可能带来的陷阱:
deffunc3(a=[]):
a.append(1)
returna
print(func3())# [1]
print(func3())# [1, 1]
???期望的输出应当是每次调用都输出一个,这里怎么了?
这是因为,Python的参数默认值在函数定义时就已经生成并存储在内存中了。如果参数是可变对象,那么每次采用默认值调用函数时都会修改内存中已经存在的值(而不是重新生成一个)。
所以当你需要为某个可变对象参数定义一个空值作为默认值,应当这样写:
deffunc3(a=None):
ifaisNone:
a= []
a.append(1)
returna
print(func3())# [1]
print(func3())# [1]
3. 类型注解
在函数定义时,你可以为每个参数及返回值做一些辅助性的注解(像注释一样),来更好的说明参数的类型:
deffunc4(
a:int,
b:'这是一个字典',
c:True=None
)->list:
print('a = {}, b = {}, c = {}'
.format(a,b,c))
returnc
func4(1, {})
# a = 1, b = {}, c = None
你可以通过标准库中的inpsect模块来获取注解内容:
frominspectimportsignature
sig=signature(func4)
print(sig)
# (a:int, b:'这是一个字典', c:True=None) -> list
print(sig.parameters['b'])
# b:'这是一个字典'
print(
sig.parameters['c'].annotation
)
# True
print(sig.return_annotation)
#
在CPython中,类型注解在执行时会被直接忽略掉,不会做任何类型检查。如果你确实确实想要在Python中做类型检查,可以利用装饰器自己来完成:
definspector(func):
defwrapper(*args,**kwargs):
# Check types here
# e.g.:
# assert isinstance(kwargs.get('a'), int)
returnfunc(*args,**kwargs)
returnwrapper
@inspector
deffunc4(a:int,b:'这是一个字典',c:True=None)->list:
pass
但是在Python中做类型检查是一个很差的想法,这会耗掉你很多的精力并且不会有任何有用的效果。Python是鸭子类型语言,类型检查违背了它动态性原则,仅仅是在很特别的情况下才会有一定的作用。在函数中通常需要的是具有所需接口的对象,而非某种类型的对象(这两句话有区别吗?)。所以,在函数中你需要的是检视某个参数是否具有某种功能而不是检视某个参数是否是某种类型。如果你非常非常希望使用类型检查,你应当使用Java语言。
鸭子类型:当一只鸟(或者随便一个什么东西),它走起来像鸭子,游起来像鸭子,叫起来也像鸭子,那Python认为它就是鸭子,不管它本质上究竟是不是鸭子。进而就可以把它当鸭子来烤或者其他用于真正鸭子的操作。
在Python3.5中,给出了详细的标准类型注解协议,有兴趣可以在PEP 484中看到完整定义。
(文章存在显示问题可以复制链接
https://github.com/houluy/wechatpubliccode/blob/master/函数参数有多少(下).md
到浏览器查看)
领取专属 10元无门槛券
私享最新 技术干货