Python学习笔记(三)组合数据类型

“Life is short, You need Python” – Bruce Eckel

Environment

  • OS: macOS Mojave
  • Python version: 3.7
  • IDE: Jupyter Notebook

0. 写在前面

Python 中的数据类型可分为基本数据类型组合数据类型,基本数据类型包括整型浮点型复数类型字符串布尔类型,组合数据类型包括列表元组字典集合,其中还有更细的分类,如整型包括不同进制,字典包括有序和无序等。本文记录了组合数据类型的常用操作、解析式以及一些底层实现的内容。

1. 列表

  • 使用方括号表示
  • 可以包含各种类型的变量
  • 序列类型
  • 可变的(可以增加、删除、修改)

1.1 创建

  • 直接用方括号定义
ls = [[True, False], 1, {'type': 'list', 'name': 'hello'}]
empty_list = [] # an empty list
  • list(iterable=()) ,传入可迭代对象
list('string') # ['s', 't', 'r', 'i', 'n', 'g']
list(range(5)) # [0, 1, 2, 3, 4]
  • 列表生成式
ls = [i for i in range(5)]
print(ls) # [0, 1, 2, 3, 4]

1.2 访问

  • 索引
ls = [9, 5, 2, ['legal', 'healthy'], 7]
print(ls[0]) # 9
print(ls[-2]) # ['legal', 'healthy']
print(ls[3][0]) # legal

# 通过索引修改元素
ls[3] = 0
print(ls) # [9, 5, 2, 0, 7]
  • 切片,将返回新的列表对象
ls = [9, 5, 2, ['legal', 'healthy'], 7]

print(ls[1:3]) # 表示从ind=1 开始到ind=2,不包括ind=3
# [5, 2]

print(ls[:3]) # 从头开始到索引 2
# [9, 5, 2]

print(ls[3:]) # 从索引 3 开始到最后
# [['legal', 'healthy'], 7]

print(ls[:]) # 得到整个列表,是常用的列表拷贝方法
# [9, 5, 2, ['legal', 'healthy'], 7]

print(ls[1::2]) # 步长为 2
# [5, ['legal', 'healthy']]

print(ls[-1:-4:-2]) # 反向索引
# [7, 2]

print(ls[::-1]) # 得到反序排列的新列表对象
# [7, ['legal', 'healthy'], 2, 5, 9]

1.3 修改

  • list.append(object) 在列表末尾加上一个元素
ls = [9, 5, 2, ['legal', 'healthy']]
ls.append(7)
print(ls) # [9, 5, 2, ['legal', 'healthy'], 7]
  • list.extend(iterable) 在列表末尾加上多个元素
ls = [9, 5, 2, ['legal', 'healthy'], 7]
ls.extend(['a', 'b', 3])
print([9, 5, 2, ['legal', 'healthy'], 7, 'a', 'b', 3])
  • list.insert(index, object) 向列表指定位置插入元素,从该位置开始的元素向后移一个位置
ls = [9, 5, 2, ['legal', 'healthy'], 7]
ls.insert(3, 0)
print(ls) # [9, 5, 2, 0, ['legal', 'healthy'], 7]
  • list.pop(index=-1) 弹出指定索引的元素
ls = [9, 5, 2, ['legal', 'healthy'], 7]
ls.pop(-2) # ['legal', 'healthy']
print(ls) # [9, 5, 2, 7]
  • list.remove(value) 从列表中删除指定元素,传入需要删除的元素。对于重复元素,调用一次只删除索引最小的那一个。好又快地需要删除所有重复元素,可以使用迭代结构结合反向索引,参考Python基础(四)控制结构中for迭代的例子。
ls = [1, 1, 1, 1, 0, 0, 1]

ls.remove(1)
print(ls)
# [1, 1, 1, 0, 0, 1]
  • list.clear() 清空列表中的元素,使原列表对象称为空列表
ls = [9, 5, 2, ['legal', 'healthy'], 7]
ls.clear()
print(ls)

1.4 常用操作符

  • 拼接 + ,连接两个列表,效果等价于 list.extend(iterable) 方法,但拼接返回连接后产生的新列表对象。总是首选使用 extend 方法而不是拼接 +
ls = [9, 5, 2] + [['legal', 'healthy'], 7]
print(ls) # [9, 5, 2, ['legal', 'healthy'], 7]
  • * 重复
ls = [1, 2, 'a'] * 3
print(ls) # [1, 2, 'a', 1, 2, 'a', 1, 2, 'a']

