Python Web 开发框架 Bottle

Bottle是一个非常精致的WSGI框架,它提供了Python Web开发中需要的基本支持:URL路由,Request/Response对象封装,模板支持,与WSGI服务器集成支持。整个框架的全部代码约有2000行,它的核心部分没有其他任何依赖,只要有Python环境就可以运行。

Bottle适用于小型的Web开发,在应用程序规模比较小的情况下可以实现快速开发。但是由于自身功能所限,对于大型的Web程序,Bottle的功能略显不足,程序员需要手动管理模块、数据库、配置等等,与Pylons等框架相比Bottle的优势就难以体现出来了。

快速入门

通过一个简单的、典型的例子描述Bottle的使用:

from bottle import Bottle, run, mako_view, request

myapp = Bottle()

@myapp.get('/hello/:name/:count#\\d+#')
@mako_view('hello')
def hello(name, count):
    ip = request.environ.get('REMOTE_ADDR')
    return dict(n=name, c=int(count), ip=ip)

run(app=myapp)

我们创建一个Bottle对象,通过decorator配置一条路由记录。Bottle的url映射支持具名参数,“/hello/:name/:count#\\d+#” 格式的参数,可以匹配/hello/(.+?)/(\d+?)的URL。在方法体中,通过environ字典获得客户端IP,这个操作和其他WSGI框架是一致的。接着通过一个字典类型将Model数据传递给View。View模板通过decorator定义,采用mako模板引擎实现,模板名为hello,他表示在当前目录下一个名叫hello.tpl的文件。

剖析

Bottle主要可以分成4个模块:

  • Router和WSGI Application
  • Request和Response, Web helpers
  • Template Adapters
  • WSGI Server Adapters

Router和WSGI Application

一个Bottle对象是一个标准的WSGI App,这使他可以与很多WSGI Server集成。每个app下管理一个Router用于针对URL映射到handler方法。根据Python decorator的特性,路由规则是在程序启动时自动执行的。

Bottle的route方法用于注册handler到router中。除此以外,还有很多function currying帮助您简单地路由GET / POST 等特定HTTP方法。

Bottle最近以来支持了hook,可以注册一些方法在每个请求之前、或之后执行。

当接受一个HTTP请求时,WSGI Server会调用WSGI App的wsgi方法。它的流程是:

  1. 根据标准的WSGI接口规范,从WSGI environ中生成一个Request对象,初始化一个response对象。
  2. 在寻找方法之前,实际上Bottle会自动执行您注册的before_hook。(Bottle._add_hook_wrapper)
  3. 根据request.path的URL在router中寻找对应的handler方法(Bottle.match_url, Router.match)。这一步中除了找到合适的handler方法,还要负责提取url中的具名参数,将结果以tuple的形式返回。
  4. 以返回的方法和参数,执行handler,获得返回值。
  5. 执行所有的after_hooks。
  6. 根据返回的不同类型,写入Response的头部。在这一步,Bottle还会应用一些filter,在Bottle中,filter是用于处理handler返回类型的。例如,一个典型的filter即内置的dict2json,他将handler返回的dict类型自动换成json。(Bottle._cast)
  7. 根据返回的HTTP状态码,对handler返回对象进行处理。调用WSGI Server的start_response方法将返回对象写给客户端。

Request和Response, Web helpers

Bottle对HTTP的请求和响应封装了Request和Response对象。它采用threadlocal的方式,由Bottle app管理生命周期。您可以在声明handler方法时不必像Java Servlet那样将他们以参数传入,这样增加了方法设计的灵活性,也使得单元测试变得相对轻松。

Request对象是对WSGI environ属性的封装,可以从中取得的属性取决于WSGI Server对PEP333的实现。

关于Request API,可以参考文档

类似的,可以参考Response API了解如何对HTTP响应进行操作

和很多Web框架一样,你不必手动去设置HTTP重定向,helper方法会简化这些操作:abort, redirect, static_file。当然,你也可以自己创建一些helper。

Template Adapters

template方法用于渲染视图,您可以使用不同的模板实现:mako, jinja2, cheetah, simpletal以及Bottle自己的简单实现。

view作为一个decorator可以简化模板的选择。与route类似,作者也提供了一些function currying来支持mako_view这样简便的写法。

Server Adapters

Bottle的Server Adapters简直可以说是WSGI Server的博览会,从这里您可以了解目前比较流行的WSGI实现:

  • flup
  • wsgiref
  • cherrypy
  • paste
  • fapws3
  • tornado
  • Google Appengine
  • twisted
  • diesel
  • meinheld
  • gunicorn
  • eventlet
  • gevent
  • rocket

当然,这些不是全部,如果要使用不在其中的WSGI Server,您只需实现一个ServerAdapter的run方法即可,需要做的就是将Bottle app传给server并启动它。

内置的run方法用于启动服务,您还可以指定一个reloader参数使Bottle在后台检查源文件的修改情况,实现热加载。
除了run方式的启动,由于Bottle app本身就是一个符合标准的WSGI app,所以也可以通过一些服务器特有的方式启动服务,例如gunicorn:
gunicorn -w 2 -D -b 127.0.0.1:18080 module:app

