没有变量,只有名字

Python 变量是盒子吗?

Python 中的变量看似很简单,实际关乎到很多概念,今天就来聊聊 Python 的变量。

什么是变量

什么是变量,以及为什么需要变量其实无需过多解释。我们在小学阶段学习数学的时候已经遇到过类似的情况,使用一个字符,比如 x 来代替未知的一个数。

数学公式

计算机编程中的变量本质作用也是一样的,用一个 标识符(名字) 来指代某个 值(Value)

编程变量

需要区分的是,数学中的 x 符号替代的一般是一个待解的未知量或者常量,而编程中的变量更多情况下是程序运行过程中变化的量。此外,编程中的等号 = 不是表示两边相等,而是表示把右边的值 赋值 给左边的变量名。

所以理解赋值,就是理解变量的关键了。

变量是盒子吗?

很多教程在介绍变量的时候,把变量比喻成盒子,赋值就是往盒子里面放各种东西,变量名就是在盒子上写的标记。

变量是盒子

这个比喻在某些编程语言里是比较贴切的,比如 C 或 Java,但是在 Python 中却并非如此。

假如 Python 变量是盒子,让我们不妨先来思考两个问题:

  1. Python 中的变量无需事先声明,为什么?
  2. Python 的变量赋值无需考虑对象的类型,为什么?

这两个问题没法用“盒子”来自圆其说。除此之外,学习到后面还有更多的情况没法用 “变量是盒子” 来解释。

正确地比喻,Python 的变量应该是标签纸,所谓赋值就是把标签贴在物体上,标签上写的名字就是变量名。

然而这毕竟是一个比喻,它虽然有助于我们理解问题,但是要想彻底的掌握 Python 中变量的概念,我们还是有必要继续探究一下。

没有 variable,只有 name

变量,是英文 variable 的直译。

如果用 variable 在 Python 的官方文档中索引这个关键字,你会发现并没有哪一章的标题是 Variable。

索引出来的唯一一个的文档的标题是:4.2. Naming and binding,也就是 命名绑定。这个标题已经道出了 Python 变量的本质。

没有变量

细心的小伙伴可能注意到,索引列表上的 free 字段。事实上,将我们引导至此的关键字是 free variable,即 自由变量,后面讲到变量作用域和闭包的时候还会重点介绍它。

整个 4.2 这一节文档虽然篇幅不大,但是透彻地介绍了 Python 中变量的若干重要概念,强烈建议仔细阅读。不想自己看文档的请继续听我来解释。

变量的三要素

既然没有 variable,那我们在讨论 变量 的时候实际上是在讨论什么呢? 还是看文档:

Names refer to objects. Names are introduced by name binding operations.

短短一句话就给出了关于 变量 的 3 个元素:

  1. Name, 名字
  2. object,对象
  3. binding, 绑定

名字(name) 指向 对象(object) ,由 绑定(binding) 操作引入。

如何理解呢?

让我们先来解释什么是对象。

什么是对象

在 Python 里有句很著名的话:一切皆对象(Everything is object)

关于 object,Python 文档术语这么解释的:

object

Any data with state (attributes or value) and defined behavior (methods).

即:

任何有状态(属性或值)和定义行为(方法)的 数据

Python 文档在解释数据模型时是这么说的:

Objects are Python’s abstraction for data. All data in a Python program is represented by objects or by relations between objects.

对象是 Python 对数据的抽象。Python 程序中的所有数据都可以表示为对象或者对象之间的关系。

技术人不打诳语,这里的一切真得就是指的一切。你已经学到或将要学的很多东西,都是对象,包括:数字、字符串、列表、元组、集合、字典、函数、类、模块等。

当然,Python 编程中还有很多不属于对象范畴的东西,比如关键字,表达式,条件语句,循环语句等语法。

从开始就说起的 赋值,其中的 ,指的就是 对象的值

但是一般情况下,我们不会这么扣字眼,所以通常当提到 /数据/对象 的时候,指的都是同一个概念。

接下来,我们从什么是数据的层面来理解什么是对象。

对象的三个基础属性

对程序来说,数据都是以二进制形式存在内存中的信息。

关于什么是二进制,以及内存的原理都是计算机专业的入门课程,这里就不展开了。提及它们的原因是它们决定了数据的另外两个必需属性:

  1. 数据必须要有类型,不然都是 0 1 代表什么意思呢?
  2. 内存中的数据有唯一的地址,不然怎么去定位数据呢?

于是我们就凑齐了对象的三个基础属性,看看文档是怎么说的:

Every object has an identity, a type and a value.

关于 valuetype 没什么好解释的了,唯有 identity 还需要额外说明下:

An object’s identity never changes once it has been created; you may think of it as the object’s address in memory.

可以这么去理解,因为内存地址过于暴露底层细节,所以 Python 其实是利用了内存地址的唯一性来为对象创建唯一的 ID。

对象是 Python 中非常重要的概念,后续还会深入介绍,请保持关注。

名字是什么

说完了对象再来说说名字,术语又叫 标识符(Identifier)

名字语法规则很简单:

  • 除了下划线和大小写字母,其它乱七八糟的符号都不能用
  • 可以有数字但是不能以数字开头
  • 下划线开头的名字有一点特殊
  • 不能用 Python 保留的 关键字
  • 区分大小写

