无处不在的同化

电影《Detachment》里面,有一幕是Henry在黑板上写了两个词:assimilate,ubiquitousness。

Assimilate,同化,吸收周围的事物,融入。

Ubiquitousness,无处不在的,everywhere。

无处不在的同化。如果所有的事情都给你展现在眼前,你还有什么想象力呢?每天被巨大的信息冲击,告诉你应该做什么,应该怎样做。古代会为了统治宣传儒家思想,现代为了刺激经济给人们灌输结婚的潜意识,制造各种节日灌输各种观点来刺激消费。

如同《1984》描述的那样,在无处不在的同化中,就产生了“双重思想”,明知是谎言,人们还是愿意去相信。这种例子在现在的生活中比比皆是:我要变漂亮,才会快乐;我应该马上结婚,才会有正常的人生;我要变瘦;我应该健身……

这是一种Marketing Holocaust。这种会大毁灭式的营销会在我们接下来的人生中,每天24小时,不停地冲击我们,让我们努力工作,变得沉默,每天在广告的夹缝中看到一丝信息,最终在沉默中灭亡。

所以我们必须保护自己的思想,与这种无处不在的、让我们变得沉默的同化力量抗争。我们必须学会阅读,激发自己的想象力,培养自己的独立意识和信仰,保护自己的思想。

 

Python对象序列化与反序列(Pickle)

我们最近想要对爬虫拿到的下载结果进行存档,这个结果是一个Python对象(我们并不想简单的存一个HTML或json,想要让整个下载过程可以还原),于是就想到了用Python内置的pickle库(腌黄瓜库),序列化对象成bytes,需要的时候可以反序列化。

通过下面的代码可以简单了解pickle的用法和功能。

可以看到pickle的用法和json有点像,但是有几点根本区别:

json是跨语言的通用的一种数据交换格式,一般用文本表示,人类可读。pickle是用来序列化Python对象,只是针对Python的,序列化的结果是二进制数据,人类不可读。而且json默认只可以序列化一部分的内置类型,pickle可以序列化相当多的数据。

另外还有一个古老的marshal也是内置的。但这个库主要是针对.pyc文件的。不支持自定义的类型,也不完善。比如不能处理循环应用,如果有一个对象引用了自己,那么用marshal的话Python解释器就挂了。

版本兼容问题

由于pickle是针对Python的,Python有不同的版本(并且2与3之间差异非常大),所以就要考虑到序列化出来的对象能不能被更高(或低?)版本的Python反序列化出来。

目前一共有5个pickle的协议版本,版本越高对应Pyhton的版本越高,0-2针对Python2,3-4针对Python3.

  • Protocol version 0 is the original “human-readable” protocol and is backwards compatible with earlier versions of Python.
  • Protocol version 1 is an old binary format which is also compatible with earlier versions of Python.
  • Protocol version 2 was introduced in Python 2.3. It provides much more efficient pickling of new-style classes. Refer to PEP 307for information about improvements brought by protocol 2. (从这个版本往后,性能有显著提高)
  • Protocol version 3 was added in Python 3.0. It has explicit support for bytes objects and cannot be unpickled by Python 2.x. This is the default protocol, and the recommended protocol when compatibility with other Python 3 versions is required.
  • Protocol version 4 was added in Python 3.4. It adds support for very large objects, pickling more kinds of objects, and some data format optimizations. Refer to PEP 3154 for information about improvements brought by protocol 4.

pickle的大多数入口函数(例如dump()dumps()Pickler构造器)都接受一个协议版本的参数,其中内置了两个变量:

  • pickle.HIGHEST_PROTOCOL目前是4
  • pickle.DEFAULT_PROTOCOL目前是3

用法

和内置的json模块接口类似,dumps()用于返回序列化结果,dump()用于序列化然后写入文件。同理也有load()loads()。其中,序列化dump(s)的时候可以指定协议的版本,反序列化的时候就不用了,会自动识别版本。这个和zip命令很像。

内置类型的序列化

大多数内置的类型都支持序列化和反序列化。需要特殊注意的是函数。函数的序列化只是取其名字和所在的module。函数的代码和属性(Python的函数是第一等对象,可以有属性)都不会被序列化。这就要求函数所在的模块在unpickle的环境中必须是可以import的,否则会出现ImportErrorAttributeError

这里有个地方很有意思:所有的lambda函数都是不可Pickle的。因为它们的名字都叫做<lambda>

自定义类型的序列化