实战

Middlewares

Middleware是WSGI的重要概念http://www.python.org/dev/peps/pep-0333/#middleware-components-that-play-both-sides 借助一些成熟的middleware可以添加一些Bottle目前不具备的功能:没错,Session。

Session Middleware最著名的选择叫做Beaker,要其用Beaker,只需要一个装饰器模式的App声明即可,您可以参考Beaker的文档.

Pylons以使用Middleware著称,而除了Routing这样核心的Middleware,包括Beaker和Authkit都可以应用在Bottle程序上。

App Mounting

Bottle的App提供一个很有用的mount方法帮助你将Web应用模块化。您可以将一个Bottle App挂载到一个父App上的某个路径,以父App启动后,父App可以为子App在一个路径下提供路由。

不过你会遇到这样的问题:

child = Bottle()

@child.get("/")
def hello():
    return "hello world"

parent = Bottle()
parent.mount(child, "/child")

很自然的,您希望打开浏览器访问http://localhost:8000/child可能看到hello world,然后却得到404.这个问题不难察觉,于是访问http://localhost:8000/child/,工作正常。

我曾给作者提过这个问题http://github.com/defnull/bottle/issues#issue/88,(补一句,Bottle的作者少有的Nice,对你提出的问题他通常都会认真解答),作者提了一个重定向的方式,不过还是没有决定把它直接加进mount方法里。

那么,我们需要一个新的mount来支持http://localhost:8000/child

def mount(parent, child, path):
    parent.mount(child, path)
    @parent.get(path)
    def redir():
        redirect(path+"/")

Google AppEngine

因为有WSGI标准,你可以用Bottle开发Google AppEngine程序。您只需要在handler文件中加入这样的代码即可:

from google.appengine.ext.webapp.util import run_wsgi_app
def main():
    run_wsgi_app(my_bottle_app)

if __name__ == '__main__':
    main()

依赖?Bottle本身是self-contained,不过你可能还需要一个强大一些的模板引擎来完成你的应用,比如Mako。这样,你需要把依赖和自己的程序一起上传到GAE。对于Mako来说,除了它自己,还依赖marksave,别忘了它。

以上就是我对Bottle的理解和使用心得,希望您在合适的场景下使用Bottle,只有这样,才能感受到这个框架所带来的乐趣。

Kindle已过万重山

契机。

早就想买个类似技术的电子书,年初的时候就有过这样的打算,几乎到了只欠东风的节骨眼上,还是畏惧了2000多的高价。事实证明等待是正确的,半年之后,盛大的Bambook发布了,Kindle3的价格也出来了,从此6寸的电子书都降到了1000人民币以下。情绪逐渐接近临界,开始和samson讨论购买的计划,熟人代购or淘宝代购。不料看到goodtiger率先出手,积聚的能量瞬间爆发。不到24的小时之后,kindle三人组成立了。。。

动机。

作为城市的低收入群体,我们有责任有义务把钱花到刀刃上。很多人说为神马不买ipad呢,功能强大得多。实际上,我的需求仅限于消费我~/Docuemnt/EBook/目录下永无止尽的文档。在电脑屏幕上,从Adobe Reader,到Simply a reader的Evince,到more than a reader的Okular,到apvlv,到zathura,即使支持了inverted colors这样的功能,终归还是不能满足舒适看书的需求。OK,只有看书这么简单,甚至你还要防止设备上多余的功能让你在看书的时候分神,你还有其他选择吗?

渠道。

什么样的渠道让能量毫无顾虑的突破临界呢,当然是有人吃过螃蟹的渠道了。这是一个中继服务,他们为你生成一个美国的地址,你只需要用这个地址在Amazon网站上注册购买,货物会寄到该地址然后转寄到国内你的真实地址。9月7号,我在Amazon网站上pre-order了Kindle3,到了9月17号,Amazon发货;9月20号,包裹到达中继服务的美国地址;支付了国际邮递费用的,9月22号包裹发货,9月23号包裹离开美国。

接下来悲剧就开始了,漫长的纠结的杳无音信,直到9月28号下午,突然收到一个陌生号码发来的短信:您的包裹需要交税!最终还是倒在了之前说的大boss手下。因为短信实在可疑,花了一天核实,到了9月30号下午终于把关税费用打给了对方。接着是漫长的十一假期,继续杳无音信。到了10月8号,终于上班了!打电话到这家公司,结果对方的电话居然坏掉,一接通就是杂音,根本没有办法对话。我心一凉,这回是不是要悲凉了。

实物

好在这回没有背到那种程度,10月11号毫无征兆地接到快递的电话,我想这事成了!

拿到Kindle,开封。一张简易说明书、一个机器,一根数据线。
DSC_0003

屏幕效果:
DSC_0004

对pdf格式,kindle3虽然原生支持,但是由于pdf本身格式的限制,只能支持到区域缩放,而不支持字体缩放。6寸的屏幕阅读还是有难度的。好在,Amazon提供了格式转换服务,您可以参考samson的预习作业把pdf转换成kindle格式。

