指针是C和C++系语言的重要概念,其本质是保存另一个变量内存地址的变量(Pointers are variables that hold the memory address of another variable.)。某些情况下,使用指针可以提高代码的效率。但是,指针也往往会对初学者造成困扰,即使是老手,也经常会出现内存管理的问题。
象Java、Python之类的语言并没有明确的指针的概念。但Python中如果需要使用类似指针行为的时候,可以模拟出指针的效果,同时又不会陷入内存管理的噩梦。
Python中为什么没有指针
看起来指针似乎违背了Python设计之禅(Zen of Python)。指针会带来隐含的修改,对初学者来说显得比较复杂,而且也为访问非预期内存块之类的危险操作提供了手段。Python倾向于对用户屏蔽类似内存管理之类的实现细节,更注重语言的实用性,甚至都不太关心执行速度。
Python中的对象
在Python中,一切都是对象。可以通过提供的内置函数isinstance()进行验证:
>>> isinstance(1, object)
True
>>> isinstance(str, object)
True
>>> isinstance(list(), object)
True
>>> def func():
pass
>>> isinstance(func, object)
True
由此可见,Python中无论基本类型、类、类实例、自定义函数等都是对象。对象至少由三部分数据组成:
引用计数(Reference Count)
对象类型(Type)
对象值(Value)
其中引用计数用来进行内存管理;对象类型用于在Python本身的实现层(CPython)保证运行期的类型安全;对象值才是对象的真实数据。
要想拨开Python指针的层层迷雾,首先需要了解Python中的两类对象。
不可变对象与可变对象
顾名思义,不可变对象(Immutable Object)就是值不能改变的对象,象int、bool、float、str、tuple等都是不可变对象,相反,list、set、directory等都是可变对象(Mutable Object)。可以使用id()函数(返回对象的内存地址)、is(两个对象具有同一个内存地址时返回True,否则返回False)对此进行验证:
>>> x = 5
>>> id(x)
8791652754384
>>> x = x + 1
>>> id(x)
8791652754416
>>> s = 'Python'
>>> id(s)
31016864
>>> s += ' test'
>>> id(s)
52447280
>>> s[0] = 'p'
Traceback (most recent call last):
File "", line 1, in
s[0]='p'
TypeError: 'str' object does not support item assignment
>>> lt = [1, 2, 3]
>>> id(lt)
52176968
>>> lt.append(4)
>>> id(lt)
52176968
>>> lt[0] = 5
>>> id(lt)
52176968
对整形对象x和字符串对象s进行操作后,得到了另一个对象(id函数返回了另一个值,说明对象的内存地址变了),对s[0]赋值时,得到TypeError,说明str对象的元素是不能进行赋值的;对list对象lt操作后,得到的还是原来的对象lt。这就说明x、s是不可变对象,lt是可变对象。
深入理解变量
Python中的“变量”与C和C++中的变量是截然不同的。甚至在Python中就不应该叫做变量(variables),而应该叫做名称(names)。听起来很怪异,但通常情况下把Python中的名称认为是变量也没关系,重要的是要理解它们的不同之处。
在C语言中,定义一个整形变量x:
int x = 2505;
挺简单的一句话,其背后发生了什么?
申请一个足够存储整型值的内存空间;
将2505赋值给该内存地址;
将x指向该值。
内存示意图如右图所示:
要修改x的值,只需要执行:
x = 2504;
该语句将新值2504赋给了变量x,覆盖了原值2505,更新后的内存显示了新值:
需要注意的是x的地址没有发生变化,仅是值变化了。这意味着x是内存地址,而不仅仅是个名字。 可以从另一个角度思考:x拥有一个内存地址(容器),该地址可以存储一个整形数,当将值赋给x时,相当于把这个值放进了x所拥有的内存地址。执行:
int y = x;
代码创建了一个新的容器(占用一段内存),并把x的值复制到该容器中,内存看起来是这样的:
虽然x的值复制给了y,但y拥有了一段新的内存,改写y值的时候对x没有影响。
再来看看Python中等价的代码:
x = 2505
除了不用声明x的类型外,和C代码没有区别。同样,代码的背后会执行以下操作:
创建一个PyObject对象(不同于Python中的对象,是CPython特有的Python中所有对象的基本结构。PyObject是C结构体,无法直接访问typecode和refcount等,不过通过sys.getrefcount()可以获取一些内部信息);
将PyObject对象的类型设置为整形;
将PyObject对象的值设置为2505;
创建一个称为x的名称;
将x指向新创建的PyObject对象;
将PyObject对象的引用计数加1。
内存示意如下:
与C语言中变量x拥有存储2505的内存块不同,Python中新创建的PyObject拥有这块内存,但是名称x并不拥有任何一块内存。
要将x的值修改为2504,执行:
x = 2504
这行代码会执行以下操作:
创建一个新的PyObject对象;
将新PyObject对象的类型设置为整形;
将新PyObject对象的值设置为2504;
将x指向新创建的PyObject对象;
将新PyObject对象的引用计数加1;
将旧PyObject对象的引用计数减1。
现在的内存示意图如下:
上图表明x指向一个对象的引用,而不拥有内存空间。x=2338语句不是一个赋值语句,而是将x绑定到一个引用。而且,旧的对象在内存中的引用计数为0,将等待被垃圾回收机制回收。
象C代码一样,将x赋值给一个新的名称y:
y = x
内存看起来就会是这个样子的:
并没有创建新的对象,只是将新名称y指向了同一个对象,可以通过 y is x 返回True确定x和y是同一个对象。同样,y也是不可变对象,如果对y进行操作,如y += 1,那么y就会绑定到一个新的对象,这时y is x就会返回False。
深刻理解Python中的对象是了解指针的重要基础,下篇文章中将会详细说明Python指针的具体内容。