如同本文开头的实验代码一样,对我们自定义的对象在大部分情况下都不需要额外的操作就可以实现序列化/反序列化的操作。需要注意的是,在反序列化的过程中,并不是调用class的__init__()来初始化出一个对象,而是新建出一个未被初始化的实例,然后恢复它的属性(非常巧妙)。伪代码如下:

如果希望在序列化的过程中做一些额外操作,例如保存对象的状态,可以使用pickle协议的魔术方法,最常见的是__setstate__()__getstate__()

安全问题(!)

pickle文档的开头就说:千万不要unpickle一个未知来源的二进制。考虑下面的代码:

这段代码unpickle出来就是导入了os.system()然后调用echo。并没有啥副作用。但如果是rm -rf /·呢?

文档给出的建议是在Unpickler.find_class()里面实现检查逻辑。函数方法在要求全局变量的时候必然调用。

压缩

pickle之后并不会自动压缩的,我觉得这个设计非常好,解耦,pickle就干pickle的事情,压缩交给别的库去做。而且你自己也可以发现,pickle之后的文件尽管不可读,但内容依然是以ascii码呈现的,并不是乱码。需要调用压缩库的compress。实测压缩之后,体积是之前的1/3左右,非常可观。

我目前已知的坑:

全局变量要保持可以导入的,这个有点难。要面对的问题是:我今天pickle的东西,在将来某一天需要打开,是否还能打开呢?

这里有几个版本:项目的版本、python的版本,pickle协议版本,项目依赖的包版本。其中python的版本和pickle版本我觉得可以放心依赖他们的向后兼容,容易解决。主要是项目和版本和依赖的版本,如果要Pickle的对象非常复杂,那么很可能老版本的备份无法兼容新版本。可能的解决办法就是对所有的依赖完全锁定,例如记录他们的hash值。如果要恢复某一个二进制序列,那么就还原出当时的特定依赖、项目的特定commit。

但是目前来说,我们的需求基本上就是pickle一个requests.Response对象。我觉得是可以依赖它们的向后兼容的。如果有一天requests有了breaking change,那么就算我们的pickle能兼容,代码也不可能兼容了,那时候可以考虑别的策略。

 

一次Mongodb的admin数据库导致的事故

Mongodb的gridfs一次插入数据的时候会自动创建几个索引,我们程序里面的账号没有createIndex权限,我需要手动创建一下。结果连接到mongo服务器之后忘记执行use xxxdb来切换数据库了,于是在admin数据库里面创建了一个索引,结果导出一边的程序报出来很多验证问题。

Mongo的admin数据库太脆弱了,只是创建一个索引就挂了。长个教训,以后千万不要手动修改它,更不要用admin保存数据

反思一下,这次操作失误其实爆出我平时一些不好的习惯。

首先,连接mongo应该指定目标数据。而我之前都是连接到admin,然后用use切换到目标数据库。这样难免会忘记。

第二,错误的在admin数据库执行createIndex,返回的结果明确显示索引创建成功。

但是我忽略了,继续在正确的数据库创建索引。不然可以早一些发现问题。

最后,创建索引应该自动化,比如gridfs这种对md5, filename创建索引的。

 

理解Python的Iterable和Iterator

今天看到一篇很有意思的文章,说Pyhton3的range是返回的什么?

很多人都会不假思索的说,这还不简单,在Python2中range()会返回list,到了Python3range已经使用xrange替换,返回的是一个迭代器(Iterator)。

恭喜你,答错了。

range()返回的是一个Iterable,并不是一个Iterator.

原理很简单,先简单说一下Iterable和Iterator,不要试图比较二者有什么不同,因为二者根本就是不同的概念。二者字面意思都非常明确:Iterable就是一个可迭代的对象,对其调用iter(Iterable)将会得到一个迭代器;而Iterator就是一个迭代器,对其调用next(Iterator)将会得到下一个元素。

Python推崇协议,说白了就是鸭子类型。你如果实现了__iter__(),(即对你调用iter()可以得到一个Iterator)那你就是一个Iterable;如果实现了__next__()__iter__()就是一个Iterator。

等等,Iterator不就是调用next()得到下一个元素就可以了?为什么Iterator也要实现Iterable的__iter__()方法,这不纯粹啊!

为什么Python的Iterator要实现__iter__()呢(通常的实现都是return self)。官方文档中说的相当清楚。

Iterators are required to have an __iter__() method that returns the iterator object itself so every iterator is also iterable and may be used in most places where other iterables are accepted.

