刘毅同学

About Python, MySQL & Life

《人类简史》笔记(1)

| Comments

发现

过年时在家看十三邀的采访罗胖的第一期节目。这才知道2015年(或者是2014年?)口碑爆棚的一本书——《人类简史》。人类简史以三十几万字的篇幅,生动的将人类这一种族的历史娓娓道来。

用400页的书去讲一个这么大的主题,它的精彩之处一定不在于文字有多么深刻。那么它是因为什么原因而获得这么多赞誉的呢?

谨慎乐观,批判阅读

洋洋洒洒的已经读了四分之一,给我的感觉是这本书引人注目之处就在于观点新异,且能用自己广博的知识储备去阐述自己的观点。确实有种耳目一新的感觉。但是我也隐约的感觉到这里面很多内容不免会充满争议,有些观点初次听到甚至让人难以接受。但是因为自己学识尚浅,虽然有一些小小的犹疑,却又说不清楚到底是哪儿不对劲。

有一点是可以肯定的,一本薄书将人类几万年的发展史穿针引线一样说了一遍,还能有满堂喝彩,其中多多少少都会有一些和『爆款公号文章』一样的味道。固然有很多人推荐阅读,但也务必谨记独立思考,批判吸收。避免出现人云亦云,拾人牙慧之嫌。

意外收获

在网上搜索了下《人类简史》读书笔记,竟发现作者尤瓦尔赫拉利在中文图书出版前就曾在Coursera上开过《人类简史》这门公开课!虽然比较遗憾的是,目前这个课程已经下架,但是还可以在B站看到。

Python Yield小结

| Comments

yield 是用来简化以下场景:函数来生成序列,并且使用遍历的方式来访问序列中的元素。

yield的实现原理理解上来说在调用yield时Python会保留函数的现场,当再次遍历时函数的状态不丢失,可以继续生成。

经典的例子斐波那契数列

问题描述

返回斐波那契数列前n个元素

Python解法

第一个版本:朴素实现
1
2
3
4
5
6
7
8
9
def fab(n):
    fab_list = list()
    i = 0
    a, b = 0, 1
    while i < n:
        a, b = b, a+b
        fab_list.append(a)
        i = i + 1
    return fab_list

第一个版本是遍历并保存所有前n项斐波那契数列的元素。最大的问题是会占用非常多的内存,当调用fab(10000)时,在我的电脑中已经是无法完成的了。

第二个版本: 简单的迭代器实现

实现一个迭代器类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Fab(object):
    def __init__(self, n):
        self.n = n
        self.a, self.b = 0, 1
        self.i = 0

    def __iter__(self):
        return self

    def next(self):
        if self.i < self.n:
            r = self.b
            self.a, self.b = self.b, self.a + self.b
            self.i = self.i + 1
            return r
        raise StopIteration()

if __name__ == '__main__':
    for i in Fab(5):
        print i

第二个版本实现了迭代器,每次调用时再生成下一个元素,因此对内存的占用是恒定值。但缺点是代码很长,不够易读。

第三个版本:yield方法
1
2
3
4
5
6
7
8
9
10
def fab(n):
    i, a, b = 0, 0, 1
    while i < n:
        a, b = b, a+b
        yield a
        i = i + 1
if __name__ == '__main__':
    for n in fab(5):
        print n
    print fab(5)

第三种yield方法兼具了第一种的简洁,第二种的高效。

fab(5)返回的是一个generator对象——让一个函数像iterator那样工作,这样在遍历的场景下既可以保持代码简洁,又保持了内存使用的高效。

相关介绍链接

  1. Improve Your Python: ‘yield’ and Generators Explained
  2. Python yield 使用浅析

Python流式压缩和解压缩调研总结

| Comments

Changelog

Time Description
2016-01-05 小幅修改
2015-12-26 initial version

最近两周在研究和实现了xtrabackup流式压缩上传&下方案,这里做一下总结分享。

