Mar 3

Python int缓存的那点事 不指定

felix021 @ 2013-3-3 02:42 [IT » Python] 评论(1) , 引用(0) , 阅读(15100) | Via 本站原创
早先翻python代码的时候是有注意到 intobject 里有缓存这档事,不过没细看。昨天有人在sf问起为什么有如下现象:
引用
>>> a = 1.0
>>> b = 1.0
>>> a is b
False
>>> a = 1
>>> b = 1
>>> a is b
True

于是又翻开python的源码 python2.6.8/Objects/intobject.c ,可以看到这些代码(略做简化):
#define NSMALLPOSINTS          257
#define NSMALLNEGINTS          5
/* References to small integers are saved in this array so that they
  can be shared.
  The integers that are saved are those in the range
  -NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).
*/
static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];

PyObject *
PyInt_FromLong(long ival)
{
    register PyIntObject *v;
    //如果 -5 <= ival && ival < 257, 命中缓存~
    if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) {
        v = small_ints[ival + NSMALLNEGINTS];
        Py_INCREF(v);
        return (PyObject *) v;
    }
    if (free_list == NULL) { //这个是另一个优化,相当于内存池,用链表实现
        if ((free_list = fill_free_list()) == NULL)
            return NULL;
    }
    /* Inline PyObject_New */
    v = free_list;
    free_list = (PyIntObject *)Py_TYPE(v);
    PyObject_INIT(v, &PyInt_Type);
    v->ob_ival = ival;
    return (PyObject *) v;
}

而PyFloat_Object并没有(也不适合)实现这样的缓存,所以就可以解释上面的情况了。

更进一步,可以用257来验证一下,的确是超出了缓存的范围:
引用
>>> a = 257
>>> b = 257
>>> a is b
False


然后手贱做了另一个测试,蛋疼了:
引用
>>> a = 257; b = 257; a is b
True

也就是说如果让解释器一次执行的话,解释器又会再优化它,让a、b引用同一个对象。//注:这里对于float和str类型的常量效果是一样的。

为了搞清楚解释器到底是怎么实现这一点的,又把代码翻出来。之前翻的时候对解释器的执行流程已经有大致的了解了。make得到的python解释器是从 Module/python.c 里的 main() 函数开始的,调用链大约是这样:
引用
main() @Modules/python.c
    Py_Main() @Modules/main.c
        PyRun_AnyFileExFlags() @Python/pythonrun.c
            PyRun_SimpleFileExFlags
                PyRun_FileExFlags()


从 PyRun_FileExFlags 开始,才能看到底层代码正式登场:
引用
PyRun_FileExFlags()
    mod_ty *mod = PyParser_ASTFromFile() //把文件转换成AST(Abstract Syntax Tree)
        node *n = PyParser_ParseFileFlagsEx() //生成CST(Concrete Syntax Tree)
            parsetoke() //逐个解析token
                ps = PyParser_New()
                for (;;)
                    PyTokenizer_Get() //获取下一个token
                    PyParser_AddToken(ps, ...) //将token加入到CST中
        mod = PyAST_FromNode(n, ...)  //将CST转换成AST
            递归调用 ast_for_xxx 生成AST,同时过滤CST中的冗余信息
                其中ast_for_atom中调用了parsenumber, 它调用PyInt_FromLong()
    run_mod(mod, ...) //执行AST
        co = PyAST_Compile(mod, ...) //将AST转换成CFG(Control Flow Graph) bytecode
            PyFuture_FromAST()
            PySymtable_Build() //创建符号表
            co = compiler_mod() //编译ast为bytecode
        PyEval_EvalCode(co, ...) //执行bytecode
            PyEval_EvalCodeEx()

注:更详细的Python编译解释流程可参见这一系列: http://blog.csdn.net/atfield/article/category/256448

