双木成林:喋喋不休

I leave no trace of wings in the air, but I am glad I have had my flight.

Archive for the ‘Python’ tag

WSGI初探

with one comment

此文来自本人原JavaEye博客。原文地址

前言

本文不涉及WSGI的具体协议的介绍,也不会有协议完整的实现,甚至描述中还会掺杂着本人自己对于WSGI的见解。所有的WSGI官方定义请看http://www.python.org/dev/peps/pep-3333/

WSGI是什么?

WSGI的官方定义是,the Python Web Server Gateway Interface。从名字就可以看出来,这东西是一个Gateway,也就是网关。网关的作用就是在协议之间进行转换。

也就是说,WSGI就像是一座桥梁,一边连着web服务器,另一边连着用户的应用。但是呢,这个桥的功能很弱,有时候还需要别的桥来帮忙才能进行处理。

下面对本文出现的一些名词做定义。wsgi app,又称应用 ,就是一个WSGI application。wsgi container ,又称容器 ,虽然这个部分常常被称为handler,不过我个人认为handler容易和app混淆,所以我称之为容器。 wsgi_middleware ,又称*中间件*。一种特殊类型的程序,专门负责在容器和应用之间干坏事的。

一图胜千言,直接来一个我自己理解的WSGI架构图吧

可以看出,服务器,容器和应用之间存在着十分纠结的关系。下面就要把这些纠结的关系理清楚。

WSGI应用

WSGI应用其实就是一个callable的对象。举一个最简单的例子,假设存在如下的一个应用:

