跳转至

2.3.数据类型

Windows 10
Python 3.8.5 @ MSC v.1916 64 bit (AMD64)
Latest build date 2020.11.28

Python 3 内置了一些基本数据类型,其分类和层次关系如下:

  • 原子类型

    • None
    • NotImplemented
    • ellipsis (... and Ellipsis)
    • Number
      • int
        • bool
      • float
      • complex
  • 容器类型

    • 序列
      • list
      • tuple
      • str
      • bytes (byte array)
    • 映射
      • dict
    • set

constant

Python 内置了一些常量类型,包括 NoneNotImplementedellipsis,这些类型只有单一值,也只有一个对象有这个值(单例)。ellipsis 有两个字面值:...Ellipsis

sequence

序列 是按顺序保存对象的容器 (containers)。可以通过整数索引访问序列中的每一个对象。将其称为容器是因为它们是一种包含任意数量对象的对象。任意数量意味着它们可以包含 0 个元素。

某些编程语言可能使用诸如”向量“或“数组”之类的词来指代顺序容器。虽然序列和数组都是顺序容器,逻辑上都是连续的,但是物理上可能并不是连续的。例如,数组的元素往往都是储存在内存中的连续区域(物理意义上);而序列元素的真实储存位置可能是分散的。

在 C 或 Java 中,其数组具有静态分配的大小。在 Java 中,数组的索引出界将引发异常,但在 C 语言中,数组索引出界可能不会被检测到。Python 则提供了两种序列,可变的序列 list 和 不可变的序列 tuple。可变意味着序列的元素可以被修改、删除、添加。

str 是 Python 3 的字符串,本质是 Unicode 字符的不可变序列。Python 没有 char 类型,即字符类型,因为字符可以用只有一个元素的字符串表示。bytes 是 Python 3 的不可变 bytes (字节)序列,即文本字符串的二进制形式。

实际上,Python 为我们提供了任意大小的字符串对象,这是通过动态创建新字符串而不是修改现有字符串来实现的。 在其他语言上有丰富经验的程序员可能会问,通过修改原始字符串来得到新字符串是否是实现此目的的最有效率的方法。或者,他们认为让可变字符串在这种情况下是更简单的方法。简短的答案是,Python的存储管理使对不可变字符串的使用变得最简单、最有效率。

效率体现在多个方面,最常见的是时间和内存使用量。可变字符串占用较少的内存。但是,这仅在良性的特殊情况下才是正确的,在这种情况下,仅在固定大小的缓冲区中替换或缩小原字符串。如果字符串扩展后超出缓冲区的大小,则程序必须崩溃或出现异常,或者必须切换到动态内存分配。

Python 从一开始就只使用动态内存分配。通过尝试访问字符串缓冲区之外的内存,C 程序通常会遇到严重的安全问题。Python 则通过使用不可变字符串对象的动态分配避免了这个问题。

处理可变字符串可以使用更少的时间;更改字符串或从字符串中删除字符时,固定长度的缓冲区将需要较少的内存开销。基于这两点,我们应该思考如何有效地处理字符串,而不是抱怨 Python 提供不可变的字符串。

在文本密集型程序中,我们可能要避免创建单独的字符串对象。相反,我们可能想要创建一个输入缓冲区(input buffer)。我们创建 slice 对象(来引用缓冲区中的字符),而不是字符串, slice 对象描述了唯一的输入缓冲区中的开始和结束位置。如果随后需要操纵输入缓冲区的这些片段,只要按需创建新的字符串对象。

在这种情况下,我们的应用程序旨在提高效率。当我们想要灵活性和简单性时,则使用 Python 字符串对象。

dictionary

Python 提供了创建各种不同映射类型的规定,但内置只有一种映射类型,即字典。dict 将键 (key) 映射到值。key 可以是具有一致哈希值的任何 Python 对象,key 引用的值可以是任何类型的Python对象。

字典不保留元素的插入顺序,而是通过快速计算 key 的哈希值来获取每个元素在字典中的位置,其元素被插入到与 key 的哈希值相关的位置。因此 key 对象必须具有一个不变的哈希值,这意味着 key 必须是不可变的对象。

一个常见的编程需求是将命名的异构数据保存到容器。数据库就是一个例子,数据库的不同记录可能具有不同的类型。C 或 C++ 的 struct 可以做到这一点。这种命名的数据集合可能最好用一个类或一个命名元组来处理,但映射类型也适用于具有命名字段的异构数据。

许多语言使用静态定义的具有命名字段的容器来保存这些数据,但 Python 的字典是动态的,允许我们在运行时添加字段名称。

有一些术语将字典视为(key: value)对的容器。字典也可以称为 associative array(关联数组)。 以序列为代表的普通数组使用数字索引,但是字典的索引是 key 对象,每个键都关联(映射)到对应的值。

哈希的一种常见替代方法是使用某种有序结构来维护 key。这可能是树或列表,这将导致其他类型的映射。例如,Python collections 模块中有一种有序字典。

set

set(集合)可能是最简单的容器,因为它不保存元素的顺序,元素也没有特定的标识。元素本身作为自己的标识,因此集合中的元素不能重复。

Python data type and C structure

标准的 Python 实现是用 C 编写的,这意味着每个 Python 对象只是一个巧妙伪装的 C 结构。Python 对象不仅包含它的值,还包含其他信息。例如,当我们在 Python 中定义一个整数时,比如 x=100x 不仅仅是一个原始的整数。它实际上是一个指向复合 C 结构的指针,该结构包含几个值。

通过查看 Python 3.4 的源代码,整数类型的定义实际上是类似这样的:

struct _longobject {
    long ob_refcnt;
    PyTypeObject *ob_type;
    size_t ob_size;
    long ob_digit[1];
};

Python 3.4 中的一个整数实际上包含四个部分:

  • ob_refcnt:这是一个引用计数,帮助 Python 自动分配和释放内存。
  • ob_type:用于编码变量的类型。
  • ob_size:指定以下数值成员的大小。
  • ob_digit:包含实际的整数值。

这意味着与 C 之类的编译型语言相比,在 Python 中储存一个整数需要更多的开销,如下图所示。

其中,PyObject_HEAD是结构的一部分,包含ob_refcntob_type等。

C 的整数变量本质上是字节编码为整数值的内存块位置的标签,而 Python 整数变量是指向 Python 对象内存位置的指针,该 Python 对象包含了整数值和一些额外信息。储存这些额外的信息需要付出一些性能代价,但这能够让 Python 自由、动态地编程。

Python 的标准可变容器是 list。列表的元素可以是同质的,例如下面的整数列表:

list(range(0, 6))
[0, 1, 2, 3, 4, 5]

Python 的list非常灵活,我们也可以创建异构列表:

lst = [True, "2", 3.0, 4]
[type(item) for item in lst]
[bool, str, float, int]

为了让列表可以包含各种类型的对象,列表不能直接储存对象,而是储存对象的地址,并且列表中的每个元素都必须包含自己的类型信息、引用计数和其他信息——也就是说,每个元素都是一个完整的 Python 对象。