通过加入一些调试代码,可以窥探到内部的执行流。例如,在PyParser_AddToken中输出token的名称和类型编码;在PyParser_ParseFileFlagsEx()之后调用PyNode_ListTree(),可以看到生成的CST树(修改list1node()可以让它打印出更容易阅读的版本);修改PyInt_FromLong(),让它在ival=257的时候输出创建的object的id(CPython实现中 id 其实就是指针的值)。加上一些代码以后,编译python,执行test.py可以看到如下输出:
引用
felix021@ubuntu-server:~/src/python2.7-2.7.3$ cat test.py
a = 257
b = 0x101
print a is b
felix021@ubuntu-server:~/src/python2.7-2.7.3$ ./python -d test.py
PyParser_ParseFileFlagsEx
    type =    1, token: [a]
    type =  22, token: [=]
    type =    2, token: [257]
    type =    4, token: []
    type =    1, token: [b]
    type =  22, token: [=]
    type =    2, token: [0x101]
    type =    4, token: []
    type =    1, token: [print]
    type =    1, token: [a]
    type =    1, token: [is]
    type =    1, token: [b]
    type =    4, token: []
    type =    4, token: []
    type =    0, token: []
PyNode_ListTree:
                    <1>a  //type=1表示是NAME
    <22>=
                    <2>257  //type=2表示是NUMBER
  <4> //这是NEWLINE
                    <1>b
    <22>=
                    <2>0x101
  <4>
    <1>print
                  <1>a
          <1>is
                  <1>b
  <4>
<4>
<0>  //这是ENDMARKER
Before PyAST_FromNode
    name = a
    ival = 257, id = 22699048 //注意这个id和下一个id不一样
    name = b
    ival = 257, id = 22698784
    name = b
    name = a
After PyAST_FromNode
True #这一行是print a is b的输出

从输出可以看到,解析源码生成CST的时候(输出的CST已经滤掉了非TERMINAL的node),每个token还保留着原始的字符串(例如0x101和257),而在CST到AST的转换过程中(PyAST_FromNode),解释器为每一个NUMBER都创建了一个PyIntObject。然而在程序运行的最终结果里可以看到,a is b的结果是True,也就是说,从AST转换到CFG并执行(run_mod)的过程中,解释器做了适量的优化,将 a 和 b 都指向了同一个 int 对象。

由于对CFG不熟,相应的代码还不太看得懂,所以暂时只能烂尾了,如果以后看懂了,再来补充。

续集:Python int缓存的那点事[续]
Jan 20
这周写了个模块,让python可以跟后台的C服务通信了。

总体上来说给python写个模块还是比较容易的,比给php写模块要舒服多了,但是还是遇到一个问题:给php写模块的时候可以用MINIT完成初始化、MSHUTDOWN完成清理;给Python写模块,有对应的模块初始化函数,但是却没有对应的清理函数,实在是令人蛋疼。

需求其实很简单(应该也很普遍吧?),只是要在python退出之前执行一点代码,保证在初始化的时候分配的一些资源能够被释放;但是模块本身并没有提供这样的机制,只好想其他办法了。

最简单的是用C语言本身提供的 atexit ,不过这是在main()结束或者调用exit()时 - 也就是说,在整个C环境要结束了的情况下才会运行(但是在关闭所有打开的文件之前)。

虽然有效,但是由于它不是python提供的机制,多少让人有点不太放心,所以还是看看别人怎么用的吧。

Google搜了一下"python  module  destructor",stackoverflow上说的是,可以用python的 atexit 模块。这是一个纯python模块,源码可以在 /usr/lib/pythonX.X/atexit.py 看到,其实就是通过 atexit.register() 注册退出函数,并将 atexit._run_exitfuncs() 函数绑定到 sys.exitfunc 。sys.exitfunc会在Py_Finalize()这个函数中被调用(call_sys_exitfunc())。

在C模块里调用它,最简单的办法是在模块的初始化函数里加入类似这样一句(注意换行和缩进):
    PyRun_SimpleString(
            "import atexit\n"
            "def __modname_clean():\n"
            "    import modname\n"
            "    modname.clean()\n"
            "atexit.register(__modname_clean)\n");

