数据与变量
数据是所有编程语言的核心,程序很多时候,就是为了把一些数据(输入)转换成另一些数据(输出)。对于 Python 来说,数据尤其重要,数据处理,机器学习等高度数据相关的工作都是 Python 最主要的领域。
在语句 print("Hello, World!")
中,"Hello, World!" 就是一个数据。
Python 常用的数据类型
数据常常被划分为不同的数据类型,因为不同的类型的数据常常需要不同的处理方法。比如数值数据可以进行加减乘除运算;字符串则更多是进行截断,衔接等操作。下文列出了 Python 语言中最常用的数据类型。在这里,只需要对它们有个大致印象,后续章节会详细解释这些数据类型的使用方法。
- 数值型:
- 整型(int): 代表整数,例如:1, 100, -33。
- 浮点型(float): 代表实数,例如:3.14, -0.001。
- 复数型(complex): 代表复数,例如:3 + 4j。
- 布尔型(bool): 只有两个值:True 和 False。通常用于条件测试。
- 序列类型:
- 集合类型:
- 集合 (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 的变量。这种为一个变量设置值的操作,被称为赋值语句。井号 #
表示注释,在程序中,井号后面的文字都只是说明文字,不参与程序运行。
类型提示
创建变量的时候,也可以为其添加类型提示(Type Hints),比如:
name: str = "谷或载"
age: int = 30
height: float = 5.9
is_student: bool = False
上面程序,变量名之后跟了一个冒号,冒号后面的文字指明了变量的数据类型。大多数编程语言,一旦指定了变量的数据类型,编译器或解释器就会自动检查变量的数据类型是否正确,如果有错误会报错。但是 Python 并不会施加任何真正的数据类型限制,这些类型提示仅仅是提示了预期的变量类型。运行下面的程序,Python 不会汇报任何错误提示:
age: int = "Qizhen Ruan"
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 语言中,很多类型的数据(如整数、浮点数和字符串等)是不可变的,这意味着一旦为变量分配了值,就不能再修改这个值。当然 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 的命名约定,全大写的变量名通常被视为常量。尽管这并不会防止在程序中更改变量的值,但它向其他开发者发出了一个信号,即这个变量的值不应该被改变。比如下面定义的变量,在程序运行过程中,是不应该改变它们的值的:
PI = 3.14159
MAX_SIZE = 100
有时候多个常量会被包装成一组常量,比如代表每种特定颜色常量:红、黄、蓝可以被包装成一组名为 color 的常量,这种就是枚举类型,我们会在后面对其做详细介绍。
此外,为了防止常量中的数据被改动,还可以考虑使用类对数据进行包装,并控制数据的访问。相关的方法会在属性装饰器一节做详细介绍。