Skip to main content

数据与变量

数据是所有编程语言的核心,程序很多时候,就是为了把一些数据(输入)转换成另一些数据(输出)。对于 Python 来说,数据尤其重要,数据处理,机器学习等高度数据相关的工作都是 Python 最主要的领域。

在语句 print("Hello, World!") 中,"Hello, World!" 就是一个数据。

Python 常用的数据类型

数据常常被划分为不同的数据类型,因为不同的类型的数据常常需要不同的处理方法。比如数值数据可以进行加减乘除运算;字符串则更多是进行截断,衔接等操作。下文列出了 Python 语言中最常用的数据类型。在这里,只需要对它们有个大致印象,后续章节会详细解释这些数据类型的使用方法。

  • 数值型:
  • 布尔型(bool): 只有两个值:True 和 False。通常用于条件测试。
  • 序列类型:
    • 字符串(str): 由零或多个字符组成的文本,由单引号或双引号包裹,例如:"Hello", 'Python'。
    • 列表(list): 有序的对象集合,例如:[1, 2, 3, 'a', 'b']
    • 元组(tuple): 有序的不可变对象集合,例如:(1, 2, 3, 'a', 'b')。
  • 集合类型:
    • 集合 (set): 一个无序不重复元素集,基本功能包括关系测试和消除重复元素。例如:{1, 2, 3}。
    • 冻结集合 (frozenset): 与集合类似,但是它是不可变的。
  • 映射类型:
    • 字典 (dict): 无序的键:值对集合,键必须是唯一的。例如:{'name': 'John', 'age': 30}。
  • None 类型:
    • NoneType:只有一个值 None。用于表示一个值的缺失或空。
  • 二进制类型:
    • 字节序列(bytes): 包含字节的不可变序列,例如:b'hello'。
    • 字节数组 (bytearray): 包含字节的可变序列。
    • 内存视图 (memoryview): 通过 memoryview 对象对其它的数据结构的内存进行访问。
  • 文件类型: 用于文件操作,例如:读取或写入文件。
  • 其他数据类型: 很多 Python 的模块和库都对数据类型做了扩展,在使用了这些模块的程序中很可能会遇到其他数据类型,如 numpy 库中定义的 ndarray 或 pandas 库中定义的的 DataFrame 等。

后文将对这些类型数据的用法再做详解。在继续介绍数据前,我们先要介绍一下“变量”(Variable)。

变量

在编程语言里,通常不是直接使用原始数据本身,而是要给数据起个名字,程序中通过名字来使用数据。这些数据的名称,或者叫标签,这就是“变量”。变量可以使我们能够方便的对数据进行标记、存储和操作。它其实和中学数学课上学习的方程的变量的含义是非常类似的。

比如下面这个打印函数的参数直接使用了数据本身:

print(5 + 5 + 5 + 5 + 5)

如果我们想在让程序打印 3 + 3 + 3 + 3 + 3 的结果呢?那需要把程序中每个数据 5 都替换成 3,比较麻烦,如果给数据起个名字,比如叫做 x:

x = 5
print(x + x + x + x + x)

同时,在函数使用数据时,把数据的名字 x。程序运行的结果是一样的,但是当我们在想打印 3 + 3 + 3 + 3 + 3,只要把 x 的值改为 3 就可以了,就不需要再去修改每一处的数据了。

以下是关于 Python 中变量的几个基本概念和特点:

声明和赋值

赋值语句

在 Python 中,不需要显式地声明变量或其类型。当为变量赋值时,Python 会自动创建变量。

变量的数据可以是数值、字符串或者任何其它类型的数据。

