2010年12月17日星期五

此daemon非彼daemon

跳槽之后被各种琐事缠绕,好久都没有写文章了,今天匆匆记一笔

最近被daemon折磨了很久。原来一个程序要成为一个合格的daemon也是很不容易的,不光要进行两次fork,还有一大堆注意事项(PEP-3143):
  • Close all open file descriptors.
  • Change current working directory.
  • Reset the file access creation mask.
  • Run in the background.
  • Disassociate from process group.
  • Ignore terminal I/O signals.
  • Disassociate from control terminal.
  • Don't reacquire a control terminal.
  • Correctly handle the following circumstances:
    • Started by System V init process.
    • Daemon termination by SIGTERM signal.
    • Children generate SIGCLD signal.

偏偏我碰到的情况更特殊一点。这段代码的作者肯定是没看过上面这些条条框框的,把自己fork了两次就变身daemon了,然后在某些时候还用multiprocessing模块把自己再复制了几份来异步的做一些事情。在调试一个bug的时候,我注意到了multiprocessing 模块里讲 Process.daemon 属性时是这么说的:
The process’s daemon flag, a Boolean value. This must be set before start() is called.

The initial value is inherited from the creating process.

When a process exits, it attempts to terminate all of its daemonic child processes.

Note that a daemonic process is not allowed to create child processes. Otherwise a daemonic process would leave its children orphaned if it gets terminated when its parent process exits. Additionally, these are not Unix daemons or services, they are normal processes that will be terminated (and not joined) if non-daemonic processes have exited.


这实在是一段很难懂的话。就像伞兵生来就是要被包围的,守护进程daemon生来就是要被父进程抛弃的,然后再被init进程收养,沉默的完成自己的任务。但这段话却明确的说,当进程退出的时候,它会把自己的子daemon进程都干掉……那daemon还守护个毛啊?而且这里还说守护进程是不能创建子进程的,这更离奇了,太不公平了吧?这是赤果果的歧视啊。

后来我发现自己有点先入为主了。daemon只是一个概念而已,而非一个实际的标签,系统中的进程实际都是一样的。multiprocess模块的 Process.daemon 只是 Process 对象的一个属性而已,是需要开发者自己设置的而非linux帮你产生啊。理解了这一点,上面的这段说明就完全可以理解了。daemonic 类型的 Process 对象就是具有这些属性而已。这里的 daemon 和我们平常理解的 daemon 虽然有联系,但确是不同的了。

有个和我有相同困惑的人09年在python-list发了一封邮件问这个问题,后来又自己理解了,我也是看到他的邮件才领悟的。

* http://www.python.org/dev/peps/pep-3143/#correct-daemon-behaviour
* http://mail.python.org/pipermail/python-list/2009-May/1203871.html

2010年8月2日星期一

介绍一个Vim的插件:Slime.vim

http://technotales.wordpress.com/2007/10/03/like-slime-for-vim/

Slime不是Vim的原生插件,它的创意来自于玩Emacs的阶级兄弟。它的功能很简单,就是将Vim的一个寄存器中的内容传送到screen的一个window中去执行。

这东西有什么用呢?最有用的地方在于简化REPL(read-eval-print loop)这个循环,降低程序员调试代码的时间成本。对于Python这个问题其实不是那么突出,因为Python的命令行程序对于以文件形式作为输入的 python代码很友好。但是其他语言的命令行程序似乎不是这样。Slime的动力就是Ruby和Lisp这样的语言。开一个Clojure的命令行交互界面麻烦的很,而且它的功能和Python命令行没得比,更不用说ipython了。所以Slime看起来就非常好用了。

2010年7月6日星期二

读《Coders At Work》前三章

我水平比较低,看了前几章就感觉学到了许多,不由得想写一写,为自己总结一下。

人在不同的时间、环境和机遇下所渴望得到的知识是不同的,这些只是现在的我所体味到的好东西。但书中还讲了很多,但也许我以后再读才能品出其 中的滋味,也许你现在就能品出来。这也就是一本好书的魅力所在吧