实现的效果是备份不在本地暂存,压缩内置到上传逻辑中,通过配置文件可以配置压缩细节,对上传使用者来讲是『无感知』的。利用压缩提高了上传效率和备份空间的使用率。

压缩率和压缩性能的比较

  • lz4
  • gzip/pigz
  • lzop
  • qpress

结论:在兼顾压缩率和压缩占用CPU资源以及压缩效率几个方面,最终选择了lzop。

lz4据说是压缩最快的算法,最终没有选择,有几个方面的原因:

  • Python支持不完善,因为我的需求是流式的上传和下载,压缩源是一个tar stream,和压缩后的文件不会落在本地,而是直接上传到远端。
  • 可运维性不高:lzo文件没有找到和gzip, lzop这样的命令行工具可以一个命令来对一个完整的lz4文件管理。当调用file a.lz4 显示的文件是data,这样就无法确认这个文件到底是否正确了。

gzip给力的地方:

  • gzip是目前应用最为广泛的格式了,看过很多压缩率评测的文章,gzip压缩率十分给力,在compresslevel是7到9时,压缩比很给力。
  • Python也是在standard library中支持gzip的压缩和解压缩。

gzip不够给力的地方:

  • 压缩速度慢,在高压缩率上很慢。但是compress level在2~3时,gzip无论是压缩比还是压缩效率上都很有竞争优势
  • Python标准库gzip压缩对stream支持不够,流式压缩期望的方法是:传入一个fileobj,返回一个fileobj。而Gzip模块直接用的话是不支持这种方式的。

然而gzip不足点是可以弥补的。 首先是压缩速度上,默认gzip是不支持并行化的压缩,最多只能有单核的性能,这是压缩性能的瓶颈。但是gzip是有其他工具支持并行压缩的 —— pigz就是并行压缩版本的gzip,测试结果显示pigz可以充分利用多核并行化的性能,让压缩时间有明显的减少,当然代价就是load也会成倍的增长(24核机器上,我只测试了2~8个线程数)

其次是流式压缩可以通过Python轻松实现数据分块并行压缩。这里很重要的一点是:可以将每个数据分块看做一个独立文件压缩最终可以合成一个符合Gzip格式的压缩文件 简单的看了下的pigz的代码注释,发现pigz的原理也是这样子的。Python中使用生产者和消费者模式(生产者:一个线程专门来从数据源拉数据,数据存入Queue中,多个压缩进程负责压缩,把压缩后数据再放入到PriorityQueue按顺序组合成一个文件即可,当然也可以直接分块上传到远端服务器)。这也是我实现的第一个版本的流式压缩方案。这个方案主要的弱点在于内存资源占用比较大:无论是从数据源读取的原始数据块还是压缩后的压缩数据块,在最终写入/上传前都需要缓存在内存中。 在流式压缩上传的场景下,压缩上传的输出端是REST上传接口,因此并行压缩的内存占用上一定程度上受上传接口的性能影响。极端的情况,当上传速度很慢时,为了不影响压缩效率则需要在内存中开辟更大的内存buffer来放入等待压缩的数据块,这时就会占用到比较大的内存资源。不过也可以通过限制上传队列大小,在队列满的情况,数据源的write会阻塞等待。

我的第二个方案的是利用pigz外部工具在读取数据源前利用管道先接入到pigz,在从pigz直接读取到压缩后的数据流。这个方案算是最终方案的替代方案,之所以没有最终使用,原因在于和lzop相比,在压缩率相近的情况下,pigz消耗了更多的CPU资源。

对比方案:pigz -p 2 -2 vs lzop -c ,即通过2线程压缩等级2和默认的lzop对比,前者压缩比可以高出10%,但是Load比后者高出了2倍不止,同时速度上也慢了25%。