当然,kindle3内置一个浏览器,如果你能够忍受黑白屏幕和它的屏幕刷新速度,可以尝试一下。
DSC_0005
UA显示是Linux操作系统,Webkit核心。

费用

最后透露一下费用。Kindle3 Wifi的价格是$139,Amazon美国邮递免运费。中继服务国际邮递的费用是$21.79,海关关税190元人民币。最近汇率动荡得厉害,今天的汇率已经是1:6.6405了。总价是
echo “(139+21.79)*6.6405+190″ | bc
1257.7259

今天看到淘宝上已经有1150的价格了,应该是少了关税。

A fine day

好久没有聊生活了,最近心绪比较杂乱,虽说不像以前那么忙,不过也没像以前那样有时间来贫一下。好在今天倒是这么多年难得的一个顺风日,一切顺利。

上周体检今天出了结果,全部PASS。虽然有一点点心动过速,不过没有大碍。我一直担心的谷丙转氨酶这次终于没有出乱子。其实这半年加班加上压力比较大,有的时候能明显感觉到胸闷,体检之前心里还真没底。不过好在是还年轻,还能顶一顶。尽管是有紧张的因素,但是心动过速也算是个预警吧,以后要加强锻炼。这话不知道说了多少遍了。

另一件事,Kindle终于毫无征兆的到了。9月6号我们kindle三人组(12)通过comgateway订了kindle3,到了9月17号Amazon发货,9月23号从comgateway发货。之后就杳无音信,从comgateway上的跟踪系统看根本就是绝望。结果到了9月28号正在跟samson闲聊的时候,突然收到一条莫名奇妙的短信,说您的货物已经到了北京库到家,要交税,真是又喜又悲。这还没完,花了一天confirm短信的真实性,直到30号中午才把税款打出去。接着就是无尽的国庆假期,漫漫7天过后,迫不及待打电话到库到家,结果8号一天占线,9号好不容易接通了,对方电话却坏了全是杂音跟不听不见说话。说实在的要不是有pipitu先前购物的经验,我这时候就觉得被骗了。好在,好在,今天上午顺丰快递总算是把东西送来了!

有关kindle的细节,过两天再和大家慢慢唠。

今天正好还是妈妈的生日,晚上我们找了一家山西饭馆饱餐一顿,感觉好极了!

The 4k Story

从 Redis 2.0 开始,Redis的作者就不断地被问道,你为什么要自己造一个VM轮子呢。尽管作者在FAQ里说明了,但是仍然有很多不同意见。

反向代理Varnish的开发人员,Poul-Henning Kamp 写了一篇文章,What’s wrong with 1975 programming ?,锋芒毕露,矛头直指竞争对手Squid,顺便也打击一大片牵连到了Redis的作者Antirez。他说:

I have spent many years working on the FreeBSD kernel, and only rarely did I venture into userland programming, but when I had occation to do so, I invariably found that people programmed like it was still 1975.

Kamp兄有来到user-space之后一夜回到解放前的感觉,又好像摇晃着饮料瓶子对着Antirez说:你Out啦!也许是因为作者就是个内核开发者,所以Varnish对操作系统的Virtual Memory机制充分信任,把Squid对内存的手动管理称为wasted work。”So Welcome to Varnish, a 2006 architecture program. ”

还有用户也提出

Redis doesn’t use OS swap. According to Salvatore Sanfilippo, the creator of Redis, it was because the page size of 4KB was too big. I personally don’t think that helps but it’d be better if Redis preallocated specified amount of buffer pool and bring related objects to the same page to increase locality of reference, instead of letting the heap manager blindly fragment objects. In my opinion, the page size of 32 bytes is too small, considering that the hardware architectures and the compilers are optimized for the conventional page size. In that scale, even the latency of reading something from RAM could be dominant (RAM is too slow for CPU, therefore it’s got L1/L2 cache), and RAM has the pipelined burst mode to pre-fetche memory contents at a few clock cycles, before they are actually requested.

5号,Antirez在博客上写了回击 What’s wrong with 2006 programming?,他认为:

  • OS Swap在一些情况下会导致客户端阻塞
  • 4K大小的Page可能包含很多key,其中总有一些被访问到,导致操作系统无法swap这些page
  • 使用自己实现的Paging为程序提供了极大的自由度,包括作者提到的2.2将会引入的数据压缩、新的数据结构以及自定义的过期算法

前段时间,Foursquare用MongoDB时,因为Sharding方法一些疏漏把大量的数据集中到了一台机器上,导致一台EC2实例内存耗尽无法工作。Mongodb的内部机制就是mmap,我的同事做过相关的测试,当内存耗尽时,读写操作都使用磁盘,这时mongodb的性能是完全无法使用的。事后10gen的开发人员Horowitz总结出现问题的原因总结出现问题的原因时,其中很重要的一点是

Document size is less than 4k. Such documents, when moved, may be too small to free up pages and, thus, memory.

看了这个原因,Redis的作者Twitter上大喜:”Real world instance of my 4k page + small objects concerns”