第一章出场的大牛是Jamie Zawinski。对他的采访给我印象最深刻的地方是他的成长。他高中接触计算机之后通过参加地方性的 User Group聚会认识了卡耐基梅陇大学(CMU)的老师,然后屁颠屁颠的跑去“实习”。Jamie在CMU读了本科,然后仍然是依靠这位老师找到 了自己的第一份工作。之后他又遇到了Peter Norvig,并跑到Berkerly去了。他的学会的第一门计算机语言是Lisp,他写出了 XEmacs,他从不信任gdb,他是Netscape的主要开发者之一,我们每天看到的XScreenSaver也是他的习作。这需要怎样的天分、机遇 和努力,才能成就这一切呢?现在的Jamie是一家夜店的老板,已经不再写程序了。这似乎真的有一丝“哥已不在江湖,而江湖上还流传着哥的传说”的意味啊

第二位出场的大牛是Brad Fitzpatrick。对他的采访给我印象最深刻的地方是他的创业历程。LiveJournal从无到有,从小到大,都是他努力的成果。他可以今天写 Javascript,第二天却考虑Linux内核的网络算法。Things are always on fire! 今天经验不足的程序员时常会犯过 度设计的毛病,而那时的他却没有任何可以参考的东西。他也提到如果知道自己的网站能有这么大的发展和负载,肯定会好好的考虑架构啊算法啊之类的东西。但是 世界是没有如果的,他的网站很幸运的生存了下来。这就是一个产品是如何进化的。

第三位出场的大牛是Douglas Crockford。对他的采访给我音像最深刻的地方是他对代码阅读的强调。什么是好代码?在他看来,好 代码的第一要义就是可读性(readability),其次是正确性(currectness),最后才是效率(efficiency)。当然,这可能和 Javascript的特性有关,毕竟这的确是一门非常难以驾驭的语言。他谈到了他和他的同事们是如何进行code reading的,也谈到了他在面试 时对面试者有怎样的偏好。我在看这本书之前由于Javascript的缘故就看过几部Douglas的演讲,他是一个非常可爱的白胡子老头。即使你没有时间,至少也应该看看他的《The JSON Saga》 的分享,非常有意思。

这三篇采访都提到了一些共有的话题,其中一个是问你是如何设计一个系统的,是top-down,还是bottom-up,抑或是 middle-out?不同的人有不同的习惯。我对这么问题的印象这么深是因为在我注意到它之前,有人问过我一个类似的问题,而我的回答很悲剧。回头再读 此书看到这个问题,不由的生出“从前有一份答案摆在我的面前,而我没有珍惜……”的哀叹啊

2010年6月22日星期二

Things I learned from python-list recently

1. 有了list comprehension还要map函数干什么?需要么?不需要么?

map函数只在一种情况下比list comprehension有优势,那就是它可以被当做一个参数被传递给其他函数。当然,一般来说大家也很少这么干……


2. ord() 只能产生无符号整数(unsigned char, 0-255),如果你在一个字节一个字节的读取流,那么把这每个自己转换成有符号整数的方法有很多:

(a) 用标准库里的 array 模块
(b) 用标准库里的 struct 模块
(c) 用 bytearray 数据类型
(d) signed = unsigned if unsigned <= 127 else unsigned - 256
     signed = (unsigned & 127) - (unsigned & 128)
     signed = (unsigned & 127) * 2 - unsigned
     signed - unsigned - 2 * (unsigned & 128)


3. evaluate 1^2+2^2+3^2-4^2-5^2+6^2+7^2+8^2-9^2-10^2+...-2010^2, where each three consecutive + must be followed by two - (^ meaning ** in this context)

>>> sum((1, 1, 1, -1, -1)[(x-1) % 5] * x**2 for x in xrange(1, 2011))


4. 应该小心的将反斜杠(\)作为行延续符(line continuation)使用。虽然我在pep8中和其他关于pythonic的文档中都看到了这一点,但我在维护现有代码的时候并没法更多的实践这一点,因为这些代码的作者甚至不知道pep8的存在,更不用说什么将每行长度限制在80个字符以内的狗屁龟腚了。

5. 函数也是可以被pickle的,但是不能被marshal

2010年6月5日星期六

让url适应浏览器

今天看到一篇有趣的文章:http://benlast.livejournal.com/29164.html

起因是Firefox缓存哈希算法的一个bug,会使资源不能够充分的缓存。Google意识到了这一点,并且对自己的url做了一些相应的变化,以避开Firefox的这个bug,使更多的网站图片的到缓存,改善用户体验。

一般来说我们优化网站的思路都是从网站本身去找原因,缓存、js、静态页面等等,这是我第一次意识到不仅浏览器应该适应网站,网站也可以主动去适应浏览器。

当然,也只有像Firefox这样开源的项目才可以让你去适应,IE的bug你可能知道么?

