跳转至

Python的集合框架

collections官网翻译为容器数据类型,在Java中称为集合框架,名字不同而已。Python内置了一些数据类型,dict , list , set , 和 tuple。某些情况下这些数据容器不够用,我们得继承基本数据容器,然后实现定制化的功能。比如原始的字典是无序的,假如需要有序的字典怎么办?集合框架collections就提供了OrderedDict这样的数据容器。除此之外集合框架提供了以下类型:

名称 含义
namedtuple() 创建命名元组子类的工厂函数
deque 类似列表(list)的容器,实现了在两端快速添加(append)和弹出(pop)
ChainMap 类似字典(dict)的容器类,将多个映射集合到一个视图里面
Counter 字典的子类,提供了可哈希对象的计数功能
OrderedDict 字典的子类,保存了他们被添加的顺序
defaultdict 字典的子类,提供了一个工厂函数,为字典查询提供一个默认值 |
UserDict 封装了字典对象,简化了字典子类化
UserList 封装了列表对象,简化了列表子类化
UserString 封装了列表对象,简化了字符串子类化

namedtuple 命名元组

tuple创建以后是不可更改的,访问tuple只能使用下标索引的方式。凡是用Python连接过数据库的都知道,查询数据库返回的结果是tuple类型。如果字段较多,超过5个字段,操作就有点麻烦。

如果使用ORM框架可以很好的解决这一痛点。类直接映射表,类的属性就是表的字段,用“点”就能补全出要使用的字段名称。除此之外我们还可以用namedtuple。尤其是创建CSV文件和读取数据库时非常有效。

我们先来看看namedtuple函数的基本使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>> Point = namedtuple('Point', ['x', 'y'])
>>> p = Point(11, y=22)     # instantiate with positional or keyword arguments
>>> p[0] + p[1]             # indexable like the plain tuple (11, 22)
33
>>> x, y = p                # unpack like a regular tuple
>>> x, y
(11, 22)
>>> p.x + p.y               # fields also accessible by name
33
>>> p                       # readable __repr__ with a name=value style
Point(x=11, y=22)
>>> p._asdict()             # 转换为字典
{'x': 11, 'y': 22}

接下来模拟数据库查询,遍历返回结果,会发现操作每条数据就像操作对象一样方便。这里会用到方法_make()创建新的实例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from collections import namedtuple

# 数据库返回结果
data = (
    ('Tom', 12, 'tom@qq.com'),
    ('Jim', 15, 'jim@163.com'),
    ('Ana', 13, 'Ana@126.com')
)

User = namedtuple('User', ['name', 'age', 'email'])

# 类型声明
user: User

for user in map(User._make, data):
    print(f"{user.name}, {user.age}, {user.email}")

# 输出
# Tom, 12, tom@qq.com
# Jim, 15, jim@163.com
# Ana, 13, Ana@126.com

读取CSV文件和数据库的使用,跟上一个例子差不多,下面给出源码

EmployeeRecord = namedtuple('EmployeeRecord', 'name, age, title, department, paygrade')

# 最好做类型声明,使用的时候可以点出属性
emp: EmployeeRecord

import csv
for emp in map(EmployeeRecord._make, csv.reader(open("employees.csv", "rb"))):
    print(emp.name, emp.title)

import sqlite3
conn = sqlite3.connect('/companydata')
cursor = conn.cursor()
cursor.execute('SELECT name, age, title, department, paygrade FROM employees')
for emp in map(EmployeeRecord._make, cursor.fetchall()):
    print(emp.name, emp.title)

计数器Counter

作为计数器对象,Counter非常方便,提供了几个统计方法非常好用。我们先来看看普通计数器的使用:

1
2
3
4
5
6
>>> from collections import Counter 
>>> cnt = Counter()
>>> for word in ['red', 'blue', 'red', 'green', 'blue', 'blue']:
...     cnt[word] += 1 # 不需要考虑key是否覆盖
>>> cnt
Counter({'blue': 3, 'red': 2, 'green': 1})

除了创建空的计数器,还可以创建初始化参数,参数就是要统计的对象,字符串、列表、或者字典等可迭代类型。

1
2
3
4
5
6
7
8
    >>> c = Counter('abcdeabcdabcaba')  # count elements from a string

    >>> c.most_common(3)                # three most common elements
    [('a', 5), ('b', 4), ('c', 3)]
    >>> sorted(c)                       # list all unique elements
    ['a', 'b', 'c', 'd', 'e']
    >>> ''.join(sorted(c.elements()))   # list elements with repetitions
    'aaaaabbbbcccdde'

默认字典defaultdict

访问字典key的时候会碰到一个问题,如果key存在就正常返回,如果key不存在则抛出KeyError的异常。通常使用字典的get方法,如果getkey不存在,默认值可以自定义,但这只能解决不抛出KeyError的异常。其余问题还是不好处理。

比如我们有一个清单是这样的,颜色使用次数。(你也可以理解成用户的购物明细数据)

1
2
3
4
5
6
7
s = [
    ('yellow', 1),
    ('blue', 2),
    ('yellow', 3),
    ('blue', 4),
    ('red', 1)
]

如果要统计出每种颜色使用了多少次,大致的思路是创建结果字典,遍历s,碰到新的key就直接赋值,碰到已经存在的key,value值相加就可以了。当然也可以用上面的Counter类,判断的步骤都省了。

现在需求变了,需要把每种颜色使用的次数放在列表里。就是把相同的key的value合并在一起,不是加在一起了。当然你肯定也有办法做到。这里我们使用defaultdict使得代码更加pythonic

1
2
3
4
5
6
7
s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]
d = defaultdict(list) # 默认值为list
for k, v in s:
    d[k].append(v) # 直接append

sorted(d.items())
[('blue', [2, 4]), ('red', [1]), ('yellow', [1, 3])]

上面的例子defaultdict的默认值是list,也可以是整数,其实是调用了int()

1
2
3
4
5
6
7
s = 'mississippi'
d = defaultdict(int) # 默认整数,初始值为0
for k in s:
    d[k] += 1

sorted(d.items())
[('i', 4), ('m', 1), ('p', 2), ('s', 4)]

如果需要自定义默认数据怎么做呢?我们需要清楚defaultdict初始化的原理。查看源码,我们发现有一个参数default_factory来控制的。这个参数的取值可以是None,或者是callable可调用的类型。这就非常简单了,default_factory参数传一个函数就行了。

1
2
3
4
5
6
7
8
>>> x = defaultdict(lambda : 2) #  创建默认值为2
>>>x['a']
2
>>> x['b']
2
>>> y = defaultdict(lambda : [2]) # 创建默认值为[2]
>>> y['t']
[2]

总结

Python集合框架提供的这几种数据容器,并不是必须的,实现某些功能也可以不用这些数据容器,如果业务量比较大,执行效率可能不是很高。本文只介绍了Python的集合框架中的3种数据容器,这3种容器相对麻烦一点,使用场景很多,灵活运用这些数据容器,可以使得你程序更加高效和pythonic。

注:本文参考Python3官方文档,部分例子也来自官方文档。

关注我

评论