从现在开始, 这一系列的搬砖, 是来自 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 还封装很多常有的线性结构, 慢慢来整.