列表、元组

本篇文章主要记录学习Python序列类型。

我们了解到的Python中的数据结构可能有:字符串、列表、字节序列、数据、XML元素等。

它们的共同点都有一套厉害的操作:迭代、切片、排序、还有拼接。

本篇文章目录结构如下:

1. 了解Python中的内置序列类型

  • 容器序列

容器序列存放任意类型的对象的引用

list、tuple、collections.deque(存放不同类型的数据)

  • 扁平序列

扁平序列存放的是值而不是引用。换句话说,扁平序列其实是一段连续的内存空间。由此可见扁平序列其实更加紧凑,但是它里面只能存放字符、字节和数值这种基础类型

str、bytes、bytearray、memoryview、array.array(只能存放一种类型)

按照能否修改来分类

  • 可变序列(MutableSequence)

list、bytearray、memoryview、array.array、collections.deque

  • 不可变序列(Sequence)

tuple、str、bytes

2. 列表推导和生成器表达式

list comprehension 和generator expression

使用它们写出的代码,更具有可读性且高效的

2.1 列表推导

# 把一个字符串编程Unicode码位的列表
symbols = ‘hello world!‘
codes = []
for symbol in symbols:
    codes.append(ord(symbol))  # 把ord方法把字符转换位ASCII,https://baike.baidu.com/item/ASCII/309296
print(codes)  # output: [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33]
# 换成列表推导写法
symbols = ‘hello world!‘
codes_list = [ord(symbol) for symbol in symbols]
print(codes_list)  # output: [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33]

# 字典的写法
codes_dict = {symbol: ord(symbol) for symbol in symbols}
print(codes_dict)

# 元组的写法
codes_tuple1 = tuple(ord(symbol) for symbol in symbols)
print(codes_tuple1)  # output: (104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33)

codes_tuple = (ord(symbol) for symbol in symbols)
# codes_tuple变成一个生成器了,列表推导一般不太会这样写,除非你想生成一个生成器表达式
print(codes_tuple)  # output: <generator object <genexpr> at 0x000001E524538C10>
# 使用list对它转换
print(list(codes_tuple))  # output: [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33]

从上面两个例子,很容易看出来列表推导写法读起来更方便,平时我们看到更多的是列表推导,其实字典、元组也可以的。

列表推导可以帮助我们把一个序列或其他可迭代类型中的元素过来或是加工,然后再新建一个列表

列表推导的作用只有一个:生成列表。如果像生成其他序列类型,就要使用到生成器表达式。

2.2 生成器表达式

生成器表达式背后遵守了迭代器协议,利用next方法可以逐个地产出元素,直至产完元素后引发Stopiteration异常。

它与列表推送区别在于:前者不会建立一个完整的列表,然后把这个列表传递给某个变量或者某个构造函数里,很显然地看出生成器表达能够节省内存。

生成器表达式的语法跟列表推导差不多,只不过把方括号换成圆括号而已,前面的例子中codes_tuple变量赋值前就是一个生成器表达式,我们再以那个例子来演示。

symbols = ‘hello world!‘
codes_tuple = (ord(symbol) for symbol in symbols)  # 由括号围起来这个就是一个生成器表达式,赋值给codes_tuple变成了一个生成器
for i in codes_tuple: #1
    print(i)
# 这里把输出省略了

上面代码中的#1,生成器表达式逐个产出元素,我们先知道这样的写法是可以避免额外的内存占用。

3. 列表和元组

3.1 列表和元素的方法和属性

那些由object类支持的方法没有列出来

列表 元组
s._add_(s2) ? ? s + s2,拼接
s._iadd_(s2) ? s += s2,就地拼接
s.append(e) ? 在尾部添加一个新元素
s.clear() ? 删除所有元素
s.copy() ? 列表的浅复制
s._contains_(e) ? ? s是否包含e
s.count() ? ? e在s中出现的次数
s._delitem_(p) ? 把位于p的元素删除
s.extend(it) ? 把可迭代对象it追加给s
s._getitem_(p) ? ? s[p],获取位置p的元素
s._getnewargs_() ? 在pickle中支持更加优化的序列化
s.index(e) ? ? 在s中找到元素e第一次出现的位置
s.insert(p, e) ? 在位置p至之前插入元素e
s._iter_() ? ? 获取s的迭代器
s._len_() ? ? len(s),元素的数量
s._mul_(n) ? ? s * n,n个s的重复拼接
s._imul_(n) ? s *= n,就地重复拼接
s._rmul_(n) ? ? n * s,反向拼接
s.pop([p]) ? 删除最后或者是(可选的)位于p的元素,并返回它的值
s.remove(e) ? 删除s中的第一次出现的e
s.reverse() ? 就地把s的元素倒序排列
s._reversed_() ? 返回s的倒序迭代器
s._setitem_(p, e) ? s[p] = e,把元素e放在位置p,替代已经在那个位置的元素
s.sort([key], [reverse]) ? 就地对s中的元素进行排序,可选的参数由键(key)和是否倒序(reverse)

