数据结构与算法 (01)

从现在开始, 这一系列的搬砖, 是来自 Python Cookbook 的一本经典的书,也是我前两年看的比较多一本, 总体感觉写得还是蛮不错的. 当做复习了, 也是打算再重温一遍. 感觉还是, 经典的, 经得起时间验证的东西, 才是有价值的哦. sql 暂时缓缓了.

最近一个多月都没有 看过 Python 了, 我的第一武器, 再不用就生锈了, 集中精力在客户的 BI 以及用到的 SQL, 基本的查询, 应该是没有问题了, 套娃也写过在4月中旬的时候, 写过一次, 两百多行 的 sql 后, 我感觉算是一个进阶了, 对于sql, ... 然后再遇到慢慢练习吧. 现在又来回到 Python, 搬砖这些 案例呢,也是为了更一不了解 Python 语言的特性吧, 虽然之前也叨叨了很多了. 学无止境嘛毕竟.

对我个人来讲, 与其去寻找高效的学习方法, 不如不寻找, 直接抄一遍就理解了一大半了其实, 也不是抄, 还是基于解读的基础去做

序列解包 unpack

需求

给定一个包含 N 个元素的元组或是序列, 如何对里面的值进行 "拆包" 到另外的 N 个变量

方案

Python 中有个比较形象的词, 叫做 "拆包" , 很简单, 就直接拆. 当然前提是, 外面用来接收的变量数量, 要跟序列中的一样多哦.

回想一波, 在 C 中, (我当年学过一点点, 到指针那就放弃了), 如何交换两个变量的值, 答案是引入一个中间变量嘛.

a = 10
b = 20

# 交换 a, b 的值
tmp = b

b = a
a = tmp

这样就交换了, 当然这里不用 C 写的, 演示意思而已. 而 Python 呢, 则是不要需引入中间变量的

a = 10
b = 20

# 交换 a, b 位置
a, b = b, a

为啥 Python 可能这样写呢, 原因在于 Python 中的 "=" 表示 指向的关系, Python 中的变量就可大致理解为 C 中的指针, Python 变量不存储值, 二十指针 (值,对象的地址) . 我想, 这也是 Python 变量 不需要事先声明类型 的原因, 因为根本就存储值, 而是一个指针.

>>> p = (4, 5)
>>> x, y = p
>>> x
4
>>> y
5
>>>
>>> data = ['youge', 89, 90.1, (2020,5,4)]
>>> name, shares, price, date = data
>>> name
'youge'
>>> date
(2020, 5, 4)

直接用变量来对应接收里面对应的值, 这就是拆包呀.

>>> name, shares, price, (year, month, day) = data
>>> name
'youge'
>>> year
2020
>>> month
5
>>> day
4

注意点就是, 拆包 unpack 的时候, 不能多, 也不能少哦

>>> p
(4, 5)
>>> x, y, z = p
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected 3, got 2)

更多的场景是, 只要是序列结构的(可迭代对象) , 如 字符串, 文件对象, 迭代器, 生成器等都是可以的呢.

>>> s = "youge"
>>> a, b, c, d, e = s
>>> a
'y'
>>> e
'e'

对于那些, 不需要, 但是又必须要接收的, 通常都要 "_" 来占位, 这种写法在循环, 或者在 遍历 DataFrame 的时候, 我是经常会用的, 看上去不会产生歧义, 不然别人会觉得, 如果用一个变量接收, 但该变量又没用到, 是蛮奇怪的.

>>> data = ['youge', 'Python', 'M', 24]
>>> name, _, gender, age = data
>>> name
'youge'
>>> age
24

序列组包 package

需求

接收变量个数 小于 序列值个数, 如何让其不报 ValueError 的异常.

方案

用 星号 * 来进行组包.

这个在函数中是经常用到的, 把多个输入参数, 组包用 * args 这样的元组形式, 或是以 ** kwargs 这样的 字典形式来接收, 保证函数的鲁棒性嘛. 我之前写梯度下降算法的时候, 就有用到过它, 还有掉一些 莫名奇妙的 API 的时候, 也是会用 * 来接收一些奇奇怪怪的输出, 使程序不至于崩溃.

def drop_first_last(score):
    """统计取出最高,低分数的平均成绩, score 假设是按升序排的"""
    first, *middle, last = score
    return sum(middle) / (len(score) -2)

# test
score = [60, 70, 80, 90, 100]
drop_first_last(score)

输出 80.0 没毛病. 而且, * 的部分, 默认都是以 list 的形式来存储的.

>>> *trailing, current = [1, 3, 4, 2, 5, 666]
>>> trailing
[1, 3, 4, 2, 5]
>>> current
666

