2.2 Python的数据结构

Python为我们提供了最基础的数据存储结构,在数据的ETL过程中可能需要借助Python原生的数据结构来处理数据。本节将讲述Python的几大基础数字类型和结构。

在标准的Python数据类型中,有些是可变的,有些是不可变的。不可变就意味着你不能对它进行操作,只能读取。

  • 不可变数据:Number(数字)、String(字符串)、Tuple(元组)。
  • 可变数据:List(列表)、Dictionary(字典)、Set(集合)。

可以用Python内置的type()函数查看数据的类型,如:

type(123) # 返回int
# int
a = "Hello"
type(a) # 返回str
# str

也可以用isinstance来判断数据是不是指定的类型:

isinstance(123, int) # 123是不是整型值
# True
isinstance('123', int)
# False
isinstance(True, bool)
# True

2.2.1 数字

Python的数字类型可以存储数学中的各种数字,包括常见的自然数、复数中的虚数、无穷大数、正负数、带小数点的数、不同进制的数等。

x = 1    # int, 整型
y = 1.2  # float, 浮点
z = 1j   # complex, 复数

可以对数字进行以下运算:

a = 10
b = 21
# 数值计算
a + b # 31
a - b # -11
a * b # 210
b / a # 2.1
a ** b # 表示10的21次幂
b % a # 1 (取余)
# 地板除,相除后只保留整数部分,即向下取整
# 但如果其中一个操作数为负数,则取负无穷大方向距离结果最近的整数
9//2 # 4
9.0//2.0 # 4.0
-11//3 # -4
-11.0//3 # -4.0

2.2.2 字符串

字符串可以是一条或多条文本信息。在Python中非常容易定义字符串,字符串的计算处理也非常方便。

可以对字符串进行切片访问(同时适用于字符、列表、元组等)。字符串从左往右,索引从0开始;从右往左,索引从–1开始。可以取字符串中的片段,切片索引按左闭右开原则:

var = 'Hello World!'
# 按索引取部分内容,索引从0开始, 左必须小于右
# 支持字符、列表、元组
var[0] # 'H'
# 从右往左,索引从-1开始
var[-1] # '!'
var[-3:-1] # 'ld'
var[1:7] # 'ello W'(有个空格,不包含最后一位索引7)
var[6:] # 'World!' (只指定开头,包含后面所有的字符)
var[:] # 'Hello World!'(相当于复制)
var[0:5:2] # 'Hlo'(2为步长,按2的倍数取)
var[1:7:3] # 'ello W' -> 'eo'
var[::-1] # '!dlroW olleH'(实现反转字符功能)

下面是一些最常用的字符操作:

len('good') # 4 (字符的长度)
'good'.replace('g', 'G') # 'Good' (替换字符)
'山-水-风-雨'.split('-') # ['山', '水', '风', '雨'] (用指定字符分隔,默认空格)
'好山好水好风光'.split('好') # ['', '山', '水', '风光']
'-'.join(['山','水','风','雨']) # '山-水-风-雨'
'和'.join(['诗', '远方']) # '诗和远方'
'good'.upper() # 'GOOD' (全转大写)
'GOOD'.lower() # 'good' (全转小写)
'Good Bye'.swapcase() # 'gOOD bYE' (大小写互换)
'good'.capitalize() # 'Good' (首字母转大写)
'good'.islower() # True (是否全是小写)
'good'.isupper() # False (是否全是大写)
'3月'.zfill(3) # '03月' (指定长度,如长度不够,前面补0)

2.2.3 布尔型

在计算机世界中,0和1是基本元素,代表了开或关、真或假两种状态。在Python里,True和False分别代表真和假,它们都属于布尔型。布尔型只有这两个值。

如果我们检测变量,以下情况会得到假,其他情况为真:

  • None、False
  • 数值中的0、0.0、0j(虚数)、Decimal(0)、Fraction(0, 1)
  • 空字符串""、空元组()、空列表[]
  • 空字典{}、空集合set()
  • 对象默认为True,除非它有bool()方法且返回False,或有len()方法且返回0

以下是一些典型的布尔运算:

a = 0
b = 1
c = 2
a and b # 0(a为假,返回假的值)
b and a # 0(b为真,返回a的值)
a or b # 2 (a为假,返回b的值)
a and b or c # 2
a and (b or c) # 0 (用类似数学中的括号提高运算优先级)

# not的注意事项
not a # True
not a == b # True
not (a == b) # True(逻辑同上)
a == not b # 这条有语法错误, 正确的如下:
a == (not b) # True

# and的优先级高于or。首先,'a'为真,'a' and 'b'返回'b';然后,'' or 'b'返回'b'
'' or 'a' and 'b' # 'b'

2.2.4 列表

列表是用方括号组织起来的,每个元素用逗号隔开,每个具体元素可以是任意类型的内容。通常元素的类型是相同的,但也可以不相同,例如:

x = [] # 空列表
x = [1, 2, 3, 4, 5]
x = ['a', 'b', 'c']
x = ['a', 1.5, True, [2, 3, 4]] # 各种类型混杂
type(x) # list 类型检测

列表和字符串一样支持切片访问,可以将字符串中的一个字符当成列表中的一个元素。以下是一些常用的列表操作:

a = [1, 2, 3]
len(a) # 3(元素个数)
max(a) # 3(最大值)
min(a) # 1(最小值)
sum(a) # 6(求和)
a.index(2) # 1(指定元素位置)
a.count(1) # 1(求元素的个数)
for i in a: print(i) # 迭代元素
sorted(a) # 返回一个排序的列表,但不改变原列表
any(a) # True(是否至少有一个元素为真)
all(a) # True(是否所有元素为真)
a.append(4) # a: [1, 2, 3, 4](增加一个元素)
a.pop() # 每执行一次,删除最后一个元素
a.extend([9,8]) # a: [1, 2, 3, 9, 8](与其他列表合并)
a.insert(1, 'a') # a: [1, 'a', 2, 3](在指定索引位插入元素,索引从0开始)
a.remove('a') # 删除第一个指定元素
a.clear() # [](清空)

另外,需要熟练掌握列表的推导式,可以由可迭代对象快速生成一个列表。推导式就是用for循环结合if表达式生成一个列表,这是一个非常方便紧凑地定义列表的方式,可以大大减少代码量。

# 将一个可迭代的对象展开,形成一个列表
[i for i in range(5)]
# [0, 1, 2, 3, 4]

# 可以将结果进行处理
['第'+str(i) for i in range(5)]
# ['第0', '第1', '第2', '第3', '第4']

# 可以进行条件筛选,实现取偶数
[i for i in range(5) if i%2==0]

# 拆开字符,过滤空格,全变成大写
[i.upper() for i in 'Hello world' if i != ' ']
# ['H', 'E', 'L', 'L', 'O', 'W', 'O', 'R', 'L', 'D']

2.2.5 元组

元组(tuple)跟列表(list)非常相似,二者之间的差异是元组不可改变,而列表是可以改变的。元组使用圆括号(),列表使用方括号[]。

元组的索引机制跟列表完全一样。元组是不可修改的,我们修改元素时,就会报错,但是我们可以修改混杂类型里的列表类型数据。

另外,我们需要掌握元组的解包操作,这些操作可以让我们灵活地赋值、定义函数、传参,非常方便。

x = (1,2,3,4,5)
a, *b = x # a占第一个,剩余的组成列表全给b
# a -> 1
# b -> [2, 3, 4, 5]
# a, b -> (1, [2, 3, 4, 5])

a, *b, c = x # a占第一个,c占最后一个,剩余的组成列表全给b
# a -> 1
# b -> [2, 3, 4]
# c -> 5
# a, b, c -> (1, [2, 3, 4], 5)

2.2.6 字典

字典是Python重要的数据结构,由键值对组成。在客观世界中,所有的事件都有它的属性和属性对应的值,比如某种花的颜色是红色,有5个花瓣。其中颜色和花瓣数量是属性,红色和5是值。我们用属性(key)和值(value)组成“键值对”(key-value)这样的数据结构。它可以用以下方法定义:

d = {} # 定义空字典
d = dict() # 定义空字典
d = {'a': 1, 'b': 2, 'c': 3}
d = {'a': 1, 'a': 1, 'a': 1} # {'a': 1} key不能重复,重复时取最后一个
d = {'a': 1, 'b': {'x': 3}} # 嵌套字典
d = {'a': [1,2,3], 'b': [4,5,6]} # 嵌套列表