def application(environ, start_response):
  status = '200 OK'
  output = 'World!'
  response_headers = [('Content-type', 'text/plain'),
                      ('Content-Length', str(12)]
  write = start_response(status, response_headers)
  write('Hello ')
  return [output]

这个WSGI应用简单的可以用简陋来形容,但是他的确是一个功能完整的WSGI应用。只不过给人留下了太多的疑点,environ是什么?start_response是什么?为什么可以同时用write和return来返回内容?

对于这些疑问,不妨自己猜测一下他的作用。联想到CGI,那么environ可能就是一系列的环境变量,用来表示HTTP请求的信息,比如说method之类的。start_response,可能是接受HTTP response头信息,然后返回一个write函数,这个write函数可以把HTTP response的body返回给客户端。return自然是将HTTP response的body信息返回。不过这里的write和函数返回有什么区别?会不会是其实外围默认调用write对应用返回值进行处理?而且为什么应用的返回值是一个列表呢?说明肯定存在一个对应用执行结果的迭代输出过程。难道说他隐含的支持iterator或者generator吗?

等等,应用执行结果?一个应用既然是一个函数,说明肯定有一个对象去执行它,并且可以猜到,这个对象把environ和start_response传给应用,将应用的返回结果输出给客户端。那么这个对象是什么呢?自然就是WSGI容器了。

WSGI容器

先说说WSGI容器的来源,其实这是我自己编造出来的一个概念。来源就是JavaServlet容器。我个人理解两者有相似的地方,就顺手拿过来用了。

WSGI容器的作用,就是构建一个让WSGI应用成功执行的环境。成功执行,意味着需要传入正确的参数,以及正确处理返回的结果,还得把结果返回给客户端。

所以,WSGI容器的工作流程大致就是,用webserver规定的通信方式,能从webserver获得正确的request信息,封装好,传给WSGI应用执行,正确的返回response。

一般来说,WSGI容器必须依附于现有的webserver的技术才能实现,比如说CGI,FastCGI,或者是embed的模式。

下面利用CGI的方式编写一个最简单的WSGI容器。关于WSGI容器的协议官方文档并没有具体的说如何实现,只是介绍了一些需要约束的东西。具体内容看PEP3333中的协议。

#!/usr/bin/python
#encoding:utf8  

import cgi
import cgitb
import sys
import os  

#Make the environ argument
environ = {}
environ['REQUEST_METHOD'] = os.environ['REQUEST_METHOD']
environ['SCRIPT_NAME'] = os.environ['SCRIPT_NAME']
environ['PATH_INFO'] = os.environ['PATH_INFO']
environ['QUERY_STRING'] = os.environ['QUERY_STRING']
environ['CONTENT_TYPE'] = os.environ['CONTENT_TYPE']
environ['CONTENT_LENGTH'] = os.environ['CONTENT_LENGTH']
environ['SERVER_NAME'] = os.environ['SERVER_NAME']
environ['SERVER_PORT'] = os.environ['SERVER_PORT']
environ['SERVER_PROTOCOL'] = os.environ['SERVER_PROTOCOL']
environ['wsgi.version'] = (1, 0)
environ['wsgi.url_scheme'] = 'http'
environ['wsgi.input']        = sys.stdin
environ['wsgi.errors']       = sys.stderr
environ['wsgi.multithread']  = False
environ['wsgi.multiprocess'] = True
environ['wsgi.run_once']     = True  

#make the start_response argument
#注意,WSGI协议规定,如果没有body内容,是不能返回http response头信息的。
sent_header = False
res_status = None
res_headers = None  

def write(body):
    global sent_header
    if sent_header:
        sys.stdout.write(body)
    else:
        print res_status
        for k, v in res_headers:
            print k + ': ' + v
        print
        sys.stdout.write(body)
        sent_header = True  

def start_response(status, response_headers):
    global res_status
    global res_headers
    res_status = status
    res_headers = response_headers
    return write  

#here is the application
  def application(environ, start_response):
    status = '200 OK'
    output = 'World!'
    response_headers = [('Content-type', 'text/plain'),
                        ('Content-Length', str(12)]
    write = start_response(status, response_headers)
    write('Hello ')
    return [output]  

#here run the application
result = application(environ, start_response)
for value in result:
    write(value)

看吧。其实实现一个WSGI容器也不难。

不过我从WSGI容器的设计中可以看出WSGI的应用设计上面存在着一个重大的问题就是:为什么要提供两种方式返回数据?明明只有一个write函数,却既可以在application里面调用,又可以在容器中传输应用的返回值来调用。如果说让我来设计的话,直接把start_response给去掉了。就用application(environ)这个接口。传一个方法,然后返回值就是status, response_headers和一个字符串的列表。实际传输的方法全部隐藏了。用户只需要从environ中读取数据处理就行了。。

可喜的是,搜了一下貌似web3的标准里面应用的设计和我的想法类似。希望web3协议能早日普及。

Middleware中间件

中间件是一类特殊的程序,可以在容器和应用之间干一些坏事。。其实熟悉python的decorator的人就会发现,这和decoraotr没什么区别。

下面来实现一个route的简单middleware。

class Router(object):
    def __init__(self):
        self.path_info = {}
    def route(self, environ, start_response):
        application = self.path_info[environ['PATH_INFO']]
        return application(environ, start_response)
    def __call__(self, path):
        def wrapper(application):
            self.path_info[path] = application
        return wrapper

这就是一个很简单的路由功能的middleware。将上面那段wsgi容器的代码里面的应用修改成如下:

router = Router()  

#here is the application
@router('/hello')
def hello(environ, start_response):
    status = '200 OK'
    output = 'Hello'
    response_headers = [('Content-type', 'text/plain'),
                        ('Content-Length', str(len(output)))]
    write = start_response(status, response_headers)
    return [output]  

@router('/world')
def world(environ, start_response):
    status = '200 OK'
    output = 'World!'
    response_headers = [('Content-type', 'text/plain'),
                        ('Content-Length', str(len(output)))]
    write = start_response(status, response_headers)
    return [output]
#here run the application
result = router.route(environ, start_response)
for value in result:
    write(value)

这样,容器就会自动的根据访问的地址找到对应的app执行了。

延伸

写着写着,怎么越来越像一个框架了?看来Python开发框架真是简单。。

其实从另外一个角度去考虑。如果把application当作是一个运算单元。利用middleware调控IO和运算资源,那么利用WSGI组成一个分布式的系统。

好吧,全文完

Written by linluxiang

三月 3rd, 2011 at 8:40 下午

Posted in Python,技术

Tagged with , ,

Python闭包再研究

with 2 comments

此文来自本人原JavaEye博客,原文地址

前两天写了一篇文章,讲了一下Python的闭包。刚好今天又看到一个小问题,和Python闭包有点相关。顺手记录下来。

如下一段代码

funcs = []
for i in xrange(10):
    def bar(n):
        return n + i
    funcs.append(bar)  

print funcs[3](5)

这段代码中,我们期望得到的结果是3+5为8。但是实际得到的结果是什么呢?是14。

14是怎么来的?

反汇编看看:

7           0 LOAD_FAST                0 (n)
            3 LOAD_GLOBAL              0 (i)
            6 BINARY_ADD
            7 RETURN_VALUE

注意i是global。

得到14的原因就是,funcs[3]这个函数对象获取i的值,是在执行的时候。而i的作用域是global。也就是说,当这个func开始执行的时候,i已经变成9了。那么结果当然等于14了。

从这个结果看,以上代码和下面代码效果是等价的。

funcs = []
for i in xrange(10):
    pass  

def bar(n):
    return n + i  

funcs.append(bar)#这句重复10遍
print funcs[3](5)

很无趣吧。那么考虑一下,如果把i丢到闭包来做会怎样?

funcs = []
def foo(m):
    for i in xrange(m):
        def bar(n):
            return n + i
        funcs.append(bar)
foo(10)
print funcs[3](5)

很遗憾,结果依然是14.

反汇编代码如下:

9           0 LOAD_FAST                0 (n)
            3 LOAD_DEREF               0 (i)
            6 BINARY_ADD
            7 RETURN_VALUE

唉,只是傻傻的远程访问而已。

“所有的bar代码中,i仅仅只是在closure中的一个引用而已。指向的依然是同一个对象。当这个对象被改变,所有的bar执行的时候获得的值都是修改后的值”。

顺手写了一段JavaScript来测试,发现结果是一样的。也是会全局改变。具体代码如下:

但是用haskell实现了一个,完全符合预期的结果。

main = do
    let funcs = [(\n -> n + i) | i <- [0..9] ]
    let x0: x1 : x2: x3: xs = funcs
    return (x3 5)

返回结果是8。

看来Python对FP的支持还是比不上Haskell这种正统的函数式语言。

个人觉得如果Python要发展FP的话,可以考虑如下解决方案:区别本地变量和闭包变量。当声明函数的时候将闭包变量拷贝一份到本地,同时保留指向原闭包对象的引用。在搜索名字的时候,始终都是先搜索本地变量,再搜索本地闭包变量副本,再搜索全局变量。当需要修改的时候,如果是本地变量就直接修改。如果是闭包变量的时候,将原来闭包真正指向的对象进行修改,同时覆盖掉本地的闭包副本。

这个方法只是暂时的考虑,没有仔细的推敲。乍看之下似乎可以解决问题。

不过,Python毕竟是OO的语言。没有Haskell或者Lisp那种天生的作用域的控制能力。唉。那就用OO的方式来搞Python把。

Written by linluxiang

三月 3rd, 2011 at 7:38 下午

Posted in Python,技术

Tagged with

MacOSX下Python2.5版本的locale的编码问题

without comments

此文来自本人JavaEye博客,原文地址

今天更新mercurial的时候遇到了一个问题。

执行hg,结果报错:LookupError: unknown encoding: x-mac-simp-chinese

想到这个问题我以前在用django的时候碰到过,原来以为是django的问题,现在才知道原来是普遍的python的问题。

去hg的源代码里面minirst.py里面看了一下,发现是直接调用mercurial的encoding函数的encoding这个变量。

找到encoding.py里面,

try:
    encoding = os.environ.get("HGENCODING")
    if not encoding:
        encoding = locale.getpreferredencoding() or 'ascii'
        encoding = _encodingfixers.get(encoding, lambda: encoding)()
except locale.Error:
    encoding = 'ascii'

原来是locale这个模块搞的鬼。。

去locale.py里面看了一下,发现以下代码:

if sys.platform in ('win32', 'darwin', 'mac'):
    # On Win32, this will return the ANSI code page
    # On the Mac, it should return the system encoding;
    # it might return "ascii" instead
    def getpreferredencoding(do_setlocale = True):
        """Return the charset that the user is likely using."""
        import _locale
        return _locale._getdefaultlocale()[1]

尝试执行了一下,直接返回了’x-mac-simp-chinese’

为了了解正确的结果,python2.6 -c ‘import locale; print(locale.getpreferredencoding());’返回结果’UTF-8′.

而UTF-8正是我设置的LC_ALL和LANG的结果。

看来是这个_locale模块搞得鬼。不过_locale啊。看名字就是c写的。为了省力。直接把

if sys.platform in ('win32', 'darwin', 'mac'):

改成了

if sys.platform in ('win32'):

然后顺手搜索了一下locale.py中的_locale,把所有的都改了。

执行hg,一切正常。

顺带搜了一下这个问题python的buglist里面有没有,果然看到了。http://bugs.python.org/issue1276。不过略看了一下,发现python2.5.x被无情的忽略了。看来只能自己hack了。:)。

Written by linluxiang

二月 23rd, 2011 at 6:58 下午

Posted in Python,技术

Tagged with ,

Python中globals对象的回收顺序分析

without comments

此文来自本人原JavaEye博客,原文地址

先提示,本文需要一定的python源码基础。许多内容请参考《python源码剖析》。下面切入正题。

今天在群里有人问了一个问题。形如如下的一段程序。

class person:
    sum = 0
    def __init__(self,name):
        self.name=name
        person.sum += 1  

    def __del__(self):
        person.sum -= 1
        print "%s is leaving" % self.name  

a = person('a')
a2 = person('a2')

这段程序的预期的执行结果应该是”a is leaving”和”a2 is leaving”。但是实际上却出乎意料之外,实际的执行结果如下:

a is leaving
Exception exceptions.AttributeError: "'NoneType' object has no attribute 'sum'" in
<bound method person.__del__ of <__main__.person instance at 0x4a18f0>> ignored

为什么引用的名字不同造成的结果会有这么大的差别呢?

分析表面的原因,是person这个引用被指向了None。那他是不是真的是None呢?

__main__.person
None
Exception exceptions.AttributeError: "'NoneType' object has no attribute 'sum'"
 in <bound method person.__del__ of <__main__.person instance at 0x4a18c8>> ignored

看来是真的变成了None了。

初步分析原因,应该是程序在执行结束以后,python虚拟机清理环境的时候将”person”这个符号先于”a2″清理了,所以导致在a2的析构函数中无法找到”person”这个符号了。

但是转念一想还是不对,如果是”person”符号找不到了,应该是提示“name ‘person’ is not defined”才对。说明”person”这个符号还在,那”person”指向的class_object对象还在吗?改变程序为以下格式:

class person:
    sum = 0
    def __init__(self,name):
        self.name=name
        person.sum += 1  

    def __del__(self):
        #person.sum -= 1
        self.__class__.sum -= 1 #1
        #print "%s is leaving" % self.name  

a = person('a')
a2 = person('a2')

#1就是修改部分,利用自身的__class__来操作。运行结果一切正常。

说明python虚拟机在回收的过程中,只是将”person”这个符号设置成None了。这个结论同时带来2个问题:第一,为什么会设置成None?第二:为什么”person”会先于”a2″而晚于”a”被回收?

先来分析第二个问题。第一反应是不是按照字母的顺序来回收?但是马上这个结论被推翻。”a”和”a2″都在”person”的前面。那么难道是根据globals()这个字典的key顺序来回收?执行一下globals().keys()方法,得到以下结果:

['a', '__builtins__', '__file__', 'person', 'a2', '__name__', '__doc__'];

看来的确是这样。

但是为什么是这样?要得出这个结论,看来只有去python源码中找答案了。

大家都知道,python代码在运行的时候,会存在一个frameobject对象来表示运行时的环境。类似于c语言的栈帧,也有点像lisp的函数的生存空间,看起来答案要从frameobject.c这个文件中去找了。

在frameobject.c中发现了一个函数:static void frame_dealloc(PyFrameObject *f)。看来解决问题的关键就在眼前。

在frame_dealloc里面截取了以下一段代码:

Py_XDECREF(f->f_back);
Py_DECREF(f->f_builtins);
Py_DECREF(f->f_globals);//1
Py_CLEAR(f->f_locals);
Py_CLEAR(f->f_trace);
Py_CLEAR(f->f_exc_type);
Py_CLEAR(f->f_exc_value);
Py_CLEAR(f->f_exc_traceback);

原来减少了引用啊。。关于Py_DECREF这个宏,python源码里面的解释是这样的:

The macros Py_INCREF(op) and Py_DECREF(op) are used to increment or decrement reference counts. Py_DECREF calls the object’s deallocator function when the refcount falls to 0;

这么说来,我们就要去找f_globals的析构函数了。f_globals是个什么呢?当然是PyDictObject了。证据么遍地都是啊,比如随手找了一个,在PyFrameObject * PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals,PyObject *locals)这个函数里面有一段代码:

#ifdef Py_DEBUG
    if (code == NULL || globals == NULL || !<span style="color: #ff0000;">PyDict_Check(globals)</span> ||
        (locals != NULL && !PyMapping_Check(locals))) {
        PyErr_BadInternalCall();
        return NULL;
    }
#endif

PyDict_Check。。。检查是否是Dict对象。好吧,此处略过,直接奔向dictobject.c看看里面的代码。

static void
dict_dealloc(register dictobject *mp)
{
    register dictentry *ep;
    Py_ssize_t fill = mp->ma_fill;
    PyObject_GC_UnTrack(mp);
    Py_TRASHCAN_SAFE_BEGIN(mp)
    <span style="color: #ff0000;">for (ep = mp->ma_table; fill > 0; ep++) {
        if (ep->me_key) {
            --fill;
            Py_DECREF(ep->me_key);  #
            Py_XDECREF(ep->me_value); #仅仅只是引用计数减一
        }
    }</span>
//以下略

哈哈哈。还真是按照key的顺序来一个一个清除的。

不过,怎么又回到了Py_DECREF啊?

看来最终解释这个问题要回到GC上面了。

其实从这个地方也可以看出第一个问题的答案了,为什么是None?

从上面代码可以看出,dictobject对象在析构的时候,仅仅只是将value的引用计数减一,至于这个对象什么时候被真正回收,其实是由GC决定而不确定的。也就是说为什么是None,是因为减一了以后,凑巧GC到了而已。

根据Python本身的文档。

Warning: Due to the precarious circumstances under which __del__() methods are invoked, exceptions that occur during their execution are ignored, and a warning is printed to sys.stderr instead. Also, when __del__() is invoked in response to a module being deleted (e.g., when execution of the program is done), other globals referenced by the __del__() method may already have been deleted. For this reason, __del__() methods should do the absolute minimum needed to maintain external invariants. Starting with version 1.5, Python guarantees that globals whose name begins with a single underscore are deleted from their module before other globals are deleted; if no other references to such globals exist, this may help in assuring that imported modules are still available at the time when the __del__() method is called.

Python不能保证_del被调用的时候所有的引用都有,所以,尽量不要overried类的del_方法。

到此结束。

Written by linluxiang

二月 23rd, 2011 at 6:51 下午

Posted in Python,技术

Tagged with

如何使用logger将错误信息输出到日志

with one comment

此文来自本人原JavaEye博客,原文地址

今天在做分析http错误请求处理的时候遇到一个问题:当发生500错误的时候,如何将错误输出到日志当中呢?

搜了一下python的doc,在logging模块中有不起眼的一行代码提供了解决方案。

mylog.error('error!', exc_info=True)

其中,mylog就是Logger实例,当exc_info设置为True的时候,logger就会自动的调用sys.exc_info()函数,将traceback的信息打印到日志中。

Written by linluxiang

二月 21st, 2011 at 8:12 下午

Posted in Python,技术

Tagged with

优化Python的DES加密程序有感

without comments

此文来自本人原JavaEye博客,发表时间2009年3月24日,可能有些东西现在看来是过时的,甚至是错误的,不过为了保证原来写的时候的感觉,所以没有修改。只在括号中注明了现在的看法。原文地址

最近信息安全的老师布置了作业。要求实现DES算法。。写了1天,优化了1天。。。小有些心得。。
首先感慨一下DES算法。。真是对人对机器都不友好的算法。。竟然还有诡异的S-BOX操作。。。
第二感慨一下Python对2进制不那么方便的支持。。连bin函数都没有。。虽然3.0有了。。可惜2.5没有。。只能自己实现,一大损失效率的地方啊。

好,接下来说说优化过程。

首先是单线程改多线程。。原来是将明文分成64bit一块的序列,每块单独加密。改成每一个64bit的块开一个线程操作。(这个在今天看来完全搞笑)

单线程加密解密一个1000长度的字符串所需时间大致和多线程加密10000长度的相等。效率提升明显。(提升效率很奇怪,忘了自己当初怎么测试的了)

其次是优化异或过程。原来的异或是整体将字符串转成整数然后再异或,发现效率很低。第一次修改为将2个字符串的每一位单独取出转成整形以后异或,效率提升不明显。后来上课时候突然想到,异或操作其实完全可以抛弃整数。1位的2个字符串的异或操作只有4种,写一个字典,用想要异或的2个字符串作为key,结果作为value。简单实现了一下。

def Xor(s1, s2):
    """
    字符串xor 

    s1 -- 第一个字符串
    s2 -- 第二个字符串
    """
    data={}
    data['0','1']=data['1','0']='1'
    data['0','0']=data['1','1']='0'
    s=[data[s1[x],s2[x]] for x in range(0,s1.__len__())]
    return ''.join(s)

三就是优化10进制转2进制。因为python没有提供 bin 函数。于是自己实现了一个。

dec2bin = lambda n:''.join([hex2bin(x) for x in hex(n)[2:].rstrip('L')]

但是在用profile做测试的时候发现的。程序运行中大量的用到了这个函数。很占cpu时间。经过观察发现主要是2个地方使用很多,第一就是异或操作的时候,因为python提供的异或只能在整数之间进行,第二就是在S_BOX运算的时候。

异或操作已经优化了。不需要10进制转2进制了。。剩下就是S_BOX运算了。经过观察发现,S_BOX里面最大的数也不过 15,于是想到可以穷举所有S_BOX里面的数字的值对应的二进制。更改代码如下。

def easyDec2Bin(s):
    """
    简单的10进制转2进制,用于简化s_box操作 

    s -- 10进制数
    """
    data={0:'0000',1:'0001',2:'0010',3:'0011',4:'0100',
              5:'0101',6:'0110',7:'0111',8:'1000',9:'1001',
              10:'1010',11:'1011',12:'1100',13:'1101',14:'1110',
              15:'1111'}
    return data[s]

第四步优化也是通过profile发现的。。发现程序运行过程中,大量的时间消耗在一个lambda函数上。

b= ''.join(map(lambda x:data[x],s))

这个函数的作用是生成一个字符串。。尝试着把这个lambda函数改成列表操作。

b= ''.join([data[x] for x in s]

效率提升明显。。。原来一直以为map操作比较快。看来并非如此。难怪Guido大叔要推广列表操作了。。

最后一步优化就是用了psyco模块,将程序变成JIT的。。效率提升很大。。。(现在看来其实这一步是最关键的。自己当初好傻)

最终优化的结果就是,加密解密1个40k的文件,原本需要将近50秒的时间,现在10秒不到就完成了。。。。

效果提升明显。不过总觉得还有提升的空间。。尝试把多线程改成Stackless版本的试试看。。。。希望能有更大的提高 。。。。(这句话现在看来完全是搞笑,看来当时的自己还是很稚嫩啊)

Written by linluxiang

二月 19th, 2011 at 12:59 上午

Posted in Python,技术

Tagged with

使用greenlet中一点简单心得

with one comment

想要对greenlet的switch机制进行测试,于是写了以下这段代码。


from greenlet import *

def test1(x, y):
    print 'in test1'
    z = gr2.switch(x+y)
    print z
    print 'out test1'

def test2(u):
    print 'in test2'
    print u
    gr1.switch(42)
    print 'out test1'

gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch("hello", " world")

运行结果:

in test1
in test2
hello world
42
out test1

从这个层面可以看出。greenlet的协程其实是一个切换机制。就是主动将自己的运行过程让给别的协程。切换需要有目标,并且可以在switch里面传入参数。如果是第一次运行,switch的参数就是作为协程的run的函数的参数传入。如果是运行中,就作为switch的返回值。
以上隐含的内容是,如果在运行中想要切换到别的协程去做事,那么不能保证那个协程一定会返回结果。
考虑在greenlet上面加上一个隐含回复机制。

Written by linluxiang

二月 16th, 2011 at 2:08 下午

Posted in Python,技术

Tagged with ,