变量的名字必须是一串大小写英文字母、数字和下划线_的组合,并且变量名不能以数字开头。比如,name_1 是一个合法的变量名,而 1_name 不是。 Python 的变量命名是大小写敏感的,这意味着 Variable 和 variable 是两个不同的变量。习惯上,变量名中不应该包含任何大写字母,Variable 不是一个符合 Python 习惯的变量名。 变量名不能包含空格,但可使用下划线来分隔其中的单词。比如,is_student 是一个合法的变量名,但 is student 不是。 要避免使用 Python 中已经有了特定用途的名称作为变量名,比如 print 是 Python 中个打印函数的名字,就不适合再拿来做变量名了。 虽然在语法上我们可以用任何单词作为变量名称,但是,给每个变量使用具有描述性的恰当的名称,对于一段程序是否容易被人读懂和理解至关重要。

作为对比,有些语言,比如 C 语言,变量必须预先声明,然后才可以使用;有些语言,比如 JavaScript 还有专门的关键字 “var” 来定义变量;还有一些语言,比如 PHP 要求每个变量的名字都必须以一个特殊字符 “$” 开头。与这些语言相比,Python 对于变量的限制是最宽松的了。

下面的程序创建了一个数值为 5 的变量:

x = 5  # 创建一个名为 x 的整数变量,并赋值 5

上面这段程序,x 是变量名,5 是数据。等号 = 表示复制操作,运行这段程序后,就会得到一个值为 5 的变量。这种为一个变量设置值的操作,被称为赋值语句。井号 # 表示注释,在程序中,井号后面的文字都只是说明文字,不参与程序运行。

特别提一句,在 Python 中,可以使用中文等非英语字符作为名称,不仅是变量名,将来还会提到函数名、名等都可以使用中文。这对于不熟悉英语的编程者是个优势,但是它也很容易引起问题,在整个 unicode 字符集中,存在着大量看起来极其相似,但编码不同的字符,使用它们会让程序变得非常难以理解。在本书中,所有名称还都是以英文命名。

姓名 = '谷或载'  # 这是一个使用中文作为变量名的示例

类型提示

创建变量的时候,也可以为其添加类型提示(Type Hints),比如:

name: str = "Qizhen Ruan"
age: int = 30
height: float = 5.9
is_student: bool = False

上面程序,变量名之后跟了一个冒号,冒号后面的文字指明了变量的数据类型。大多数编程语言,一旦指定了变量的数据类型,编译器或解释器就会自动检查变量的数据类型是否正确,如果有错误会报错。但是 Python 并不会并不会施加任何真正的数据类型限制,这些类型提示仅仅是提示了预期的变量类型。运行下面的程序,Python 不会汇报任何错误提示:

age: int = "ruanqizhen"

Python 自己虽然自己不会检查变量数据类型,但是类型提示对于代码的可读性非常有帮助。很多代码检查工具(如 mypy)也依赖于类型定义对代码进行类型检查。在 Pythora 星球上,程序中变量都有类型提示。

多变量赋值

Python 可以使用一个等号为多个变量赋值,变量名和变量名之间,数据和数据之间用逗号分隔。这在很多时候可以简化程序,比如说,在定义二维平面上位置的时候,总是要有一个 X 轴数据,和一个 Y 轴数据,这样的两个数据可以总是同时定义:

x, y = 3, 5
lat, lon = 33.4, 77.98

一个常用的操作是把两个变量中的数据进行交换,多数语言都需要引入一个临时变量来解决这样的问题,但在 Python 中,这个操作可以非常简洁:

x, y = 3, 5
x, y = y, x # x 和 y 中的数据被交换了,现在 x == 5, y == 3

如果给一个变量赋值多个数据,Python 会自动把所有数据都打包成元组

z = 3, 5
print(z) # 输出: (3, 5)

如果多个变量的值相同,也可以使用链式赋值,比如:

x = y = z = 4

上面的代码就相当于下面这三行代码:

x = 4
y = x
z = y

请读者分析一下下面这个程序的输出是什么:

x, y = y, x = z = 3, 5
print(x, y, z)

使用链式赋值一般用于不可变数据类型,因为所有的变量都将指向同一个数据,可能会引起混乱。

动态类型

Python 是动态类型的语言。这意味着可以在程序运行时更改变量的类型。与之对比,多数语言中,变量的数据类型都是固定的,一旦声明,就不能再改变了。