这大致相当于在python脚本里用eval执行了一段代码(没有指定globals、locals和compile_flag)。也可以用如下几乎等价的纯C代码
//注:m是在模块初始化函数中的定义的当前模块对象
    PyObject *pClean = PyObject_GetAttrString(m, "clean");
    if (pClean != NULL)
    {
        PyObject *pName = PyString_FromString("atexit");
        PyObject *pMod = PyImport_Import(pName);
        Py_DECREF(pName);
        if (pMod != NULL)
        {
            PyObject *pFunc = PyObject_GetAttrString(pMod, "register");
            if (pFunc && PyCallable_Check(pFunc))
            {
                PyObject *pArgs = Py_BuildValue("(O)", pClean);
                PyObject *ret = PyObject_CallObject(pFunc, pArgs);
                //if (ret == NULL) ; //sth went wrong
                Py_XDECREF(pArgs);
                Py_XDECREF(ret);
            }
            Py_XDECREF(pFunc);
        }
        Py_XDECREF(pMod);
    }
    Py_XDECREF(pClean);

由于错误处理、引用计数的代码占了一大半,所以代码这么长……

这种方法的好处是,它在python运行环境结束之前执行注册的函数,所以注册的函数仍然可以使用绝大部分python提供的功能(你甚至可以再import新的模块,但是最好别用thread...)。然而atexit有一个蛋疼的缺陷:尽管python在sys模块的doc里写的是“Assigning to sys.exitfunc is deprecated; use the atexit module instead.”,但是sys.exitfunc仍然是任意python脚本都可以修改的!所以通过atexit.register()注册的函数并不能100%保证被运行。

再回过头来看Py_Finalize()函数,这里其实是有很大槽点的:它有一个长达十一行的"sundry finalizers"段!
    /* Sundry finalizers */
    PyMethod_Fini();
    PyFrame_Fini();
    PyCFunction_Fini();
    PyTuple_Fini();
    PyList_Fini();
    PySet_Fini();
    PyString_Fini();
    PyByteArray_Fini();
    PyInt_Fini();
    PyFloat_Fini();
    PyDict_Fini();

也就是说,它为许多内置的module逐一硬编码了清理函数,却没有实现一个清理机制!

甚至在这后面还有一段注释:
    /* XXX Still allocated:
      - various static ad-hoc pointers to interned strings
      - int and float free list blocks
      - whatever various modules and libraries allocate
    */
坑爹啊。。。

再往下看,在Py_Finalize()的最后一行终于出现了本篇的另一个主角:call_ll_exitfuncs(); 这个函数的内容很简单:逐一执行 exitfuncs 这个数组里保存的函数。而 exitfuncs 这个数组的内容,则是由 Py_AtExit() 函数填进去的:
#define NEXITFUNCS 32  //另一个坑:最多只能注册32个exitfunc
static void (*exitfuncs[NEXITFUNCS])(void);
static int nexitfuncs = 0;

int Py_AtExit(void (*func)(void))
{
    if (nexitfuncs >= NEXITFUNCS)
        return -1;
    exitfuncs[nexitfuncs++] = func;
    return 0;
}

我最终采用的解决方案是这个:Py_AtExit()。

最后总结对比一下上述3种方案:

1. C语言的 atexit() : 使用链表保存注册的函数,只要内存够,数量没限制。在Python完全结束后执行。
2. Python的 atexit 模块:使用 list 保存注册的函数,在register的时候还可以带参数,在Python解释器仍然完整的情况下执行,一般来说很够用;但由于使用的sys.exitfunc可能会被其他脚本使用,并不能100%保证有效,故,有强迫症的慎用。或者应当在doc里明确说明。
3. Python C API的 Py_AtExit() :在Python环境几乎完全结束的时候被调用,最多只能注册32个函数(所以最好要检查返回值)。其实从流程上来说,在这之后马上就是main的return或者exit()函数调用,所以跟atexit基本上差不多。

