跳至主要内容

Python 数据结构教程

了解 Python 数据结构:学习数据类型以及原始与非原始数据结构,如字符串、列表、栈等。
· 15分钟 阅读

使用 AI 探索

数据结构是一种组织与存储数据的方式,使其能够被高效地访问和处理。它们定义了数据之间的关系,以及可以对数据执行的操作。已经定义了许多不同的数据结构,帮助数据科学家和计算机工程师将精力集中在解决更大的问题上,而不至于迷失在数据描述与访问的细节中。

在本教程中,您将学习各种 Python 数据结构,并了解它们的实现方式:

  • 抽象数据类型与数据结构
  • 原始数据结构
    • 整数(Integer)
    • 浮点数(Float)
    • 字符串(String)
    • 布尔(Boolean)
  • 非原始数据结构
    • 数组(Array)
    • 列表(List)
    • 元组(Tuple)
    • 字典(Dictionary)
    • 集合(Set)
    • 文件(File)

diagram

在我们的免费课程 Intro to Python for Data Science 中,您可以进一步学习如何在数据科学场景中使用 Python。课程将介绍 Python 的基本概念。通过该课程,您将了解方法、函数和 NumPy 包。

抽象数据类型与数据结构

如同在引言中所读到的,数据结构帮助您关注全局而不陷于细节。这被称为数据抽象。

现在,数据结构实际上是抽象数据类型(ADT,Abstract Data Types)的实现。这种实现需要使用一些编程构造与基本数据类型,从物理视角来表示数据。

通常,在计算机科学中,数据结构可以分为两类:原始与非原始数据结构。前者是最简单的数据表示形式,而后者更为高级:它们在更复杂的数据结构中包含原始数据结构,以满足特定目的。

原始数据结构

这些是最基础的数据结构。它们是数据操作的构件,包含纯粹、简单的数据值。Python 有四种原始变量类型:

  • Integers
  • Float
  • Strings
  • Boolean

在接下来的章节中,您将进一步了解它们!

Integers

您可以使用整数来表示数值数据,更具体地说,是从负无穷到正无穷的整数,如 4、5 或 -1。

Float

“Float” 是“浮点数”的意思。它用于表示有理数,通常以小数形式结束,例如 1.11 或 3.14。

查看下面的 DataCamp Light 代码块,试试一些整数和浮点数的操作!

# Floats
x = 4.0
y = 2.0

# Addition
print(x + y)

# Subtraction
print(x - y)

# Multiplication
print(x * y)

# Returns the quotient
print(x / y)

# Returns the remainder
print(x % y) 

# Absolute value
print(abs(x))

# x to the power y
print(x ** y)

注意,在 Python 中,您无需显式声明变量或数据的类型。这是因为它是动态类型语言。动态类型语言中,对象可存储的数据类型是可变的。

String

字符串是字母、单词或其他字符的集合。在 Python 中,您可以通过将一串字符放在一对单引号或双引号内来创建字符串。例如:'cake'、"cookie" 等。

您也可以对两个或多个字符串使用 + 操作符来连接它们,如下例所示:

x = 'Cake'
y = 'Cookie'
x + ' & ' + y
'Cake & Cookie'

以下是一些可以对字符串执行的其他基本操作;例如,您可以使用 * 将字符串重复若干次:

# Repeat
x * 2
'CakeCake'

您还可以对字符串进行切片,也就是选取字符串的部分内容:

# Range Slicing
z1 = x[2:]  
print(z1)
# Slicing
z2 = y[0] + y[1]
ke
Co

注意,字符串也可以是字母数字的组合,但 + 操作依然用于字符串连接。

x = '4'
y = '2'
x + y
'42'

Python 拥有许多内置方法或辅助函数来处理字符串。替换子串、将段落中的特定单词大写、在一个字符串中查找另一个字符串的位置,都是常见的字符串操作。看看以下示例:

将字符串首字母大写

str.capitalize('cookie')

