Day 25:Python 最被低估的模块 collections 3 个常用类总结及案例解读
发布日期:2022年3月12日 18:02 阅读: 284 访问: 284
Python 自带许多很好的模块(libraries),能够非常方便的解决一些实际问题。而选择对合适的函数,使用好某些模块,能帮助我们少些很多行代码。今天,与大家一起学习一个非常有用的模块——collections。这个模块,提供与容器相关的、更高性能的数据类型,它们比通用容器 dict、list、set 和 tuple 更强大。主要介
Python 自带许多很好的模块(libraries),能够非常方便的解决一些实际问题。
而选择对合适的函数,使用好某些模块,能帮助我们少些很多行代码。
今天,与大家一起学习一个非常有用的模块——collections。
这个模块,提供与容器相关的、更高性能的数据类型,它们比通用容器 dict、list、set 和 tuple 更强大。
主要介绍,collections 模块常用的 3 种数据类型。
NamedTuple
对于数据分析或机器学习领域,用好 NamedTuples 会写出可读性更强、更易于维护的代码。
大家回忆这种熟悉的场景。
你正在做特征工程,比较偏爱 list,所以把特征都放到一个 list 中。
然后,喂到机器学习模型中。
很快,你将会意识到数百个特征位于此 list 中,这就是事情变糟糕的开始。
如下,把如下 14 个特征,装到一个 list 类型的变量 features 中:
In [5]: features = ['id','age','height','name','address','province','city','town','country','birth_address','father_name', 'monther_name','telephone','emergency_telephone']
假设,我负责维护某乡村,上千行居民信息。
现在有个新任务,刚刚调查过户口,有了一份新数据,现在要比较下,哪些居民的居住地址(对应字段 address)、联系电话(对应字段 telephone)、出生地信息(对应字段 birth address)发生了变化,统计出这些居民。
先确定,三个字段在 features 中的索引;然后,导入老数据,刚统计的居民数据;比较三个字段,只要一个有不同,就认为有变化,并装入到信息变化的居民列表中。
很快,写出下面的代码:
def update_persons_info(old_data,new_data):
# 假定老数据,新数据 id 按照行都卡对好。
changed_list = []
for line in new_data:
new_props = line.split() # 使用空格分隔新版行数据
for old in old_data:
old_props = old.split() # 使用空格分隔老版行数据
if old_props[11] != new_props[10]: # 假定新版数据中 telephone 的索引为 10;
changed_list.append(old.props[11])
elif old_props[4] != new_props[6]: # 假定新版数据中 address 的索引为 6;
changed_list.append(old.props[11])
elif old_props[9] != new_props[3]: # 假定新版数据中 birth_address 的索引为 3;
changed_list.append(old.props[11])
return changed_list
以上代码,出现整数索引 0、3、4、6、10、11,代码可读性比较差,如果没有注释,可能日后我都不知道这些索引代表什么意思。
但是,如果使用 NamedTuple 去处理,乱为一团的事情,将会迅速变得井然有序。
再也不会为一系列的整数索引而犯愁!
先了解 NamedTuple 的基本使用,导入 NamedTuple 包,如下:
In [14]: from collections import namedtuple
创建一个带有 14 个属性,名字为 Person 的 NamedTuple 实例 Person,如下:
In [18]: Person = namedtuple('Person',['id','age','height','name','address','province','city','town','country','birth_address','father_name', 'monther_name','telephone','emergency_telephone'])
如下,调用实例 Person,创建一个 id=3 的 Person 对象:
In [29]: a = ['']*11
In [30]: Person(3,19,'xiaoming',*a)
Out[30]: Person(id=3, age=19, height='xiaoming', name='', address='', province='', city='', town='', country='', birth_address='', father_name='', monther_name='', telephone='', emergency_telephone='')
使用 NamedTuple 重新改写上面的任务:
def update_persons_info(old_data,new_data):
changed_list = []
for line in new_data:
new_props = line.split()
new_person = Person(new_props) # new_props 与 Person 参数卡对好
for old in old_data:
old_props = old.split()
old_person = Person(old_props)
if old_person.id != new_person.id:
changed_list.append(old_person.id)
elif old_person.address != new_person.address:
changed_list.append(old_person.address)
elif old_person.birth_address != new_person.birth_address:
changed_list.append(old_person.birth_address)
return changed_list
效果对比明显,改后的代码,3 处条件比较地方,没有用到一个整数索引,提高了代码可读性。
同时,也增强了代码的可维护性。当新导入的文件,特征列的顺序与原来不一致时,无需改动那 3 处条件比较之处,但是原来版本就必须要修改,相对更繁琐,不好被维护。
以上所述,NamedTuple 优点明显,但是同样缺点也较为明显,一个 NamedTuple 创建后,它的属性取值不允许被修改,也就是属性只能是可读的。
如下,xiaoming 一旦创建后,所有属性都不允许被修改。
In [33]: Person = namedtuple('Person',['id','age','height','name','address','province','ci
...: ty','town','country','birth_address','father_name', 'monther_name','telephone','e
...: mergency_telephone'])
In [34]: a = ['']*11
In [35]: xiaoming = Person(3,19,'xiaoming',*a)
In [36]: xiaoming.age = 20
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-36-14208077a3a9> in <module>
----> 1 xiaoming.age = 20
AttributeError: can't set attribute
Counter
Counter 正如名字那样,它的主要功能就是计数。我们在分析数据时,基本都会涉及计数,真的家常便饭。
习惯使用 list 的朋友,往往会这样统计:
sku_purchase = [3, 8, 3, 10, 3, 3, 1, 3, 7, 6, 1, 2, 7, 0, 7, 9, 1, 5, 1, 0]
d = {}
for i in sku_purchase:
if d.get(i) is None:
d[i] = 1
else:
d[i] += 1
d_most = dict(sorted(d.items(), key=lambda item: item[1], reverse=True))
print(d_most)
# 最受欢迎的商品(键为商品编号),排序结果:
# {3: 5, 1: 4, 7: 3, 0: 2, 8: 1, 10: 1, 6: 1, 2: 1, 9: 1, 5: 1}
但是,如果使用 Counter,能写出更加简化的代码。
首先,导入 Counter 类:
In [35]: from collections import Counter
然后,使用一行代码搞定:
In [42]: Counter(sku_purchase).most_common()
Out[42]:
[(3, 5),(1, 4),(7, 3),(0, 2),(8, 1),(10, 1),(6, 1),(2, 1),(9, 1),(5, 1)]
仅仅一行代码,便输出统计结果。并且,输出按照购买次数的由大到小排序好的列表,比如,编号为3 的商品最受欢迎,一共购买了 5 次。
除此之外,使用 Counter 能快速统计,一句话中单词出现次数,一个单词中字符出现次数。如下所示:
In [41]: Counter('i love python so much').most_common()
Out[41]:
[(' ', 4),
('o', 3),
('h', 2),
('i', 1),
('l', 1),
('v', 1),
('e', 1),
('p', 1),
('y', 1),
('t', 1),
('n', 1),
('s', 1),
('m', 1),
('u', 1),
('c', 1)]
DefaultDict
DefaultDict 能自动创建一个被初始化的字典,也就是每个键都已经被访问过一次。
首先,导入 DefaultDict:
In [44]: from collections import defaultdict
创建一个字典值类型为 int 的默认字典:
In [45]: d = defaultdict(int)
创建一个字典值类型为 list 的默认字典:
In [46]: d = defaultdict(list)
In [47]: d
Out[47]: defaultdict(list, {})
统计下面字符串:
from collections import defaultdict
每个字符出现的位置索引:
d = defaultdict(list)
s = 'from collections import defaultdict'
for index,i in enumerate(s):
d[i].append(index)
print(d)
defaultdict(<class 'list'>, {'f': [0, 26], 'r': [1, 21], 'o': [2, 6, 13, 20], 'm': [3, 18], ' ': [4, 16, 23], 'c': [5, 10, 33], 'l': [7, 8, 29], 'e': [9, 25], 't': [11, 22, 30, 34], 'i': [12, 17, 32], 'n': [14], 's': [15], 'p': [19], 'd': [24, 31], 'a': [27], 'u': [28]})
当尝试访问一个不在字典中的键时,将会抛出一个异常。但是,使用 DefaultDict 帮助我们初始化。
如果不使用 DefaultDict,就需要写 if -else
逻辑。
如果键不在字典中,手动初始化一个列表 [],并放入第一个元素——字符的索引 index。就像下面这样:
d = {}
s = 'from collections import defaultdict'
for index,i in enumerate(s):
if i in d:
d[i].append(index)
else:
d[i] = [index]
print(d)
# 结果如下:
{'f': [0, 26], 'r': [1, 21], 'o': [2, 6, 13, 20], 'm': [3, 18], ' ': [4, 16, 23], 'c': [5, 10, 33], 'l': [7, 8, 29], 'e': [9, 25], 't': [11, 22, 30, 34], 'i': [12, 17, 32], 'n': [14], 's': [15], 'p': [19], 'd': [24, 31], 'a': [27], 'u': [28]}
虽然也能得到同样结果,但是很显然,使用 DefaultDict,代码更加简洁。
排序词
排序词(permutation):两个字符串含有相同字符,但字符顺序不同。
from collections import defaultdict
def is_permutation(str1, str2):
if str1 is None or str2 is None:
return False
if len(str1) != len(str2):
return False
unq_s1 = defaultdict(int)
unq_s2 = defaultdict(int)
for c1 in str1:
unq_s1[c1] += 1
for c2 in str2:
unq_s2[c2] += 1
return unq_s1 == unq_s2
defaultdict,字典值默认类型初始化为 int,计数默次数都为 0。
统计出的两个 defaultdict:unq_s1、unq_s2,如果相等,就表明 str1、str2 互为排序词。
下面,测试:
r = is_permutation('nice', 'cine')
print(r) # True
r = is_permutation('', '')
print(r) # True
r = is_permutation('', None)
print(r) # False
r = is_permutation('work', 'woo')
print(r) # False
单词频次
使用 yield 解耦数据读取 python_read 和数据处理 process。
- python_read:逐行读入
- process:正则替换掉空字符,并使用空格,分隔字符串,保存到 defaultdict 对象中。
from collections import Counter, defaultdict
import re
def python_read(filename):
with open(filename, 'r', encoding='utf-8') as f:
for line in f:
yield line
d = defaultdict(int)
def process(line):
for word in re.sub('\W+', " ", line).split():
d[word] += 1
调用两个函数,使用 Counter 类统计出频次的排序:
for line in python_read('test.txt'):
process(line)
frequency = Counter(d).most_common()
print(frequency)
小结
list、tuple、set、dict 等,对大家来说都比较熟知。
今天,与大家一起学习,比它们更强大的三种数据类型:
- NamedTuple:替换整数索引,使用可读性更好的字符串
- Counter:快速计数
- DefaultDict:默认初始化某类型的字典值