注意,使用 * 创建多维列表可能出现 bug!

使用 * 创建一个 4 x 3 的二维列表将失败

ls = [[0] * 3] * 4
ls
# [[0, 0, 0]
#  [0, 0, 0]
#  [0, 0, 0]
#  [0, 0, 0]]

# 改变 0, 0 为止的元素,却造成所有第一行元素的改变
ls[0][0] = 1
ls
# [1, 0, 0]
# [1, 0, 0]
# [1, 0, 0]
# [1, 0, 0]

原因是 ls * n 是将 n 份列表对象的浅拷贝连接(深浅拷贝问题参考Python基础(十) Part 1)。解决方法为,改用列表解析式(本文 Part 5.1)。

  • 关系操作符,对第一个元素进行比较
list1 = [1, 2, 3]
list2 = [4, 1, 3]
print(list1 > list2) # False

list3 = ['a', 'b', 'c']
print(list1 > list3) # 报错 TypeError
  • 成员资格符 in
ls = [123, ['xyz', 666], 456]
print(123 in ls) # True
print('xyz' in my_list) # False,因只能判断一级对象
print('xyz' not in my_list) # True
print('xyz' in my_list[-2]) # True

1.5 其他常用方法

  • list.count(value) 返回列表中某一元素值出现的次数
ls = [123, 456] * 6
print(ls.count(123)) # 6
  • list.index(value, start=0, stop=9223372036854775807) 返回待查找元素值在列表中第一次出现位置的索引,注意查找范围不包括 stop 索引