规则简单,但是想要取好名字可没有看上去那么简单。如何命名和理解变量无关,以后再专门讨论,本文就此略过。

绑定的方式

有了对象和名字,下面就是如何把这两者 绑定(Binding)

把对象绑定到一个名字上的最常见的方式是 赋值语句(Assignment statement),这也是通常介绍变量的教程中唯一提到的形式。

实际上,以下行为都是在做名字绑定:

  • 函数传参
  • def 定义函数
  • class 定义类
  • for 语句
  • import 语句
  • with ... as 语句
  • except ... as 语句

这些语句绑定的名字和赋值语句产生的名字(变量)在使用的时候并没有本质差别。

理解了这些之后,再回头去看把数字、字符串不同类型的值赋给同一个变量名,就没什么意外的了。只是名字绑定的一小部分特例而已。

变量是什么

介绍完了 3 个重要概念:

  1. 名字
  2. 对象
  3. 绑定

可以用它们来凑出变量的定义:变量是 绑定了对象的名字

虽然得出这么一个定义,但是还剩最关键的问题没有解释清楚。我们前面罗列的各种操作它们都在绑定,但是 绑定到底是在干什么?

变量的本质

下面的语境中,名字和变量是同义词。

当名字出现在绑定语句之外的地方,它们代表的是什么呢?

比如下面这个最简单的赋值语句,左侧的 x 我们已经理解了,它是一个名字,先后被赋值(即被绑定)两次,右边那个 x 现在是什么?

x = 1
x = x + 2

你的第一反应肯定是想,不就是被绑定的那个对象吗?(我的第一反应也是这样的)

让我换个更具体的问法:

假设用 Python 中的字典结构来保存变量信息,Key 显然是 变量名字,那么对应的 Value 是什么?

Key (名字)Value (?)
x1
y“DavyCloud”

直观的感觉确实应该就是 对象,然而实际 并不是

Value 保存的其实是 对象的引用

Key (名字)Value (对象的引用)
x1 的引用
y“DavyCloud” 的引用

如果是保存的是对象本体,那变量是盒子的比喻就没毛病了。

对象引用是什么

我们前面说了,每个对象在内存中都有唯一的地址,可以这么理解:

Python 内部在处理绑定的时候保存的是对象的地址,但是让我们在使用的时候看到的是对象。

也就是说,当我们把名字/变量传递出去的时候,接受方收到的也是对象的地址,并不会去复制或改变对象本体。

a = []
b = a      # b 和 a 绑定的是同一个列表对象

说起来很抽象,但其实这是非常自然的做法,举个现实中的例子:

假设我的频道或公众号 DavyCloud 你很喜欢,你订阅或者是分享给你的朋友:

channel = "DavyCloud"
subscribed += channel
share(channel, to)

显然,不论多少人关注,DavyCloud 本体自始至终只有一个。那么出现在大家订阅列表里的,就是我的频道的引用。

由此可见,对象引用实际上是一个很符合现实世界的形式,我们真的不用过多关心底层的内存地址这些细节。

变量和可变对象

当然,在你订阅之后,我的频道确实也发生了改变(订阅人数增加),这说明频道这个对象是 可变的(mutable)。相对应的还有 不可变的(immutable) 对象。这里的 指的是对象本身的值发生了变化。

和变量做个对比:

通过赋值(也可以是其它绑定操作)改变变量的时候,变的是绑定关系,也可以看成是对象 id 变了(因为每个对象的 id 都不一样)

绑定操作没有办法改变对象本身的,只能通过改变对象的属性来改变对象,也就是对象的 id 前后保持不变,但是值发生了变化。

获取对象的属性操作,例如 obj.attr 完全可以看成是一种另类的变量名,所以赋值操作和普通变量是一样的。

解绑和删除

有绑定自然就有解绑。绝大多数的解绑操作是隐藏的。

当对一个变量重新赋值,绑定到了新对象上,其实背后自然就是解绑了旧的对象。

变量自己是不会去关心旧对象的,只有 Python 在内部记录下情况,旧对象的引用数减少了。

对象的引用计数是 Python 语言的内部实现,并不属于对象的属性。这点和前面的频道订阅数是不一样的。

当一个对象的引用计数减少为 0,这个对象就要面临被垃圾回收的命运,即把它占用的内存释放出来。

主动的解绑操作是通过 del 语句实现的,如果不理解变量,可能看到这个名字会以为是把对象删除了。

通过我们上面的分析,现在应该明白,del 删除的其实只是 变量,即 名字 和 对象引用,所以有 2 个后果:

  1. 被删掉的名字不再可用,如果再试图访问会报 NameError
  2. 对象的引用计数减 1,如果对象的引用数为 0,它会被垃圾回收

小结

本文深度分析了 Python 中变量的概念,指出了变量是由三种元素组成:

  • 名字
  • 对象
  • 绑定

因为涉及知识点较多,为了专注于变量本质,有些地方粗略带过。

费了这么多篇章,实际只讲解完了 4.2.1. Binding of names 中的几句话。文档中解释变量作用域的部分,留待以后再详述。

本文主要以抠字眼讲概念为主,后面会补充更多实例。透彻理解变量的本质,介绍起 Python 中的很多语法概念也将会变得很轻松。


如果本文对你有帮助,请 点赞分享关注,谢谢!

Davy
Davy
学习📚 技术👨‍💻 投资📈

一些心得体会,希望能对你有所帮助🚀。