'Cookie'

按字符数获取字符串的长度。注意,空格也计入最终结果:

str1 = "Cake 4 U"
str2 = "404"
len(str1)
8

检查字符串是否只包含数字

str1.isdigit()
False
str2.isdigit()
True

用其他字符串替换字符串的部分内容

str1.replace('4 U', str2)
'Cake 404'

在其他字符串中查找子串;返回在字符串中找到该子串的最低索引或位置:

str1 = 'cookie'
str2 = 'cook'
str1.find(str2)
0
      子串

'cook'

      出现在

'cookie'

      的开头。因此,您需要引用在

'cookie'

      中找到该子串的位置。在此例中,

0

    被返回,因为位置计数从 0 开始!
str1 = 'I got you a cookie'
str2 = 'cook'
str1.find(str2)
12
      类似地,子串

'cook'

      在

'I got you a cookie'

    中的位置是 12。请记住从 0 开始计数,且空格也占位置!

您可以在 Python 文档中找到字符串方法的完整列表,参见这里。

Boolean

这种内置数据类型只取两个值:True 和 False,它们常与整数 1 和 0 互换使用。布尔值在条件与比较表达式中非常有用,如下例所示:

x = 4
y = 2
x == y
False
x > y
True
x = 4
y = 2
z = (x==y) # 比较表达式(计算结果为 False)
if z: # 基于 z 的真/假进行条件判断
    print("Cookie")
else: print("No Cookie")
No Cookie

数据类型转换

有时,您会接手他人的代码,需要将整数转换为浮点数或反之。或者,您发现自己一直在用整数,但实际需要的是浮点数。在这类情况下,您可以转换变量的数据类型!

要在 Python 中检查对象的类型,请使用内置函数 type(),如下所示:

i = 4.0
type(i)
float

当您将某个实体从一种数据类型更改为另一种时,这被称为“类型转换(typecasting)”。数据转换通常有两种:隐式(也称强制)和显式(也称为强制转换)。

隐式数据类型转换

这是自动的数据转换,由编译器为您处理。 看看下面的示例:

# A float
x = 4.0 

# An integer
y = 2 

# Divide `x` by `y`
z = x/y

# Check the type of `z`
type(z)
float

在上面的例子中,您无需显式地将 y 的数据类型更改为浮点数即可进行浮点除法。编译器已为您隐式完成。

很简单吧!

显式数据类型转换

这种类型转换由用户定义,这意味着您必须显式告知编译器去更改某些实体的数据类型。请考虑下面的代码段,以便完整理解:

x = 2
y = "The Godfather: Part "
fav_movie = y + x
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-51-b8fe90df9e0e> in <module>()
      1 x = 2
      2 y = "The Godfather: Part "
----> 3 fav_movie = y + x


TypeError: Can't convert 'int' object to str implicitly

上述示例报错的原因是编译器无法理解您是在尝试进行连接还是加法,因为数据类型混用。您试图将一个整数与一个字符串相加。

这显然不匹配。

要解决这个问题,您需要先将 int 转换为 string,然后才能进行连接。

注意,并非总能把一种数据类型转换为另一种。此处可用的一些内置数据转换函数包括:int()、float() 和 str()。

x = 2
y = "The Godfather: Part "
fav_movie = (y) + str(x)
print(fav_movie)
The Godfather: Part 2

非原始数据结构

非原始类型是数据结构家族中更复杂的成员。它们不仅存储一个值,而是以各种形式存储一组值。

在传统计算机科学中,非原始数据结构分为:

  • 数组
  • 列表
  • 文件

Array

首先,Python 中的数组是一种紧凑的基本数据类型集合,数组中的所有元素必须是相同的数据类型。不过,与 C++ 或 Java 等其他语言不同,数组在 Python 中并不那么常用。

通常,人们在 Python 中提到数组时,实际上指的是列表。然而,两者之间存在根本差异,稍后您会看到。对 Python 而言,数组可以看作是存储某类列表的一种更高效方式,但这种列表的元素类型相同。