ls = [123, 456, 789, 123, 'abc', 123]
print(ls.index(123)) # 0
print(ls.index(123, -2)) # 5
print(ls.index(123, 1, 4)) # 3
print(ls.index(123, 1, 3) # 若没有该值的元素,则报错ValueError
  • list.reverse() 将列表对象倒序
ls = [9, 5, 2, ['legal', 'healthy'], 7]
ls.reverse()
print(ls) # [7, ['legal', 'healthy'], 2, 5, 9]
  • list.sort(*, key=None, reverse=False) 将原列表对象按元素排序(默认为升序),支持仅包含 int 和(或) float 类型元素的列表,支持仅包含 int 和(或) float 类型元素的列表
ls.sort(reverse=True)
print(ls) # [93, 78, 58.0, 23.0, 15]
  • list.copy() ,和切片复制等价,均为浅拷贝
ls = [0, 1, [2, 3], 4]
ls_slide_copy = ls[:]
ls_copy = ls.copy()
print(id(ls), id(ls_slide_copy), id(ls_copy)) # 三者不同

# 为浅拷贝
ls[0], ls[2][0] = 666, 666
print(ls_slide_copy) # [0, 1, [666, 3], 4]
print(ls_copy) # [0, 1, [666, 3], 4]

2. 元组

  • 使用圆括号表示
  • 可以包含各种类型的变量
  • 序列类型
  • 不可变的(不可增加、删除、修改)

2.1 创建

  • 直接使用圆括号。当只有一个元素时,可以省略括号加上一个逗号
tp = (9, 5, 2, ['legal', 'health'], 7)
print(tp) # (9, 5, 2, ['legal', 'health'], 7)

# a tuple with a single element
number = (6)
tp_with_single_elem = 6, # 圆括号可省略
print(type(number), type(tp_with_single_elem)) # <class 'int'> <class 'tuple'>
  • 使用 tuple(iterable=()) 函数,传入可迭代对象
tp = [9, 5, 2, ['legal', 'health'], 7]
print(tp) # (9, 5, 2, ['legal', 'health'], 7)

2.2 访问

  • 解包。序列类型的可迭代对象都可以这样操作,但只有逗号而无括号时默认为元组,方便着来
def foo(x):
    return x * 2, x / 2, x ** 2 # 打包返回

res = foo(6)
print(type(res)) # <class 'tuple'>
res1, res2, res3 = res # 解包赋值
print(res1, res2, res3) # 12, 3.0, 36
# 遍历解包赋值
for loss, aux in [('loss1', 'aux1'), ('loss2', 'aux2')]:
	print(loss, aux)
  • 索引。同列表
# 虽然元组是不可变的,但是其中包含的列表中的元素是可变的
tp = (9, 5, 2, ['legal', 'healthy'], 7)
tp[-2][0] = 'illegal'
print(tp) # (9, 5, 2, ['illegal', 'healthy'], 7)
  • 切片。同列表

2.3 常用操作符

由于无法对元组添加、删除操作,因此元组对象没有像列表中的那些方法。但拼接、删除可以通过操作符实现,不过不是对原元组对象操作,而是返回结果的新对象。

  • 拼接 +
tp = (9, 5) + (2, ['legal', 'healthy']) + (7,)
print(tp) # (9, 5, 2, ['legal', 'healthy'], 7)
  • * 重复
print(6 * (6, 8)) # (6, 8, 6, 8, 6, 8, 6, 8, 6, 8, 6, 8)
print(6 * 6,) # 36,   这样无法实现重复
  • 关系操作符。同列表
  • 成员资格符。同列表

2.4 常用方法

  • tuple.count(value) 。同列表
  • tuple.index(value, start=0, stop=9223372036854775807) 。同列表
  • 没有tuple.copy()方法,元组只能通过切片复制(浅拷贝)
new_tp = tp[:]
tp[-2][0] = 'illegal'
print(tp) # (9, 5, 2, ['illegal', 'healthy'], 7)
print(new_tp) # (9, 5, 2, ['illegal', 'healthy'], 7)

3. 字典

  • 使用花括号表示
  • 通过“键”-“值”的映射实现数据存储和访问,键是不可重复的
  • 无序的,属于映射类型
  • 键是不可变的,必须是可哈希对象,如数字、字符串、元组
{(1, 2): 'one_two', 3: 'three'} # 可
{[1, 2]: 'one_two', 3: 'three'} # 报错 TypeError

3.1 创建

  • 直接使用花括号
# 当出现相同大小的整数和浮点数作为键,保留浮点数键,其对于的值以后传入的为准
dc = {(1, 2): 666, 3.0: 'hi', 3: 'legal', 'string': 'healthy'}
print(dc) # {(1, 2): 666, 3.0: 'legal', 'string': 'healthy'}
  • dict(self) 函数,传入一个以二元组为元素的元组
dc = dict((((1, 2), 666), (3.0, 'hi'), (3, 'legal'), ('string', 'healthy')))
print(dc) # {(1, 2): 666, 3.0: 'legal', 'string': 'healthy'}

3.2 访问

  • 以键为索引,获得值
dc = {(1, 2): 666, 3: 'legal', 'string': 'healthy'}
print(dc[3]) # 'legal'
print(dc[0]) # 报错 KeyError
  • dict.get(key, default=None) ,若访问的键存在,则返回对于的值,若访问的键不存在,则向对象中添加相应的键,其对应的值为向 default 传入的值。不能以关键字参数形式传入
# 统计一个字符串中字符出现的频率
subject = 'ophthalmol'
dc = {}
for s in subject:
    dc[s] = dc.get(s, 0) + 1
print(dc) # {'o': 2, 'p': 1, 'h': 2, 't': 1, 'a': 1, 'l': 2, 'm': 1}
  • dict.setdefault(key, default=None) ,若访问的键存在,则返回对应的值;若访问的键不存在,则向对象中添加相应的键,其对应的值为向 default 传入的值,并和返回设定默认值。但不能以关键字参数形式传入
dc = {(1, 2): 666, 3: 'legal', 'string': 'healthy'}
dc.setdefault('new_key', 'new_item')
print(dc) # {(1, 2): 666, 3: 'legal', 'string': 'healthy', 'new_key': 'new_item'}
  • dict.keys() 返回一个可迭代对象,为所有的键
dc = {(1, 2): 666, 3: 'legal', 'string': 'healthy'}
print(dc.keys()) # dict_keys([(1, 2), 3, 'string'])
  • dict.values() 返回一个可迭代对象,内容为所有的值
dc = {(1, 2): 666, 3: 'legal', 'string': 'healthy'}
print(dc.values()) # dict_values([666, 'legal', 'healthy'])
  • dict.items() 返回一个可迭代对象,为所有的键与值组成的二元组
dc = {(1, 2): 666, 3: 'legal', 'string': 'healthy'}
print(dc.items()) # dict_items([((1, 2), 666), (3, 'legal'), ('string', 'healthy')])

3.3 修改

  • dict[key] = value 直接设定一个不存在的键,并赋一个值。(当键存在时,修改原有的值)
dc = {(1, 2): 666, 3: 'legal', 'string': 'healthy'}
dc[-1] = 'zero'
print(dc) # {(1, 2): 666, 3: 'legal', 'string': 'healthy', -1: 'zero'}
  • dict.update() ,将其他字典或以二元组为元素的可迭代序列连接到原字典对象中
# 加入其他字典对象的内容
dc = {(1, 2): 666, 3: 'legal', 'string': 'healthy'}
other_dc = {0: 'zero', 1: 'one'}
dc.update(other_dc)
print(dc) # {(1, 2): 666, 3: 'legal', 'string': 'healthy', 0: 'zero', 1: 'one'}

# 加入以二元组为元素的可迭代序列
dc = {(1, 2): 666, 3: 'legal', 'string': 'healthy'}
other_dc = [(0, 'zero'), (1, 'one')]
dc.update(other_dc)
print(dc) # {(1, 2): 666, 3: 'legal', 'string': 'healthy', 0: 'zero', 1: 'one'}
  • dict.pop(k) ,传入需要弹出的键-值对中的键,弹出对应的键
dc = {(1, 2): 666, 3: 'legal', 'string': 'healthy'}
pop_out = dc.pop(3)
print(pop_out) # legal
print(dc) # {(1, 2): 666, 'string': 'healthy'}
  • dict.popitem() ,随机弹出一个键值对,以元组形式返回
dc = {(1, 2): 666, 3: 'legal', 'string': 'healthy'}
pop_out = dc.popitem()
print(pop_out) # ('string', 'healthy')
print(dc) # {(1, 2): 666, 3: 'legal'}
  • dict.clear() ,清空字典对象
dc = {(1, 2): 666, 3: 'legal', 'string': 'healthy'}
dc.clear()
print(dc) # {}

3.4 常用操作符

  • 成员资格符,判断所给的键是否在字典中
dc = {(1, 2): 666, 3: 'legal', 'string': 'healthy'}
print((1, 2) in dc, 0 in dc) # True False

3.5 其他常用方法

  • dict.copy() ,浅拷贝
dc = {'number': 666, 0: 'zero', 'list': ['legal', 'healthy']}
new_dc = dc.copy()

dc['list'][0] = 'illegal'
print(new_dc) # {'number': 666, 0: 'zero', 'list': ['illegal', 'healthy']}

4. 集合

  • 用花括号表示
  • 一组互不相等的元素
  • 无序的
  • 元素是不可变的,必须是可哈希对象,如数字、字符串、元组,集合可以视为只有键没有值的字典

4.1 创建

  • 直接使用花括号
st = {(1, 2), 3, 'string'}
print(st) # {(1, 2), 3, 'string'}

# 注意,空集合无法直接创建
empty_st = {}
print(type(empty_st) # dict
  • set() 函数,空集合只能使用该函数创建
# 传入可迭代对象
st = set(((1, 2), 3, 'string'))
print(st) # {(1, 2), 3, 'string'}

# 创建空集合
empty_st = set()
print(empty_st) # set()

# 传入字典时,返回字典的键构成的集合
st = set({1:'one', 2:'two', 3:'three'})
print(st) # {1, 2, 3}

4.2 访问

无法索引,因此无法访问到指定元素

4.3 修改

  • set.add() 增加一个元素
st = {0, 1, 2, 3}
st.add(4)
print(st) # {0, 1, 2, 3, 4}
  • set.remove() 移除一个元素,若元素不存在,则报错
st = {0, 1, 2, 3}
st.remove(1) # 若元素不在集合中则报错 KeyError
print(st) # {0, 2, 3}
  • set.discard() 移除一个元素,若元素不存在,啥也不做
st = {0, 1, 2, 3}
st.discard(4)
print(st) # {0, 1, 2, 3}
  • set.pop() 移除并返回集合中的任意一个元素
st = {0, 1, 2, 3}
print(st.pop()) # 0
print(st) # {1, 2, 3}
  • set.clear() 清空集合,使之成为空集
st = {0, 1, 2, 3}
st.clear()
print(st) # set()

4.4 常用操作符

  • 交集
st1 = {0, 1, 2}
st2 = {1, 2, 3}
print(st1 & st2) # {1, 2}
  • 并集
st1 = {0, 1, 2}
st2 = {1, 2, 3}
print(st1 | st2) # {0, 1, 2, 3}
  • 差集
st1 = {0, 1, 2}
st2 = {1, 2, 3}
print(st1 - st2) # {0}
  • 对称差分
st1 = {0, 1, 2}
st2 = {1, 2, 3}
print(st1 ^ st2) # {0, 3}
  • <=< 一个集合是另一个的子集,后者更严格(还要求两集合不相同)
st1 = {0, 1, 2, 3}
st2 = {0, 1, 2, 3}
st3 = {0, 1, 2, 3, 4}

print(st1 <= st2, st1 < st2) # True, False
print(st1 <= st3, st1 < st3) # True, True
  • >=> 一个集合是另一个的超集,后者更严格(还要求两集合不相同)
st1 = {0, 1, 2, 3, 4}
st2 = {0, 1, 2, 3, 4}
st3 = {0, 1, 2, 3}

print(st1 >= st2, st1 > st2) # True, False
print(st1 >= st3, st1 > st3) # True, True

5. 解析式

解析式(comprehension),又称推导式,使生成列表、字典、集合的代码更加简洁。此外,还有生成器解析式。

5.1 列表解析式

又称列表推导式,其语法为 [expression for value in iterable if condition],其中 if 条件为可选。(if 分支和 for 迭代结构参考 Python基础(四)控制结构

  • 一维
# 生成一个列表,其元素为 10 以内的偶数的平方
[i**2 for i in range(10) if i % 2 == 0]
# [0, 4, 16, 36, 64]
  • 多维
# 创建 4 x 3 的二维列表
[[0] * 3 for _ in range(4)]
# [[0, 0, 0]
#  [0, 0, 0]
#  [0, 0, 0]
#  [0, 0, 0]]

5.2 字典解析式

语法为 {key: value for (key, value) in iterable if condition},其中 if 条件为可选。

{i: i + 1 for i in range(3)}
# {0: 1, 1: 2, 2: 3}

5.3 集合解析式

语法为 {value for value in iterable if condition},其中 if 条件为可选。

# 生成 10 以内的奇数的集合
{i for i in range(10) if i % 2 != 0}
# {1, 3, 5, 7, 9}

5.4 生成器解析式

解析式的创建是一个变化的过程,而元组是不可变的,因此没有元组推导式。尝试解析式的语法无法得到元组,而是将得到生成器(generator)。生成器的相关内容参考Python基础(十)深浅拷贝与三大器 Part 3

gen = (i for i in range(5))
type(gen) # <class 'generator'>

6. 数据存储方式

6.1 列表与元组的区别

列表与元组的相同之处为,其中的元素实际上是地址,引用的内容分散地存放在内存其他位置,保证了这些组合数据类型能够容纳不同类型的数据作为元素。

它们之间的区别之一为,同样的长度下,列表所需要的存储空间比元组更大(使用 sys.getsizeof 函数查看对象的占用的内存,以 byte 为单位)

import sys

ls = []
print(sys.getsizeof(ls)) # 64

tp = ()
print(sys.getsizeof(tp)) # 48
import sys

ls = [1, 'two', {3: 'three'}]
print(sys.getsizeof(ls)) # 88

tp = (1, 'two', {3: 'three'})
print(sys.getsizeof(tp)) # 72

原因在于,由于列表是可变的,系统总会为其额外分配一些内存(over-allocating),而对于元组并不会

ls = []
for i in range(10):
    print(sys.getsizeof(ls))
    ls.append(i)
# 64
# 96
# 96
# 96
# 96
# 128
# 128
# 128
# 128
# 192

tp = ()
for i in range(10):
    print(sys.getsizeof(tp))
    tp += (i,)
# 48
# 56
# 64
# 72
# 80
# 88
# 96
# 104
# 112
# 120

6.2 字典与列表的区别

字典较列表的索引速度更快,原因在于,字典中元素的存储是稀疏的,使用哈希函数的方式通过键对值进行索引,而列表中地址元素的存储是连续的,每次索引都需要从第一个元素开始。

列表和字典的索引速度差别

import time

ls = list(range(30000))
ls_ = list(range(30000))

since = time.time()

count = 0
for each in ls_:
    if each in ls:
        count += 1

print('time:', time.time() - since)
# time: 3.642605781555176
import time

dc = {i:i for i in range(30000)}
ls_ = list(range(30000))

since = time.time()

count = 0
for each in ls_:
    if each in dc:
        count += 1

print('time:', time.time() - since)
# time: 0.0056841373443603516

哈希函数的内容参考fishc的你知道 Python 的字典(Dict)是如何存储的吗

所以

  • 字典是无序的,hash 值对应项的顺序与字典项显示的顺序可能不同

  • 字典需要的空间比列表大,实现了以空间换时间

ls = [1, 2, 3]
print(sys.getsizeof(ls)) # 88

dc = {0: 1, 1: 2, 2: 3}
print(sys.getsizeof(dc)) # 240
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值