x = 5     # x 是整数
x = "hi" # 现在 x 是字符串

动态数据类型的优点是简洁灵活,非常适合编写小型的,临时使用的程序。很多编程语言最初主要是为开发小型程序设计的,它们都会倾向于采用动态类型。其中一些语言后来的应用场景已经远远超出了最初的设计,用其开发的程序规模越来越大。然而在大型程序中,动态数据类型带来的麻烦缺远超过其优点,比如,它导致无法在程序运行前检查是否有数据类型错误、代码可读性差、程序运行效率低下,还更难进行优化等等。所以,很多语言,比如 JavaScript,PHP 等,后来都演化出了禁用动态数据类型的版本,用于更好的支持大型程序开发。

变量的“变”

解释程序的时候,我们经常会说:“把变量 x 中的数据变成 5”之类的话。但是这样的说法非常不严谨,它没有说清楚,到底是把变量指向一个不同的数据,还是说变量仍然指向同一个数据,只是这个数据的值变了。

在 Python 中,使用赋值语句,改变的一定是变量的指向。比如,我们先让 x = 5,然后再让 x = 3,那么这时候,一定是 x 指向了不同的数据,而不是原来那个数据从 5 变成了 3。

x = 5     # x 指向 5
x = 3 # 现在 x 指向了另一个数据

不同的变量是可以指向同一个数据的,如果赋值语句的两边分别是两个变量,那么就表示左边的变量将也指向右边变量指向的数据。

x = [1,2]     # x 指向 [1,2]
y = [1,2] # x 和 y 指向了不同的数据,但是他们的值都是 [1,2]

x = [1,2] # x 指向 [1,2]
y = x # y 也指向了 x 指向的那个数据

数据的可变性

既然赋值语句改变的总是变量的指向,那么数据本身可以被改变吗?

在一些编程语言中,比如 C 语言中,所有的数据都是可变的(除非特别用 const 声明),比如,用户可以把一个整数的数值从 1 改为 2。但在 Python 语言中,有一些 类型的数据(如整数、浮点数和字符串等)是不可变的,这意味着一旦为变量分配了值,就不能更改该值。但很多其它类型的数据也还是可以被改变的。

在下面的示例程序中,列表数据是一个典型的可变数据,但字符串是不可变数据。

a = [1, 2, 3]
a[0] = 5
print(a) # 打印输出: [5, 2, 3]

b = "Tom"
b = "Jerry"
print(b) # 打印输出: "Jerry"

一个列表数据可能是由多个其它的数据组成的,比如上面程序中的变量 a 是一个由三个整数组成的列表。语句 a[0] = 5 表示把这个列表的第 0 个元素替换成 5。

我们生活中,给物品排序计数,总是从 1 开始的,第 1 个、第 2 个...这样数下去。然而,绝大多数的编程语言,包括 Python,都是从 0 开始计数的。一堆数据,最前面的总是第 0 个,然后后面才是第 1 个、第 2 个...

所以,当程序把列表的第 0 个元素替换成 5 之后,变量 a 所指向的数据就变成了 [5, 2, 3]

Python 中的字符串数据是不可变的,比如,我们没办法把字符串 "Tom" 的第二个字母换成改成其它的字母。但我们还是可以让变量 b 指向另一个字符串数据。

需要格外注意的是,上面讨论的是数据本身是否可变,而不是变量是否可变。变量总是可以指向一个新的数据的。

引用型变量

在 Python 中,变量存储的是数据的引用,而不是数据本身。这意味着,当你为一个变量赋值为另一个变量时,两者实际上都指向同一个数据,而不是具有相同数值的两个数据。我们怎么证明这一点呢?看下面的程序:

a = [1, 2, 3]
b = a
b[0] = 5
print(a) # 输出: [5, 2, 3]

程序中,运行语句 b = a,表示变量 b 也将指向变量 a 所指向的数据,它们都指向了同一个数据。如果后续的程序改变了变量 b 的数据,那么 变量 a 也会产生同样的变化。