也可用这种 * 的语法, 做类似于切片的的功能哦. 比如, 我有一个销售数据, 假设就是个列表, 1-4月份, 假设想要看看最近一个月, 和前面 3个月的 均值对比. 伪代码就是这样的.

*trailing_sales, current_salse = sales

trailing_avg = sum(trailing_sales) / len(sales)

return avg_comparison(trailing_avg, current_salse)

星号表达式, 在迭代元素为可变长元组序列是, 还是很有用的, 其实, 我个人觉得, 在某些方面上, 是可以用来代替切片的, 我个人是挺害怕那种 下标索引的, 虽然大量再用, 但内心拒绝的, 因此偶尔用 * 也蛮好.

records = [
    ('foo', 1, 2),
    ('bar', 'youe'),
    ('foo', 3, 4)
]

def do_foo(x, y):
    print('foo', x, y)
    
def do_bar(s):
    print('bar', s)
    
    
# test
for tag, *args in records:
    if tag == 'foo':
        do_foo(*args)
        
    elif tag == 'bar':
        do_bar(*args)
        
foo 1 2
bar youe
foo 3 4

再来一些, 列表分割的例子.

line = 'nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false' 

uname, *fields, homedir, sh = line.split(":")

print(uname)
print(homedir)
print(sh)

nobody
/var/empty
/usr/bin/false

如果想解包一些元素, 然后立马抛弃它们, 可用 "* _ " 这样的无意义名称哦

record = ['youge', 50, 123.45, (4, 5, 2020)]

name, *_, (*_, year) = record

print(name)
print(year)
youge
2020

将一个列表分割为两部分, 不用切片. 我感觉是最为常用的, 工作中也是经常这样玩的.

items = [1, 10, 9, 2, 3, 6]
head, *tail = items

print(head)
print(tail)
1
[10, 9, 2, 3, 6]
def dec_sum(items):
    """对列表元素递归求和"""
    head, *tail = items
    
    return head + dec_sum(tail) if tail else head


# test
dec_sum(items)
31

保留最后 N 个元素 (记录)

需求

在进行迭代 (遍历) 操作的时候, 保留最后有限几个元素的的历史记录

方案

collections.deque 队列来实现.

deque 我之前也自己实现过, 封装为一个类, 用 list 作为 底层结构, 两端分别作为 队尾 和队首, 蛮简单的. 这里就不展开了, 重在技巧性, 而非重复造轮子( 造轮子, 专门弄了一个模块, 全是自己造的那种).

在使用 collection.deque(maxlen=N) 构造函数会新建一个固定大小的队列. 当新的元素加入, 并且队列已经满的时候, 对首的元素会自动被移除掉 ( 先进先出)

from collections import deque

q = deque(maxlen=3)

q.append(1)
q.append(2)
print(q)

q.append(3)
print(q)

q.append(4)
print(q)

q.append(5)
print(q)
deque([1, 2], maxlen=3)
deque([1, 2, 3], maxlen=3)
deque([2, 3, 4], maxlen=3)

一眼就看出端倪了, 这个队列的底层也是 list 的呀, list 的左边是对首, 右边是队尾.

case

多行文本匹配, 根据关键词, 返回其满足条件的最后 N 行.

from collections import deque

def key_search(lines, pattern, history=5):
    # 保留N条, 就构建长度为N的队列
    previous_lines = deque(maxlen=history)
    for line in lines:
        # 判断关键词是否在该行中
        if pattern in line:
            yield line, previous_lines
        # 不论如何, 都将每行给加入到队列中
        previous_lines.append(line)
        
  # test 用 Python 之禅
with open ('D:/test_data/Python_this.txt') as f:
#     print(f.readlines())
    for line, prev_lines in key_search(f, 'easy', 5):
        for prev_line in prev_lines:
            print(prev_line, end=' ')
There should be one-- and preferably only one --obvious way to do it.
 Although that way may not be obvious at first unless you're Dutch.
 Now is better than never.
 Although never is often better than *right* now.
 If the implementation is hard to explain, it's a bad idea.

小结

  • 序列 (可迭代对象) 如 列表, 元组, 文件对象, 生成器 ... 等的 拆包 unpack 和组包 package
  • 可变长输入输出的 拆组包, 用 * 和 _ 占位的灵活应用, 增强代码的鲁棒性
  • collections.deque 队列的初始, collections 还封装很多常有的线性结构, 慢慢来整.
posted @ 2020-05-04 23:14  致于数据科学家的小陈  阅读(155)  评论(0编辑  收藏  举报