在 Python 中,数组由 array 模块提供支持,需要在初始化和使用前先导入。存储在数组中的元素在数据类型上是受限的。数据类型在创建数组时指定,并通过类型码表示,这是一个单字符,如下例中的 I:

import array as arr
a = arr.array("I",[3,6,9])
type(a)
array.array

Python 数组文档页面提供了有关可用类型码以及 array 模块所提供功能的更多信息。

List

Python 中的列表用于存储异构项的集合。列表是可变的,这意味着您可以在不改变其身份的情况下更改其内容。列表由方括号 [ 和 ] 表示,元素之间以逗号 , 分隔。列表内置于 Python 中:您无需单独调用。

x = [] # Empty list
type(x)
list
x1 = [1,2,3]
type(x1)
list
x2 = list([1,'apple',3])
type(x2)
list
print(x2[1])
apple
x2[1] = 'orange'
print(x2)
[1, 'orange', 3]

注意:正如您在上面 x1 的例子中所见,列表也可以存放同质项,因此可以满足数组的存储功能。除非您需要对该集合应用某些特定操作,否则这没有问题。

Python 提供了许多方法来操作和处理列表。向列表添加新项、从列表中移除某些项、对列表进行排序或反转,都是常见的列表操作。让我们看看其中一些的实际效果:

  • 使用 append() 将 11 添加到 list_num 列表。默认情况下,该数字将被添加到列表末尾。
list_num = [1,2,45,6,7,2,90,23,435]
list_char = ['c','o','o','k','i','e']

list_num.append(11) # Add 11 to the list, by default adds to the last position
print(list_num)
[1, 2, 45, 6, 7, 2, 90, 23, 435, 11]
  • 使用 insert() 在 list_num 列表的索引或位置 0 处插入 11
list_num.insert(0, 11)
print(list_num)
[11, 1, 2, 45, 6, 7, 2, 90, 23, 435, 11]
  • 借助 remove() 从 list_char 中移除首次出现的 'o'
list_char.remove('o') 
print(list_char)
['c', 'o', 'k', 'i', 'e']
  • 从 list_char 中移除索引为 -2 的项
list_char.pop(-2) # Removes the item at the specified position
print(list_char)
['c', 'o', 'k', 'e']
list_num.sort() # In-place sorting
print(list_num)
[1, 2, 2, 6, 7, 11, 11, 23, 45, 90, 435]
list.reverse(list_num)
print(list_num)
[435, 90, 45, 23, 11, 11, 7, 6, 2, 2, 1]

如果您想了解更多关于 Python 列表的内容,可以阅读教程 18 Most Common Python List Questions!

数组 vs. 列表

既然您已经了解了 Python 的列表,或许会疑惑为什么还需要数组。原因在于它们在可执行的操作方面存在根本差异。对于数组,您可以轻松地对其所有元素逐一执行操作,而列表可能并非如此。下面是一个示例:

array_char = array.array("u",["c","a","t","s"])
array_char.tostring()
print(array_char)
array('u', 'cats')

您之所以能够对 array_char 使用 tostring() 函数,是因为 Python 知道数组中的所有项类型相同,因此该操作对每个元素的行为一致。因此,在处理大量同质数据类型时,数组非常有用。由于 Python 无需分别记住每个元素的数据类型细节,在某些用例中,数组可能比列表更快且更省内存。

谈到数组,还值得一提的是 NumPy 数组。NumPy 数组在数据科学领域被广泛用于处理多维数组。总体而言,它们比 array 模块和 Python 列表更高效。对 NumPy 数组的读写更快,并支持“向量化”操作,如按元素加法。此外,NumPy 数组在处理大型稀疏数据集时也很高效。想了解更多,请查看 DataCamp 的 Python Numpy Array Tutorial。

下面是一些入门的 NumPy 数组代码:

import numpy as np