请读者考虑一下,运行下面程序的输出结果是什么?

a = [1, 2, 3]
b = [1, 2, 3]
b[0] = 5
print(a)

使用链式赋值的时候,尤其需要注意这个问题,比如:

x = y = []
x.append(10)
print(x, y) # 输出:[10] [10] 因为 x 和 y 指向同一个列表

作用域

除了上面提到的这几点,作用域也是变量的一个重要属性,也就是变量在什么地方有效、哪些范围内可变。关于这个问题,我们将在介绍了函数之后再来讨论,参考章节函数和变量的作用域

删除变量

我们可以使用 del 语句删除一个变量。变量被删除之后,再尝试访问该变量就会出错:

x = 10
del x
# print(x) # x 已经不存在了,在运行这一句会出错

删除变量其实不常用,可以放心的说,一般的小程序中都用不到。它的用途主要有两点,一是在处理大数据的时候,可以帮助节约内存;二是处于安全的需要,我们可能需要确保某些变量在后续程序中不能被访问。

有些编程语言语言,比如 C 语言,自身没有回收内存的功能能,程序中的内存管理完全依赖编程人员自己编程实现。在这些语言中,所有为数据开辟的内存,都必须在时候后得到释放,否则就会产生内存泄露。当然,也不能释放太早,否则就会出现野指针,数据混乱等诸多问题。现在主流的编程语言大多具有一定的内存垃圾回收机制了,它们可以自动释放那些程序中不会再用到的内存。程序员可以不必再担心内存的使用,只要专注于程序逻辑本身就行了,程序员为存储数据开辟的内存,系统会自动在适当时候将其释放回收。比如,比较具有代表性的是 Java 的内存垃圾回收机制。

Python 也有垃圾回收机制,可以自动管理内存释放资源。但不得不说,Python 的内存管理能力逊色于 Java,一个根本原因在于Java 是编译型语言,而 Python 是解释型语言。Java 在运行一个程序前,先要对整个程序进行词法语法分析。它可以比较准确的知道一个数据在什么时候会被使用,在什么时候就不再需要了。因此可以很好的控制数据装入移除的时机。但 Python 在运行程序的时候,是读一行就运行一行。甚至可以是程序员写一行,运行一行,再写一行,再运行一行。Python 只敢把离开了作用域的变量和数据,也就是那些程序无法再访问到的数据,移除出内存。否则,它是不可能知道,程序员会不会突然又写了一句代码,然后,读取了很久之前创建的一个变量。为了程序逻辑正确,这些变量一旦加载进内存,Python 就不会再移除它们了。

如果某个变量中保存了大量数据,而且我们知道后续程序不再需要它了,那么可以使用 del 语句将其删除,以节省内存。

常量

很多语言中,除了变量,还允许用户定义常量。常量可以理解为某个特定数据的名称、标签。这些名称一旦定义就不可以指向其它数据了,多数情况下,这些常量数据本身也不允许被改变。比如,很多语言中会定义一个常量 Pi = 3.1415926。

在 Python 中,并没有内置的方式来定义真正的常量,这意味着程序员只能使用变量来命名数据。虽然任何一个变量,它的值在程序的运行过程中都有可能会改变。不过,Python 中还是有一些约定俗成的规则和技巧可以帮助程序员模拟常量的行为。

最常见的方式是使用大写字母:按照 Python 的命名约定,全大写的变量名通常被视为常量。尽管这不会阻止程序中更改变量的值,但它向其他开发者发出了一个信号,即这个变量的值不应该被改变。比如下面定义的变量,在程序运行过程中,是不应该改变它们的值的:

PI = 3.14159
MAX_SIZE = 100

有时候多个常量会被包装成一组常量,比如代表每种特定颜色常量:红、黄、蓝可以被包装成一组名为 color 的常量,这种就是枚举类型,我们会在后面对其做详细介绍。

此外,为了防止常量中的数据被改动,还可以考虑使用对数据进行包装,并控制数据的访问。这个方法会在属性装饰器一节做详细介绍。