# 以下均可定义如下结果
# {'name': 'Tom', 'age': 18, 'height': 180}
d = dict(name='Tom', age=18, height=180)
d = dict([('name', 'Tom'), ('age', 18), ('height', 180)])
d = dict(zip(['name', 'age', 'height'], ['Tom', 18, 180]))

访问字典的方法如下:

d['name']  # 'Tom'(获取键的值)
d['age'] = 20  # 将age的值更新为20
d['Female'] = 'man'  # 增加属性
d.get('height', 180)  # 180

# 嵌套取值
d = {'a': {'name': 'Tom', 'age':18}, 'b': [4,5,6]}
d['b'][1] # 5
d['a']['age'] # 18

常用的字典操作方法如下:

d.pop('name') # 'Tom'(删除指定key)
d.popitem() # 随机删除某一项
del d['name']  # 删除键值对
d.clear()  # 清空字典

# 按类型访问,可迭代
d.keys() # 列出所有键
d.values() # 列出所有值
d.items() # 列出所有键值对元组(k, v)

# 操作
d.setdefault('a', 3) # 插入一个键并给定默认值3,如不指定,则为None
d1.update(dict2) # 将字典dict2的键值对添加到字典dict
# 如果键存在,则返回其对应值;如果键不在字典中,则返回默认值
d.get('math', 100) # 100
d2 = d.copy() # 深拷贝,d变化不影响d2

d = {'a': 1, 'b': 2, 'c': 3}
max(d) # 'c'(最大的键)
min(d) # 'a'(最小的键)
len(d) # 3(字典的长度)
str(d) # "{'a': 1, 'b': 2, 'c': 3}"(字符串形式)
any(d) # True(只要一个键为True)
all(d) # True(所有键都为True)
sorted(d) # ['a', 'b', 'c'](所有键的列表排序)

2.2.7 集合

集合(set)是存放无顺序、无索引内容的容器。在Python中,集合用花括号{}表示。我们用集合可以消除重复的元素,也可以用它作交、差、并、补等数学运算。以下是它的定义方法:

s = {} # 空集合
s = {'5元', '10元', '20元'} # 定义集合
s = set() # 空集合
s = set([1,2,3,4,5]) # {1, 2, 3, 4, 5}(使用列表定义)
s = {1, True, 'a'}
s = {1, 1, 1} # {1}(去重)
type(s) # set(类型检测)

集合没有顺序,没有索引,所以无法指定位置去访问,但可以用for遍历的方式进行读取。以下是一些常用的操作:

s = {'a', 'b', 'c'}

# 判断是否有某个元素
'a' in s # True

# 添加元素
s.add(2) # {2, 'a', 'b', 'c'}
s.update([1,3,4]) # {1, 2, 3, 4, 'a', 'b', 'c'}

# 删除和清空元素
s.remove('a') # {'b', 'c'}(删除不存在的会报错)
s.discard('3') # 删除一个元素,无则忽略,不报错
s.clear() # set()(清空)

集合的数学运算如下:

s1 = {1,2,3}
s2 = {2,3,4}

s1 & s2 # {2, 3}(交集)
s1.intersection(s2) # {2, 3}(交集)
s1.intersection_update(s2) # {2, 3}(交集,会覆盖s1)

s1 | s2  # {1, 2, 3, 4}(并集)
s1.union(s2) # {1, 2, 3, 4}(并集)

s1.difference(s2) # {1}(差集)
s1.difference_update(s2) # {1}(差集,会覆盖s1)

s1.symmetric_difference(s2) # {1, 4}(交集之外)

s1.isdisjoint(s2) # False(是否没有交集)
s1.issubset(s2) # False (s1是否是s2的子集)
s1.issuperset(s2) # False(s1是否是s2的超集,即s1是否包含s2中的所有元素)

2.2.8 小结

在本节,我们学习了Python的几大原生数据结构,虽然Pandas提供了更为人性化的数据结构,方便我们进行数据的分析处理,但在一些场景下还是需要利用Python中这些数据结构的特点进行精细化的数据处理。