3.2 切片

在python里,像列表(list)、元组(tuple)和字符串(str)都支持切片操作。

我们先来了解切片公式如下代码标出:

s[a:b:c]
# 这句话意思就是对s在a和b之间以c为间隔取值,而且它们还可以为负值
# s为切片对象,可以是字符串,列表,元组

# 示例
a = [1, 2, 3, 4, 5, 6]
print(a[-3])     # output: 4             # 取倒数第三个元素
print(a[-3:-1])  # output: [4, 5]        # 取倒数第三个元素到倒数第一个元素之前
print(a[::-2])   # output: [6, 4, 2]     # 反向开始取间隔为2的元素
3.2.1 给切片赋值

把赋值放在赋值语句的右边,或把它作为del操作的对象,就可以对序列进行嫁接、切除或就地修改。

b = [1, 2, 3, 4, 5, 6]
# 赋值对象不是一个切片时
b[-3] = -333
print(b)  # output: [1, 2, 3, -333, 5, 6]

# 赋值对象是一个切片时
b[1:3] = [100]  # 如果赋值对象是一个切片,那么赋值语句右边必须是一个可迭代对象,否则会报TypeError错误
print(b)  # output:[1, 100, -333, 5, 6]

# 使用del删除
del b[:3]  # 删除列表b前三个元素
print(b)  # output: [5, 6]
如何理解切片对象?

其实这块内容我自己也没深入了解太多,暂时的,我把类似如下这样的写法理解为一个切片对象

s[a:b:c],s[:b:c],s[::c],s[a],对第一个作出解释:s在a和b之间以c为间隔取值

  1. 对于单个索引s[a],正如上面的代码中给出的b[-3] = -333,容易看出给单个索引赋值了一个整数,如果你想赋值其它类型也行,比如function等等

  2. 对于s[a:b:c],s[:b:c],s[::c]这样的写法,语句右侧必须赋值是一个可迭代对象

  • 看完上面还是不能理解切片对象的话,我们引入slice,翻译过来”片",python中它就是一个切片对象
print(slice.__doc__)  # 查看slice方法说明
"""
slice(stop)
slice(start, stop[, step])

Create a slice object.  This is used for extended slicing (e.g. a[0:10:2]).
"""
c = [1, 2, 3, 4, 5, 6]
s1 = slice(-3) # 新建一个切片对象赋值给s1,slice(-3) 等同于 s[:-3],这里的s是序列
print(s1)      # output: slice(None, -3, None)
print(c[s1])   # output: [1, 2, 3] # s1就是一个切片对象
print(c[:-3])  # output: [1, 2, 3] # [:-3]就一个切片对象

看完这里,我猜你大概对于切片对象有少许头绪了吧?

最后总结下:a:b:c作为索引被[]包围起来返回就一个切片对象,同理的,slice(a,b,c)也是一个切片对象

正如上面代码中s1和[:-3],这两都是一个切片对象

3.3 列表拼接

前面我们说了切片赋值,简单演示了del操作,这一节了解下列表拼接、追加

d = [1, 2, 3, 4, 5, 6]
e = [‘a‘, ‘b‘]
# 简单的拼接
print(d.__add__(e))  # output: [1, 2, 3, 4, 5, 6, ‘a‘, ‘b‘]
print(d)             # output: [1, 2, 3, 4, 5, 6]
print(id(d))         # output:2551295785280

# 就地拼接
print(d.__iadd__(e)) # output: [1, 2, 3, 4, 5, 6, ‘a‘, ‘b‘]
print(d)             # output: [1, 2, 3, 4, 5, 6, ‘a‘, ‘b‘]
print(id(d))         # output:2551295785280

# 追加(就地拼接)
d.extend(e)
print(d)             # output: [1, 2, 3, 4, 5, 6, ‘a‘, ‘b‘, ‘a‘, ‘b‘]
print(id(d))         # output:2551295785280

3.4 list.sort方法和内置函数sorted

list.sort方法会就地排序,也就是说不会把原列表赋值一份,这也是这个方法的返回值是None的原因。

在这种情况下返回None其实是Python的一个惯例:出自【流畅的python】

如果一个函数或者方法对对象进行的是就地改动,那它就应该返回None,好让调用者知道传入的参数发生了改动,而且并未产生新的对象。

例如,random.shuffle函数也遵守了这个惯例。

与list.sort相反的是内置函数sorted,它会新建一个列表作为返回值。

这个方法可以接受任何形式的可迭代对象作为参数,甚至包括不可变序列或生成器。而不管sorted接受的是怎么样的参数,它最后都会返回一个列表。

"""
list.sort方法和sorted函数,都有两个可选的关键字参数。
reverse
	如果等于True,则按降序排序,默认值是False
key
	一个参数函数,这个函数会被应用在序列里的每一个元素,所产生的结果将是排序算法依赖的关键
"""

# 示例