p.s. 另一个尝试但是失败了的方案:建立一个新的type X_Type,在它的tp_dealloc里头写入清理代码,以X_Type建立一个新的class X,在模块的初始化函数中new一个X,撂着,并期望Py_Finalize()里的PyGC_Collect()会把它回收掉。不知道还差了点什么,如果哪位大牛知道,还望不吝赐教。
Dec 13
这事儿其实只要有了拼音库,就挺简单的。我从 pinyin4j 这个项目里搞了一份出来。虽然这个库是为java写的,不过要提取倒是相当简单:到这里 下载pinyin4j-2.5.0.zip,解压得到里头的 lib/pinyin4j-2.5.0.jar ,再解压得到里头的 pinyindb/unicode_to_hanyu_pinyin.txt 。

这个文件的结构很简单,每一行的基本结构是这样的:
引用
4E04 (shang4,shang3)

前面的 4E04 是汉字的Unicode编码,空格分隔,然后括号里面包含了所有读音(而且看起来像是按频率排序好了的),每个读音分别给出了拼音的声母韵母以及声调。

在这个基础上解析它就相当简单了。
f = open("unicode_to_hanyu_pinyin.txt", "r")

py = {}
for l in f:
    l = l.strip()  #行末回车
    key, val = l.split(' ')
    sd = val[1:-1].replace('u:', 'v').split(',')  #去掉左右括号,把u:转为v(驴 lv),然后按逗号分隔
    arr_sd = []
    for i in sd:
        arr_sd.append({'py': i[0:-1], 'tone': i[-1]})  #把每个读音的声调分离出来
    py[int(key, 16)] = arr_sd  #把unicode编码转成10进制作为key

f.close()


当然,你也可以把它存入一个key/value数据库(memcachedb什么的就挺好)。以上面的例子的话,把一个字符串中的汉字转换成拼音就很简单了:
def convert(str, encoding = 'utf-8'):
    ret = ''
    for i in str.decode(encoding):
        w = ord(i)
        if py.has_key(w):
            ret += "%s-%s " % (py[w][0]['py'] , py[w][0]['tone'])
        else:
            ret += i
    return ret


要注意的一点是,“驴”的拼音被标记为 lu:2 ,而不是打字时习惯使用的 lv ,如果有需要的,还得再加个简单的转换逻辑。

实际上这个库里头还支持通用拼音(貌似是台湾地区使用过的)等其他转换方式,有需要的同学可以自己考据一下其结构。

p.s. 在python里头可以用 unichr(0x4E04) 得到这个unicode对应的字符,也可以用 ord('率'.decode('utf-8'))  得到这个字符的unicode编码(注意替换字符的原始编码)。至于十进制和十六进制的转换,也很简单, hex(32768) 得到 '0x8000' 而int('0x8000', 16)就能得到32768。python真好用。
Oct 31

pexpect 使用范例 不指定

felix021 @ 2012-10-31 10:13 [IT » Python] 评论(0) , 引用(0) , 阅读(4878) | Via 本站原创
在shell里头,有些程序(比如ssh)的交互是从pty中读取的数据,直接的重定向无法解决,因此需要用到expect这种东西。不过这货的编译比较麻烦,各种依赖。幸好有个pexpect是纯python的,存档留个记录。
#!/usr/bin/python

import sys
import pexpect

password = 'password'
expect_list = ['(yes/no)', 'password:']

p = pexpect.spawn('ssh username@localhost ls')
try:
    while True:
        idx = p.expect(expect_list)
        print p.before + expect_list[idx],
        if idx == 0:
            print "yes"
            p.sendline('yes')
        elif idx == 1:
            print password
            p.sendline(password)
except pexpect.TIMEOUT:
    print >>sys.stderr, 'timeout'
except pexpect.EOF:
    print p.before


