Python——可变类型与不可变类型(即为什么函数默认参数要用元组而非列表)
Python 的内建标准类型有一种分类标准是分为可变类型与不可变类型:
- 可变类型:列表、字典
- 不可变类型:数字、字符串、元组
因为变量保存的实际都是对象的引用,所以在给一个不可变类型(比如 int)的变量 a 赋新值的时候,你实际上是在内存中新建了一个对象,并将 a 指向这个新的对象,然后将原对象的引用计数 –1.
比如下面的示例:
>>> id(1),id(2)
(507098784, 507098816)
>>> a = 1
>>> id(a)
507098784
>>> a = 2
>>> id(2)
507098816
这里的第一步显示 1 和 2 的 id 就已经在内存中保存了这两个对象,随后给 a 赋不同的值,也只是在改变它的引用而已。
但列表就不同:
>>> b = [0]
>>> id(b)
171805000
>>> b[0] = 1
>>> id(b)
171805000
看起来虽然 b 的 id 没有变,其值却变化了。真的是这样吗?
>>> b = [0]
>>> id(b[0])
507098752
>>> b[0] = 1
>>> id(b[0])
507098784
其实只是一个嵌套引用啦,容器类型管理着对复数个元素的引用,这些元素可以是可变类型也可以是不可变类型,对于不可变类型的元素,当你改变他的值的时候,所发生的事情和最前面举得例子是一样的。另外注意不要简单将容器类型与可变类型划等号,因为还有一个“元组”特例。
变量类型是否可变有一个很重要的应用之处就是作为定义函数的默认参数的时候,形如 def foo(attrs=(1,2)): return 之类。这里设定容器类型的默认参数 attrs 使用了元组而不用列表的原因在于:列表作为一种可变类型非常的不靠谱。当脚本执行到函数定义之处的时候,解释器会对参数表达式做一次“预演算”,并把值保存到内存之中,之后每次调用这个函数的时候,都不会再重新运算其参数表达式,而是直接从“预演算”的结果处取值(引用)。所以如果你的默认参数写了一个列表进去,那么每次你调用这个函数时对这个列表所做的更改都会被保存下来。就像这样:
>>> def biggest(n, store=[0]):
store[0] = max(n, store[0])
return store[0]
>>> biggest(3)
3
>>> biggest(9)
9
>>> biggest(5)
9
这是一个返回历史最大值(仅限正数,因为我不知道该怎么表示极小值)的函数,这个函数没有使用全局变量,却可以在重复调用的时候记录下曾输入过的最大值,靠的就是这个 store=[0] 的默认参数。这个参数的值 [0] 在函数定义的时候就被写入内存之中了,之后每次调用都会引用那个地址,所以是可以当一个全局变量一样使用,却比直接定义的全局变量安全。
其实我想说的是,一般情况下不要用列表做参数,而是用元组。当然除非你知道自己在干什么,否则这只会带来麻烦。上面举了一个正面的例子是因为我看到这个话题的地方就是拿这个特性来做一个 trick 在用。他实现的是一个简单的 timer,每一次调用这个计时函数,都会打印出自上一次调用起到本次调用所经过的时间:
import time
def dur( op=None, clock=[time.time()] ):
if op != None:
duration = time.time() - clock[0]
print '%s finished. Duration %.6f seconds.' % (op, duration)
clock[0] = time.time()
if __name__ == '__main__':
import array
dur() # Initialise the timing clock
opt1 = array.array('H')
for i in range(1000):
for n in range(1000):
opt1.append(n)
dur('Array from append')
opt2 = array.array('H')
seq = range(1000)
for i in range(1000):
opt2.extend(seq)
dur('Array from list extend')
opt3 = array.array('H')
seq = array.array('H', range(1000))
for i in range(1000):
opt3.extend(seq)
dur('Array from array extend')
上例来自于 http://code.activestate.com/recipes/578776-a-simple-timing-function/,并且是 Python2 版本
这个函数里除了用于计时的 clock=[time.time()] 参数外,还有一个用于打印的表示当前 _main_ 函数进度的 op 字符串参数。他的函数里设计了一个逻辑:如果调用时没有给出 op 参数,那么就认为这次调用是在初始化,就像 _main_ 函数里第二行那样。其实我觉得这样不太好,因为完全可以改成:
import time
def dur(op='Undefined',clock=[time.time()]):
duration = time.time() - clock[0]
print('%s finished\t Duration:%.6f seconds.'%(op,duration))
clock[0] = time.time()
if __name__ == '__main__':
import array
dur('Initialise') # Initialise the timing clock
这样直接通过 op='Initialise' 来显式初始化计时器更直观一些,而且可以避免在已经初始化后因为忘记给出 op 参数而造成计时错误的可能,虽然可能性很小。
Python——可变类型与不可变类型(即为什么函数默认参数要用元组而非列表)的更多相关文章
- Python进阶-函数默认参数
Python进阶-函数默认参数 写在前面 如非特别说明,下文均基于Python3 一.默认参数 python为了简化函数的调用,提供了默认参数机制: def pow(x, n = 2): r = 1 ...
- 【转】Python函数默认参数陷阱
[转]Python函数默认参数陷阱 阅读目录 可变对象与不可变对象 函数默认参数陷阱 默认参数原理 避免 修饰器方法 扩展 参考 请看如下一段程序: def extend_list(v, li=[]) ...
- Python面试题目之Python函数默认参数陷阱
请看如下一段程序: def extend_list(v, li=[]): li.append(v) return li list1 = extend_list(10) list2 = extend_l ...
- 使用可变对象作为python函数默认参数引发的问题
写python的都知道,python函数或者方法可以使用默认参数,比如 1 def foo(arg=None): 2 print(arg) 3 4 foo() 5 6 foo("hello ...
- python函数默认参数为可变对象的理解
1.代码在执行的过程中,遇到函数定义,初始化函数生成存储函数名,默认参数初识值,函数地址的函数对象. 2.代码执行不在初始化函数,而是直接执行函数体. 代码实例 这要从函数的特性说起,在 Python ...
- python函数默认参数作用域
当def函数参数默认值为对象时,例如列表[],字典{} 示例1:猜测一下,会输出什么??? def ddd(a,b=[]): b.append(a) return b print(ddd(1)) pr ...
- [python]函数默认参数顺序问题
python 函数参数定义有四类: 1.必选参数:调用函数时候必须赋值的参数. a,须以正确的顺序传入函数b,调用时的数量必须和声明时的一样 def exa(x): return x #b作为参数进入 ...
- python之函数默认参数的坑
坑 当你的默认参数如果是可变的数据类型,你要小心了 例题 # 正常没毛病的操作 def func(a,b=False): print(a) print(b) func(1,True) # 在实参角度, ...
- python函数默认参数陷阱
对于学习python的人都有这样的困惑 def foo(a=[]): a.append(5) return a Python新手希望这个函数总是返回一个只包含一个元素的列表:[5].结果却非常不同,而 ...
随机推荐
- php学习之路-笔记分享20150327
由于公司需要,不得已是php也学习并用了段时间做了两个项目,现也分享出笔记.需要源文档的留下邮箱,.
- 深入浅析Node.js单线程模型
Node.js采用 事件驱动 和 异步I/O 的方式,实现了一个单线程.高并发的运行时环境,而单线程就意味着同一时间只能做一件事,那么Node.js如何利用单线程来实现高并发和异步I/O?本文将围绕这 ...
- ADO.NET系列之Command对象
ADO.NET系列之Connection对象 ADO.NET系列之Command对象 ADO.NET系列之DataAdapter对象 ADO.NET系列之事务和调用存储过程 上一篇<ADO.NE ...
- Ruby on Rails 生成指定版本的 Rails 项目
ruby-on-rails ruby 本地 Rails 默认5.1.6 版本 $ gem list --local rails (5.1.6, 5.1.5, 5.1.4) 使用 version 生成指 ...
- C# 一些代码小结--使用文件记录日志
C# 一些代码小结--使用文件记录日志 public class FaceLog { public static void AppendInfoLog(string errMsg) { try { s ...
- powerviot install in sharepoint 2013
1.在app server安装powerviot for sharepoint,然后进行配置powerviot 2.powerviot配置之前需要设置security token service以及e ...
- win10中shift+右键,在此处打开cmd窗口
通过添加注册表项,实现右击“在此处打开命令行功能” 注册表位置:HKEY_CLASSES_ROOT\Directory\Background\shell\ win10系统用标识右键菜单打开命令行的键, ...
- 二:maven构建module
通常情况下,我们一个项目是需要分多个模块的,这是我们用maven管理项目就需要构建一个多模块的项目: 通常的结构是一个模块中有一个主项目,下面包含多个子项目,如果是web项目则子项目中有一个是java ...
- 深入理解Java接口和抽象类
对于面向对象编程来说,抽象是它的一大特征之一.在Java中,可以通过两种形式来体现OOP的抽象:接口和抽象类.这两者有太多相似的地方,又有太多不同的地方.很多人在初学的时候会以为它们可以随意互换使用, ...
- 解决后台json数据返回的字段需要替换的问题
有时候后台json数据返回的字段含有“id”,也有可能是有时候为了减少代码的冗余,两页面之间只是数据模型个别属性的区别,所以这时候最好是用到模型属性的替换,用新的属性替换返回的json数据的字段.这里 ...