fruits = [‘longan‘, ‘apple‘, ‘banana‘, ‘watermelon‘]  # 开一间水果店,里面有龙眼,苹果,香蕉,西瓜
# 就地排序,改变了原有顺序, 按照水果名字长度排序
list.sort(fruits, key=len)
print(fruits)  # output: [‘apple‘, ‘longan‘, ‘banana‘, ‘watermelon‘]
# 换成下面这样写也行,就地排序
fruits.sort()
print(fruits)  # output: [‘apple‘, ‘longan‘, ‘banana‘, ‘watermelon‘]

# 改变水果店摆放水平的顺序
print(sorted(fruits, reverse=True))  # output: [‘watermelon‘, ‘longan‘, ‘banana‘, ‘apple‘]

mobile_phone = {‘huawei p50‘: 4488, ‘xiaomi 10‘: 3999, ‘iphone 12‘: 5499, ‘oppo x3‘: 4499}  # 开一间手机店

# 按照手机店里的价格从高到低排序
print(sorted(mobile_phone.items(), key=lambda x: x[1], reverse=True))
# output: [(‘iphone 12‘, 5499), (‘oppo x3‘, 4499), (‘huawei p50‘, 4488), (‘xiaomi 10‘, 3999)]

3.5 元组拆包

元组拆包就是平行赋值,就是把一个可迭代对象里的元素,一并赋值到由对应的变量组成的元组中

now_date = (2021, 7, 31)
year, month, date = now_date  # 元组拆包
print(year, month, date)  # output: 2021 7 31

# 用*来处理剩下的元素
year, *args = now_date
print(year, args)  # output: 2021 [7, 31]

# 用*来处理所有元素 
print(*now_date)  # output: 2021 7 31

在Python中,函数用*args来获取不确定数量的参数已经很流行了。

*作为前缀在一个变量名前面,这个变量可以出现在赋值表达式的任意位置。

3.6 带名字的元组

collections.nametuple是一个工厂函数,它可以为元组的每个位置分配一个名字,更具有可读性。

"""
collections.namedtuple(typename , field_names , * , rename=False , defaults=None , module=None ) ?
	typename是一个类名
	field_names是个位置对应的字段,它可以是序列比如[‘x‘, ‘y‘]或者单个字符串由空格或逗号分隔比如‘x y‘或‘x,y‘
	rename=True,则把无效的字段名自动替换为位置(索引)名称,无效字段比如:def,重复出现的字段名
"""
一副无大小王的扑克牌
from collections import namedtuple
from random import choice

Card = namedtuple(‘Card‘, [‘点‘, ‘花色‘])  # 扑克牌花色


class Poker:
    ranks = [str(i) for i in range(2, 11)] + list(‘JQKA‘)
    suits = [‘黑桃‘, ‘红心‘, ‘梅花‘, ‘方块‘]

    def __init__(self):
        self._card = [Card(rank, suit) for rank in self.ranks for suit in self.suits]

    def __len__(self):
        """
        自定特殊方法,使得像len()函数那样查看该类
        """
        return len(self._card)

    def __getitem__(self, item):
        """
        自动支持切片操作
        """
        return self._card[item]

p = Poker()
print(len(p))    # 扑克牌有多少张 output: 52
print(p[1])      # 抽取特定的纸牌 output: Card(点=‘2‘, 花色=‘红心‘)
print(choice(p)) # 随机抽取一张牌 output: Card(点=‘6‘, 花色=‘红心‘)

"""
这里为什么可以直接使用choice?--> 避免重新造*。
查看源码:
    def choice(self, seq):
        # Choose a random element from a non-empty sequence.
        # raises IndexError if seq is empty
        return seq[self._randbelow(len(seq))]
很显然seq在这里就是充当Card类的实例对象p,当导入后变成:
p[self._randbelow(len(p))] --> p[x],x随机返回一张未洗牌前的扑克牌位置
p[x] --> 抽取特定的纸牌
"""

上面例子展示了如何实现__getitem__和__len__这两个特殊方法。

  • 如何使用特殊方法

特殊方法的存在是为了被Python解释器调用的,无需我们亲手去调用它们。

也就是说没有my_object._len_()这种写法,而直接使用len(my_object),它是会报错的。

如果my_object是一个自定义类的对象,那么Python会自己去调用由我们亲手实现的__len__方法。

很多时候,特殊方法的调用是隐式的,比如for i in x:这个语句,背后其实用的是iter(x),而这个函数的背后则是x._iter_()方法。

4. 总结

这篇文章主要讲述了Python标准库中部分序列类型,同时也略过了很多内容,还有文中只字未提的数组array、双向队列deque等等。

列表推导和生成器表达式需要熟练地使用它们,在很多时候,当你阅读别人的代码时,会经常碰到它,因为它真的异常强大。

最后以一副无大小王的扑克牌案例,加深对特殊方法的理解,尽管它可能与这篇文章的主题不太相关,但我觉得也很有必要提前了解。

与君共勉!

列表、元组

上一篇:模型权重的保存与加载 回调函数的使用


下一篇:常见DOS操作