通过 Cython 带你认清 Python 变量的本质

简介: 通过 Cython 带你认清 Python 变量的本质

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,那么你能不能基于对象的地址反推出对象是什么呢?

相关文章
|
5天前
|
机器学习/深度学习 算法 数据可视化
8种数值变量的特征工程技术:利用Sklearn、Numpy和Python将数值转化为预测模型的有效特征
特征工程是机器学习流程中的关键步骤,通过将原始数据转换为更具意义的特征,增强模型对数据关系的理解能力。本文重点介绍处理数值变量的高级特征工程技术,包括归一化、多项式特征、FunctionTransformer、KBinsDiscretizer、对数变换、PowerTransformer、QuantileTransformer和PCA,旨在提升模型性能。这些技术能够揭示数据中的潜在模式、优化变量表示,并应对数据分布和内在特性带来的挑战,从而提高模型的稳健性和泛化能力。每种技术都有其独特优势,适用于不同类型的数据和问题。通过实验和验证选择最适合的变换方法至关重要。
16 5
8种数值变量的特征工程技术:利用Sklearn、Numpy和Python将数值转化为预测模型的有效特征
|
2天前
|
测试技术 Python
Python MagicMock: Mock 变量的强大工具
Python MagicMock: Mock 变量的强大工具
22 8
|
2天前
|
存储 编译器 Linux
Cython 和 Python 的区别
Cython 和 Python 的区别
7 0
|
2天前
|
存储 缓存 API
比较一下 Python、C、C 扩展、Cython 之间的差异
比较一下 Python、C、C 扩展、Cython 之间的差异
6 0
|
4天前
|
Python
Python变量用法——单下划线变量名_ 原创
Python变量用法——单下划线变量名_ 原创
14 0
|
4天前
|
Python
Python变量用法——变量解包
Python变量用法——变量解包
13 0
|
2月前
|
Python
|
2月前
|
Ubuntu Linux 数据安全/隐私保护
使用Cython库包对python的py文件(源码)进行加密,把python的.py文件生成.so文件并调用
本文介绍了在Linux系统(Ubuntu 18.04)下将Python源代码(`.py文件`)加密为`.so文件`的方法。首先安装必要的工具如`python3-dev`、`gcc`和`Cython`。然后通过`setup.py`脚本使用Cython将`.py文件`转化为`.so文件`,从而实现源代码的加密保护。文中详细描述了从编写源代码到生成及调用`.so文件`的具体步骤。此方法相较于转化为`.pyc文件`提供了更高的安全性。
41 2
|
2月前
|
Python
Python变量的作用域_参数类型_传递过程内存分析
理解Python中的变量作用域、参数类型和参数传递过程,对于编写高效和健壮的代码至关重要。正确的应用这些概念,有助于避免程序中的错误和内存泄漏。通过实践和经验积累,可以更好地理解Python的内存模型,并编写出更优质的代码。
18 2
|
2月前
|
存储 数据采集 人工智能
一:《Python基础语法汇总》— 变量与数据类型
【8月更文挑战第15天】本篇文章详细讲述了关于变量的命名规范,id()函数及Python的数据类型与强制转换
24 2