lzop给力的地方: 实际测试中,lzop兼顾性能和压缩比同时压缩占用的CPU上是最平衡的。压缩速度稍慢于lz4,压缩比上可以达到gzip等级2~3的水平,同时cpu占用率和内存上比gzip低。

lzop不给力的地方:

  • Python bindings 接口较少,不能直接用来流式压缩上传。
    • 通过 外挂 方式也可以完成。(其实即使支持的好,也需要多进程来并行工作,和外挂差别不大)

总结

  • lzop和pigz这两个压缩方案上在实际应用上都很有竞争力,只是我涉及的项目需求是生产环境上尽量不占用过多CPU,MEM资源,可以在压缩比上做妥协。因此选择了lzop。如果你在意CPU资源,更在乎压缩数据大小,则pigz是不错的选择。
  • 外挂 方式的数据压缩方案虽然集成上让Python程序有更多的外部依赖,但是考虑到Python本身并没有真正的线程的并行方案(你想要并行也要生成多个进程),其实资源占用区别不大。同时方案选型上也更加灵活。
压缩工具 描述 参数 优势 劣势
gzip 最常用的压缩工具 -2 压缩比在压缩参数大于2时很高,平台上通用很高,所有Linux发行版都会有预装,同时tar也集成了gzip压缩 单线程,较慢
pigz 多线程版gzip,主页, github 多线程并行+gzip压缩算法,无论从性能上还是压缩输出上都很不错,是个很不错的选择 压缩效率和系统资源占用成正比
lzop

Python中的subprocess与Pipe

| Comments

一、纠结的困境

Python在多进程上不是很令人满意,尤其是subprocess模块。当用Python实现一个shell脚本中的管道时就出现了比较尴尬的局面。

大数据量的管道问题

subprocess模块有两种方式来和生成的子进程交互:waitcommunicate。 关于wait文档中有以下说明:

Warning This will deadlock when using stdout=PIPE and/or stderr=PIPE and the child process generates enough output to a pipe such that it blocks waiting for the OS pipe buffer to accept more data. Use communicate() to avoid that.

即当stdout/stdin设置为PIPE时,使用wait()可能会导致死锁。因而建议使用communicate

而对于communicate,文档又给出:

Note The data read is buffered in memory, so do not use this method if the data size is large or unlimited.

communicate会把数据读入内存缓存下来,所以当数据很大或者是无限的数据时不要使用。

那么问题来了:当你要使用Python的subprocess.Popen实现命令行之间的管道传输,同时数据源又非常大(比如读取上GB的文本或者无尽的网络流)时,官方文档不建议用wait,同时communicate还可能把内存撑爆… /images/1.jpg

探究实现

Python在系统编程领域可以理解为就是C语言的一种简写,因为无论是用C语言还是Python都是对系统Linux/Windows API的使用,只是相比于C,Python封装后由于是解释型语言而变得易于编写和调试。

在Python中subprocess是用来创建新的进程,同时创建管道连接子进程的stdout, stderr, stdin(可选)。然后执行子进程,最后得到子进程的return code。

This module intends to replace several older modules and functions: os.system os.spawn os.popen popen2. commands.

所以可以理解为Python意图用subprocess模块来统一之前若干生成子进程的方式。

关于subprocess的基础用法再次暂时略过,网上相关的内容很多,有时间再赘述。这里重点说一下shell多管道流对应的python实现。

一个shell多管道脚本的改写

shell编程领域,将cli工具结合强大的pipe,可以一行代码就能完成相对复杂的工作,尤其是在文本编辑上。如以下的例子:

1
ps aux | egrep 'xtrabackup|innobackupex' | grep -v grep | awk '{print $2}' | xargs kill

这是(我比较常用的)杀掉备份进程的一行命令。大致流程如下图所示。

流程图

那么如果如何将上述代码转换为python脚本表达呢?

推荐的做法:分别生成subprocess子进程,同时用管道相连。