简单翻译一下,就是说Iterator也要求实现__iter__(),因为很多地方接收的参数是一个Iterable,如果所有的Iterator都是Iterable,那么这些用Iterable地方都可以无障碍地使用Iterator了。比如说for循环吧,关于for循环,我在Python的wiki中(已经比较老了)找到这样的描述:

 Basically, any object with an iterable method can be used in a for loop. Even strings, despite not having an iterable method – but we’ll not get on to that here.

即,for循环拿到一个Iterable的Iterator,然后使用这个Iterator进行迭代。如果Iterator实现了__iter__()方法,那么for循环就可以无障碍地对Iterator进行迭代了,Neat!想象一下,Python的生成器也是Iterator,如果for循环不能支持对Iterator迭代,生不如死啊。

所以对Iterator就有了这样一个“过分”的要求。我们可以认为,所有的Iterator都是Iterable。那么回到最初的问题,为什么range()反回的是一个Iterable而不是Iterator呢?

Iterator是有状态的,只能遍历一次,是“消费型”的,不可以“二次消费”。Iterable是没有状态的(这里不太严谨,这句话暂且不提是Iterator的Iterable),每一次对Iterable调用iter()都会得到一个新的迭代器。

考虑我们平时使用range(),我们认为这是一个表示范围的一个容器。可以使用这个容器去初始化成别的容器,这没有任何问题。

倘若range()返回的是迭代器,那么上面这个看起来在正常不过的代码就有麻烦了:

 

读到这里,你应该完全理解Iterable和Iterator了,再说点题外话。

有人可能觉得(我之前也这么觉得),这样的实现太不纯粹了,要是Iterator就是Iterator,Iterable就是Iterable不好吗?规定接口只接受Iterator,如果有Iterable的话必须手动获得一个新的Iterator然后使用。可以是可以,但是想象一下每次都初始化一个新的Iterator多费劲啊。并且肯定有地方会定义一个临时变量,万一忍不住忘记这已经迭代完了,被迭代第二次呢?

对Iterator进行迭代,是很符合人类思维的事情,因为迭代器本来就是可以迭代的。

对Iterable进行迭代,也是很符合人类思维的事情,”可迭代的对象“本来也是可迭代的,每次迭代的时候在内部初始化一个Iterator进行迭代。

所以现在想想,其实Iterator的这种行为就非常合理了。

Python是一门讲求实用主义的语言。随着越来越多地接触和使用这一门语言,我也越来越体会到这句话。另一个例子是Pyhton的元祖,这个数据结构的灵感来自ABC语言的compoundscompounds可以作为字典(在ABC中字典叫table)的合成键,也支持平行赋值。仅此而已。如果你想使用它,要么使用平行赋值取出全部的元素,要么作为一个整体来使用。这样compounds的作用变得非常纯粹——一个没有字段名字的记录的集合。而Python的tuple,却支持了迭代、下标、切片,给人的印象更像是frozenlist!概念上已经失去了compounds的纯粹。

但是这种实用主义的哲学让Python比ABC更灵活,更好用也更成功。

Although practicality beats purity.

Python 之禅如是说。

 

Python3 str混入bytes的解码问题

问题:"abc\\xe2\\x86\\x92"是一个str,其中混入了一些被错误转义的字符,需要解码得到"abc→",其中,b'\xe2\x86\x92'.decode()会得到

TL;DR

解释

"abc\\xe2\\x86\\x92"是一个str,其中\\是一个字符,前面一个反斜杠将后面一个转义了。所以这一共是15个字符。而我想要的事后面12个字符其实是表示一个utf-8编码的unicode字符。

Python提供了unicode-escape编码/解码器。可以编码/解码字面意思的unicode。编码的时候将\xe2四个字节,转换成一个字节。解码的时候,将3个字节表示的一个unicode字符,先展开unicode的表示形式,例如\u4f60,然后包括\,一个字符占一个字节,一共6个字节。

Encoding suitable as the contents of a Unicode literal in ASCII-encoded Python source code, except that quotes are not escaped. Decodes from Latin-1 source code. Beware that Python source code actually uses UTF-8 by default.

unicode-escape实际上做了两件事:

  1. escape。encode的时候,将一个unicode字符转换成多个字符,字面形式表示。decode的时候,将多个字符转成一个unicode字节。
  2. 正常encode/decode做的事情,提供strbytes之间的转换,但是只使用latin-1编码。

"abc\\xe2\\x86\\x92"中后半部分是bytes,所以应该先转换成bytes的正确格式。

现在是15个字符,后面12个应该是一个字符,按照unicode解释。

根据上面提到的,现在decode变成了latin-1编码的str,但实际上这是utf-8编码的,所以应该用latin-1解码然后重新用utf-8编码。

参考:

  1. 转换\\
  2. v2ex讨论