arr_a = np.array([3, 6, 9])
arr_b = arr_a/3 # Performing vectorized (element-wise) operations 
print(arr_b)
[ 1.  2.  3.]
arr_ones = np.ones(4)
print(arr_ones)
[ 1.  1.  1.  1.]
multi_arr_ones = np.ones((3,4)) # Creating 2D array with 3 rows and 4 columns
print(multi_arr_ones)
[[ 1.  1.  1.  1.]
 [ 1.  1.  1.  1.]
 [ 1.  1.  1.  1.]]

传统上,列表数据结构还可进一步分为线性与非线性数据结构。Stacks 和 Queues 被称为“线性数据结构”,而 Graphs 和 Trees 则是“非线性数据结构”。这些结构及其概念相对复杂,但由于与现实模型相似而被广泛使用。在本教程中,您将对这些主题有所一瞥。

注意:在线性数据结构中,数据项按顺序(即线性)组织。数据项被依次遍历,在一次运行中可以遍历线性数据结构中的所有数据项。然而,在非线性数据结构中,数据项并非按顺序组织。元素可能与多个元素相连,从而反映这些项之间的特殊关系。在一次运行中,非线性数据结构中的所有数据项可能无法全部遍历。

Stacks

栈是根据后进先出(LIFO)原则插入和移除对象的容器。设想晚宴上的一摞盘子——盘子总是从最上面添加或取走。在计算机科学中,这一概念用于表达式求值与语法解析、调度算法/例程等。

在 Python 中,可以使用列表来实现栈。向栈中添加元素称为“压入(push)”操作,而移除或删除元素则称为“弹出(pop)”操作。注意,在 Python 中处理栈时,您确实可以使用 pop() 方法:

# Bottom -> 1 -> 2 -> 3 -> 4 -> 5 (Top)
stack = [1,2,3,4,5] 
stack.append(6) # Bottom -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 (Top)
print(stack)
[1, 2, 3, 4, 5, 6]
stack.pop() # Bottom -> 1 -> 2 -> 3 -> 4 -> 5 (Top)
stack.pop() # Bottom -> 1 -> 2 -> 3 -> 4 (Top)
print(stack)
[1, 2, 3, 4]

Queue

队列是根据先进先出(FIFO)原则插入和移除对象的容器。现实中一个很好的例子是售票处前的排队:人们按到达顺序被服务,最先到的人也最先离开。队列有多种类型。

列表并不适合实现队列,因为对列表末尾进行 append() 和 pop() 并不快,会产生内存移动成本。此外,在列表末端插入以及从开头删除也不够快,因为这需要元素位置的整体移动。

Graphs

在数学和计算机科学中,图是由节点(也称为顶点)构成的网络,节点之间可能相连也可能不相连。连接两个节点的线或路径称为边(edge)。如果边具有特定的流向,则为有向图,该有向边称为弧(arc)。若未指定方向,则该图为无向图。

这听起来也许很理论化,并且深入研究会相当复杂。然而,图是一个重要概念,尤其在数据科学中,常用于建模现实问题。社交网络、化学与生物中的分子研究、地图以及推荐系统都依赖图与图论原理。

下面给出一个使用Python 字典实现的简单图,帮助您上手:

graph = { "a" : ["c", "d"],
          "b" : ["d", "e"],
          "c" : ["a", "e"],
          "d" : ["a", "b"],
          "e" : ["b", "c"]
        }

def define_edges(graph):
    edges = []
    for vertices in graph:
        for neighbour in graph[vertices]:
            edges.append((vertices, neighbour))
    return edges

print(define_edges(graph))
[('a', 'c'), ('a', 'd'), ('b', 'd'), ('b', 'e'), ('c', 'a'), ('c', 'e'), ('e', 'b'), ('e', 'c'), ('d', 'a'), ('d', 'b')]

您可以用图做一些很酷的事,比如判断两个节点之间是否存在路径、寻找两节点之间的最短路径、确定图中的环等。