当然,也只能敬佩Google中的强人能想到这个主意,哪个公司的工程师能在维护这样一个网站的同时还熟悉浏览器的各种缺陷?

2010年5月30日星期日

[非技术] 战争进行时

现在三更半夜的,我在一台陌生的电脑上写这篇blog。女友加班,我来接她,但是看来事还没做完,所以我临时找了一台她同事的电脑上网。今天我也加了一天的班,任务就是让毒霸变得更好,让360去见鬼。
 
珠规院的工作环境真好啊,独立的大隔间工作台,24寸的显示器……我看到了遨游,点开开始上网。当然,我也不由自主的检查了一下xp右下角的图标们,发现了NOD32以及……360安全卫士……没有网盾……
 
当然,这是别人的电脑,我还是忍住了卸载360的冲动。
 
一分钟之后360提示升级,升级到第二步提示此电脑已经安装了网盾,显然我是不会让360把网盾卸掉的,于是我取消了这次升级,然后360很愤愤然的在右下角弹了一个框框,说金山网盾欺负他。我把这个弹窗关掉,30秒之后网盾也弹了一个框框,说360不是东西。你来我往,热闹得很。
 
作为一个Linux用户,这真是一次难得的机会,让我体验到了这场“战争”不是嘴上说的,是正在进行的。网盾的确有做的不好,或者说不对的地方,那就是和遨游捆绑在一起,而且是秘密的捆绑在一起。就像我刚才说了,右下角的图标中就没有网盾,但是进程管理器里面你可以看到kswebshield.exe这个进程在工作。这是在损害用户的知情权,是非常不好的用户体验。但360绝对不是什么好鸟,最可恶的就是用语言对用户进行恐吓和威胁,披着“安全”的皮推销私货。这是一种不那么容易被看出来的伎俩,但我想假以时日,众多的普通用户也迟早会发现这一点。

这两天我也会关注一下新浪微博上关于“金山”和“360”两个关键字的评论。双方都有硬伤。对金山有利的证据包括上个星期网友贴出的网盾和360安全卫士可以共存的视频,以及可牛论坛上的那篇360升级文件分析的帖子;对周鸿祎有利的证据就是今天,或者说昨天,网盾的升级包出了一个bug,使得网盾真的不能兼容360了。呵呵,真是忙中出错。周拿这一点大说特说,也说明在此之前,360安全卫士和金山网盾完全是可以共存的么,360的这次蓄谋已久的进攻的理由完全就是借口。
 
这是一场迟早要打的战争,只是我没想到会来的这么快。我也不想去翻那些周鸿祎的旧账,我只是觉得我周围毒霸的开发同事都很单纯,考虑界面、功能、用户体验等等,都是从如何做好自己出发,而并不是时刻想着怎么打击别人。
 
今天我可以拒绝360一次,这台电脑的主人会拒绝下一次么?我们在24小时之内又会失去一位用户了么……

2010年5月27日星期四

RLE 压缩算法


RLE 压缩算法是一个很简单的压缩字符串的算法,不知道的自己百度一下哈。前一段时间有人考我,让我快速实现它,于是我写了一个非常naive的函数。debug了之后它长的这个样子:

def rle_compress(input_str):
    """ RLE Compression
    >>> rle_compress('get uuuuuuuuuup'):
    '1g1e1t1 10u1p'
    """
    if len(input_str) == 0:
        return ''

    if len(input_str) == 1:
        return '1' + input_str

    tmp, counter, output = input_str[0], 0, ''
    for i in xrange(len(input_str)):
        if input_str[i] == tmp:
            counter += 1
        elif counter > 0:
            output += (str(counter) + tmp)
            tmp = input_str[i]
            counter = 1
    output += (str(counter) + tmp)

    return output

其实这个真的不够Pythonic哦。Pythonic的方式应该是这样的,灰常nb的one-liner:

from itertools import groupby

def rle_compress_iter(input_str):
    """ RLE Compression, using *groupby* from itertools
    >>> rle_compress('get uuuuuuuuuup'):
    '1g1e1t1 10u1p'
    """
    return ''.join([str(len(list(group))) + name for name, group in

        groupby(input_str)])

当然,我是写不出这么强大的函数的,思路来自于《Expert Python Programming》一书。(打个广告,这本书非常好,您得备一本!)

问题来了,虽然使用python的iterator/generator绝对是每一个python程序员应该实践的best practice,但这两个函数的性能差距还真是有点大(反复执行1000次的总用时,以下计时皆是):

