Pickle——基于栈的编程语言

Python的pickle模块是个相当方便的序列化数据的方法。但它究竟是如何运行的,对于很多人来说非常神秘。实际上它很简单。pickle的输出结果其实是一段可以生成Python数据结构的程序代码。一门功能有限的基于栈的语言可以拿来写这些代码。这里说的功能有限,但仍然可以写类似for循环和if判断等语句。并且学起来还蛮带感的。

这篇文章里会用下面这个简单的解释器来从pickle对象中读取数据。把下面的代码拷到本地文件中:

#!/usr/bin/python
import code
import pickle
import sys

sys.ps1 = "pik> "
sys.ps2 = "...> "
banner = "Pik -- The stupid pickle loader.\nPress Ctrl-D to quit."

class PikConsole(code.InteractiveConsole):
    def runsource(self, source, filename="<stdin>"):
        if not source.endswith(pickle.STOP):
            return True  # more input is needed
        try:
            print repr(pickle.loads(source))
        except:
            self.showsyntaxerror(filename)
        return False

pik = PikConsole()
pik.interact(banner)

然后用Python启动:

$ python pik.py
Pik -- The stupid pickle loader.
Press Ctrl-D to quit.
pik>

到目前还没什么神奇的。接下来,最容易创建的对象是那些空的集合,比如说一个空列表:

pik> ].
[]

创建空字典和空元组也是类似的:

pik> }.
{}
pik> ).
()

切记每段pickle数据流都是用符号.来结束的。这个操作符将栈顶对象弹栈并返回之。假如你输入一串整数,然后用.结束数据流。最后的结果将是你最后输入的内容:

pik> I1
...> I2
...> I3
...> .
3

诚如所见,一个整数用符号I开头,换行符结尾来表示。字符串和浮点数的表示方法也是类似的:

pik> F1.0
...> .
1.0
pik> S'abc'
...> .
'abc'
pik> Vabc
...> .
u'abc'

有了上面的基础,可以来点复杂的例子了——创建一个复合对象。之后你会看到,Python里面会大量用到元组,所以先来个元组的例子:

pik> (I1
...> S'abc'
...> F2.0
...> t.
(1, 'abc', 2.0)

例子里面有两个新符号,(t(只是一个标识符,它是栈中的一个对象,来告知元组构造器——t——什么时候终止。元组构造器不停的弹栈,知道到达标识符。然后它用弹出来的这些对象创建一个元组并将之压栈。你可以用多个标识符来创建嵌套的元组:

pik> (I1
...> (I2
...> I3
...> tt.
(1, (2, 3))

可以用同样的方法来创建列表或是字典:

pik> (I0
...> I1
...> I2
...> l.
[0, 1, 2]
pik> (S'red'
...> I00
...> S'blue'
...> I01
...> d.
{'blue': True, 'red': False}

唯一的区别是字典中的元素都是两个一组的键值对。顺便需要注意TrueFalse是用类似整数1和0的符号来表示的,不过前面补了个0。

然后还可以创建嵌套的列表和字典:

pik> ((I1
...> I2
...> t(I3
...> I4
...> ld.
{(1, 2): [3, 4]}

还有另外一种创建集合的方法。不试用标识符来表示对象的边界,而是创建一个空集合然后往里添加对象:

pik> ]I0
...> aI1
...> aI2
...> a.
[0, 1, 2]

符号a的意思是append。它将一个对象和一个列表弹栈;将对象添加至列表中;最后将列表压栈。下面演示了用这种方法创建嵌套列表:

pik> ]I0
...> a]I1
...> aI2
...> aa.
[0, [1, 2]]

如果嫌代码还是不够纠结,可以看这个:

pik> }S'red'
...> I1
...> sS'blue'
...> I2
...> s.
{'blue': 2, 'red': 1}

设置字典中的对象用的是符号s而不是a。并且它需要一个键值对做参数。

还可以创建递归的数据结构:

pik> (Vzoom
...> lp0
...> g0
...> a.
[u'zoom', [...]]

技巧是用“寄存器”(在pickle中叫memo)。符号p(“put”的缩写)拷贝栈顶对象到memmo中。这里用0来做这个memo的名字,不过可以用随便别的来称呼。符号g来从memo取回对象并压到栈顶。

现在有个小问题,如何创建集合(set)呢?pickle里没有集合的表示法。唯一的做法就是用内建函数*set()*来从列表或者元组上创建集合了:

pik> c__builtin__
...> set
...> ((S'a'
...> S'a'
...> S'b'
...> ltR.
set(['a', 'b'])

符号c从模块中取出对象放在栈顶。reduce通常的含义是对某个元组遍地调用某个函数,这里符号R有类似的语意,会从栈上弹出一个元组和一个函数,然后将reduce的结果压栈。所以上面的例子可以翻译成下面的Python代码:

>>> import __builtin__
>>> apply(__builtin__.set, (['a', 'a', 'b'],))

或者用星号语法:

>>> __builtin__.set(*(['a', 'a', 'b'],))

还可以这样:

>>> set(['a', 'a', 'b'])

或者直接用Python3的语法:

>>> {'a', 'a', 'b'}

符号tR运行我们执行任意标准库的代码。所以一定要注意绝对不要从不信任的来源load pickle数据。恶意攻击者可以很轻松的将一些指令混入数据中并删除你硬盘上的数据。不过同时你也可以拿这项功能来做些奇葩的事情,比如启动系统里的时钟应用:

pik> cos
...> system
...> (S'xclock'
...> tR.

虽然这门受限的语言没直接支持循环,不过这仍然不能组织地球人来做循环:

pik> c__builtin__
...> map
...> (cmath
...> sqrt
...> c__builtin__
...> range
...> (I1
...> I10
...> tRtR.
[1.0, 1.4142135623730951, 1.7320508075688772, 2.0, 2.2360679774997898,
2.4494897427831779, 2.6457513110645907, 2.8284271247461903, 3.0]

也可以预先定义一个函数来做if判断:

def my_if(cond, then_val, else_val):
    if cond:
        return then_val
    else:
        return else_val

简单的用例:

>>> my_if(True, 1, 0)
1
>>> my_if(False, 1, 0)
0

不过还是收到Python本身最大递归层数的限制:

>>> def factorial(n):
...     return my_if(n == 1,
...                  1, n * factorial(n - 1))
...
>>> factorial(2)
RuntimeError: maximum recursion depth exceeded in cmp

不过一般情况下也用不着创建一个递归的pickle数据流,除非你想参加奇葩代码大赛

关于这门简单的基于栈的编程语言大概说的就是这么多了,剩下那点没说的自己看看也能搞定。看看pickle模块的源码就行。顺便看一眼pickletools模块,可以拿来反编译pickle数据。欢迎各位留言评论。

原文:Pickle: An interesting stack language