著名的“旅行商问题”实际上是在寻找一条最短路径,使其恰好访问每个节点一次并返回起点。有时图中的节点或弧会被赋予权重或成本,您可以将其视为行走难度的度量,而您的目标是找到代价最低或最易行的路径。

Trees

现实中的树根扎在土里,枝干向外伸展、承载树叶与果实,其分枝大致有序。计算机科学中,树用于描述数据的某些组织方式,只不过根在顶部,随后是向下延展的枝、叶,绘制时相较真实的树呈倒置。

进一步约定:根总在树的顶部。沿用树的比喻,后续的节点称为分支,每个分支的最后一个节点称为叶。您可以把每个分支想象成一棵更小的树。根常被称为父节点,其下所指的节点称为子节点。具有相同父节点的节点称为兄弟节点。是否理解为什么它也被称作“家谱树”?

树有助于刻画现实场景,应用广泛:从游戏领域到设计 XML 解析器,甚至 PDF 的设计原理也基于树。在数据科学中,“基于决策树的学习”是一个重要研究领域。诸如 bagging、boosting 等著名方法都使用树模型来生成预测模型。像国际象棋这样的游戏会构建包含所有可能走法的巨大树,以便分析并应用启发式方法来决定最优一步。

您可以用本教程中见过的多种数据结构来实现树结构。不过,为简单起见,该主题将在另一篇文章中讲述。

diagram

class Tree:
    def __init__(self, info, left=None, right=None):
        self.info = info
        self.left  = left
        self.right = right

    def __str__(self):
        return (str(self.info) + ', Left child: ' + str(self.left) + ', Right child: ' + str(self.right))

tree = Tree(1, Tree(2, 2.1, 2.2), Tree(3, 3.1))
print(tree)
1, Left child: 2, Left child: 2.1, Right child: 2.2, Right child: 3, Left child: 3.1, Right child: None

您已经了解了数组,也看到了列表数据结构。然而,Python 还提供了许多不同类型的数据集合机制,尽管它们可能未被纳入传统计算机科学的数据结构主题,但在 Python 编程语言的语境下非常值得了解:

  • 元组(Tuples)
  • 字典(Dictionary)
  • 集合(Sets)

Tuples

元组是另一种标准的序列数据类型。与列表的不同在于,元组是不可变的,这意味着一旦定义,您不能删除、添加或编辑其中的任何值。这在某些场景中很有用,例如您需要将控制权交给他人,但又不希望其修改集合中的数据,只允许查看或在数据的副本上单独操作。

来看元组的实现:

x_tuple = 1,2,3,4,5
y_tuple = ('c','a','k','e')
x_tuple[0]
1
y_tuple[3]
x_tuple[0] = 0 # Cannot change values inside a tuple
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-74-b5d6da8c1297> in <module>()
      1 y_tuple[3]
----> 2 x_tuple[0] = 0 # Cannot change values inside a tuple


TypeError: 'tuple' object does not support item assignment

Dictionary

如果您想实现类似电话簿的结构,字典正是您所需要的。此前介绍的其他数据结构都不适合作为电话簿。

这时字典就派上用场了。字典由键-值对组成。key 用于标识条目,而 value 顾名思义,是该条目的值。

x_dict = {'Edward':1, 'Jorge':2, 'Prem':3, 'Joe':4}
del x_dict['Joe']
x_dict
{'Edward': 1, 'Jorge': 2, 'Prem': 3}
x_dict['Edward'] # Prints the value stored with the key 'Edward'.
1

您可以对字典应用许多其他内置功能:

len(x_dict)
3
x_dict.keys()
dict_keys(['Prem', 'Edward', 'Jorge'])
x_dict.values()

dict_values([3, 1, 2])

这段代码展示了如何使用 Python 字典来存储与访问键-值对。

首先,代码调用 len(),传入 x_dict 作为参数。这会返回字典中的键-值对数量,即 3。