manual compress: 0.663447s
original calls: 2.723927s

测试用的文本是wikipedia上关于Issac Assimov的一段介绍(为了能让RLE算法有点成就感,我自己往里面加了一些重复的字符):

TEXT = """
The Foooooooooooooooooooooooooundation Series is a science fiction series by Isaac Asimov which covers a
span of about 500 years. It ccccccccccconsists of seven volumes that are closely linked to
each other, alttttttttttthough they can be read separately. The term "Foundation Series"
is often used more generaaally to include the Robot Series and Empire Series,
which are set in the same fictional universe, but in earlier time periods. In
total, there are fifteen nnnnnnnnnnnnnnnnnovels and dozens of short stories written by Asimov,
and six novels written by other authors after his death, expanding the
timeeeeeeeeeeeeeeeeee
spanned by more than nnnnnnnnnnnnnnnnnnnnnnntwenty thousand years. The series is highly acclaimed,
winninggggggggggggggggggggggggg the one-time Hugo Award for "Best All-Time Series" in 1966.[1]
"""

我XX,怎么会这样……
好吧,也许不是itertools.groupby的错,我写这个函数的时候只是希望尽快的能够通过doctest。让我们来优化一下吧。

第一次尝试:
用加号来连接name和group的长度?用 '%d%s' % (....) 如何?

return ''.join(['%d%s' % (len(list(group)), name) for name, group
        in groupby(input_str)])

事实证明,结果很悲惨:

strformat calls: 3.933721s

第二次尝试:
group本身也是一个iterator,用 len(list(group)) 是完全没有优化过的第一想法,应该可以有更好的办法来求出一个iterator的长度,比如这样:

def rle_compress_iter_leniter(input_str):
    def leniter(iterator):
        l = 0
        for _ in iterator:
            l += 1
        return l
    return ''.join([str(leniter(group)) + name for name, group in groupby(input_str)])

哈,虽然这样看起来很丑,但是进步不少哦:

leniter calls: 0.928560s

需要说一下的是iterator本身是肯定不会有 len() 函数可以使用的,iterator 可能是无穷的。

第三次尝试:

说过了,上一种方法太丑了,我想让它看起来更优雅一点,用 list comprehension 吧:、

return ''.join([str(sum([1 for _ in group])) + name for name, group
        in groupby(input_str)])

用到了内置的sum函数来求出iterator的长度,但是看起来效果不是很好:

strsum calls: 1.262965s

第四次尝试:
再看一次代码,发现用sum函数求和其实没必要,这个list的长度就等于iterator的长度,所以用len来替换sum也许会更好一些:

return ''.join([str(len([1 for _ in group])) + name for name,
        group in groupby(input_str)]

结果是的确改进了一些,但是还是没有leniter函数快:

strlen calls: 1.059268s

第五次尝试:
用str()来把整数转化为字符串也许还可以改进,要把一个对象转化为字符串,我们还可以用repr()来试试:

return ''.join([repr(len([1 for _ in group])) + name for name,
        group in groupby(input_str)])

OK,这次真的有提高哦,真的很高哦,超过了leniter:

repr calls: 0.840248s

第六次尝试:
上上个星期网易的大神沈崴来金山帮我们对一个程序进行了一番改造。他的代码中有一些“很奇怪”的地方,其实就是我们这些python的新手没怎么接触过的“历史用法”。用'`'(backtick) 来代替 repr() 就是一例:

return ''.join([`len([1 for _ in group])` + name for name, group in
        groupby(input_str)])

事实证明backtick比直接调用repr()还要快!

backtick calls: 0.784968s

---------------------------

真的很接近最初的naive算法了,但我有点黔驴技穷了。如果有谁还有更好的办法,请一定发邮件告诉我。

one-liner真的让你想用lambda来代替def来定义这个函数,但其实lambda会更慢……

虽然naive的代码很傻很天真,但是pythonic的函数始终没能追上它。(至少我这种臭水平是没辙了)下次在写对性能有要求的python代码时,一定要记得放弃那些花花招式,先从最简单开始。

Python的struct.pack函数的一点小小需要注意的地方

struct包是用来对数据和二进制字符串进行相互转换的,如果不讲求效率,这东西还是蛮好用的。上个星期在摆弄的时候,忽然发现了一个有趣的地方:

>>> import struct
>>> struct.pack('B', 1)
'\x01'
>>> struct.pack('H', 200)
'\xc8\x00'
>>> struct.pack('BH',1, 200)
'\x01\x00\xc8\x00'
>>> struct.calcsize('BH')
4

可以看到,struct.pack('B', 1) 与 struct.pack('H', 200) 的结果之和和 struct.pack('BH', 1, 200) 的输出不一样。后者的结果中多了一个 '\x00'。想到我们一般都是用的CPython,这就好理解了。C的struct中存在内存对齐的做法,这样可以得到更好的性能。Python的struct包在默认情况下是不会干涉这一点的,所以就出现了上面的情况。想要去掉这个 '\x00' 也很简单,在格式字符串之前加上 '=' 即可,像这样:

>>> struct.pack('=BH',1, 200)
'\x01\xc8\x00'

2010年5月2日星期日

用site包添加Python的导入路径

我前两天在Buzz上抱怨Windows系的同事在CentOS下喜欢在/data目录下建一个programfiles目录来安装软件是一件非常汗的事情,有同学回复说我是Linux原教旨主义者。我完全不是,我只是希望用什么就像什么,尽量沿着best practices的路子走,不要自己发明轮子,像我现在维护的代码,把Python用成了Java。这么用可以么?没事,当然可以,但是这是这肯定是在走弯路,而且在弯路上不知道埋伏着什么bug,会在你不注意的时候跳出来咬你。昨天我就是因为这样的bug而加班的。虽然硬着头皮上也能解决,最后也能学到东西,但我希望不要总是用这种方式才能学到东西,因为我很想在五一这个美好的假日能够捧上一本好书惬意的在阳光下睡觉。

问题的起源是我没法把在一个用apache+mod_wsgi跑起来的application之中import任何包,标准库也不行,比如logging或者sys。这个application原来是用tornado直接跑的。我想用框架写网站的同学可能都没有遇到过,我也是。一开始以为这大概是apache的配置问题,但左查右查都没有结果,因为后来管机器同事一气之下把python/apache/mod_wsgi全部重新make/install了一遍就好了,最后也没查出原因。没有logging这样的模块,仅仅靠apache那点可怜的日志,鬼知道后面出的错如何解决。

CentOS是个很保守的系统,自带的Python还是2.4的,升级是不可能完成的任务。于是一般的做法是自己编译安装一个Python2.5/2.6来用。我这时才发现programfiles这个奇葩的所在。/usr/local/目录下面也不是标准的bin/lib/share...的目录结构,而是和这个/data/programfiles一一对应的软链接-_-。在让mod_wsgi使用我们自己编译安装的Python2.5之后,发现无法import第三方的库,但这个第三方的库明明用easy_install安装过啊,2.4/2.5都装了,咋还不行捏?经过差不多是把显示器贴到脸上去的检查,发现标准的路径(sys.path)里写的是 /usr/local/lib/python2.5/site-packages,而我们的山寨路径是 /usr/loca/python/lib/python2.5/site-packages。这两个有区别哈,但是第一眼真的很难看出来。

OK,把山寨路径加入到sys.path中?还是不行。我现在的感情真的很脆弱,干脆坏事做到底,直接把 /usr/local/lib/python2.5 目录删了,改成软链接连到 /usr/local/python/lib/python2.5 目录下。现在第三方库可以了。哦,不,还不是全部可以,而是有的可以有的不行……囧!实验了多次,发现一个问题,.pth文件指向的那些路径下的包都没法import。上google,上stackoverflow,上baidu,".pth"这个关键字相关的东西非常少,人家一般都给你匹配成path。就在我晕头转向之际,不知道什么时候手一抖,点开了一个stackoverflow问题的链接,里面有一位大神说,只把路径加入到sys.path是不够的,这样.pth文件不会被解析,要用site.addsitedir才行。哎呀呀,终于成了……

不写了,爬山去了。

2010年3月13日星期六

40岁的程序员

今天在csdn上看到一篇帖《程序员四十很尴尬》,其中的文字让我也觉得很尴尬:

……
领导说有人要来公司,让我去见见,沿海大企业上过班,老大意思是,年龄偏大,技术过时.但朋友推荐,又不好拒绝,言下之意让我婉拒.

等我赶到,人已经到了,中年人,40岁左右,果然有些偏大,领导说了一下公司近况,很长很含糊.我基本上不知所云,努力观察来人.说实在的,人到中年,做 编码显然已经不合适,应该可以看得出来是在行政部门待过的人,果然他自我介绍的时候提到以前在政府部门上班,然后下海,去了沿海,在一家大公司上班.据说 规模过万人.做erp,会vb和FOXPRO数据库,.net也会,但是不熟.他很努力的试图表达自己在用户体验方面有些经验,他的到来可能会在这个方向 上给公司产品带来提升,在外面久了,想回来.今天特意过来了解一下.也明确的说:自己现在的位置很尴尬.开发已经做不了,想在公司寻求维护的相关的职位.
……

尴尬之处显而易见:
  1. “人到中年,做 编码显然已经不合适”:自我心理暗示,这既是楼主自己的想法,也是来面试的老大哥的想法
  2. “以前在政府部门上班,然后下海”:半路出家,没有技术背景,起点不高。当然,这样的情况大有人在
  3. 在大公司做erp,使用vb/Foxpro,.net会而不熟:.net时代之前的vb?Foxpro?这都是上个世纪的东西了……

这就是我们上一辈程序员的结局么……他们之中有成功者,但这位老大哥显然不是他们之中的一员。CS/IT这个行业还非常非常的年轻,分支繁多,变化极其迅速,人的能力只能学习和追踪一个小的领域的技术和动态。即便是Python这么小众的语言,信息量仍然大的我无法接受。但如果不在每天工作之余看书看文档,落后是必然的。这位四十岁的程序员只是一个极端的例子,其实看看自己身边的许多三十多岁的同事,你就能看到自己的未来。有的同事虽然已经成家,但仍然在锐意进取,学习和尝试新的技术和框架,跟踪业界最新的发展;而有的同事已经被家事、被孩子、被各种琐事拖累得没有力气再前进了,只能吃老本了。有人说程序员做不过三十五,但我觉得那些不断前进的同事不显老,还能和我们这些刚毕业的小年轻争论问题,他们的经验和见解让我获益良多;而和有些同事在讨论问题的时候我们的想法会出乎他们的意料,我们的做法会让他们充满疑虑,原因很简单,他们没有学习了,不了解这些新东西了。真的是应验了文章里的那句话:

我这个位置很尴尬

这又让我想起了Douglas Crockford(JSON数据格式的发明人)。这个糟老头干过程序员也干过CEO,做过技术也做过行政。但唯一不变的是,人家没有丢掉那颗活到老学到老的心。我看过他的许多演讲,平易近人幽默风趣,像一位老教授一般。想想发明C语言的Ken Thompson,他老人家现在又被google请出来写Go语言,仍然宝刀不老;再想想自己接触过的Richard Stallman,他仍然在用命令行,仍然在写程序,仍然有活跃的思维。

十年太短,我不想在十年后就放弃写程序

2010年2月28日星期日

CherryPy的一个bug

web2py开发模式的web服务器是直接copy的CherryPy的代码,前段时间我们意外的发现了这段代码的一个小问题,后来查了一下,发现CherryPy也仍然存在这个问题。

[http://www.cherrypy.org/browser/trunk/cherrypy/wsgiserver/__init__.py?rev=2650#L566]
566        # Unquote the path+params (e.g. "/this%20path" -> "/this path").
567        # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
568        #
569        # But note that "...a URI must be separated into its components
570        # before the escaped characters within those components can be
571        # safely decoded." http://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2
572        # Therefore, "/this%2Fpath" becomes "/this%2Fpath", not "/this/path".
573        try:
574            atoms = [unquote(x) for x in quoted_slash.split(path)]
575        except ValueError, ex:
576            self.simple_response("400 Bad Request", ex.args[0])
577            return
578        path = "%2F".join(atoms)
579        self.path = path

574行中,url路径被分割后全部逆转义(unquote);在578行中,url路径被用"%2F"(即斜杠“/”)恢复。

问题就出在578行。"%2F"是一个转义了的(quote)字符,url路径也应该是被转义了的,而显然atoms列表里的所有元素都已经被逆转义而没有被再次转义。将url逆转义应该是应用程序的责任。这个bug会让应用程序在开发期间产生错觉,因为从开发服务器得到的url都是被转义过的;而在生产模式使用fcgi/mod_python部署之后应用程序得到的url就变成了没有被转义的原始url,措手不及。

2010年2月3日星期三

说说 web2py

很多人使用python进行web开发的第一个框架可能是django或者web.py,但我接触的第一个框架是web2py。在web2py上进行开发也有一年的时间了,我们还是决定切换回django。web2py有很多让我们不爽的地方,在这里进行一下不完全总结:

1、死板的url
一般情况下,web2py的url是有固定模式的,即/app_name/controller_name/function_name/arg/arg?key=val&key=val。将app、控制器和函数的名称和url直接联系起来,这是web2py的特色,但这样做最明显的问题就是url会变得很长,即使是如首页这样明显应该使用最短url的地方也是如此。web2py提供了一个routes.py来解决这个问题。在routes.py中,开发者可以像许多其他框架一样使用正则来重定向url。但这不是开倒车么?早知如此,何必当初呢。

routes.py还会带来另一个问题,因为routes.py所在的位置是web2py的目录,而非每个app的目录,也就是说routes.py是独立于app的。在进行版本控制的时候,如果开发者只对自己app进行版本控制,这个routes.py就会游离于版本控制之外了。最好的办法是将整个web2py都纳入版本控制,因为在开发的过程中你会知道,你需要自己为web2py写些补丁才能正常的工作下去。

2、url只支持某些字符
在有web2py特色的url中,如果你使用了中文的参数(args),web2py会给你两个字:Invalid Request。为啥?因为web2py对args做了严格的限制,只允许大小写字母以及几个有限的符号。对于其他符号,由于正则表达式不匹配,web2py会找不到控制层函数,于是就丢给你这么一个出错信息。url中的中文一般都是经过了转义的,即使你不去转义,浏览器也会帮你转义。比如“你好”,到了url之中就会变成“%E4%BD%A0%E5%A5%BD”,于是web2py就犯傻了。

我们修改了gluon/main.py中相关的正则表达式解决了这个问题。

3、过于独立的app
web2py的app是非常独立的,有自己的静态文件和session,不同的app之间没有预设的共享机制。这样的app划分方法可能只适合于承载多个互相独立的小站点,而对于一个含有多个相对独立的复杂模块,而模块之间又需要互相通信的项目来说,web2py的这种方式就完全不合适了。在这个问题上,web2py和django形成了鲜明的对比。

4、巨慢的启动速度
启动web2py要多久?大概5-10秒之间吧。web2py在启动的时候会import大量的库以及完成检查定时任务等工作,重启web2py是很耗时的。但是python的web开发有这么一个毛病,那就是有时开发者在修改代码之后不得不重启服务器才能看得到改动的效果,这在我所见到的所有框架中都或多或少的存在。重启django的开发服务器需要多久?web.py的呢?再回头看web2py,真是慢得跟蜗牛一样了。

5、出错页面极其简陋
在开发过程中,出错是少不了的。web2py的出错页面就是一行小字:ticket xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,然后你就跟着这个叉叉页面进行认证之后才能看得到出错内容。其实使调试页面可以通过认证访问是一个很不错的主意,我只是很不明白为什么要在开发模式下也这么干,更不用说web2py根本没有区分生产模式和开发模式的方法了。web2py的调试页面本身也相当简陋,就是把traceback打印出来,再把出错的源文件整个dump出来。而其他框架是怎样的呢?当django出错的时候,web.py出错的时候,trac出错的时候,moinmoin出错的时候,出现的页面是什么?那上面不但有traceback,还有traceback每行代码的上下文,有出错现场的所有局部和全局变量的值,还有当时的http请求和回答的内容等等。这种调试页面几乎已经成为了标准,我不明白为什么web2py视而不见。

好在将标准的调试页面迁移到web2py并不是一件很困难的事情。我们自己写了一个补丁,将这个页面从web.py中迁移了过来。

6、和python的标准模块有过节
在开发过程中我们发现我们的日志出了点小问题,日志条目包含大量的重复,而且日志似乎不是实时的,logging模块的日志切分也不正常。一开始我们以为是自己的代码问题,查来查去也没查出个所以然,于是就到web2py的邮件列表里搜索,发现有人报告了同样的问题(https://groups.google.com/group/web2py/browse_thread/thread/e20d0bd2e542aa14/a660bac70d5611a4)。比较奇葩的是创始人mdipierro竟然也不知道这个bug的根源在哪里,而且也没有任何解决的动向。帖子的楼主找到一个绕过问题的方法后大家都欢呼雀跃,然后就没人理这个问题了……

顺便说一句,web2py项目从07就开放了,09年才有人暴出logging模块的问题,难道用web2py的人都不写日志的么?

web2py还会和哪些标准模块过不去?谁知道……

7、magic(过度设计)
把事情变得傻瓜化,在用户不知情的情况下就帮他们做这做那,这是windows的哲学,我很不喜欢。很可惜,web2py就是这样的一个框架。web框架的确是应该帮开发者干一些脏活累活,但是也要弄清楚界限,否则吃力不讨好。我们在web2py中就两次遇到了这样的情况。

第一次是在构造一个文件下载的服务时,发现ie6/7下载总会出错,但是代码上没有任何问题。在StackOverflow上询问过后发现是web2py很热心的帮你设置了很多header。讨论帖请见:http://stackoverflow.com/questions/1999950/download-link-fails-in-ie

第二次是在处理url时,发现args里面的空格不是被转义成%20,而是都“自动”变成了下划线。察看web2py的源代码,发现果然是web2py搞的鬼。web2py的文档提过这事儿么?一个字都没有。

8、测试?None
我真的很羡慕django的开发者,django为他们准备好了一个非常棒的测试环境,并且在文档中给出了很多例子,帮助开发者完成代码级别的测试。而web2py呢?None!没有文档,没有例子,甚至连web2py本身,也有很多没有被测试覆盖到的地方。每个app倒是有个test目录,但怎么用?有没有best practice?web2py最官方的manual上都没说这件事儿。

9、DAL就是个笑话
现在python最好的ORM非SQLAlchemy末数了。看得出来,DAL是以SQLAlchemy为目标的,但是很可惜,画虎不成反类猫。DAL生成的数据表的migrate选项是一个很好的点子,但可惜做的不是很成功。它重度依赖自动生成的.table文件,在切换数据库时经常造成DAL无法识别明明已经存在的数据表。要说bug,DAL最严重的问题大概是会把布尔类型的字段创建为char(1),然后用'T'和'F'来表示“真”和“假”。匪夷所思是么?的确,我一开始也被雷到了,但是雷着雷着也就习惯了。

10、MVC三层互通!
这是我们最为痛恨的一点了,web2py的MVC三层竟然是通的!什么意思?这就是说,你的代码在运行的时候,不需要import,model/control/view三层的所有变量以及web2py自身的函数和变量以及request/response/session这些全局变量都会被加载到一个命名空间之中去。你自己的代码干了什么,你还是可以控制的,可以让他们不要互相重名覆盖,但是你能保证你的变量名和函数名不会和web2py的出现冲突么?import机制是白发明的么?现在全世界的开发者都知道命名空间是多么的重要,竟然还有这种脑残的设计,实在让人无语。

光凭这一点,web2py就可以去下地狱了。



当然,web2py也不是一无是处,它还是有两个优点的:

1、独立部署
web2py的发布包解压缩之后就能运行,立即就可以进行开发而无需安装,这非常方便。web2py鼓励开发者将所使用的第三方库放在modules目录中,这样在部署的时候将web2py整体打包就可以了,无需依赖其他外部环境。当然,对于其他框架,使用yolk+virtualenv也可以做到这一点。

2、自带定时任务
web2py自带了对定时任务的支持,这些定时任务代码可以访问app的model层代码,也就是可以使用model层的代码操作数据库。这样定时任务就不需要再依赖操作系统,而且也可以使用开发环境中的ORM,很方便。



Anyway,web2py的作者是个大学老师,这个框架就是拿来上课用的。如果有谁还真以为这个框架能作出什么产品级的应用,那真是天真的可以。

2010年1月6日星期三

你最想掌握的技能是什么?

今天在StackOverflow上看到这个帖子:What is the one programming skill you have always wanted to master but haven’t had time?

粗粗看了一下回帖,有些人说到了一些我想掌握的东西:
* RegularExpression 正则表达式
* automated unit testing 自动化测试
* Object Oriented programming 面向对象程序设计

其他的虽然不是我的茶,但也蛮好玩的:
* Read The Art of Computer Programming, and say I understood everything without lying.(可能么……)
* 很多人都说想自己写个编译器出来,这真是和国内很多CS的大学生不谋而合。
* REALLY learning Emacs.
* Creating an OS like Windows Vista.......(LOL)
* C and hack into the Minix, Linux or BSD kernels.(这也是大多数CS学生的天真梦想)
* ……

我最想掌握什么呢?说真的这是个有点学生气的问题。工作了之后自身素质提高虽然是很重要的一个方面,但是怎么能够又快又好的完成工作才是最重要的。就我自己的兴趣而言,除了上面列出来的三点之外我还希望能够:
* Dive into PyPy
* Dive into Javascript JIT