1
2
3
4
5
6
import subprocess
import shlex
ps_proc = subprocess.Popen(shlex.split('ps aux'), stdout=subprocess.PIPE)
grep_proc = subprocess.Popen(shlex.split("egrep 'xtrabackup|innobackupex'"), stdin=ps_proc.stdout, stdout=subprocess.PIPE)
awk_proc = subprocess.Popen(shlex.split('awk "{print $2}"'), stdin=grep_proc.stdout, stdout=subprocess.PIPE)
kill_proc = subprocess.Popen(shlex.split('xargs kill'), stdin=awk_proc.stdout)

每一个子进程的stdin都是上一个子进程的stdout,除最后一个子进程外其余进程的stdout参数都是subprocess.PIPE即管道输出,这样就首先了首尾相连。

Python并发实用编程手册[Draft]

| Comments

Python并发编程常用的builtin就是2个模块:threadingmultiprocessing。其中threading因为著名的GIL,实际是伪多线程,每个thread并没有对应一个pthread;multiprocessing则是利用多进程的手段来绕过GIL达到并发的效果。

如果你的任务并非是CPU密集型,而是IO密集型或者网络应用的话——单线程时CPU总是处于等待的状态时,用threading其实影响不大。

Quora每周摘选(第1期)

| Comments

因为订阅了Quora Digest,发现推送的邮件里面好多不错的文章。这里每周会从我读到的Quora里面翻译部分精选。

Docopt – Python必备的命令行接口模块

| Comments

docopt很适合经常需要用python写命令行工具的同学使用。

docopt之前

工作需要,经常会用大块的代码来定(ren)义(rou)命令行界面的工具。代码经常是如下的样子:

1
2
3
4
5
6
7
8
import optparse

parser = optparse.OptionParser()
parser.add_option('--foo', '-f', default='1', type='int')
parser.add_option('--bar', '-b', action='store_true')
# 类似以上的代码大概几十行

opts, args = parser.parse_args()

每一个python脚本都需要提供类似的接口。因此每一次都需要写类似的代码。在写过几次后为了保持DRY原则,我将初始化parser封装为一个method放在util部分。可是依旧是逃不过重复的写parser.add_option。不止一次地我考虑干脆自己写个模板类,以后命令行的定义直接以配置文件的形式写出来,然后每次都通过读取这个配置文件自动化的去生成parser。我相信这个问题我一定不是第一个遇到,应该会有已知的模块解决这个laber intensive的工作。

正在这个时候,偶然看到Python weekly发现了docopt

docopt

docopt官网地址:http://docopt.org/

docopt的作者有一个30分钟的视频很好的介绍了docopt这个moudule。推荐大家看一下,自备梯子~ https://youtu.be/pXhcPJK5cMc

更令懒人们惊喜的是作者还制作了一个js版本的docopt,可以让你在浏览器中把玩docopt: http://try.docopt.org/

使用docopt后,代码上会更加Pythonic,具有很高的可读性,命令行接口的定义所见即所得的样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#docopt example

mydoc = """Naval Fate.

Usage:
  naval_fate.py ship new <name>...
  naval_fate.py ship <name> move <x> <y> [--speed=<kn>]
  naval_fate.py ship shoot <x> <y>
  naval_fate.py mine (set|remove) <x> <y> [--moored|--drifting]
  naval_fate.py -h | --help
  naval_fate.py --version

Options:
  -h --help     Show this screen.
  --version     Show version.
  --speed=<kn>  Speed in knots [default: 10].
  --moored      Moored (anchored) mine.
  --drifting    Drifting mine.
"""

from docopt import docopt

arguments = docopt(mydoc, version='0.1')

print arguments

这样就定义了一个丰富的命令行接口。命令行接口提供了--help--version两个基础功能。其中--help输出mydoc--version输出指定的version信息。

接口还提供了2个参数(ship, mine),每种参数还提供了不同的几种参数的组合。其中[...]内是可选参数,(...|...)是互斥参数。