Python 和其它静态语言之间有一个显著的不同,就是 Python 的变量其实只是一个名字。站在 C 语言的角度来看,Python 变量本质上就是一个指针(准确的说是引用),存储的是对象的内存地址,指针指向的内存才是对象。
所以在 Python 中,我们都说变量指向了某个对象。而在其它静态语言中,变量相当于是为某个对象起的别名,获取变量就等于获取这块内存所存储的值。但 Python 变量代表的内存所存储的不是对象,而是对象的地址。
我们用两段代码,一段 C 语言的代码,一段 Python 的代码,来看一下差别。
#include <stdio.h> void main() { int a = 123; printf("address of a = %p\n", &a); a = 456 printf("address of a = %p\n", &a); } //输出结果 /* address of a = 0x7fffa94de03c address of a = 0x7fffa94de03c */
可以看到前后输出的地址是一样的,再来看看 Python 的。
a = 666 print(hex(id(a))) # 0x1b1333394f0 a = 667 print(hex(id(a))) # 0x1b133339510
然而我们看到地址前后发生了变化,我们分析一下原因。
首先在 C 中,创建一个变量的时候必须规定好类型,比如 int a = 666,那么变量 a 就是 int 类型,以后在所处的作用域中就不可以变了。如果这时候,再设置 a = 777,那么等于是把内存中存储的 666 换成 777,但 a 的地址和类型是不会变化的。
而在 Python 中,a = 666 等于是先开辟一块内存,存储的值为 666,然后让变量 a 指向这片内存,或者说让变量 a 存储这块内存的地址。然后 a = 777 的时候,再开辟一块内存,然后让 a 指向存储 777 的内存,由于是两块不同的内存,所以它们的地址是不一样的。
所以 Python 的变量只是一个和对象关联的名字罢了,它是一个指针,代表的是对象的地址。换句话说 Python 变量就是个便利贴,可以贴在任何对象上,一旦贴上去了,就代表这个对象被引用了。
再来看看变量之间的传递,在 Python 中是如何体现的。
a = 666 print(hex(id(a))) # 0x1e6c51e3cf0 b = a print(hex(id(b))) # 0x1e6c51e3cf0
我们看到打印的地址是一样的,用一张图解释一下。
我们说 a = 666 的时候,先开辟一份内存,再让 a 存储对应内存的地址;然后 b = a 的时候,会把 a 拷贝一份给 b,所以 b 存储了和 a 相同的地址,它们都指向了同一个对象。
因此说 Python 是值传递、或者引用传递都是不准确的,准确的说 Python 是变量的赋值传递,对象的引用传递。因为 Python 变量本质上就是一个指针,所以在 b = a 的时候,等于把 a 指向的对象的地址(a 本身)拷贝一份给 b,所以对于变量来说是赋值传递;然后 a 和 b 又都是指向对象的指针,因此对于对象来说是引用传递。
另外还有最关键的一点,我们说 Python 的变量是一个指针,当传递一个变量的时候,传递的是指针;但是在操作一个变量的时候,会操作变量指向的内存。
所以 id(a) 获取的不是 a 的地址,而是 a 指向的内存的地址(在底层其实就是 a 本身);同理 b = a 也是将 a 本身,或者说将 a 存储的、指向某个具体对象的地址传递给了 b。
在 C 的层面上,显然 a 和 b 属于指针变量,那么 a 和 b 有没有地址呢?显然是有的,也就是二级指针。只不过在 Python 中你是看不到的,Python 解释器只允许你看到对象的地址。
为了更好地理解上述内容,我们看一段 Cython 代码:
# name 是一个变量,它是一个指针 name = "古明地觉" # 而在 C 中,指针是可以相互转化的 # 因此这里我们转成 void * 类型 # 而 void * 可以转成整型 print(<Py_ssize_t><void *> name) """ 2198935240400 """ # 我们得到了一串数字,因为地址本身就是一串数字 # 所以它和我们调用 id 函数的结果是一样的 print(id(name)) """ 2198935240400 """
如果你对解释器有一定了解的话,那么你应该知道变量是一个泛型指针 PyObject *,而指针存储的地址其实就是一串数字。我们将变量转成 void * 之后再转成整型,那么就能拿到它存储的数字,而这显然也是内置函数 id 所做的事情。
那么问题来了,如果我知道对象的地址,那么能不能反推出对象是什么呢?答案是可以的,只需要将上述过程逆转过来就可以了。
解释一下,首先这串数字虽然表示对象的地址,但它不具备指针的含义,很明显它就是一个普通的 Python 整数而已。如果想让它变成指针,那么需要先转成 void *,因为 void * 和整数是可以相互转化的。只不过这个整数是 C 的整数,因此要先转成 Py_ssize_t,再转成 void *。
具备指针的含义之后,再转成 object 即可拿到对象本身,是不是很神奇呢?如果不借助 Cython,那么你能不能基于对象的地址反推出对象是什么呢?