接着,代码在 x_dict 上调用 keys() 方法。这将返回一个包含字典键的视图对象。在本例中,键为 'Prem'、'Edward' 和 'Jorge',如输出所示。

然后,代码在 x_dict 上调用 values() 方法。这将返回一个包含字典值的视图对象。在本例中,值分别为 3、1 和 2,如输出所示。

Sets

集合是由互不相同(唯一)的对象组成的集合。它们对于创建只包含数据集中唯一值的列表很有用。集合是无序但可变的,在处理海量数据集时非常有帮助。

x_set = set('CAKE&COKE')
y_set = set('COOKIE')

print(x_set)
{'A', '&', 'O', 'E', 'C', 'K'}
print(y_set) # Single unique 'o'
{'I', 'O', 'E', 'C', 'K'}
print(x - y) # All the elements in x_set but not in y_set
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

<ipython-input-3-31abf5d98454> in <module>()
----> 1 print(x - y) # All the elements in x_set but not in y_set


NameError: name 'x' is not defined
print(x_set|y_set) # Unique elements in x_set or y_set or both
{'C', '&', 'E', 'A', 'O', 'K', 'I'}
print(x_set & y_set) # Elements in both x_set and y_set

{'O', 'E', 'K', 'C'}

该代码创建了两个集合:x_set 和 y_set。每个集合都是通过将一串字符传递给 set() 函数而创建的。本例中,x_set 由字符串 'CAKE&COKE' 创建,y_set 则由字符串 'COOKIE' 创建。

接下来,代码使用 print() 打印每个集合。第一个 print() 语句打印 x_set,其包含字符串 'CAKE&COKE' 中的唯一字符。输出显示 x_set 包含字符 'A'、'&'、'O'、'E'、'C' 和 'K'。同样,第二个 print() 语句打印 y_set,其包含字符串 'COOKIE' 中的唯一字符。输出显示 y_set 包含字符 'I'、'O'、'E'、'C' 和 'K'。

第三个 print() 语句尝试打印 x_set 中但不在 y_set 中的所有元素集合。然而,这行代码导致 NameError,因为变量 x 尚未定义。推测该行应为 print(x_set - y_set)。

第四个 print() 语句打印 x_set 或 y_set(或二者)中的唯一元素集合。输出显示结果集合包含两个集合中的所有唯一字符。

最后,第五个 print() 语句打印同时位于 x_set 和 y_set 中的元素集合。输出显示该集合包含字符 'O'、'E'、'K' 和 'C'。

Files

文件传统上是数据结构的一部分。尽管在数据科学行业中“大数据”已很常见,但如果一种编程语言无法存储和检索先前存储的信息,那它几乎派不上用场。您仍需利用分布在各类数据库文件中的所有数据,并将学习如何做到这一点。

在 Python 中读写文件的语法与其他编程语言类似,但处理起来更为简单。以下是一些有助于您用 Python 操作文件的基本函数:

  • open():在系统中打开文件,参数 filename 为要打开的文件名;

  • read():读取整个文件;

  • readline():一次读取一行;

  • write():将字符串写入文件,并返回写入的字符数;以及

  • close():关闭文件。

# File modes (2nd argument): 'r'(read), 'w'(write), 'a'(appending), 'r+'(both reading and writing)
f = open('file_name', 'w')

# Reads entire file
f.read() 

# Reads one line at a time
f.readline() 

# Writes the string to the file, returning the number of char written
f.write('Add this line.') 

f.close()

open() 函数的第二个参数是文件模式。它允许您指定是读取(r)、写入(w)、追加(a)还是同时读写(r+)。

若想进一步了解 Python 的文件处理,请务必查阅此页。

结论

恭喜您读到本教程的最后!这让您离征服数据科学世界的目标又近了一步。

如果您感兴趣,我们的两部分课程 Python Data Science Toolbox 将更深入探讨函数、迭代器、列表等主题。

主题