如果密码错误的话,输出
引用
The authenticity of host 'localhost (127.0.0.1)' can't be established.
RSA key fingerprint is fe:00:00::00:00:00:00:00:00:00:00:00:00:00:00.
Are you sure you want to continue connecting ((yes/no) yes
)? yes
Warning: Permanently added 'localhost' (RSA) to the list of known hosts.
username@localhost's password: password

Permission denied, please try again.
username@localhost's password: password

Permission denied, please try again.
username@localhost's password: password

Permission denied (publickey,gssapi-with-mic,password).


如果正确,则输出
引用
username@localhost's password: password

a
b
bin
code
Oct 31

[python] shellsvr 不指定

felix021 @ 2011-10-31 19:15 [IT » Python] 评论(0) , 引用(0) , 阅读(5085) | Via 本站原创
一个蛋疼的服务:向某个端口提供shell服务。

基本流程是这样的:python虚拟一个终端,载入了一个bash;然后呢,监听某个端口,连上该端口的客户端的输入当作bash的输入,将bash的输出返回给该客户端。当客户端断开的时候,bash继续运行,等待下一个客户端。

之所以倒腾出这么个东西,主要是突然想写个php在一个session里面完成一系列任务(甚至su成另外一个用户),但是system之类的函数就很难搞。基本上就是没用的东西。
#!/usr/bin/python

import socket
import os
import thread
import pty

tty = open("/dev/tty", "w")

shell_input_reader, shell_input_writer = os.pipe()
shell_output_reader, shell_output_writer = os.pipe()

def sheller(arg):
    global shell, shell_input_reader, shell_output_writer
    os.dup2(shell_input_reader, 0)
    os.dup2(shell_output_writer, 1)
    os.dup2(shell_output_writer, 2)
    while True:
        pty.spawn('/bin/bash')

def shell_to_sock(conn):
    global shell_output_reader
    while True:
        try:
            conn.send(os.read(shell_output_reader, 1024))
        except:
            break

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('127.0.0.1', 4698)) #如果希望这个服务对其他机器开放的话,那就把这个ip改成0.0.0.0或者外网的ip

sock.listen(5)

thread.start_new_thread(sheller, (1,))

while True:
    conn, addr = sock.accept()
    print >>tty, "%s:%d connected" % (addr[0], addr[1])
    thread.start_new_thread(shell_to_sock, (conn, ))

    while True:
        try:
            buf = conn.recv(1024)
            if not buf:
                break
            print >>tty, "[%s]" % buf.strip()
            os.write(shell_input_writer, buf)
        except:
            break

    conn.close()
Oct 28

[python] 从ppt中提取照片 不指定

felix021 @ 2011-10-28 01:10 [IT » Python] 评论(0) , 引用(0) , 阅读(6222) | Via 本站原创
ppt中保存的默认是原始照片;可以把照片另存,但是保存的图片实际上是压缩过后的图片,而且EXIF信息也丢失了;另存为pptx再改成zip,发现效果相同。为了找回最原始的照片,可以将其另存为xml格式,然后再写程序将其中的图片信息提取出来。

xml格式中图片的信息大致是这样存储的:
引用
<pkg:part pkg:name="/ppt/media/image5.jpeg" pkg:contentType="image/jpeg" pkg:compression="store"><pkg:binaryData> [[base64 encoded data]] </pkg:binaryData></pkg:part>

写了个简单的python脚本来完成:
import re
import base64
import os

x = open("1.xml", "r")
str = x.read()

r = re.compile(r'<pkg:part pkg:name="[^"]*\/([^"]*)".*?>\s*<pkg:binaryData>((.|\n)*?)<\/pkg:binaryData>', re.S)
m = re.findall(r, str)

os.system("mkdir files")

for i in m:
    print "write %s" % i[0]
    f = open("./files/" + i[0], "wb");
    f.write(base64.b64decode(i[1]))
分页: 2/2 第一页 上页 1 2 最后页 [ 显示模式: 摘要 | 列表 ]