数据结构与算法 (03)
继续来进行 pythonCookbook. 学习大佬代码使我快乐, 与其自己写, 还不如直接抄, 然后加上自己的注解, 这样才是最适合我的学习之道哇.
仰之弥高,钻之弥坚
老祖宗还是厉害呀, 要是我能认识到这种境界, 夫复何求呦.
序列元素去重并保持顺序
需求
在一个序列上, 保持元素顺序的前提下, 对相同元素值进行去重
方案
如果序列中的值都是 hashable 类型的, 那直接用 集合 set 或者 生成器 就轻松解决了.
def filter_dumplicate(items):
"""序列元素去重"""
seen = set()
for item in items:
if item not in seen:
# 所有的 yield item 组成 生成器对象
yield item
seen.add(item)
# test
a = [1, 5, 2, 1, 9, 1, 5, 10]
print(list(filter_dumplicate(a)))
[1, 5, 2, 9, 10]
我感觉, 这个大佬, 是不是搞得太复杂, 还是我 too simple ?
# 这样不更简单吗
ret = []
for i in a:
if i not in ret:
ret.append(i)
print(ret)
[1, 5, 2, 9, 10]
就去个重, 私以为不用那么麻烦的吧. 其实更多的是, 我们通常不用考虑其顺序, 就去重, 这样一来, 直接用集合, 一波带走.
print(set(a)) # 集合元素的顺序, 呃,,我也不清楚
{1, 2, 5, 9, 10}
这类写法, 针对元素是 hashable 的时候ok的, 但对于不可哈希, 如 dict 类型 的序列, 要去重元素的话, 也可以这样.
def dedupe(items, key=None):
"""字典元素去重"""
seen = set()
for item in items:
# key 有值则为dict,取其值, else 是单序列值
val = item if key is None else key(item)
if val not in seen:
yield item
seen.add(val)
# test 以后写函数, 优先用 yield 而不用 list 来不断 append 啦
a = [
{'x':1, 'y':2}, {'x':1, 'y':3},
{'x':1, 'y':2}, {'x':2, 'y':4}
]
print(list(dedupe(a, key=lambda d: (d['x'], d['y']))))
print(list(dedupe(a, key=lambda d: d['x'])))
[{'x': 1, 'y': 2}, {'x': 1, 'y': 3}, {'x': 2, 'y': 4}]
[{'x': 1, 'y': 2}, {'x': 2, 'y': 4}]
这个把函数改为生成器, 确实优雅呀, 学到了, 以后, 优先考虑哦. 不要再傻傻地只会 append 啦. 基于此中方法, 再对某单个字段或者属性, 或者更复杂的 数据结构来去重, 也是ok的.
可以发现, 针对于序列的遍历, 生成器 yield 则会更加通用, 而非以前傻傻滴, 先定义个空 lst , 过程中不断 append 这样就造成浪费内存了, yield 返回就行啦, 最后再统一 处理即可. 生成器, 对于文件中, 消除重复行, 也是可以的, 套路都固定的.
with open(some_file, 'r') as f:
for line in dedupe(f):
....
命名切片
需求
清理一大堆, 已经无法直视的硬编码切片下标.
方案
假设我们有一个切片, 是一个字符串中固定的位置, 如字符串或文件等.
###### 0123456789012345678901234567890123456789012345678901234567890' record = '....................100 .......513.25 ..........' cost = int(record[20:23]) * float(record[31:37])
record = '....................100 .......513.25 ..........' cost = int(record[20:23]) * float(record[31:37])
cost = int(record[20:23]) * float(record[31:37])
如果这样写切片, 维护起来简直崩溃, 于是呢,应该而给切片进行命名呀.
shares = slice(20, 33)
即用内置的 slice() 函数 创建一个切片对象, 可以被用在, 任何允许使用的方法.
items = [0, 1, 2, 3, 4, 5, 6]
# 前闭后开
a = slice(2,4)
print(items[2:4])
print(items[a])
items[a] = [88,99]
print(items)
del items[a]
print(items)
[2, 3]
[2, 3]
[0, 1, 88, 99, 4, 5, 6]
[0, 1, 4, 5, 6]
对于一个切片对象a, 还可以调用其 a.start, a.stop 等属性获取更多信息.
print(a)
print(a.start)
print(a.stop)
slice(2, 4, None)
2
4
个人感觉这玩意儿, 也没什么用呀.
序列中出现最多的元素
需求
找出序列中, 出现最多次数的元素.
方案
我做数据工作, 就会经常做这类的事情, 但我从来没有用过内置工具, 都是, 以字典的方式来进行词频统计, 感觉也行.
内置的 collections.Counter 类, 就专门来解决此类问题的, most_common() 方法, 很厉害的哦.
词频统计, 最为经典的就是.
words = [ 'look', 'into', 'my', 'eyes', 'look', 'into',
'my', 'eyes', 'the', 'eyes', 'the', 'eyes', 'the',
'eyes', 'not', 'around', 'the', 'eyes', "don't",
'look', 'around', 'the', 'eyes', 'look', 'into',
'my', 'eyes', "you're", 'under'
]
from collections import Counter
# 初始化Counter 对象, 将统计序列加进去
word_counts = Counter(words)
print(word_counts.most_common())
# 选出频率最高的3个词
print("--"*8)
top_three = word_counts.most_common(3)
print(top_three)
[('eyes', 8), ('the', 5), ('look', 4), ('into', 3), ('my', 3), ('around', 2), ('not', 1), ("don't", 1), ("you're", 1), ('under', 1)]
----------------
[('eyes', 8), ('the', 5), ('look', 4)]
作为输入, Counter 对象可以接受任意, 可哈希元素构成的序列对象. 在底层的实现上, 一个 Counter 对象就是一个车字典, 将元素映射到它出现的次数上.
# Counter 对象, 其实就是字典, value 是频次
print(word_counts['eyes'])
print(word_counts['not'])
8
1
结论就是, 词频统计之类的, 可以优先考虑 Counter 对象, 当然, 我手动弄字典也可以呀, 我乐意, 又不难.
过滤序列元素
需求
过滤序列元素, 根据某些规则
方案
最为简单和我最喜欢用的是, 列表推导式.
my_list = [1, 4, -5, 10, 2, 3, -1]
# 过滤出 大于 0 的元素
print([i for i in my_list if i > 0])
# 小于0, 且是 "奇数"
print([i for i in my_list if i < 0 and i % 2 == 1])
[1, 4, 10, 2, 3]
[-5, -1]
用列表推导式的一个问题在于, 当输入很大, 则会得到一个大的结果集, 浪费内存, 解决方案就是用 迭代器呀.
# 生成器就节约内存了, 优先考虑
pos = (i for i in my_list if i > 0 and i % 2 == 0)
print(pos)
for i in pos:
print(i)
<generator object <genexpr> at 0x00000217B924F938>
4
10
2
有时候, 过滤条件复杂, 就需要把过滤的规则给放到一个函数中啦, 然后将其放到内置的 filter() 函数中.
values = ['1', '2', '-3', '-', '4', 'N/A', '5']
def is_int(val):
try:
x = int(val)
return True
except ValueError:
return False
# test
list(filter(is_int, values))
['1', '2', '-3', '4', '5']
我感觉这 filter 和 map 函数的用法是差不多的, 一个是过滤出, True 的, 一个是做映射, 映射到每个元素. 同时, filter() 函数创建了一个迭代器.
想想, 还是列表推导式, 更加爽一点哦, 比如, 在推导的时候, 还是可以做数据转换的.
lst = [1, 4, -5, 10, -9, 2, 3, -1]
import math
print([math.sqrt(i) for i in lst if i > 0])
[1.0, 2.0, 3.1622776601683795, 1.4142135623730951, 1.7320508075688772]
不仅仅可以过滤, 结合 if - else 还能实现赋值.
# RELU 函数
print([i if i > 0 else 0 for i in my_list])
[1, 4, 0, 10, 2, 3, 0]
小结
- 序列去重且有序, 用集合 set 和 生成器 yield, 优先考虑
- 切片对象, 可用 slice 函数来创建命名对象, 便于管理
- 序列统计, 如词频统计, 优先用 collectiions.Counter().most_common() 方法, 自己写也不难.
- 过滤序列, 用列表推导式和生成器(元组推导式), 复杂结合 filter 函数, 结合 [a if aaa else b for aaa in xxx ]