Archive for the ‘Python’ Category

纠结及其他

Monday, January 5th, 2009

07年初进入现在的公司,不久后用twisted写了一个http开发框架,提供内部系统之间的http接口服务。说是框架,其实很底层,基本相当于http服务器+URL分发。所以,开发的目的也很明确,就是性能。

在一年多的应用以来,发现很多问题,依次解决,改进了许多。现在提供一些性能参数吧,给需要对python开发系统,进行性能估算的朋友一点数据。

服务器就是四核志强 E5420,2.50GHz,内存4G。除了日志基本没有硬盘IO。

跑hello world的时候,50并发可以达到1800req/s。关闭accesslog日志以后,可以达到2600req/s。当然,这个服务器只能用一个CPU核心。

在应用中,是4个twistd实例在load balance后面一起干活,如上述配置的一台服务器。刚才统计出来的,过去24小时中处理完成约2000万次的http接口调用。调用的后面是有数据库查询的,一般来说都不只一次查询。实际看到的速度接近峰值的一小时内最高可以做约140万次http接口调用,折合389req/s。

总的来说,数据库依旧是几乎所有系统的瓶颈。

再就是高速运行情况下的问题。

默认的twisted使用的是select(),并发性能不要太指望。甚至并发数稍微高一点的时候就会报什么file descriptor out of range of select()的错误。总之默认的twisted能承受的并发数很有限。

twisted是有epoll支持的,你可以在启动twistd时选择epollreactor。不过貌似并不稳定。使用epoll()方式以后,我的印象大约是每几千个请求就会报出一个文件描述符的什么错误。另外,就是在关闭程序时有个语法错误,这个绝对是twisted的bug。从2.5.0升级到8.1.0之后,关闭服务器那个报出的bug依然存在。而twisted-8.1.0在启动时还要很傻的在安装位置写一个什么缓存文件。普通用户根本没有权限,所以每次启动服务器时都会报错,但是服务器可以照常启动。

我对twisted的容忍基本限于如此了。这段时间感觉libevent很优雅,假如我的雅兴还没过去,我会尝试给libevent附带的http服务器加上python binding,也许性能会更高。

纪念学习Python三周年

Friday, December 26th, 2008

2005年圣诞节的夜晚,快要考试了,有些压抑,于是翻翻电脑上的书,找到了这个Python,于是一发不可收拾。现在已经做了2年多Python程序员了。世事难料。

greenlet:轻量级并发编程

Wednesday, December 3rd, 2008

greenlet:轻量级并发编程

译者: gashero

目录

1   动机

greenlet 包是 Stackless 的副产品,其将微线程称为 “tasklet” 。tasklet运行在伪并发中,使用channel进行同步数据交换。

一个”greenlet”,是一个更加原始的微线程的概念,但是没有调度,或者叫做协程。这在你需要控制你的代码时很有用。你可以自己构造微线程的 调度器;也可以使用”greenlet”实现高级的控制流。例如可以重新创建构造器;不同于Python的构造器,我们的构造器可以嵌套的调用函数,而被 嵌套的函数也可以 yield 一个值。(另外,你并不需要一个”yield”关键字,参考例子)。

Greenlet是作为一个C扩展模块给未修改的解释器的。

1.1   例子

假设系统是被控制台程序控制的,由用户输入命令。假设输入是一个个字符的。这样的系统有如如下的样子:

def process_commands(*args):
    while True:
        line=''
        while not line.endswith('\n'):
            line+=read_next_char()
        if line=='quit\n':
            print "are you sure?"
            if read_next_char()!="y":
                continue    #忽略指令
        process_commands(line)

现在假设你要把程序移植到GUI,而大多数GUI是事件驱动的。他们会在每次的用户输入时调用回调函数。这种情况下,就很难实现 read_next_char() 函数。我们有两个不兼容的函数:

def event_keydown(key):
    ??

def read_next_char():
    ?? 需要等待 event_keydown() 的调用

你可能在考虑用线程实现。而 Greenlet 是另一种解决方案,没有锁和关闭问题。你启动 process_commands() 函数,分割成 greenlet ,然后与按键事件交互,有如:

def event_keydown(key):
    g_processor.switch(key)

def read_next_char():
    g_self=greenlet.getcurrent()
    next_char=g_self.parent.switch()    #跳到上一层(main)的greenlet,等待下一次按键
    return next_char

g_processor=greenlet(process_commands)
g_processor.switch(*args)
gui.mainloop()

这个例子的执行流程是: read_next_char() 被调用,也就是 g_processor 的一部分,它就会切换(switch)到他的父greenlet,并假设继续在顶级主循环中执行(GUI主循环)。当GUI调用 event_keydown() 时,它切换到 g_processor ,这意味着执行会跳回到原来挂起的地方,也就是 read_next_char() 函数中的切换指令那里。然后 event_keydown() 的 key 参数就会被传递到 read_next_char() 的切换处,并返回。

注意 read_next_char() 会被挂起并假设其调用栈会在恢复时保护的很好,所以他会在被调用的地方返回。这允许程序逻辑保持优美的顺序流。我们无需重写 process_commands() 来用到一个状态机中。

2   使用

2.1   简介

一个 “greenlet” 是一个很小的独立微线程。可以把它想像成一个堆栈帧,栈底是初始调用,而栈顶是当前greenlet的暂停位置。你使用greenlet创建一堆这样的堆 栈,然后在他们之间跳转执行。跳转不是绝对的:一个greenlet必须选择跳转到选择好的另一个greenlet,这会让前一个挂起,而后一个恢复。两 个greenlet之间的跳转称为 切换(switch)

当你创建一个greenlet,它得到一个初始化过的空堆栈;当你第一次切换到它,他会启动指定的函数,然后切换跳出greenlet。当最终栈底 函数结束时,greenlet的堆栈又编程空的了,而greenlet也就死掉了。greenlet也会因为一个未捕捉的异常死掉。

例如:

from py.magic import greenlet

def test1():
    print 12
    gr2.switch()
    print 34

def test2():
    print 56
    gr1.switch()
    print 78

gr1=greenlet(test1)
gr2=greenlet(test2)
gr1.switch()

最后一行跳转到 test1() ,它打印12,然后跳转到 test2() ,打印56,然后跳转回 test1() ,打印34,然后 test1() 就结束,gr1死掉。这时执行会回到原来的 gr1.switch() 调用。注意,78是不会被打印的。

2.2   父greenlet

现在看看一个greenlet死掉时执行点去哪里。每个greenlet拥有一个父greenlet。父greenlet在每个greenlet初 始化时被创建(不过可以在任何时候改变)。父greenlet是当greenlet死掉时,继续原来的位置执行。这样,greenlet就被组织成一棵 树,顶级的代码并不在用户创建的 greenlet 中运行,而称为主greenlet,也就是树根。

在上面的例子中,gr1和gr2都是把主greenlet作为父greenlet的。任何一个死掉,执行点都会回到主函数。

未捕获的异常会波及到父greenlet。如果上面的 test2() 包含一个打印错误(typo),他会生成一个 NameError 而干掉gr2,然后执行点会回到主函数。traceback会显示 test2() 而不是 test1() 。记住,切换不是调用,但是执行点可以在并行的栈容器间并行交换,而父greenlet定义了栈最初从哪里来。

2.3   实例

py.magic.greenlet 是一个 greenlet 类型,支持如下操作:

greenlet(run=None,parent=None)

创建一个greenlet对象,而不执行。run是执行回调,而parent是父greenlet,缺省是当前greenlet。

greenlet.getcurrent()

返回当前greenlet,也就是谁在调用这个函数。

greenlet.GreenletExit

这个特定的异常不会波及到父greenlet,它用于干掉一个greenlet。

greenlet 类型可以被继承。一个greenlet通过调用其 run 属性执行,就是创建时指定的那个。对于子类,可以定义一个 run() 方法,而不必严格遵守在构造器中给出 run 参数。

2.4   切换

greenlet之间的切换发生在greenlet的 switch() 方法被调用时,这会让执行点跳转到greenlet的 switch() 被调用处。或者在greenlet死掉时,跳转到父greenlet那里去。在切换时,一个对象或异常被发送到目标greenlet。这可以作为两个greenlet之间传递信息的方便方式。例如:

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

def test2(u):
    print u
    gr1.switch(42)

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

这会打印出 “hello world” 和42,跟前面的例子的输出顺序相同。注意 test1() 和 test2() 的参数并不是在 greenlet 创建时指定的,而是在第一次切换到这里时传递的。

这里是精确的调用方式:

g.switch(obj=None or *args)

切换到执行点greenlet g,发送给定的对象obj。在特殊情况下,如果g还没有启动,就会让它启动;这种情况下,会传递参数过去,然后调用 g.run(*args)

垂死的greenlet

如果一个greenlet的 run() 结束了,他会返回值到父greenlet。如果 run() 是异常终止的,异常会波及到父greenlet(除非是 greenlet.GreenletExit 异常,这种情况下异常会被捕捉并返回到父greenlet)。

除了上面的情况外,目标greenlet会接收到发送来的对象作为 switch() 的返回值。虽然 switch() 并不会立即返回,但是它仍然会在未来某一点上返回,当其他greenlet切换回来时。当这发生时,执行点恢复到 switch() 之后,而 switch() 返回刚才调用者发送来的对象。这意味着 x=g.switch(y) 会发送对象y到g,然后等着一个不知道是谁发来的对象,并在这里返回给x。

注意,任何尝试切换到死掉的greenlet的行为都会切换到死掉greenlet的父greenlet,或者父的父,等等。最终的父就是 main greenlet,永远不会死掉的。

2.5   greenlet的方法和属性

g.switch(obj=None or *args)

切换执行点到greenlet g,同上。

g.run

调用可执行的g,并启动。在g启动后,这个属性就不再存在了。

g.parent

greenlet的父。这是可写的,但是不允许创建循环的父关系。

g.gr_frame

当前顶级帧,或者None。

g.dead

判断是否已经死掉了

bool(g)

如果g是活跃的则返回True,在尚未启动或者结束后返回False。

g.throw([typ,[val,[tb]]])

切换执行点到greenlet g,但是立即抛出指定的异常到g。如果没有提供参数,异常缺省就是 greenlet.GreenletExit 。根据异常波及规则,有如上面描述的。注意调用这个方法等同于如下:

def raiser():
    raise typ,val,tb

g_raiser=greenlet(raiser,parent=g)
g_raiser.switch()

2.6   Greenlet与Python线程

greenlet可以与Python线程一起使用;在这种情况下,每个线程包含一个独立的 main greenlet,并拥有自己的greenlet树。不同线程之间不可以互相切换greenlet。

2.7   活动greenlet的垃圾收集

如果不再有对greenlet对象的引用时(包括其他greenlet的parent),还是没有办法切换回greenlet。这种情况下会生成一个 GreenletExit 异常到greenlet。这是greenlet收到异步异常的唯一情况。应该给出一个 try .. finally 用于清理greenlet内的资源。这个功能同时允许greenlet中无限循环的编程风格。这样循环可以在最后一个引用消失时自动中断。

如果不希望greenlet死掉或者把引用放到别处,只需要捕捉和忽略 GreenletExit 异常即可。

greenlet不参与垃圾收集;greenlet帧的循环引用数据会被检测到。将引用传递到其他的循环greenlet会引起内存泄露。

ProtoBuf开发者指南 – 非官方不完整版

Monday, September 22nd, 2008

ProtoBuf开发者指南

译者: gashero

目录

1   概览

欢迎来到protocol buffer的开发者指南文档,一种语言无关、平台无关、扩展性好的用于通信协议、数据存储的结构化数据串行化方法。

本文档面向希望使用protocol buffer的Java、C++或Python开发者。这个概览介绍了protocol buffer,并告诉你如何开始,你随后可以跟随编程指导( http://code.google.com/apis/protocolbuffers/docs/tutorials.html )深入了解protocol buffer编码方式( http://code.google.com/apis/protocolbuffers/docs/encoding.html )。API参考文档( http://code.google.com/apis/protocolbuffers/docs/reference/overview.html )同样也是提供了这三种编程语言的版本,不够协议语言( http://code.google.com/apis/protocolbuffers/docs/proto.html )和样式( http://code.google.com/apis/protocolbuffers/docs/style.html )指导都是编写 .proto 文件。

1.1   什么是protocol buffer

ProtocolBuffer是用于结构化数据串行化的灵活、高效、自动的方法,有如XML,不过它更小、更快、也更简单。你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。你甚至可以在无需重新部署程序的情况下更新数据结构。

1.2   他们如何工作

你首先需要在一个 .proto 文件中定义你需要做串行化的数据结构信息。每个ProtocolBuffer信息是一小段逻辑记录,包含一系列的键值对。这里有个非常简单的 .proto 文件定义了个人信息:

message Person {
    required string name=1;
    required int32 id=2;
    optional string email=3;

    enum PhoneType {
        MOBILE=0;
        HOME=1;
        WORK=2;
    }

    message PhoneNumber {
        required string number=1;
        optional PhoneType type=2 [default=HOME];
    }

    repeated PhoneNumber phone=4;
}

有如你所见,消息格式很简单,每个消息类型拥有一个或多个特定的数字字段,每个字段拥有一个名字和一个值类型。值类型可以是数字(整数或浮点)、布 尔型、字符串、原始字节或者其他ProtocolBuffer类型,还允许数据结构的分级。你可以指定可选字段,必选字段和重复字段。你可以在( http://code.google.com/apis/protocolbuffers/docs/proto.html )找到更多关于如何编写 .proto 文件的信息。

一旦你定义了自己的报文格式(message),你就可以运行ProtocolBuffer编译器,将你的 .proto 文件编译成特定语言的类。这些类提供了简单的方法访问每个字段(像是 query() 和 set_query() ),像是访问类的方法一样将结构串行化或反串行化。例如你可以选择C++语言,运行编译如上的协议文件生成类叫做 Person 。随后你就可以在应用中使用这个类来串行化的读取报文信息。你可以这么写代码:

Person person;
person.set_name("John Doe");
person.set_id(1234);
person.set_email("jdoe@example.com");
fstream.output("myfile",ios::out | ios::binary);
person.SerializeToOstream(&output);

然后,你可以读取报文中的数据:

fstream input("myfile",ios::in | ios:binary);
Person person;
person.ParseFromIstream(&input);
cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;

你可以在不影响向后兼容的情况下随意给数据结构增加字段,旧有的数据会忽略新的字段。所以如果使用ProtocolBuffer作为通信协议,你可以无须担心破坏现有代码的情况下扩展协议。

你可以在API参考( http://code.google.com/apis/protocolbuffers/docs/reference/overview.html )中找到完整的参考,而关于ProtocolBuffer的报文格式编码则可以在( http://code.google.com/apis/protocolbuffers/docs/encoding.html )中找到。

1.3   为什么不用XML?

ProtocolBuffer拥有多项比XML更高级的串行化结构数据的特性,ProtocolBuffer:

  • 更简单
  • 小3-10倍
  • 快20-100倍
  • 更少的歧义
  • 可以方便的生成数据存取类

例如,让我们看看如何在XML中建模Person的name和email字段:

<person>
    <name>John Doe</name>
    <email>jdoe@example.com</email>
</person>

对应的ProtocolBuffer报文则如下:

#ProtocolBuffer的文本表示
#这不是正常时使用的二进制数据
person {
    name: "John Doe"
    email: "jdoe@example.com"
}

当这个报文编码到ProtocolBuffer的二进制格式( http://code.google.com/apis/protocolbuffers/docs/encoding.html )时(上面的文本仅用于调试和编辑),它只需要28字节和100-200ns的解析时间。而XML的版本需要69字节(除去空白)和5000-10000ns的解析时间。

当然,操作ProtocolBuffer也很简单:

cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;

而XML的你需要:

cout << "Name: "
     << person.getElementsByTagName("name")->item(0)->innerText()
     << endl;
cout << "E-mail: "
     << person.getElementsByTagName("email")->item(0)->innerText()
     << end;

当然,ProtocolBuffer并不是在任何时候都比XML更合适,例如ProtocolBuffer无法对一个基于标记文本的文档建模,因为 你根本没法方便的在文本中插入结构。另外,XML是便于人类阅读和编辑的,而ProtocolBuffer则不是。还有XML是自解释的,而 ProtocolBuffer仅在你拥有报文格式定义的 .proto 文件时才有意义。

1.4   听起来像是为我的解决方案,如何开始?

下载包( http://code.google.com/p/protobuf/downloads/ ),包含了Java、Python、C++的ProtocolBuffer编译器,用于生成你需要的IO类。构建和安装你的编译器,跟随README的指令就可以做到。

一旦你安装好了,就可以跟着编程指导( http://code.google.com/apis/protocolbuffers/docs/tutorials.html )来选择语言-随后就是使用ProtocolBuffer创建一个简单的应用了。

1.5   一点历史

ProtocolBuffer最初是在Google开发的,用以解决索引服务器的请求、响应协议。在使用ProtocolBuffer之前,有一种格式用以处理请求和响应数据的编码和解码,并且支持多种版本的协议。而这最终导致了丑陋的代码,有如:

if (version==3) {
    ...
}else if (version>4) {
    if (version==5) {
        ...
    }
    ...
}

通信协议因此变得越来越复杂,因为开发者必须确保,发出请求的人和接受请求的人必须同时兼容,并且在一方开始使用新协议时,另外一方也要可以接受。

ProtocolBuffer设计用于解决这一类问题:

  • 很方便引入新字段,而中间服务器可以忽略这些字段,直接传递过去而无需理解所有的字段。
  • 格式可以自描述,并且可以在多种语言中使用(C++、Java等)

然而用户仍然需要手写解析代码。

随着系统的演化,他需要一些其他的功能:

  • 自动生成编码和解码代码,而无需自己编写解析器。
  • 除了用于简短的RPC(Remote Procedure Call)请求,人们使用ProtocolBuffer来做数据存储格式(例如BitTable)。
  • RPC服务器接口可以作为 .proto 文件来描述,而通过ProtocolBuffer的编译器生成存根(stub)类供用户实现服务器接口。

ProtocolBuffer现在已经是Google的混合语言数据标准了,现在已经正在使用的有超过48,162种报文格式定义和超过12,183个 .proto 文件。他们用于RPC系统和持续数据存储系统。

2   语言指导

本指导描述了如何使用ProtocolBuffer语言来定义结构化数据类型,包括 .proto 文件的语法和如何生成存取类。

这是一份指导手册,一步步的例子使用文档中的多种功能,查看入门指导( http://code.google.com/apis/protocolbuffers/docs/tutorials.html )选择你的语言。

2.1   定义一个消息类型

@waiting …

2.2   值类型

@waiting …

2.3   可选字段与缺省值

@waiting …

2.4   枚举

@waiting …

2.5   使用其他消息类型

@waiting …

2.6   嵌套类型

@waiting …

2.7   更新一个数据类型

@waiting …

2.8   扩展

@waiting …

2.9   包

@waiting …

2.10   定义服务

@waiting …

2.11   选项

@waiting …

2.12   生成你的类

@waiting …

3   代码风格指导

本文档提供了 .proto 文件的代码风格指导。按照惯例,你将会,你将会生成一些便于阅读和一致的ProtocolBuffer定义文件。

3.1   消息与字段名

使用骆驼风格的大小写命名,即单词首字母大写,来做消息名。使用GNU的全部小写,使用下划线分隔的方式定义字段名:

message SongServerRequest {
    required string song_name=1;
}

使用这种命名方式得到的名字如下:

C++:
    const string& song_name() {...}
    void set_song_name(const string& x) {...}

Java:
    public String getSongName() {...}
    public Builder setSongName(String v) {...}

3.2   枚举

使用骆驼风格做枚举名,而用全部大写做值的名字:

enum Foo {
    FIRST_VALUE=1;
    SECOND_VALUE=2;
}

每个枚举值最后以分号结尾,而不是逗号。

3.3   服务

如果你的 .proto 文件定义了RPC服务,你可以使用骆驼风格:

service FooService {
    rpc GetSomething(FooRequest) returns (FooResponse);
}

4   编码

本文档描述了ProtocolBuffer的串行化二进制数据格式定义。你如果仅仅是在应用中使用ProtocolBuffer,并不需要知道这些,但是这些会对你定义高效的格式有所帮助。

4.1   一个简单的消息

@waiting …

4.2   基于128的Varints

@waiting …

4.3   消息结构

@waiting …

4.4   更多的值类型

@waiting …

4.5   内嵌消息

@waiting …

4.6   可选的和重复的元素

@waiting …

4.7   字段顺序

@waiting …

5   ProtocolBuffer基础:C++

@waiting …

6   ProtocolBuffer基础:Java

@waiting …

7   ProtocolBuffer基础:Python

本指南给Python程序员一个快速使用的ProtocolBuffer的指导。通过一些简单的例子来在应用中使用ProtocolBuffer,它向你展示了如何:

  • 定义 .proto 消息格式文件
  • 使用ProtocolBuffer编译器
  • 使用Python的ProtocolBuffer编程接口来读写消息

这并不是一个在Python中使用ProtocolBuffer的完整指导。更多细节请参考手册信息,查看语言指导( http://code.google.com/apis/protocolbuffers/docs/proto.html ),Python API( http://code.google.com/apis/protocolbuffers/docs/reference/python/index.html ),和编码手册( http://code.google.com/apis/protocolbuffers/docs/encoding.html )。

7.1   为什么使用ProtocolBuffer?

下面的例子”地址本”应用用于读写人的联系信息。每个人有name、ID、email,和联系人电话号码。

如何串行化和读取结构化数据呢?有如下几种问题:

  • 使用Python的pickle,这是语言内置的缺省方法,不过没法演化,也无法让其他语言支持。
  • 你可以发明一种数据编码方法,例如4个整数”12:3-23:67″,这是简单而灵活的方法,不过你需要自己写解析器代码,且只适用于简单的数据。
  • 串行化数据到XML。这种方法因为可读性和多种语言的兼容函数库而显得比较吸引人,不过这也不是最好的方法,因为XML浪费空间是臭名昭著的,编码解码也很浪费时间。而XML DOM树也是很复杂的。

ProtocolBuffer提供了灵活、高效、自动化的方法来解决这些问题。通过ProtocolBuffer,只需要写一个 .proto 数据结构描述文件,就可以编译到几种语言的自动编码解码类。生成的类提供了setter和getter方法来控制读写细节。最重要的是ProtocolBuffer支持后期扩展协议,而又确保旧格式可以兼容。

7.2   哪里可以找到例子代码

源码发行包中已经包含了,在”example”文件夹。

7.3   定义你的协议格式

想要创建你的地址本应用,需要开始于一个 .proto 文件。定义一个 .proto 文件很简单:添加一个消息到数据结构,然后指定一个和一个类型到每一个字段,如下是本次例子使用的 addressbook.proto

package tutorial;

message Person {
    required string name=1;
    required int32 id=2;
    optional string email=3;

    enum PhoneType {
        MOBILE=0;
        HOME=1;
        WORK=2;
    }

    message PhoneNumber {
        required string number=1;
        optional PhoneType type=2 [default=HOME];
    }

    repeated PhoneNumber phone=4;
}

message AddressBook {
    repeated Person person=1;
}

有如你所见的,语法类似于C++或Java。让我们分块理解他们。

@waiting …

7.4   编译你的ProtocolBuffer

现在已经拥有了 .proto 文件,下一步就是编译生成相关的访问类。运行编译器 protoc 编译你的 .proto 文件。

  1. 如果还没安装编译器则下载并按照README的安装。
  2. 运行编译器,指定源目录和目标目录,定位你的 .proto 文件到源目录,然后执行:
    protoc -I=$SRC_DIR --python_out=$DST_DIR addressbook.proto

因为需要使用Python类,所以 --python_out 选项指定了特定的输出语言。

这个步骤会生成 addressbook_pb2.py 到目标目录。

7.5   ProtocolBuffer API

不像生成的C++和Java代码,Python生成的类并不会直接为你生成存取数据的代码。而是(有如你在 addressbook_pb2.py 中见到的)生成消息描述、枚举、和字段,还有一些神秘的空类,每个对应一个消息类型:

class Person(message.Message):
    __metaclass__=reflection.GeneratedProtocolMessageType

    class PhoneNumber(message.Message):
        __metaclass__=reflection.GeneratedProtocolMessageType
        DESCRIPTION=_PERSON_PHONENUMBER

    DESCRIPTOR=_PERSON

class AddressBook(message.Message):
    __metaclass__=reflection.GeneratedProtocolMessageType
    DESCRIPTOR=_ADDRESSBOOK

这里每个类最重要的一行是 __metaclass__=reflection.GeneratedProtocolMessageType 。通过Python的元类机制工作,你可以把他们看做是生成类的模板。在载入时, GeneratedProtocolMessageType 元类使用特定的描述符创建Python方法。随后你就可以使用完整的功能了。

最后就是你可以使用 Person 类来操作相关字段了。例如你可以写:

import addressbook_pb2
person=addressbook_pb2.Person()
person.id=1234
person.name="John Doe"
person.email="jdoe@example.com"
phone=person.phone.add()
phone.number="555-4321"
phone.type=addressbook_pb2.Person.HOME

需要注意的是这些赋值属性并不是简单的增加新字段到Python对象,如果你尝试给一个 .proto 文件中没有定义的字段赋值,就会抛出 AttributeError 异常,如果赋值类型错误会抛出 TypeError 。在给一个字段赋值之前读取会返回缺省值:

person.no_such_field=1  #raise AttributeError
person.id="1234"        #raise TypeError

更多相关信息参考( http://code.google.com/apis/protocolbuffers/docs/reference/python-generated.html )。

7.5.1   枚举

枚举在元类中定义为一些符号常量对应的数字。例如常量 addressbook_pb2.Person.WORK 拥有值2。

7.5.2   标准消息方法

每个消息类包含一些其他方法允许你检查和控制整个消息,包括:

  • IsInitialized() :检查是否所有必须(required)字段都已经被赋值了。
  • __str__() :返回人类可读的消息表示,便于调试。
  • CopyFrom(other_msg) :使用另外一个消息的值来覆盖本消息。
  • Clear() :清除所有元素的值,回到初识状态。

这些方法是通过接口 Message 实现的,更多消息参考( http://code.google.com/apis/protocolbuffers/docs/reference/python/google.protobuf.message.Message-class.html )。

7.5.3   解析与串行化

最后,每个ProtocolBuffer类有些方法用于读写消息的二进制数据( http://code.google.com/apis/protocolbuffers/docs/encoding.html )。包括:

  • SerializeToString() :串行化,并返回字符串。注意是二进制格式而非文本。
  • ParseFromString(data) :解析数据。

他们是成对使用的,提供二进制数据的串行化和解析。另外参考消息API参考( http://code.google.com/apis/protocolbuffers/docs/reference/python/google.protobuf.message.Message-class.html )了解更多信息。

Note

ProtocolBuffer与面向对象设计

ProtocolBuffer类只是用于存取数据的,类似于C++中的结构体,他们并没有在面向对象方面做很好的设计。如果你想要给这些类添加更多的行为,最好的方法是包装(wrap)。包装同样适合于复用别人写好的 .proto 文件。这种情况下,你可以把ProtocolBuffer生成类包装的很适合于你的应用,并隐藏一些数据和方法,暴露有用的函数等等。 你不可以通过继承来给自动生成的类添加行为。 这会破坏他们的内部工作机制。

7.6   写消息

现在开始尝试使用ProtocolBuffer的类。第一件事是让地址本应用可以记录联系人的细节信息。想要做这些需要先创建联系人实例,然后写入到输出流。

这里的程序从文件读取地址本,添加新的联系人信息,然后写回新的地址本到文件。

#! /usr/bin/python
import addressbook_pb2
import sys

#这个函数使用用户输入填充联系人信息
def PromptForAddress(person):
    person.id=int(raw_input("Enter person ID number: "))
    person.name=raw_input("Enter name: ")
    email=raw_input("Enter email address (blank for none): ")
    if email!="":
        person.email=email
    while True:
        number=raw_input("Enter a phone number (or leave blank to finish): ")
        if number=="":
            break
        phone_number=person.phone.add()
        phone_number.number=number
        type=raw_input("Is this a mobile, home, or work phone? ")
        if type=="mobile":
            phone_number.type=addressbook_pb2.Person.MOBILE
        elif type=="home":
            phone_number.type=addressbook_pb2.Person.HOME
        elif type=="work":
            phone_number.type=addressbook_pb2.Person.WORK
        else:
            print "Unknown phone type; leaving as default value."

#主函数,从文件读取地址本,添加新的联系人,然后写回到文件
if len(sys.argv)!=2:
    print "Usage:",sys.argv[0],"ADDRESS_BOOK_FILE"
    sys.exit(-1)

address_book=addressbook_pb2.AddressBook()

#读取已经存在的地址本
try:
    f=open(sys.argv[1],"fb")
    address_book.ParseFromString(f.read())
    f.close()
except OSError:
    print sys.argv[1]+": Count open file. Creating a new one."

#添加地址
PromptFromAddress(address_book.person.add())

#写入到文件
f=open(sys.argv[1],"wb")
f.write(address_book.SerializeToString())
f.close()

7.7   读消息

当然,一个无法读取的地址本是没什么用处的,这个例子读取刚才创建的文件并打印所有信息:

#! /usr/bin/python

import addressbook_pb2
import sys

#遍历地址本中所有的人并打印出来
def ListPeople(address_book):
    for person in address_book.person:
        print "Person ID:",person.id
        print "  Name:",person.name
        if person.HasField("email"):
            print "  E-mail:",person.email
        for phone_number in person.phone:
            if phone_number.type==addressbook_pb2.Person.MOBILE:
                print "  Mobile phone #:",
            elif phone_number.type==addressbook_pb2.Person.HOME:
                print "  Home phone #:",
            elif phone_number.type==addressbook_pb2.Person.WORK:
                print "  Work phone #:",
            print phone_number.number

#主函数,从文件读取地址本
if len(sys.argv)!=2:
    print "Usage:",sys.argv[0],"ADDRESS_BOOK_FILE"
    sys.exit(-1)

address_book=addressbook_pb2.AddressBook()

#读取整个地址本文件
f=open(sys.argv[1],"rb")
address_book.ParseFromString(f.read())
f.close()

ListPeople(address_book)

7.8   扩展ProtocolBuffer

在你发不了代码以后,可能会想要改进ProtocolBuffer的定义。如果你想新的数据结构向后兼容,而你的旧数据可以向前兼容,那么你就找对了东西了,不过有些规则需要遵守。在新版本的ProtocolBuffer中:

  • 必须不可以改变已经存在的标签的数字。
  • 必须不可以增加或删除必须(required)字段。
  • 可以删除可选(optional)或重复(repeated)字段。
  • 可以添加新的可选或重复字段,但是必须使用新的标签数字,必须是之前的字段所没有用过的。

这些规则也有例外( http://code.google.com/apis/protocolbuffers/docs/proto.html#updating ),不过很少使用。

如果你遵从这些规则,旧代码会很容易的读取新的消息,并简单的忽略新的字段。而对旧的被删除的可选字段也会简单的使用他们的缺省值,被删除的重复字段会自动为空。新的代码也会透明的读取旧的消息。然而,需要注意的是新的可选消息不会在旧的消息中显示,所以你需要使用 has_ 严格的检查他们是否存在,或者在 .proto 文件中提供一个缺省值。如果没有缺省值,就会有一个类型相关的默认缺省值:对于字符串就是空字符串;对于布尔型则是false;对于数字类型默认为0。同时要注意的是如果你添加了新的重复字段,你的新代码不会告诉你这个字段为空(新代码)也不会,也不会(旧代码)包含 has_ 标志。

7.9   高级使用

ProtocolBuffer不仅仅提供了数据结构的存取和串行化。查看Python API参考( http://code.google.com/apis/protocolbuffers/docs/reference/python/index.html )了解更多功能。

一个核心功能是通过消息类的映射(reflection)提供的。你可以通过它遍历消息的所有字段,和管理他们的值。关于映射的一个很有用的地方是 转换到其他编码,如XML或JSON。一个使用映射的更高级的功能是寻找同类型两个消息的差异,或者开发出排序、正则表达式等功能。使用你的创造力,还可 以用ProtocolBuffer实现比你以前想象的更多的问题。

映射是通过消息接口提供的。

8   参考概览

@waiting …

9   C++代码生成

@waiting …

10   C++ API

@waiting …

11   Java代码生成

@waiting …

12   Java API

@waiting …

13   Python代码生成

本页提供了Python生成类的相关细节。你可以在阅读本文档之前查看语言指导。

Python的ProtocolBuffer实现与C++和Java的略有不同,编译器只输出构建代码的描述符来生成类,而由Python的元类来执行工作。本文档描述了元类开始生效以后的东西。

13.1   编译器的使用

ProtocolBuffer通过编译器的 --python_out= 选项来生成Python的相关类。这个参数实际上是指定输出的Python类放在哪个目录下。编译器会为每个 .proto 文件生成一个对应的 .py 文件。输出文件名与输入文件名相关,不过有两处修改:

  • 扩展名 .proto 改为 .py 。
  • 路径名的修改。

如果你按照如下调用编译器:

protoc --proto_path=src --python_out=build/gen src/foo.proto src/bar/baz.proto

编译器会自动读取两个 .proto 文件然后产生两个输出文件。在需要时编译器会自动创建目录,不过 --python_out 指定的目录不会自动创建。

需要注意的是,如果 .proto 文件名或路径包含有无法在Python中使用的模块名(如连字符),就会被自动转换为下划线。所以文件 foo-bar.proto 会变成 foo_bar_pb2.py

Note

在每个文件后缀的 _pb2.py 中的2代表ProtocolBuffer版本2。版本1仅在Google内部使用,但是你仍然可以在以前发布的一些代码中找到它。自动版本2开 始,ProtocolBuffer开始使用完全不同的接口了,从此Python也没有编译时类型检查了,我们加上这个版本号来标志Python文件名。

13.2   包

Python代码生成根本不在乎包的名字。因为Python使用目录名来做包名。

13.3   消息

先看看一个简单的消息声明:

message Foo {}

ProtocolBuffer编译器会生成类Foo,它是 google.protobuf.Message 的子类。这个实体类,不含有虚拟方法。不像C++和Java,Python生成类对优化选项不感冒;实际上Python的生成代码已经为代码大小做了优化。

你不能继承Foo的子类。生成类被设计不可以被继承,否则会被打破一些设计。另外,继承本类也是不好的设计。

Python的消息类没有特定的公共成员,而是定义接口,极其嵌套的字段、消息和枚举类型。

一个消息可以在另外一个消息中声明,例如 message Foo { message Bar {}} 。在这种情况下,Bar类定义为Foo的一个静态成员,所以你可以通过 Foo.Bar 来引用。

13.4   字段

对于消息类型中的每一个字段,都有对应的同名成员。

13.4.1   简单字段

如果你有一个简单字段(包括可选的和重复的),也就是非消息字段,你可以通过简单字段的方式来管理,例如foo字段的类型是int32,你可以:

message.foo=123
print message.foo

注意设置foo的值,如果类型错误会抛出TypeError。

如果foo在赋值之前就读取,就会使用缺省值。想要检查是否已经赋值,可以用 HasField() ,而清除该字段的值用 ClearField() 。例如:

assert not message.HasField("foo")
message.foo=123
assert message.HasField("foo")
message.ClearField("foo")
assert not message.HasField("foo")

13.4.2   简单消息字段

消息类型工作方式略有不同。你无法为一个嵌入消息字段赋值。而是直接操作这个消息的成员。因为实例化上层消息时,其包含的子消息同时也实例化了,例如定义:

message Foo {
    optional Bar bar=1;
}

message bar {
    optional int32 i=1;
}

你不可以这么做,因为不能做消息类型字段的赋值:

foo=Foo()
foo.bar=Bar()   #WRONG!

而是可以直接对消息类型字段的成员赋值:

foo=Foo()
assert not foo.HasField("bar")
foo.bar.i=1
assert foo.HasField("bar")

注意简单的读取消息类型字段的未赋值成员只不过是打印其缺省值:

foo=Foo()
assert not foo.HasField("bar")
print foo.bar.i #打印i的缺省值
assert not foo.HasField("bar")

13.4.3   重复字段

重复字段表现的像是Python的序列类型。如果是嵌入的消息,你无法为字段直接赋值,但是你可以管理。例如给定的定义:

message Foo {
    repeated int32 nums=1;
}

你就可以这么做:

foo=Foo()
foo.nums.append(15)
foo.nums.append(32)
assert len(foo.nums)==2
assert foo.nums[0]==15
assert foo.nums[1]==32
for i in foo.nums:
    print i
foo.nums[1]=56
assert foo.nums[1]==56

作为一种简单字段,清除该字段必须使用 ClearField() 。

13.4.4   重复消息字段

重复消息字段工作方式与重复字段很像,除了 add() 方法用于返回新的对象以外。例如如下定义:

message Foo {
    repeated Bar bar=1;
}

message Bar {
    optional int32 i=1;
}

你可以这么做:

foo=Foo()
bar=foo.bars.add()
bar.i=15
bar=foo.bars.add()
bar.i=32
assert len(foo.bars)==2
assert foo.bars[0].i==15
assert foo.bars[1].i==32
for bar in foo.bars:
    print bar.i
foo.bars[1].i=56
assert foo.bars[1].i==56

13.4.5   枚举类型

@waiting …

13.4.6   扩展

@waiting …

13.5   服务

13.5.1   接口

一个简单的接口定义:

service Foo {
    rpc Bar(FooRequest) returns(FooResponse);
}

ProtocolBuffer的编译器会生成类 Foo 来展示这个服务。 Foo 将会拥有每个服务定义的方法。在这种情况下 Bar 方法的定义是:

def Bar(self,rpc_controller,request,done)

参数等效于 Service.CallMethod() ,除了隐含的 method_descriptor 参数。

这些生成的方法被定义为可以被子类重载。缺省实现只是简单的调用 controller.SetFailed() 而抛出错误信息告之尚未实现。然后调用done回调。在实现你自己的服务时,你必须继承生成类,然后重载各个接口方法。

Foo继承了 Service 接口。ProtocolBuffer编译器会自动声响相关的实现方法:

  • GetDescriptor :返回服务的 ServiceDescriptor 。
  • CallMethod :检测需要调用哪个方法,并且直接调用。
  • GetRequestClass 和 GetResponseClass :返回指定方法的请求和响应类。

13.5.2   存根(Stub)

ProtocolBuffer编译器也会为每个服务接口提供一个存根实现,用于客户端发送请求到服务器。对于Foo服务,存根实现是 Foo_Stub 。

Foo_Stub 是Foo的子类,他的构造器是一个 RpcChannel 。存根会实现调用每个服务方法的 CallMethod() 。

ProtocolBuffer哭并不包含RPC实现。然而,它包含了你构造服务类的所有工具,不过选择RPC实现则随你喜欢。你只需要提供 RpcChannel 和 RpcController 的实现即可。

14   Python API

@waiting …

15   其他语言

@waiting …

如何在较新的S60 3rd手机上安装PyS60

Sunday, September 14th, 2008

以前在我的Nokia 5500上安装pys60一帆风顺,可是后来有个同事拿来个E71,结果安装后找不到Python的启动图标,一头雾水了好久。直到近日给我的N95 8GB安装时也是一样的问题。安装一切顺利,安装后却找不到Python的图标。网上找了好一会才发现是少安装了一个东西- “PythonScriptShell_1_4_4_3rdEd.SIS” ,这个东西专门提供Python图标用的。真是晕死……,为啥官方文档不早说。

此提示来自于博客 “http://hi.baidu.com/xncyy/blog/item/60dbc524b76734318744f9a6.html” ,感谢此博客作者。

好了,问题就这么简单,把两个东西都装上以后,就可以在主菜单的”应用程序”目录里找到了。

刚才无意间发现搜索”pys60″,我的博客居然在google上排名第二位。这是自zenoss以来第二个达到这个水平的关键词,撒花……

httplib/urllib/urllib2很不靠谱

Thursday, August 7th, 2008

如题,httplib在连接同一个DNS得到的多个IP时有bug导致Operation now in progress错误。而urllib/urllib2更是基于httplib的。

最近咬咬牙,升级到Python2.5.2了,也许升级版本会解决这个问题。不过我更加倾向于使用curl。

纠结啊,纠结

Tuesday, July 29th, 2008

豆瓣上的twisted小组是我建立的,发现一个朋友把twisted称为“纠结”,发现很切题,我喜欢。

那个累死人的项目接近尾声以来,帖子越来越水了。

python-memcache内存泄露问题

Monday, July 28th, 2008

作为广为流行的、独霸一方的一个组件,python-memcache是给python连接memcache用的(废话)。可是就这么个东西居然是如此的垃圾,每每让我惊叹。

1、跟同事合作开发系统,他用Java/PHP,我用Python,需要共同通信的部分用了4台memcache做分布。Java与PHP连接memcache的组件用得相同的分布算法,兼容很好,如果一个键存到一台机器上,就绝对不用再去另外一台上遍历。而Python的这个,用了作者自己写的一套山寨分布算法,与Java/PHP的不兼容。丫的要是Python在TIOBE上排第一名也就算了。Java和PHP都是牛x大发了的东西,你还敢搞不兼容。

2、一套系统,需要定期清除memcache,使另一个系统在发现找不到cache时来访问我的接口。每秒钟执行20次左右,遍历4台memcache上清除共8个键(我也没写兼容的分布算法,于是遍历)。正常运行状况下发现服务器启动时占用8MB内存,可是每10秒左右就增加 2MB的内存使用量。系统上线后两三个小时就把2GB内存占光了,于是死掉。丫的调试了我4天时间,还熬过两次到4:00,最后才发现是python-memcache里面的内存泄露。我已经不在乎具体哪个点在泄漏了。愤怒之下,自己用socket实现了memcache的通信协议,实现了清除指定键的功能。

见过山寨的东西(Pylons),没见过这么山寨的(python-memcache)。

附使用socket实现的memcache协议,清除key部分:
def clear_cache(md5v):
“””使用socket方式清除memcache”””
key1=KEY_CDNJAVA_MEMCACHE1%{‘md5v':md5v}
key2=KEY_CDNJAVA_MEMCACHE2%{‘md5v':md5v}
for mcs in setting.mcserverlist:
hostport=mcs.split(‘:’,1)
if len(hostport)==1:
host=hostport[0]
port=11211
else:
host,port=hostport
port=int(port)
try:
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.settimeout(1)
s.connect((host,port))
cmd=’delete %s\r\n’%key1
s.send(cmd)
s.recv(1024)
cmd=’delete %s\r\n’%key2
s.send(cmd)
s.recv(1024)
s.close()
except socket.error,ex:
print ‘[EX][clear_cache] %s’%str(ex)
return

twisted线程池的一种实现方式

Monday, July 28th, 2008

一个具有并发控制的计数器,控制当前线程数量。线程数不足时就用reactor.callInThread()启动线程。并任线程自然结束。当然,需要在让主控函数形成循环来不停的生成新的线程。这种循环比较适合用reactor.callLater()来实现,而不是用while True的循环。毕竟twisted是事件驱动的,如果一个函数死循环了,那么其他函数就没有执行的机会了。

在对twisted中使用线程,多次尝试之后,我就是使用了上面的方法。感觉比以前靠谱了很多。

不写代码,使用twistd实现ftp服务器

Sunday, July 27th, 2008

用twisted很久了,常常惊叹于其内置的N多小功能,每次发现都有惊喜。今天调试程序期间看了一眼twistd这个程序的参数,发现ftp等等一堆子命令。尝试一番后还真的启动了一个临时的ftp服务器,对调试有很大帮助。使用方法如下:

twistd ftp

这时启动的ftp服务器的HOME目录为/usr/local/ftp,匿名用户名为anonymous,密码为随便的一个邮箱地址,端口号为2121。

可以通过如下命令查看此服务器的其他参数:

twistd ftp –help

比较有用的就是–root,来指定登陆的根目录。还有就是–port指定端口号。

不过要注意的是,这个服务器仅用于调试,不要应用于产品环境,因为它并没有做安全性方面的工作。

在Windows下会抛出一个ImportError异常。虽然我尝试修改源代码,不过看来还是不支持。再次鄙视一下M$。

PyS60编写的Nokia手机短信备份程序

Wednesday, July 23rd, 2008

用于将手机的收件箱、发件箱、已发信箱、草稿箱中的全部短信备份到存储卡上。分成4个文件来备份以示区分。适用于怕老婆检查手机者,哈哈哈哈。

需要在手机上安装好PyS60的最新版。至于源程序,请跟帖留下电子邮件地址,我会选择时间发邮件给你。

越来越看好S60这个平台了,甚至略有希望于google andriod出现之前symbian可以迅速的完成开源流程并占领山寨机操作系统市场。由于PyS60的文档尚没有翻译完的任何一章,所以还要过些时候才会贴出来。

使用SQLObject

Monday, June 2nd, 2008

使用SQLObject

译者: gashero

目录

1   导入模块

from sqlobject import *

2   定义MySQL使用的URI连接

mysqluri="mysql://user:password@host:port/database"

端口号为3306,一定要指定的。否则按照旧式连接方法里面,端口port属性设为None,就会抛出TypeError异常,因为要求必须是一个整数类型的端口号。如果按照新式的URI方式连接,如果不指定端口号则port默认为0,也会出现连接失败的问题。

sqlite的连接:

sqlite:///full/path/to/database
sqlite:/C|full/path/to/database
sqlite:/:memory:

postgre数据库的连接:

postgres://user@host/database?debug=&cache=
postgres://host:5432/database

3   连接

conn=connectionForURI(mysqluri)
sqlhub.processConnection=conn

4   定义一个表格类

class Person(SQLObject):
    firstName=StringCol()
    middleInitial=StringCol(length=1,default=None)
    lastName=StringCol()

如果没有定义sqlhub,则需要使用 Person._connection=conn 来指定连接。

5   创建表格

Person.createTable()

可以指定参数ifNotExists=True,仅仅在表格不存在时才创建这个表格。

6   自动索引

自动给出一个叫做id的索引,所以不需要人为指定。

在MySQL中定义是:

INT PRIMARY KEY AUTO_INCREMENT

需要使用这个字段时,使用.id属性。

7   创建一个对象

就是创建类的实例:

Person(firstName="John",lastName="Doe")

在SQLObject中的NULL/None并不是代表缺省。NULL代表完全不同的事物、正文或者是人。有时NULL也代表缺省(default),有时代表不适用,有时代表未知。如果希望缺省一个值,可以使用NULL或其他。

SQLObject的default不同于数据库的default。SQLObject从不使用数据库的default。

注意,创建一个对象时,构造方法的参数必须要指定列名,也就是映射类的属性名,否则会出现异常。

8   空值

如果在Person的实例中省略了firstName和lastName则会产生错误,因为没有赋予缺省值。如果是赋予了缺省值的如middleInitial字段,则会被设为NULL值,等同于数据库的None。

9   查询

可以使用类的.get(n)方法得到已经存在的实例。

当创建一个对象时,SQLObject会自动将其存入数据库,而不像其他系统那样,需要手动提交修改。另外,对已有对象的属性修改也会直接存入数据库。

列可以按照属性来存取。注意,对象是独一无二的(unique),如果两次获取同一ID的记录,则获得的是同一对象。这种机制可以确保多个线程存取 同一对象时的一致性。当然,多个线程之间可以不共享对象实例。但是在使用事务(transaction)时,因为事务的隔离性而不成立。

10   开启调试显示

同步显示SQL语句和调试状态。建立连接时使用debug选项:

mysql://user:passwd@host:port/database?debug=t

或:

Person._connection.debug=True

还可以选用的其他选项包括debugOutput(缺省为False),cache(True),autoCommit(True),debugThreading(False)。

在可以看到SQL语句的情况下可以清除的理解工作过程。只需在连接URI中加入”?debug=t”即可。或者设置debug属性。这样,所有的SQL语句都会打印到控制台。这是可靠的,也推荐使用。

11   set方法

用于微弱的提高性能,一次指定多个属性,如:

>>> p.set(firstName="Robert",lastName="Hope Jr.")

12   懒惰的更新

缺省时,每修改一个属性就会导致一个UPDATE的发生,或者每次调用 .set() 时。如果想要避免(avoid)这种仿佛的更新,加入 _lazyUpdate=True 到类定义。这样只有在每次调用 inst.syncUpdate() 或者 obj.sync() 时才写入更新, .sync() 也同时会取出数据库中的最新数据,而 .syncUpdate() 并不这样做。

如果一个实例含有”.sqlmeta.dirty”属性,就是用来指示含有未提交数据的属性。则插入动作会立即提交,这时是没有办法延迟提交的。

13   一对多联系

就是需要建立外键。例如一个地址本类,需要建立外键对应到Person类。

class Address(SQLObject):
    street=StringCol()
    city=StringCol()
    state=StringCol(length=2)
    zip=StringCol(length=9)
    person=ForeignKey('Person')
Address.createTable()

这样通过ForeignKey()方法建立对Person类的外键。实际上是引用了Person对象。实际是按照类的名字(字符串)来引用一个类。数据库中对应person_id列,对应指向person列。

注意:SQLObject使用字符串来引用一个类是因为很多时候也许一个类还不存在。class关键字作为一个命令,在导入一个模块时才执行,并绑定类的名字和类。所以为了确保类之间的引用正确,在所有的类都装载完成之后才建立类之间的联系。

在需要一个人Person对应多个地址时,可用如下连接:

class Person(SQLObject):
    ...
    addresses=MultipleJoin('Address')

在我们已经拥有了Person类的情况下,可以用如下方式修改:

Person.sqlmeta.addJoin(MultipleJoin('Address',joinMethodName='addresses'))

大多数时候可以在创建SQLObject类之后再修改他们。在类定义中使用*Col对象属性等同于使用类方法addColumn()。

然后我们就可以使用自动联系aPerson.addresses,这将会返回一个列表。例如:

>>> p.addresses
[]
>>> Address(street='123 W Main St',city='Smallsville',
...         state="MN",zip='55407',person=p)
<Address 1 ...>
>>> p.addresses
[<Address 1 ...>]

多连接(Multiple Join)类似于关系连接(Related Join),返回结果的列表。你可能更喜欢得到SelectResults对象,可以使用SQLMultipleJoin和SQLRelatedJoin。

14   多对多联系

这个例子中包含用户(user)和角色(role)两个对象,其间包含多对多联系,将使用RelatedJoin来实现。

class User(SQLObject):
    class sqlmeta:
        #user是一些数据库的保留字,所以用了别名
        table="user_table"
    username=StringCol(alternateID=True,length=20)
    #暂时先定义这个,其他还可以有很多字段
    role=RelatedJoin("Role")
class Role(SQLObject):
    name=StringCol(alternateID=True,length=20)
    users=RelatedJoin('User")
User.createTable()
Role.createTable()

注意:使用sqlmeta类。这个类用于存储多种元信息(metadata)。这是SQLObject 0.7中新引入的特性。查看Class sqlmeta节了解详细。

使用:

bob=User(username="bob")
tim=User(username="tim")
admin=Role(name='admin')
editor=Role(name='editor')
bob.addRole(admin)
bob.addRole(editor)
tim.addRole(editor)
>>> bob.roles
[<Role 1 name="admin">, <Role 2 name='editor'>]
>>> tim.roles
[<Role 2 name='editor'>]
>>> admin.users
[<User 1 username='bob'>]
>>> editor.users
[<User 1 username='bob'>, <User 2 username='tim'>]

这会自动生成一个中间表role_user。用来同时引用其他类。这个表不会成为一个暴露(expose)的类,这个表的行也不会等同于Python对象。这种多对多的关系完全被隐藏了。

如果想要自己创建中间表,用来增加一些字段,可以查看标准的SQLObject的add/remove方法来工作。假设你可以提供连接列和其他类的 正确连接,也无法通过这些方法插入扩展信息,而且也无法设置默认值。(Assuming that you are providing the join with the correct joinColumn and otherColumn arguments, be aware it’s not possible to insert extra data via such methods, nor will they set any default value.)。

如前述的User/Role系统,如果创建了UserRole中间表,通过创建两个外键来建立MTM(多对多Many-to-Many)联系,并且 附加了DateTimeCol类型的字段(缺省为当前时间)。那么这个列在使用addRole()方法加入role之前会保持为空。

你可能会注意到列加入了扩展保留字alternateID。可以使用alternateID=True来确保该字段的值唯一(uniquely)。 有如确保用户名必须唯一一样。这个标识符会成为主键,且可以访问的。对于这种字段,如果添加了相同值,则会抛出异常 pysqlite2.dbapi2.IntegrityError: column [列名] is not unique。

注意:SQLObject必须确保主键是唯一且不可改变的。可以通过SQLObject来改变主键,但是你要自己提供确保数据一致性的机制。正是因为这个原因,才推荐使用无意义的整数ID,这样可以确保在未来改变时比较安全。

一个alternateID类创建一个类方法,形如byUsername来对应一个叫做username的列(也可以使用alternateMethodName关键字参数来重载)。如下使用:

>>> User.byUsername('bob')
<User 1 username='bob'>
>>> Role.byName('admin')
<Role 1 name='admin'>

15   选择多个对象(查询)

查询才是真正有用的东西,比显示联系重要的多。select是一个类方法,可以按照如下方式使用:

>>> Person._connection.debug=True
>>> peeps=Person.select(Person.q.firstName=="John")
>>> list(peeps)
 1/Select : 使用的SQL语句
 1/COMMIT : auto
[<Person 1 firstName='John' ...>]

这个例子放回使用John作为firstName的所有人。一个使用表达式的复杂例子:

>>> peeps=Person.select(
...     AND(Address.q.personID==Person.q.id,
...         Address.q.zip.startswith('504')))
>>> list(peeps)
..............
[]

属性q用于给出存取特定对象的结构化查询子句。所有被q所引用的列名将会转换成SQL语句。当然也可以手工生成SQL语句:

>>> peeps=Person.select("""address.id=person.id AND
...                        address.zip LIKE '504%'""",
                        clauseTable=['address'])

注意必须使用clauseTable(子表)来指定子表。如果使用q属性,SQLObject会自动计算出(figure out)所需要使用的扩展信息类。

你也可以使用MyClass.sqlrepr来手工指定任何SQL语句,而在使用q属性时是自动指定的。

还可以使用orderBy关键字创建select语句中的”ORDER BY”。orderBy获取一个字符串,表示数据库的列名,例如Person.q.firstName。也可以使用”-colname”来反向排序。或者 调用MyClass.select().reversed()。

也可以使用类实例的_defaultOrder属性指定缺省的排序列。如果在这时需要获得未排序的结果,使用orderBy=None。

select的结果是一个生成器(generator),可以用于后续调用。所以SQL仅在列出选择结果时才执行,或者使用list()返回所有结 果时。当列举查询结果时,每次取回一个行。这种方法可以在返回结果很大时避免将所有结果放入内存。也可以使用.reversed()而不必获得所有结果实 体,取而代之的是自动修改了SQL语句来获得需要的结果。

还可以对查询结果分片。这将会修改SQL语句,所以peeps[:10]将会把”LIMIT 10″加入SQL语句中。如果切片无法反映到SQL(如peeps[:-10]),则执行查询之后,对查询结果列表进行操作。当然,这只是会出现在使用负索引时。

大多数情况会得到多个查询结果对象。如果不希望这样,可以加入关键字MyClass.select(…,distinct=True),对应SQL中的SELECT DISTINCT。

你也可以通过count得到查询结果的个数,比如MyClass.select().count()。这将会导致一个COUNT(*)查询。这时并不会从数据库中取得对象,而仅仅是获得结果数量。

在少数特别注重效率的时候,效率实际上是依赖于批处理的使用方法。提高排序和查找效率的好办法是使用索引。且缓存比切片更好。

在这种情况下缓存意味着响应所有的结果。可以使用list(MyClass.select(…))来实现。可以在规定的时间内保存查询结果,来让用户分页查看结果。这样,第一次查询会看上去花费更多的时间,但是后面的页面显示却非常快速。

更多关于查询子表的问题参见”SQLBuilder documentation”。

16   selectBy方法

除了.select之外的另一个选择是.selectBy。按照如下工作:

>>> peeps=Person.selectBy(firstName="John",lastName="Doe")

每个关键字对应一个列,且所有的键值对都是以AND进行逻辑连接。返回结果是SelectResult。所以可以切片,计数,排序等等。

17   sqlmeta类

这是在SQLObject 0.7中引入的,允许使用一种清晰的方式指定数据库的元数据,而不需要使用类的命名空间中的属性来指定。

有一些特别的属性可以用在这个类当中,可以用于改变类的行为。他们包括:

  1. table 数据库的表名,是从style派生而来的,仅用于没有指定类名时。如果没有指定名字,且没有定义可选的style,则标准方式是指定MixedCase为mixed_case。
  2. idName 指定数据库表的主键名,如未指定时继承自style。缺省为id。
  3. idType 设置ID时的强制函数。缺省为int。
  4. style 一个样式(style)对象,这个对象允许使用其他算法翻译Python属性和类名称与数据库列名和表名之间的对应关系。参考”Changing the Naming Style”了解更多。它是一个IStyle实例的接口。
  5. lazyUpdate 布尔值,缺省为False。如果为True,那么改变属性时不会自动更新查询。而等待调用inst.syncUpdates()或inst.sync()时才执行更新。
  6. defaultOrder 查询数据时的缺省排序方式。
  7. cacheValues 布尔值,缺省为True。如果为True,保存在行中的值一直缓存到inst.expire()被调用。如果设为False,属性值不会被缓存,所以每次 存取一个属性的值都会使用查询来返回结果。如果需要处理多进程并发处理时,也许需要这样。当然也可以使用事务(transactions),这不是默认 的。
  8. registry SQLObject使用字符串来连接一个类,且这些类必须避开模块名,而又是又会出现在不同的系统中的命名冲突。这个属性提供了类的命名空间。
  9. fromDatabase 布尔值,缺省为False。如果为True,创建一个类时会自动查询数据库的各个列,如果有缺失的列则自动加上。
  10. columns 形如{columnName:anSOColInstance}形式的字典。可以通过只读属性获取列的信息。
  11. columnList columns的列表,用于需要一个有序而牢固的列信息列表时使用。
  12. columnDefinitions 类似columns的字典,但是包含列的原始定义信息。并非是特定类的,且没有逻辑。
  13. joins 这个类的所有联系对象。
  14. indexes 这个类的所有索引。
  15. createSQL 创建表之后的SQL查询。createSQL可以是单行的SQL命令或者是一些SQL命令组成的列表,或者是按照数据库名(dbNames)和值组成的字典,值可以是单行SQL或者是SQL列表。这经常用于ALTER TABLE命令来修改表定义。
  16. expired 布尔值。如果为True,那么下次存取对象列属性时,将会执行查询。

在上一版本的SQLObject中这些属性是作为类的属性而直接存在的,属性名前加上一个下划线。现在推荐将代码改成新的样式(style)。这些旧的方法在SQLObject 0.8释出时将不再支持。

注意:当继承SQLObject时,sqlmeta属性不会继承。也无法通过sqlmeta.columns词典访问父类列对象。

18   使用sqlmeta

按照如下代码:

class MyClass(SQLObject):
    class sqlmeta:
        lazyUpdate=True
        cacheValues=False
    columnA=StringCol()
    columnB=IntCol()
    def _set_attr1(self,value):
        #设置值时需要做的事情
    def _get_attr1(self):
        #取得值时需要作的事情

如上定义将会创建表my_class(表名在更改style属性时会有所不同),包含两个列columnA和columnB。还有第三个可以被存取 的属性MyClass.attr1。sqlmeta改变了MyClass的行为,可以延迟更新,并告知不要使用缓存,所以每次请求信息时都会查询数据库。

19   SQLObject

除了sqlmeta和列规范之外,其他的特定属性可以设置在类中。这些属性可以用除了_connection属性之外都可以在sqlmeta中定义。如果在SQLObject 0.7中使用,可以得到不赞成使用的警告。最好及时修改代码来保持对未来的兼容。

  1. _connection 使用的连接对象,从DBConnection类实现。也可以在包定义中使用__connection__供使用,但是必须确保在类定义之前定义 __connection__。也可以在实例创建时传递connection对象,有如transaction中描述的那样。如果已经定义了 sqlhub.processConnection,则会忽略这个属性而使用sqlhub。如果只有少数几个类使用相同的连接是有好处的,除了 (besides)使用了多种类型。
  2. _table 这是旧样式(style)的属性,等同于sqlmeta类中的table属性。
  3. _joins 同上,对应于sqlmeta的joins属性。
  4. _cacheValues 同上,对应于sqlmeta的cacheValues属性。
  5. _idName 同上,对应于sqlmeta的idName属性。
  6. _style 同上,对应于sqlmeta的style属性。

20   自定义对象

自定义类往往需要一些自定义方法,但是需要注意一些细节。

初始化对象:

有两种方式实例化SQLObject对象,从数据库引出和插入数据库。相同的是都要创建Python对象。这导致了__init__的微笑差异。

一般来说,不需要更改__init__。作为替换的_init方法,可以在引出或插入之后执行。方法定义如同_init(self,id, connection=None,selectResults=None),也许你喜欢使用_init(self,*args,**kw)的形式。注意, 如果重载方法,一定要调用SQLObject._init(self,*args,**kw)。

添加魔术属性:

你可以使用任何已有的技术来定义这种新样式中的方法,包括classmethod(类方法),static(静态方法),和property(属 性),而且还可以使用捷径。比如你有一个方法名以_set_、_get_、_del_、_doc_开始,它将会被用于创建属性。所以,如果对应 Person的ID下包含图像在/var/people/images目录下,可以使用:

class Person(SQLObject):
    # ...
    def imageFilename(self):
        return 'images/person-%s.jpg'%self.id
    def _get_image(self):
        if not os.path.exists(self.imageFilename()):
            return None
        f=open(self.imageFilename())
        v=f.read()
        f.close()
        return v
    def _set_image(self,value):
        f=open(self.imageFilename(),'w')
        f.write(value)
        f.close()
    def _del_image(self,value):
        os.unlink(self.imageFilename())

然后,可以像使用普通属性(attribute)一样使用.image属性(property)。对它的修改也会直接反映到文件系统当中。这是保存无意义数据的好方法。

同样也可以传递一个image关键字参数到构造方法或者set方法,形如Person(…,image=imageText)。所有的方法 (_get_、_set_等)都是可选的,你可以使用其中的任何一个而省略其他的。这样如果只定义了_get_attr方法,那么attr属性就是只读 的。

重载列属性:

重载数据库列属性时有些复杂。例如(for instance),想要在改变一个人名字的时候执行特定代码。在大多数系统中你需要自己实现这个功能,然后再调用父类的代码。但是父类(SQLObject)并不知道子类的列。

SQLObject创建了形如_set_lastName的方法定义你的列,当时当你再次想要使用时却发现父类没有相关引用(即不可以写 SQLObject._set_lastName(…),因为SQLObject类并不知道你的类的列)。你需要自己重载_set_lastName 方法。

为了处理这种问题,SQLObjec类创建了两个方法作为getter和setter,例如:_set_lastName和_SO_set_lastName。所以可以截获所有对lastName的更改:

class Person(SQLObject):
    lastName=StringCol()
    firstName=StringCol()
    def _set_lastName(self,value):
        self.notifyLastNameChange(value)
        self._SO_set_lastName(value)

或者你可能想要包含电话号码的数字,需要限制长度,可以按照如下格式:

import re
class PhoneNumber(SQLObject):
    phoneNumber=StringCol(length=30)
    _garbageCharactersRE=re.compile(r'[\-\.\(\) ]')
    _phoneNumberRE=re.compile(r'^[0-9]+$')
    def _set_phoneNumber(self,value):
        value=self._garbageCharactersRE.sub('',value)
        if not len(value)>=10:
            raise ValueError(
                'Phone numbers must be at least 10 digits long')
        if not self._phoneNumberRE.match(value):
            raise ValueError,'Phone numbers can contain only digits'
        self._SO_set_phoneNumber(value)
    def _get_phoneNumber(self):
        value=self._SO_get_phoneNumber()
        number='(%s) %s-%s'%(value[0:3],value[3:6],value[6:10])
        if len(value) > 10:
            number+=' ext.%s'%value[10:]
        return number

在修改从属性中获得的数据时必须小心。有些时候,人们希望设置与返回的值相同。这个例子中我们在存入数据库之前去除了一些字符,并在取出的时候重新格式化了。这个方法(反对存取属性)的优点之一是程序员往往希望将这些分开。

当然,还应该注意,这些转换在存入和取出时都会发生,但是在查询的时候却不会发生。所以如果你将值从Pythonic形式转换为SQLish形式 时,你的查询(当使用.select()或者.selectBy()方法)需要使用SQL/Database形式(因为这些命令是按照SQL来在数据库上 运行的)。

取消属性定义(Undefined attributes)

还有一个有用的特性,因为你有时需要返回奇怪的结果。SQLObject在你设置一个未定义的属性时不会跑出异常;这很好解释,并且不会改变数据库。他的工作方式有如其他Python类一样,但是在SQLObject类中却没有。

这在有些时候可能会出问题,如果你已经有了一个’name’属性,而你却写了’a.namme=”Victor”‘,这时不会跑出异常,但是却是错误的。

21   参考手册(Reference)

上面的信息可以让你快速进入工作,下面的信息让你定义更加完整。

22   Col类,定义列

列的列表是Col对象的列表。这些对象本身并没有功能,用于定义列。

  1. dbName 数据库的列名,如果不指定你指定的Python名称将会从大小写混用的形式转换到小写加下划线的形式。
  2. default 列的缺省值,在创建一个新行时使用。如果指定了一个可调用对象或函数,将会调用这个函数,并且使用其返回值。所以你可以使用 DateTimeCol.now作为当前时间的缺省值。或者你可以使用sqlbuilder.func.NOW()来设置数据库使用NOW()内部函数。 如果你不指定缺省值,则在调用这个记录的new时会抛出异常。
  3. alternateID 这是一个布尔型变量,缺省为False。指定列是否作为ID属性,例如用户名,尽管并不一定是主键。如果是这样,会添加一个类方法,例如 byUsername将会返回这个对象,使用laternateMethodName,当你希望使用by*类似的名称时,例如 alternateMethodName=”username”。这个列将会被声明为UNIQUE。
  4. unique 如果为True,当SQLObject创建一个表格时,将会指定这个列为UNIQUE。
  5. notNone 如果为True,则这个列不允许使用空值,用于创建表格。
  6. sqlType 这个列的SQL类型,例如INT、BOOLEAN等。你可以使用下面的类来定义,但是有时候使用sqlType更容易一些。仅在SQLObject创建表格时有效。

23   列类型

ForeignKey类可以替换Col来使用,当列是其他表的外键时。一般使用方法如ForeignKey(‘Role’),在这个列子中创建了一 个到表Role的引用。这基本等同于Col(foreignKey=’Role’,ssqlType=’INT’)。这会创建两个属性,role,会返回 Role的实例;roleID会返回与之关联的role的整数ID。

Col还有一些其他子类,用于SQLObject创建表格时指示列的类型。

  1. BLOBCol 二进制数据列,目前只能在MySQL、PostgreSQL、SQLite中使用。

  2. BoolCol 创建一个BOOLEAN列在Postgre,或者INT在其他数据库中。而且会将”t”/”f”或者0/1转换到数据库后端。

  3. CurrencyCol 等同于DecimalCol(size=10,precision=2)。注意DecimalCol可能不会返回正确的值,这个列可能共享一些行为。注意阅读DecimalCol的注意事项。

  4. DateTimeCol 日期时间,一般返回datetime或mxDateTime对象。

  5. DateCol 一个日期对象,一般返回datetime或mxDateTime对象。

  6. TimeCol 一个日期对象,一般返回datetime或mxDateTime对象。

  7. DecimalCol 以10为基础的,正确数据,使用关键字参数size指定存储的数字位数,precision指定小数点位数。警告:偶尔会发生在DecimalCol值, 尽管正确的存入数据库,但是可能返回浮点数而不是decimals。你可以自己测试一下,也可以试着导入Decimal类型,在你的数据库适配器导入 SQLObject之前。

  8. EnumCol 枚举类型,包含有限个数的字符串值。给出列表中可能的字符串,依靠enumValues关键字参数。MySQL有内置的本地ENUM类型,但是在其他数据库中也可以工作,只不过效率上并不占优势。

  9. FloatCol 浮点数

  10. ForeignKey 其他表/类的外键,例如user=ForeignKey(‘User’)

  11. IntCol 整数

  12. PickleCol 一种扩展的BLOBCol,这个列可以存储/取出任何Python对象;实际上是使用了Python的pickle来对对象串行化的压缩和解压缩的,而最终存取的是字符串。

  13. StringCol 一个字符串列。String(character)列。扩展关键字如下:

    length:如果给定了则字段类似于VARCHAR(length)。如果未指定则使用TEXT字段类型。

    varchar:如果包含了length,则用于区别CHAR和VARCHAR,缺省为True,使用VARCHAR。

  14. UnicodeCol StringCol的子类,接受dbEncoding关键字参数,缺省为”UTF-8″。其值在存取数据库的过程中被编码和解码。在使用UnicodeCol进行查询时有些限制:

    只有简单的q-magic字段支持,不允许表达式;只支持”==”和”<>”操作符。

    如下为示例代码:

    MyTable.select(u'value'==MyTable.q.name)
    MyTable.select(MyTable.q.name<>u'value')
    MyTable.select(OR(MyTable.q.col1==u'value1',MyTable.q.col2<>u'value2'))
    MyTable.selectBy(name=u'value')
    MyTable.selectBy(col1=u'value1',col2=u'value2')
    MyTable.byCol1(u'value1') #假设col1是一个alternetID字段

    如下为错误代码:

    MyTable.select((MyTable.q.name+MyTable.q.surname)==u'value')

    如下情况,必须先转换编码:

    MyTable.select((MyTable.q.name+MyTable.q.surname)==u'value'.encode(dbEncoding))

24   两个类/表之间的关系

必须使用ForeignKey来处理表的外键,后台实际使用连接(join)。

25   多连接和SQL多连接:一对多

查看”One-to-Many关系”查看一对多连接的例子。

多连接(MultipleJoin)返回结果类表,而SQLMultipleJoin返回SelectResults对象。

少数关键字参数允许MultipleJoin构造器:

joinColumn:

列名

subprocess模块指南

Monday, June 2nd, 2008

subprocess模块指南

翻译: gashero

目录

从Python2.4开始引入。

subprocess模块用于产生一个新的进程,并且连接到其input/output/error管道,并获取返回码。这个模块准备用于替换很多其他旧模块中的函数,例如 os.system 、 os.spawn* 、 os.popen* 、 popen2.* 、 commands.* 。

详细细节见下面。

1   使用subprocess模块

模块中只是定义了一个类叫做 Popen

class Popen(args,bufsize=0,executable=None,stdin=None,stdout=None,stderr=None,preexec_fn=None,close_fds=False,shell=False,cwd=None,env=None,universal_newlines=False,startupinfo=None,creationflags=0)

参数描述如下:

@waiting…

1.1   便利函数

该模块定义了两个快捷函数:

call(*popenargs,**kwargs)

使用指定的参数运行命令,并等待命令结束,然后返回返回码。参数与Popen结构相同,例如:

retcode=call(["ls","-l"])

check_call(*popenargs,**kwargs)

使用指定参数运行命令,等待结束。如果退出码是0则返回,否则抛出 CalledProcessError 异常。而该异常对象包含了返回码,称为 returncode 属性。

参数与Popen结构相同,例如:

check_call(["ls","-l"])

从Python2.5开始引入。

1.2   异常

子进程中抛出的异常可以传递到父进程。另外,异常对象包含一个附加属性叫做 child_traceback ,是一个字符串,包含了子进程那里可以看到的错误回调信息。

最常见的异常是 OSError 。例如尝试执行一个不存在的文件。应用应该防备OSError异常。

在调用Popen而使用了无效的参数时会抛出ValueError异常。

check_call() 将会抛出 CalledProcessError ,在进程返回码不是0时。

1.3   安全

不像其他popen函数,这个实现不会隐含调用 /bin/sh 。这意味着所有的字符,甚至shell的元字符,都可以安全的传递到子进程。

2   popen对象

Popen的实例拥有如下方法:

poll()

检查子进程是否结束,并返回returncode属性。

wait()

等待子进程结束,返回returncode属性。

communicate(input=None)

与进程交互:发送数据岛stdin。从stdout和stderr读取数据,直到接收到end-of-file。等待进程结束。可选的input参数是要发送到子进程的字符串,或者为None。

communicate() 返回元组 (stdout,stderr) 。

注意:读取的数据缓存在内存里,所以在数据很大或者不受限制时不要使用这个方法。

如下属性也可用:

stdin

如果stdin参数为PIPE,这个属性提供了子进程的标准输入,否则为None。

stdout

如果stdout参数为PIPE,……

stderr

如果stderr参数为PIPE,……

pid

子进程的进程ID。

returncode

子进程的返回码,在还没有返回时则为None。一个否定值-N表示子进程是被信号N所终止的(仅Unix)。

3   使用subprocess替换旧模块

我的编程知识体系基本搭建完成

Monday, May 5th, 2008

我的编程知识体系基本搭建完成

我的编程知识体系由四种语言构成:C/Python/Erlang/Lua。排序按照熟悉程度。这四种语言分别用于处理不同的用途。

C:不用太多解释,C是王道,无论是以前、现在、还是未来。

Python:用以实现大规模逻辑的高层次编程语言,而我现在也是个Python程序员。

Erlang:用以实现高可用性、高并发能力、高性能的分布式服务器。不过在我看来并不适合做所有的事情,做服务器也就够了。

lua:用以实现高可移植能力的程序,用于嵌入式脚本。

lua是我最后一门搞定的语言,尽管现在还不是很熟,但是至少干完了一个项目了。

搭建好的这个知识体系会让我至少在两年内不会再去考虑学习更多的编程语言,并主要使用这四种编程语言完成几乎所有的工作。

sendpkt已经更新到winpcap4.0.1了

Monday, April 28th, 2008

sendpkt已经更新到winpcap4.0.1了

因为一直崇尚“最新的软件不一定是最好的”,所以向来不适用最新版本的各类软件,不过终于在所有朋友的催促之下,将winpcap由3.1升级到4.0.1了。并且提供了sendpkt的新windows二进制发行版。

sendpkt是提供win32/linux兼容接口的发包函数库。倪补了长久以前python只能抓包不能发包的问题,另外同时提供的兼容接口,也提高了程序的可移植性和适用范围。

最近因为给朋友帮忙封装了供lua使用的winpcap,现在已经积累了好多东西了,可以考虑以后把一些常用的组包和ARP查询等函数一起加入提供方便。再者,就是仔细考虑一下自己封装一个供win32使用的使用mingw编译的libnet包。毕竟libnet总比winpcap自带的那个要专业许多,也省事许多。

至于解包,我现在还没找到比较合适的库,一直自己写呢,现在已经完善而稳定的包含ethernet、arp、ip包三种,tcp包几天内就要开始写了。

http://sendpkt.googlecode.com

我要重写pypcap

Monday, April 28th, 2008

我要重写pypcap

接触信息安全由来已久,因为一直用python比较多,于是刚开始玩抓包时也是用pypcap的。开始时很方便,发现原来生活可以这么美好,抓包原来可以放在循环里的。后来,问题一点点出来了。列举一下,当然这里也有我对pcap知识不够全面的问题:

1、在不同机器上setnonblock后效果不同,有些机器上设置以后可以支持Ctrl+C的中止,而有些机器上则直接进入死循环,死得很惨。
2、pcap_next_ex()函数丢失了,这个可是用于支持可超时抓包的重要工具。
3、建立pcap对象时无法指定超时,同样很好理解,不过这里也顺便把pcap_dispatch()函数搞得很惨。

pypcap最初是用pyrex写的,很好的避免了很多复杂性,不过同样的问题是丢失了很多细节控制。最终的发行版本已经是使用pyrex转换后的C代码了。

直到最近我给lua加上pcap的支持模块时,才开始真正接触pcap的更多细节,这时候恍然大悟。我在pypcap中郁闷好久的功能,其实pcap本身已经提供了,只是被pypcap给忽略掉了。唉,以前我还经常嘲笑有IDE综合症的人呢,现在自己也不小心落入了这个陷阱。

Python的产品环境部署

Wednesday, April 23rd, 2008

Python的产品环境部署

作者: gashero

目录

1   Python的版本及安装

产品环境最常用的Python运行环境是Python 2.4.x系列,至今最新版本是2.4.4。而Centos4.x默认附带的Python为2.3.x系列,不支持某些Python2.4.x的功能和第三方插件。需要重新安装。

1.1   Python 2.4.4的安装

首先得到Python2.4.4的安装包 Python-2.4.4.tar.bz2 ,解压/编译/安装的步骤与一般的tarball方式相同:

$ su
# tar xjvf Python-2.4.4.tar.bz2
# cd Python-2.4.4
# ./configure
# make
# make install

这样的过程最终会把Python安装到 /usr/local/bin 下。

1.2   修改选择的Python版本

经过这样的编译安装之后,系统中会同时有两个版本的Python存在。默认情况下,会使用新安装的Python-2.4.4。不过对于某些运行方 式,如crontab,则会调用旧版本的。所以应该删除所有python->python2.3的符号链接,并且建立python2.4-> python的符号链接。如果有直接命名为python的Python2.3版本,那么直接删除即可。

查找所有可以被$PATH索引到的Python可执行文件版本:

$ whereis python

2   安装普通的Python第三方模块

一般的纯Python第三方模块的安装过程如下,假设模块名称为 XXX-x.y.z.tar.gz

$ tar xzvf XXX-x.y.z.tar.gz
$ cd XXX-x.y.z
$ su
# python setup.py install
... ...
# exit
$ python
>>> import XXX
>>>

如上过程已包含测试,即模块的导入成功。

3   安装特别的扩展模块

有些特别的扩展模块需要特别的安装方式。

3.1   MySQLdb

Python连接MySQL数据库的扩展模块。需要在机器上已经安装了MySQL的客户端开发包,包含已经安装的源码。Python还需要可以找到MySQL客户端的配置时(configure)的配置文件。

安装过程如下(已忽略解压缩过程):

# python setup.py config
# python setup.py build
# python setup.py install

4   通过 easy_install 安装扩展模块

这是PEAK开发的新的Python扩展包方式,使用方式还不是很广泛,但是有些模块必须使用他来安装。比如 MySQLdb-1.2.2 ,在使用相同的安装命令时,内部就会调用 easy_install 来安装。

安装命令与默认的方式相同。

另一种安装方式:先下载 easy_install 然后通过他在线安装Python扩展模块,有如ubuntu的apt-get安装方式,不需要先下载好模块的文件。这种方式需要先下载 ez_setup.py ,地址如下 http://peak.telecommunity.com/dist/ez_setup.py

下载后执行该模块即可在线安装最新版本的 easy_install

# python ez_setup.py

通过这种方式就安装成功了 easy_install ,之后可用如下方式安装扩展模块 XXX

# easy_install XXX

比如安装MySQLdb:

# easy_install MySQLdb

一般不推荐使用这种方式安装,因为所有的模块都是必须在线安装,产品环境的很多服务器是不允许上网的。另外,这种方式并不方便控制需要安装的模块的版本。这种版本差异可能是致命的,所以必须非常严格的控制。

StacklessPython简要笔记

Wednesday, April 23rd, 2008

StacklessPython简要笔记

作者: gashero

目录

1   微进程tasklet

stackless的基本构成单元,一种可调度对象。

1.1   建立微进程对象

示例:

import stackless
def print_x(x):
    print x

stackless.tasklet(print_x)('one')
stackless.run()

微进程建立后并不运行,直到调用 stackless.run() 才开始运行。

1.2   调度

微进程执行的函数内部可以调用 stackless.schedule() 来暂停当前微进程而把执行权交给执行队列的下一个微进程。

这也是在stackless中实现协程的基本方式。

1.3   轻量级进程

class HackySacker:
    def __init__(self):
        #todo...
        self.channel=stackless.channel()
        stackless.tasklet(self.messageLoop)()
    def messageLoop(self):
        while True:
            message=self.channel.receive()
            if message=='exit':
                return
            #todo...
            stackless.schedule()    #这里注意要调度一下

执行速度比线程的更快,而且可以允许10000个微进程而不死掉。

2   通道channel

用于微进程之间传递信息和控制微进程的运行流程。可以替代传统线程程序中的Queue模块的互斥队列。

2.1   交换信息

示例:

import stackless
channel=stackless.channel()
def recv_tasklet():
    msg=channel.receive()
    print msg
def send_tasklet():
    channel.send('hello')

调用 channel.receive() 时会阻塞当前微进程,等待消息到来。调用 channel.send() 时也会阻塞,直到有微进程读取了消息。

2.2   任务分发

如果一个入口微进程将消息源源不断的发送到通道,而多个工作者微进程等待接受消息,那么每个消息只能被一个微进程所接受,而不是被所有接受。

2.3   事件管理器

使用channel实现的一个基类,用于事件的等待与处理。

class EventHandler:
    def __init__(self,*outputs):
        if outputs==None:
            self.outputs=[]
        else:
            self.outputs=list(outputs)
        self.channel=stackless.channel()
        stackless.tasklet(self.listen)()
    def listen(self):
        while True:
            val=self.channel.receive()
            self.processMessage(val)
            for output in self.outputs:
                self.notify(output)
    def processMessage(self,val):
        pass
    def notify(self,output):
        pass
    def registerOutput(self,output):
        self.outputs.append(output)
    def __call__(self,val):
        self.channel.send(val)

功能:

  1. 通过 listen 方法,持续的监听通道上传来的消息。
  2. 通过 processMessage 处理收到的消息。
  3. 通过 notify 方法将收到的结果发送到输出端。
  4. registerOutput 可以添加新加的输出端。
  5. __call__ 可以方便的用对象后加参数来给它发消息。

3   协程coroutine

使用通道实现,两个函数都在循环中等待对方发来的消息。

3.1   乒乓球的例子

import stackless
ping_channel=stackless.channel()
pong_channel=stackless.channel()
def ping():
    while ping_channel.receive():   #在此阻塞
        #todo...
        pong_channel.send('from ping')
def pong():
    while pong_channle.receive():
        #todo...
        ping_channel.send('from pong')
stackless.tasklet(ping)()
stackless.tasklet(pong)()
stackless.tasklet(ping_channel.send)('startup') #启动循环的微进程
stackless.run()

在Twisted中使用线程

Wednesday, April 16th, 2008

在Twisted中使用线程

译者: gashero

目录

1   以线程安全的模式运行代码

Twisted中的大部分代码都不是线程安全的。例如protocol向transport写入数据就不是线程安全的。因此我们需要一种方法来在主事件循环中进行调度。者可以使用函数 twisted.internet.interfaces.IReactorThreads.callFromThread 来实现:

from twisted.internet import reactor

def notThreadSafe(x):
    """做一些非线程安全的事情"""
    # ...

def threadSafeScheduler():
    """以线程安全方式运行"""
    reactor.callFromThread(notThreadSafe,3) #将会运行notThreadSafe(3)在主时间循环中

Note

译者注

callFromThread 意指从线程调用,这个方法是供线程调用的,并且使其指定的函数加入到主事件循环中执行。比如worker线程可以调用此方法将提交结果的函数加入到主事件循环中。这样就可以确保多线程的运行worker,而有可以使用线程安全的方式提交结果。

2   在线程中运行代码

有时我们希望在线程中运行代码,比如阻塞的存取API。Twisted提供了这样做的方法在 IReactorThread API 中。附加的工具在包 twisted.internet.threads 中提供。这些方法允许我们把任务排队以后在线程池中运行。

例如,在线程中运行一个函数,我们可以:

from twisted.internet import reactor

def aSillyBlockingMethod(x):
    import time
    time.sleep(2)
    print x

# 在线程中运行
reactor.callInThread(aSillyBlockingMethod,"2 secodns have passed")

Note

译者注

callInThread 意指在线程中运行,调用该方法需要在主事件循环中,而执行其传入的函数则是在线程中。可以与上一节提供的 callFromThread`结合使用,即在worker线程函数中调用 `callFromThread 提交结果。

3   工具函数

工具函数作为 twisted.internet.reactor 的一部分API提供,但是并不是在 twisted.internet.threads 中实现的。

如果我们有多个方法需要在线程中以队列方式运行,我们可以做:

from twisted.internet import threads

def aSillyBlockingMethodOne(x):
    import time
    time.sleep(2)
    print x

def aSillyBlockingMethodTwo(x):
    print x

# 排队后在线程中运行两个方法
commands=[(aSillyBlockingMethodOne,["calling first"])]
commands.append((aSillyBlockingMethodTwo,["and the second"],{}))
threads.callMultipleInThread(commands)

如果我们希望得到函数的运行结果,那么我们可以使用Deferred:

from twisted.internet import threads

def doLongCalculation():
    # ... do long calculation here ...
    return 3

def printResult(x):
    print x

# 在线程中运行,并且通过 defer.Deferred 获取结果
d=threads.deferToThread(doLongCalculation)
d.addCallback(printResult)

如果你希望在reactor线程中调用一个方法,并且获取结果,你可以使用 blockingCallFromThread

from twisted.internet import threads,reactor,defer
from twisted.web.client import getPage
from twisted.web.error import Error

def inThread():
    try:
        result=threads.blockingCallFromThread(reactor,getPage,"http://twistedmatrix.com/")
    except Error,exc:
        print exc
    else:
        print result
    reactor.callFromThread(reactor.stop)

reactor.callInThread(inThread)
reactor.run()

blockingCallFromThread 将会返回对象或者抛出异常,或者通过抛出到传递给他的函数。如果传递给它的函数返回了一个Deferred,他会返回Deferred回调的值或者抛出异常到errback。

4   管理线程池

线程池是在 twisted.python.threadpool.ThreadPool 中实现的。

我们可以修改线程池的大小,增加或者减少可用线程的数量,可以这么做:

from twisted.internet import reactor
reactor.suggestThreadPoolSize(30)

缺省的线程池大小依赖于使用的reactor,缺省的reactor使用最小为5个,最大为10个。在你改变线程池尺寸之前,确保你理解了线程和他们的资源使用方式。

在Twisted2.5.0中使用线程

译者: gashero

刚才翻译了对应版本8.0.0的Twisted的线程指南,但是我还是在用2.5.0,所以这里只记录与8.0.0的差异,不做重新翻译。

当你开始使用线程之前,确保你在启动程序的时候使用了如下:

from twisted.python import threadable
threadable.init()

这回让Twisted以线程安全的方式初始化,不过仍然注意,Twisted的大部分仍然不是线程安全的。

以线程安全的方式运行代码中初始化多了两行:

from twisted.python import threadable
threadable.init(1)

2.5.0的文档中没有 blockingCallFromThread 的例子。也许根本就没有这个方法。

实际我下载文档的版本是2.4.0,不过应该与2.5.0一样的。

编程语言的哲学

Wednesday, March 12th, 2008

今天晚饭时,听到几个同事在谈用什么IDE,谈到了JBuilder、Eclipse、Netbean等等,提到了自动生成类模板,自动填充setter/getter等等。想想我自己,现在只用vim已经两年了。

我以前是用过一小段Java代码的,那时巨喜欢Eclipse,感觉那就是未来型的IDE,包含的功能太先进了。那个时候我也会用vim,不过除了写hello.java以外,真的没办法用。没法记忆那么多东西。

现在想来,也许我无法用vim写java代码的原因并不是vim的缺陷,毕竟有些语言是可以只用vim的,比如C、Python、Erlang。

如果一种编程语言所做的工作必须要依赖强大的IDE支持,是否从某种意义上暗示了编程语言设计的失败呢?

记不得哪本书上的牛人说过,人类可以理解的代码行数是很有限的,当然,人类可以记忆的东西也是很有限的。所以,我相信,一个糟糕的编程语言设计,加上强大的工具,其实带来的还是很低的工作效率。我在几年前放弃Delphi最直接的理由就是一个让我感到失望的编辑器,而我又没有其他选择。

最后,引用一句:世界上有三种程序员,一种用vi,一种用emacs,另一种用其他。

使用Twisted创建SSL-WEB服务器

Tuesday, January 22nd, 2008

使用Twisted创建SSL-WEB服务器

翻译: gashero

Twisted web howto中并没有提及如何创建SSL加密的WEB服务器。其实这个过程很简单,只是需要一些信息而已,所以这里讲一下。

首先,创建SSL服务器需要一个私钥文件,和一个服务器证书。这里假设你是个开发者,只想要一个测试系统,所以也没必要去搞一个正式的公共证书来。OpenSSL就是你想要的。首先,我们生成一个私钥文件:

$ openssl genrsa > privkey.pem

然后生成一个自签名的SLL证书:

$ openssl req -new -x509 -key privkey.pem -out cacert.pem -days 1000

好了,第一个挑战已经搞定了。下一步就是创建服务器了。假设你已经有了一个twisted web服务器,所以你也知道该在哪个端口监听。我们导入 ssl 模块,创建一个SSL服务器上下文,然后调用 reactor 的 listenSSL 方法:

from twisted.internet import reactor,ssl
sslContext=ssl.DefaultOpenSSLContextFactory(
    '/path/to/privkey.pem',
    '/path/to/cacert.pem',
)
reactor.listenSSL(port,site,contextFactory=sslContext)

同样在 twisted.application 中也很容易创建带有上下文的站点,不过这里就懒得介绍了。

gashero附加的内容:

需要使用ssl的功能还需要另外安装一堆东西。包括openssl(0.9.8g)、pyOpenSSL(0.6.0)。安装openssl时,debian源里面没有dev版本,只能自己编译,注意配置时要修改安装路径:

$ ./config --prefix=/usr/local

这样以后就可以正确的编译安装pyOpenSSL了,否则默认情况下找不到OpenSSL。

ThreadPool的使用

Thursday, December 27th, 2007

ThreadPool的使用

笔记: gashero

目录

1   简介

Nicky介绍给我使用的,其接口与其他很多线程池包装都差不多,不过因为只有一个模块,比较容易附带在程序中,所以研究下。 python threadpool 据介绍代码来自《Python in a Nutshell》的14.5节。

2   简单的使用

代码中给出的例子:

>>> pool=ThreadPool(poolsize)
>>> requests=makeRequests(some_callable,list_of_args,callback)
>>> [pool.putRequest(req) for req in requests
>>> pool.wait()

可见使用步骤如下:

  1. 建立线程池对象,其实是个线程管理器
  2. 建立计算任务对象Request
  3. 将计算任务对象放入线程池当中
  4. 等待计算完成

3   接口文档

英文见 http://chrisarndt.de/en/software/python/threadpool/api/

makeRequests(callable,args_list,callback=None,exc_callback=None)

创建多个计算请求,并允许有不同的参数。

参数列表中的每一个元素是两个元素的元组,分别是位置参数列表和关键字参数字典。

class ThreadPool

线程池类,发布工作请求并收集结果。

__init__(self,num_workers,q_size)

构造函数,设置线程池工作线程数量和最大任务队列长度。 num_workers 是初始化时的线程数量。如果 q_size>0 则会限制工作队列的长度,并且在工作队列满时阻塞继续插入工作请求的任务。

createWorkers(self,num_workers)

增加工作线程数量。

dismissWorkers(self,num_workers)

减少工作线程数量。

pool(self,block)

处理队列中的新结果。也就是循环的调用各个线程结果中的回调和错误回调。不过,当请求队列为空时会抛出 NoResultPending 异常,以表示所有的结果都处理完了。这个特点对于依赖线程执行结果继续加入请求队列的方式不太适合。

putRequest(self,request,block=True,timeout=0)

加入一个任务请求到工作队列。

wait(self)

等待执行结果,直到所有任务完成。

class WorkerThread

工作者线程,供ThreadPool内部使用,不必关注。其自定义方法也只有一个。

class WorkRequest

任务请求类。

__init__(self,callable,args=None,kwds=None,requestID=None,callback=None,exc_callback=None)

创建一个工作请求。

4   ThreadPool的递归任务管理问题

如果ThreadPool执行的任务中还会添加任务则需要多考虑几个问题。

如果一个这样的任务正在运行,尚未完成时任务列表就已经空了,那么ThreadPool会立即抛出 NoResultsPending 异常,以告知 wait() 方法所有任务都完成了。而事实上,还有一个线程尚未执行完成。

这种情况下,可以自己设置一个退出条件自己重新实现 wait() 方法。在循环中调用 poll(True) 方法。对于抛出的 NoResultsPending 异常视而不见。并自己设置循环的退出方法。

5   回调函数的使用

建立任务请求时有两种回调函数 callback 和 exc_callback ,他们的回调接口为:

callback(request,result)

exc_callback(request,sys.exc_info())

其中 request 为 WorkRequest 对象。而 result 则是调用线程函数正确的返回结果。 sys.exc_info() 为发生异常时返回的信息。 sys.exc_info() 是一个拥有3个元素的元组。分别为:

  • 异常类 :发生异常的类
  • 异常实例 :如上异常类的实例,包含更多详细信息
  • 跟踪信息 :traceback对象,可以显示错误的行号等等具体的错误信息

Warning

注意,如果没有设置 exc_callback 则发生异常时会将异常信息写入 callback 回调函数。如果同时没有设置 callback 和 exc_callback 则发生任何异常都不会有提示,根本无法调试。

5.1   使用 sys.exc_info() 信息

由于发生异常时返回的 sys.exc_info() 内容并不易读,所以可以用如下方式定制错误回调函数,将错误信息打印出来,或者可选的输出到日志文件。

import traceback
def exc_callback(excinfo):
    errorstr=''.join(traceback.format_exception(*excinfo))
    print errorstr

这样的显示结果就如同控制台中看到的错误跟踪一样了。

在应用中嵌入Python

Wednesday, December 19th, 2007

在应用中嵌入Python

翻译: gashero

前面的章节讨论如何扩展Python,如何生成适合的C库等。不过还有另一种情况:通过将Python嵌入C/C++应用以扩展程序的功能。Python嵌入实现了一些使用Python更合适的功能。这可以有很多用途,一个例子是允许用户裁减需要的Python功能。也可以用于默写使用Python编写更加方便的功能。

嵌入Python与扩展很像。扩展Python时,主程序是Python解释器,但是嵌入Python则主程序并不是Python的-是程序的其他部分调用Python来实现一些功能。

所以,如果要嵌入Python,你可以提供自己的主程序,这个主程序需要初始化Python解释器。至少需要调用函数 Py_Initialize() (对于MacOS,调用 PyMac_Initialize())。可以选择是否传入命令行参数到Python。然后你就可以在应用的任何地方调用Python解释器了。

有几种方法调用解释器:可以传递一个包含Python语句的字符串到 PyRun_SimpleString() ,也可以传递一个stdio文件指针和一个文件名(用于识别错误信息)到 PyRun_SimpleFile() 。你也可以调用前几章介绍的底层操作直接控制Python对象。

可以在目录 Demo/embed/ 中找到嵌入Python的例子。

目录

1   高层次嵌入

嵌入Python最简单的形式是使用高层次的接口。这个接口专门用于执行Python脚本,而不需要与应用程序直接交互。例子可以在一个文件中展示:

#include <Python.h>
int
main(int argc, char* argv[]) {
    Py_Initialize();
    PyRun_SimpleString("from time import time,ctime\n"
            "print 'Today is',ctime(time())\n");
    Py_Finalize();
    return 0;
}

如上代码首先使用 Py_Initialize() 初始化Python解释器,随后执行硬编码中的Python脚本来打印日期和时间。最后 Py_Finalize() 关闭了解释器。在真实应用中,你可能希望从其他方式获取Python脚本,文件、编辑器、数据库等。从文件获取的方式更适合使用 PyRun_SimpleFile() 函数,可以省去分配内存空间和载入文件的麻烦。

2   超越高层嵌入:预览

高层次的接口可以方便的执行Python代码,但是交换数据就很麻烦。如果需要,你可以使用低层次的接口调用。虽然多写一些C代码,但是却可以完成很多功能。

仍然要提醒的是,Python的扩展与嵌入其实很像,尽管目的不同。前几章讨论的大多数问题在这里也同样适用。可以参考用C扩展Python时一些步骤:

  1. 转换Python类型到C类型
  2. 传递参数并调用C函数
  3. 转换返回值到Python

当嵌入Python时,接口需要做:

  1. 转换C数据到Python
  2. 调用Python接口程序来调用Python函数
  3. 转化返回值到C

有如你所见,数据转换的步骤用于跨语言的数据交换。唯一的不同是两次数据转换之间调用的函数。当扩展时,你调用C函数,当嵌入时,调用Python函数。

这一章不会讨论Python和C之间的数据转换。并且假设你会使用手册来处理错误,自此只会讨论与扩展解释器不同的部分,你可以到前面的章节找到需要的信息。

3   纯扩展

第一个程序是执行一段Python脚本中的函数。有如高层接口一节,Python解释器并不会自动与程序结合。

运行一段Python脚本中的函数的代码如下:

#include <Python.h>

int
main(int argc, char *argv[])
{
    PyObject *pName, *pModule, *pDict, *pFunc;
    PyObject *pArgs, *pValue;
    int i;

    if (argc < 3) {
        fprintf(stderr,"Usage: call pythonfile funcname [args]\n");
        return 1;
    }

    Py_Initialize();
    pName = PyString_FromString(argv[1]);
    /* Error checking of pName left out */

    pModule = PyImport_Import(pName);
    Py_DECREF(pName);

    if (pModule != NULL) {
        pFunc = PyObject_GetAttrString(pModule, argv[2]);
        /* pFunc is a new reference */

        if (pFunc && PyCallable_Check(pFunc)) {
            pArgs = PyTuple_New(argc - 3);
            for (i = 0; i < argc - 3; ++i) {
                pValue = PyInt_FromLong(atoi(argv[i + 3]));
                if (!pValue) {
                    Py_DECREF(pArgs);
                    Py_DECREF(pModule);
                    fprintf(stderr, "Cannot convert argument\n");
                    return 1;
                }
                /* pValue reference stolen here: */
                PyTuple_SetItem(pArgs, i, pValue);
            }
            pValue = PyObject_CallObject(pFunc, pArgs);
            Py_DECREF(pArgs);
            if (pValue != NULL) {
                printf("Result of call: %ld\n", PyInt_AsLong(pValue));
                Py_DECREF(pValue);
            }
            else {
                Py_DECREF(pFunc);
                Py_DECREF(pModule);
                PyErr_Print();
                fprintf(stderr,"Call failed\n");
                return 1;
            }
        }
        else {
            if (PyErr_Occurred())
                PyErr_Print();
            fprintf(stderr, "Cannot find function \"%s\"\n", argv[2]);
        }
        Py_XDECREF(pFunc);
        Py_DECREF(pModule);
    }
    else {
        PyErr_Print();
        fprintf(stderr, "Failed to load \"%s\"\n", argv[1]);
        return 1;
    }
    Py_Finalize();
    return 0;
}

这段代码从argv[1]中载入Python脚本,并且调用argv[2]中的函数,整数型的参数则是从argv数组后面得来的。如果编译和链接这个程序,执行如下脚本:

def multiply(a,b):
    print "Will compute",a,"times",b
    c=0
    for i in range(0,a)
        c=c+b
    return c

结果将是:

$ call multiply multiply 3 2
Will compute 3 times 2
Result of call: 6

虽然这个程序的代码挺多的,但是大部分其实都是做数据转换和错误报告。主要关于嵌入Python的开始于:

Py_Initialize();
pName=PyString_FromString(argv[1]);
/* Error checking of pName left out */
pModule=PyImport_Import(pName);

初始化解释器之后,使用 PyImport_Import() 导入模块。这个函数需要字符串作为参数,使用 PyString_FromString() 来构造:

pFunc=PyObject_GetAttrString(pModule,argv[2]);
/* pFunc is a new reference */
if (pFunc && PyCallable_Check(pFunc)) {
    ...
}
Py_XDECREF(pFunc);

载入了模块以后,就可以通过 PyObject_GetAttrString() 来获取对象。如果名字存在并且可以执行则可以安全的调用它。程序随后构造参数元组,然后执行调用:

pValue=PyObject_CallObject(pFunc,pArgs);

函数调用之后,pValue要么是NULL,要么是返回值的对象引用。注意在检查完返回值之后要释放引用。

4   扩展嵌入的Python

至今为止,嵌入的Python解释器还不能访问应用程序本身的功能。Python的API允许扩展嵌入的Python的解释器。所以,Python可以获得其所嵌入的程序的功能。这听起来挺麻烦的,其实并不是那样。只要简单的忘记是应用程序启动了Python解释器。

可以把程序看作一对功能的集合,可以写一些胶水代码来来让Python访问这些功能,有如你在写一个普通的Python扩展一样。例如:

static int numargs=0;

/* Return the number of arguments of the application command line */
static PyObject*
emb_numargs(PyObject *self, PyObject *args)
{
    if(!PyArg_ParseTuple(args, ":numargs"))
        return NULL;
    return Py_BuildValue("i", numargs);
}

static PyMethodDef EmbMethods[] = {
    {"numargs", emb_numargs, METH_VARARGS,
     "Return the number of arguments received by the process."},
    {NULL, NULL, 0, NULL}
};

添加上面的代码到 main() 函数。同样,插入如下两个语句到 Py_Initialize() 函数之后:

numargs=argc;
Py_InitModule("emb",EmbMethods);

这两行代码初始化numargs变量,并且使得 emb.numargs() 函数更加易于被Python嵌入的解释器所理解。通过这个扩展,Python脚本可以做如下事情:

import emb
print "Number of arguments",emb.numargs()

在实际的应用程序中,方法需要导出API以供Python使用。

5   在C++中嵌入Python

有时候需要将Python嵌入到C++程序中,而你必须有一些要注意的C++系统的细节,一般来说你要为这个程序写一个main()函数,然后使用C++编译器来编译和链接程序。而这里不需要因为使用C++而重新编译Python本身。

6   链接必备条件

configure 脚本执行时,可以正确的生成动态链接库使用的导出符号,而这些却不会自动被嵌入的静态链接的Python所继承,至少是在Unix。这是用于静态链接运行库(libpython.a)并且需要载入动态扩展(.so)的方式。

问题是一些入口点是使用Python运行时定义的而仅供扩展模块使用。如果嵌入应用不使用任何这些入口点,一些链接器不会包含这些实体到最终可执行文件的符号表。一些附加的选项可以用于告知连接器不要删除这些符号。

对于不同的平台,想要正确的检测该使用何种参数是非常困难的,但是幸运的是Python配置好了这些值。只要通过已经安装的Python解释器,启动交互解释器然后执行如下会话即可:

>>> import distutils.sysconfig
>>> distutils.sysconfig.get_config_var('LINKFORSHARED')
'-Xlinker -export-dynamic'

字符串的内容就是生成的选项。如果字符串为空,则不需要任何的附加选项。LINKFORSHARED的定义与Python顶层Makefile中的同名变量相同。

使用C/C++扩展Python

Monday, December 10th, 2007

使用C/C++扩展Python

翻译: gashero

如果你会用C,实现Python嵌入模块很简单。利用扩展模块可做很多Python不方便做的事情,他们可以直接调用C库和系统调用。

为了支持扩展,Python API定义了一系列函数、宏和变量,提供了对Python运行时系统的访问支持。Python的C API由C源码组成,并包含 “Python.h” 头文件。

编写扩展模块与你的系统相关,下面会详解。

目录

1   一个简单的例子

下面的例子创建一个叫做 “spam” 的扩展模块,调用C库函数 system() 。这个函数输入一个NULL结尾的字符串并返回整数,可供Python调用方式如下:

>>> import spam
>>> status=spam.system("ls -l")

一个C扩展模块的文件名可以直接是 模块名.c 或者是 模块名module.c 。第一行应该导入头文件:

#include <Python.h>

这会导入Python API。

Warning

因为Python含有一些预处理定义,所以你必须在所有非标准头文件导入之前导入Python.h 。

Python.h中所有用户可见的符号都有 PyPY 的前缀,除非定义在标准头文件中。为了方便 “Python.h” 也包含了一些常用的标准头文件,包括<stdio.h>,<string.h>,<errno.h>,<stdlib.h>。如果你的系统没有后面的头文件,则会直接定义函数 malloc() 、 free() 和 realloc() 。

下面添加C代码到扩展模块,当调用 “spam.system(string)” 时会做出响应:

static PyObject*
spam_system(PyObject* self, PyObject* args) {
    const char* command;
    int sts;
    if (!PyArg_ParseTuple(args,"s",&command))
        return NULL;
    sts=system(command);
    return Py_BuildValue("i",sts);
}

调用方的Python只有一个命令参数字符串传递到C函数。C函数总是有两个参数,按照惯例分别叫做 self 和 args 。

self 参数仅用于用C实现内置方法而不是函数。本例中, self 总是为NULL,因为我们定义的是个函数,不是方法。这一切都是相同的,所以解释器也就不需要刻意区分两种不同的C函数。

args 参数是一个指向Python的tuple对象的指针,包含参数。每个tuple子项对应一个调用参数。这些参数也全都是Python对象,所以需要先转换成C值。函数 PyArg_ParseTuple() 检查参数类型并转换成C值。它使用模板字符串检测需要的参数类型。

PyArg_ParseTuple() 正常返回非零,并已经按照提供的地址存入了各个变量值。如果出错(零)则应该让函数返回NULL以通知解释器出错。

2   关于错误和异常

一个常见惯例是,函数发生错误时,应该设置一个异常环境并返回错误值(NULL)。异常存储在解释器静态全局变量中,如果为NULL,则没有发生异常。异常的第一个参数也需要保存在静态全局变量中,也就是raise的第二个参数。第三个变量包含栈回溯信息。这三个变量等同于Python变量 sys.exc_type 、 sys.exc_value 、 sys.exc_traceback 。这对找到错误是很必要的。

Python API中定义了一些函数来设置这些变量。

最常用的就是 PyErr_SetString() 。参数是异常对象和C字符串。异常对象一般由像 PyExc_ZeroDivisionError 这样的对象来预定义。C字符串指明异常原因,并最终存储在异常的第一个参数里面。

另一个有用的函数是 PyErr_SetFromErrno() ,仅接受一个异常对象,异常描述包含在全局变量 errno 中。最通用的函数还是 PyErr_SetObject() ,包含两个参数,分别为异常对象和异常描述。你不需要使用 Py_INCREF() 来增加传递到其他函数的参数对象的引用计数。

你可以通过 PyErr_Occurred() 获知当前异常,返回当前异常对象,如果确实没有则为NULL。一般来说,你在调用函数时不需要调用 PyErr_Occurred() 检查是否发生了异常,你可以直接检查返回值。

如果调用更下层函数时出错了,那么本函数返回NULL表示错误,并且整个调用栈中只要有一处调用 PyErr_*() 函数设置异常就可以。一般来说,首先发现错误的函数应该设置异常。一旦这个错误到达了Python解释器的主循环,则会中断当前执行代码并追究异常。

有一种情况下,模块可能依靠其他 PyErr_*() 函数给出更加详细的错误信息,并且是正确的。但是按照一般规则,这并不重要,很多操作都会因为种种原因而挂掉。

想要忽略这些函数设置的异常,异常情况必须明确的使用 PyErr_Clear() 来清除。只有在C代码想要自己处理异常而不是传给解释器时才这么做。

每次失败的 malloc() 调用必须抛出一个异常,直接调用 malloc() 或 realloc() 的地方要调用 PyErr_NoMemory() 并返回错误。所有创建对象的函数都已经实现了这个异常的抛出,所以这是每个分配内存都要做的。

还要注意的是 PyArg_ParseTuple() 系列函数的异常,返回一个整数状态码是有效的,0是成功,-1是失败,有如Unix系统调用。

最后,小心垃圾情理,也就是 Py_XDECREF() 和 Py_DECREF() 的调用,会返回的异常。

选择抛出哪个异常完全是你的个人爱好了。有一系列的C对象代表了内置Python异常,例如 PyExc_ZeroDivisionError ,你可以直接使用。当然,你可能选择更合适的异常,不过别使用 PyExc_TypeError 告知文件打开失败(有个更合适的 PyExc_IOError )。如果参数列表有误, PyArg_ParseTuple() 通常会抛出 PyExc_TypeError 。如果参数值域有误, PyExc_ValueError 更合适一些。

你也可以为你的模块定义一个唯一的新异常。需要在文件前部声明一个静态对象变量,如:

static PyObject* SpamError;

然后在模块初始化函数(initspam())里面初始化它,并省却了处理:

PyMODINIT_FUNC
initspam(void) {
    PyObject* m;
    m=Py_InitModule("spam",SpamMethods);
    if (m==NULL)
        return NULL;
    SpamError=PyErr_NewException("spam.error",NULL,NULL);
    Py_INCREF(SpamError);
    PyModule_AddObject(m,"error",SpamError);
}

注意实际的Python异常名字是 spam.error 。 PyErr_NewException() 函数使用Exception为基类创建一个类(除非是使用另外一个类替代NULL)。

同样注意的是创建类保存了SpamError的一个引用,这是有意的。为了防止被垃圾回收掉,否则SpamError随时会成为野指针。

一会讨论 PyMODINIT_FUNC 作为函数返回类型的用法。

3   回到例子

回到前面的例子,你应该明白下面的代码:

if (!PyArg_ParseTuple(args,"s",&command))
    return NULL;

就是为了报告解释器一个异常。如果执行正常则变量会拷贝到本地,后面的变量都应该以指针的方式提供,以方便设置变量。本例中的command会被声明为 “const char* command” 。

下一个语句使用UNIX系统函数system(),传递给他的参数是刚才从 PyArg_ParseTuple() 取出的:

sts=system(command);

我们的 spam.system() 函数必须返回一个PY对象,这可以通过 Py_BuildValue() 来完成,其形式与 PyArg_ParseTuple() 很像,获取格式字符串和C值,并返回新的Python对象:

return Py_BuildValue("i",sts);

在这种情况下,会返回一个整数对象,这个对象会在Python堆里面管理。

如果你的C函数没有有用的返回值,则必须返回None。你可以用 Py_RETUN_NONE 宏来完成:

Py_INCREF(Py_None);
return Py_None;

Py_None 是一个C名字指定Python对象None。这是一个真正的PY对象,而不是NULL指针。

4   模块方法表和初始化函数

把函数声明为可以被Python调用,需要先定义一个方法表:

static PyMethodDef SpamMethods[]= {
    ...
    {"system",spam_system,METH_VARARGS,
    "Execute a shell command."},
    ...
    {NULL,NULL,0,NULL}    /*必须的结束符*/
};

注意第三个参数 METH_VARARGS ,这个标志指定会使用C的调用惯例。可选值有 METH_VARARGS 、 METH_VARARGS | METH_KEYWORDS 。值0代表使用 PyArg_ParseTuple() 的陈旧变量。

如果单独使用 METH_VARARGS ,函数会等待Python传来tuple格式的参数,并最终使用 PyArg_ParseTuple() 进行解析。

METH_KEYWORDS 值表示接受关键字参数。这种情况下C函数需要接受第三个 PyObject* 对象,表示字典参数,使用 PyArg_ParseTupleAndKeywords() 来解析出参数。

方法表必须传递给模块初始化函数。初始化函数函数名规则为 initname() ,其中 name 为模块名。并且不能定义为文件中的static函数:

PyMODINIT_FUNC
initspam(void) {
    (void) Py_InitModule("spam",SpamMethods);
}

注意 PyMODINIT_FUNC 声明了void为返回类型,还有就是平台相关的一些定义,如C++的就要定义成 extern “C” 。

Python程序首次导入这个模块时就会调用initspam()函数。他调用 Py_InitModule() 来创建一个模块对象,同时这个模块对象会插入到 sys.modules 字典中的 “spam” 键下面。然后是插入方法表中的内置函数到 “spam” 键下面。 Py_InitModule() 返回一个指针指向刚创建的模块对象。他是有可能发生严重错误的,也有可能在无法正确初始化时返回NULL。

当嵌入Python时, initspam() 函数不会自动被调用,除非在入口处的 _PyImport_Inittab 表。最简单的初始化方法是在 Py_Initialize() 之后静态调用 initspam() 函数:

int
main(int argc, char* argv[]) {
    Py_SetProgramName(argv[0]);
    Py_Initialize();
    initspam();
    //...
}

在Python发行版的 Demo/embed/demo.c 中有可以参考的源码。

Note

从 sys.modules 中移除模块入口,或者在多解释器环境中导入编译模块,会导致一些扩展模块出错。扩展模块作者应该特别注意初始化内部数据结构。同时要注意 reload() 函数可能会被用在扩展模块身上,并调用模块初始化函数,但是对动态状如对象(动态链接库),却不会重新载入。

更多关于模块的现实的例子包含在Python源码包的Modules/xxmodule.c中。这些文件可以用作你的代码模板,或者学习。脚本 modulator.py 包含在源码发行版或Windows安装中,提供了一个简单的GUI,用来声明需要实现的函数和对象,并且可以生成供填入的模板。脚本在 Tools/modulator/ 目录。查看README以了解用法。

5   编译和连接

如果使用动态载入,细节依赖于系统,查看关于构建扩展模块部分,和关于在Windows下构建扩展的细节。

如果你无法使用动态载入,或者希望模块成为Python的永久组成部分,就必须改变配置并重新构建解释器。幸运的是,这对UNIX来说很简单,只要把你的代码(例如spammodule.c)放在 Modules/ Python源码目录下,然后增加一行到文件 Modules/Setup.local 来描述你的文件即可:

spam spammodule.o

然后重新构建解释器,使用make。你也可以在 Modules/ 子目录使用make,但是你接下来首先要重建Makefile文件,使用 make Makefile 命令。这对你改变 Setup 文件来说很重要。

如果你的模块需要其他扩展模块连接,则需要在配置文件后面加入,如:

spam spammodule.o -lX11

6   在C中调用Python函数

迄今为止,我们一直把注意力集中于让Python调用C函数,其实反过来也很有用,就是用C调用Python函数。这在回调函数中尤其有用。如果一个C接口使用回调,那么就要实现这个回调机制。

幸运的是,Python解释器是比较方便回调的,并给标准Python函数提供了标准接口。这里就不再详述解析Python代码作为输入的方式,如果有兴趣可以参考 Python/pythonmain.c 中的 -c 命令代码。

调用Python函数,首先Python程序要传递Python函数对象。当调用这个函数时,用全局变量保存Python函数对象的指针,还要调用 Py_INCREF() 来增加引用计数,当然不用全局变量也没什么关系。例如如下:

static PyObject* my_callback=NULL;
static PyObject*
my_set_callback(PyObject* dummy, PyObject* args) {
    PyObject* result=NULL;
    PyObject* temp;
    if (PyArg_ParseTuple(args,"O:set_callback",&temp)) {
        if (!PyCallable_Check(temp)) {
            PyErr_SetString(PyExc_TypeError,"parameter must be callable");
            return NULL;
        }
        Py_XINCREF(temp);
        Py_XINCREF(my_callback);
        my_callback=temp;
        Py_INCREF(Py_None);
        result=Py_None;
    }
    return result;
}

这个函数必须使用 METH_VARARGS 标志注册到解释器。宏 Py_XINCREF() 和 Py_XDECREF() 增加和减少对象的引用计数。

然后,就要调用函数了,使用 PyEval_CallObject() 。这个函数有两个参数,都是指向Python对象:Python函数和参数列表。参数列表必须总是tuple对象,如果没有参数则要传递空的tuple。使用 Py_BuildValue() 时,在圆括号中的参数会构造成tuple,无论有没有参数,如:

int arg;
PyObject* arglist;
PyObject* result;
//...
arg=123;
//...
arglist=Py_BuildValue("(i)",arg);
result=PyEval_CallObject(my_callback,arglist);
Py_DECREF(arglist);

PyEval_CallObject() 返回一个Python对象指针表示返回值。 PyEval_CallObject() 是 引用计数无关 的,有如例子中,参数列表对象使用完成后就立即减少引用计数了。`PyEval_CallObject()` 返回一个Python对象指针表示返回值。 PyEval_CallObject() 是 引用计数无关 的,有如例子中,参数列表对象使用完成后就立即减少引用计数了。

PyEval_CallObject() 的返回值总是新的,新建对象或者是对已有对象增加引用计数。所以你必须获取这个对象指针,在使用后减少其引用计数,即便是对返回值没有兴趣也要这么做。但是在减少这个引用计数之前,你必须先检查返回的指针是否为NULL。如果是NULL,则表示出现了异常并中止了。如果没有处理则会向上传递并最终显示调用栈,当然,你最好还是处理好异常。如果你对异常没有兴趣,可以用 PyErr_Clear() 清除异常,例如:

if (result==NULL)
    return NULL;  /*向上传递异常*/
//使用result
Py_DECREF(result);

依赖于具体的回调函数,你还要提供一个参数列表到 PyEval_CallObject() 。在某些情况下参数列表是由Python程序提供的,通过接口再传到回调函数。这样就可以不改变形式直接传递。另外一些时候你要构造一个新的tuple来传递参数。最简单的方法就是 Py_BuildValue() 函数构造tuple。例如,你要传递一个事件对象时可以用:

PyObject* arglist;
//...
arglist=Py_BuildValue("(l)",eventcode);
result=PyEval_CallObject(my_callback,arglist);
Py_DECREF(arglist);
if (result==NULL)
    return NULL;  /*一个错误*/
/*使用返回值*/
Py_DECREF(result);

注意 Py_DECREF(arglist) 所在处会立即调用,在错误检查之前。当然还要注意一些常规的错误,比如 Py_BuildValue() 可能会遭遇内存不足等等。

7   解析传给扩展模块函数的参数

函数 PyArg_ParseTuple() 声明如下:

int PyArg_ParseTuple(PyObject* arg, char* format, ...);

参数 arg 必须是一个tuple对象,包含传递过来的参数, format 参数必须是格式化字符串,语法解释见 “Python C/API” 的5.5节。剩余参数是各个变量的地址,类型要与格式化字符串对应。

注意 PyArg_ParseTuple() 会检测他需要的Python参数类型,却无法检测传递给他的C变量地址,如果这里出错了,可能会在内存中随机写入东西,小心。

任何Python对象的引用,在调用者这里都是 借用的引用 ,而不增加引用计数。

一些例子:

int ok;
int i,j;
long k,l;
const char* s;
int size;
ok=PyArg_ParseTuple(args,"");
/* python call: f() */

ok=PyArg_ParseTuple(args,"s",&s);
/* python call: f('whoops!') */

ok=PyArg_ParseTuple(args,"lls",&k,&l,&s);
/* python call: f(1,2,'three') */

ok=PyArg_ParseTuple(args,"(ii)s#",&i,&j,&s,&size);
/* python call: f((1,2),'three') */

{
    const char* file;
    const char* mode="r";
    int bufsize=0;
    ok=PyArg_ParseTuple(args,"s|si",&file,&mode,&bufsize);
    /* python call:
        f('spam')
        f('spam','w')
        f('spam','wb',100000)
    */
}

{
    int left,top,right,bottom,h,v;
    ok=PyArg_ParseTuple(args,"((ii)(ii))(ii)",
        &left,&top,&right,&bottom,&h,&v);
    /* python call: f(((0,0),(400,300)),(10,10)) */
}

{
    Py_complex c;
    ok=PyArg_ParseTuple(args,"D:myfunction",&c);
    /* python call: myfunction(1+2j) */
}

8   解析传给扩展模块函数的关键字参数

函数 PyArg_ParseTupleAndKeywords() 声明如下:

int PyArg_ParseTupleAndKeywords(PyObject* arg, PyObject* kwdict, char* format, char* kwlist[],...);

参数arg和format定义同 PyArg_ParseTuple() 。参数 kwdict 是关键字字典,用于接受运行时传来的关键字参数。参数 kwlist 是一个NULL结尾的字符串,定义了可以接受的参数名,并从左到右与format中各个变量对应。如果执行成功 PyArg_ParseTupleAndKeywords() 会返回true,否则返回false并抛出异常。

Note

嵌套的tuple在使用关键字参数时无法生效,不在kwlist中的关键字参数会导致 TypeError 异常。

如下是使用关键字参数的例子模块,作者是 Geoff Philbrick (phibrick@hks.com):

#include "Python.h"

static PyObject*
keywdarg_parrot(PyObject* self, PyObject* args, PyObject* keywds) {
    int voltage;
    char* state="a stiff";
    char* action="voom";
    char* type="Norwegian Blue";
    static char* kwlist[]={"voltage","state","action","type",NULL};
    if (!PyArg_ParseTupleAndKeywords(args,keywds,"i|sss",kwlist,
            &voltage,&state,&action,&type))
        return NULL;
    printf("-- This parrot wouldn't %s if you put %i Volts through it.\n",action,voltage);
    printf("-- Lovely plumage, the %s -- It's %s!\n",type,state);
    Py_INCREF(Py_None);
    return Py_None;
}

static PyMethodDef keywdary_methods[]= {
    /*注意PyCFunction,这对需要关键字参数的函数很必要*/
    {"parrot",(PyCFunction)keywdarg_parrot, METH_VARARGS | METH_KEYWORDS,"Print a lovely skit to standard output."},
    {NULL,NULL,0,NULL}
};

void
initkeywdarg(void) {
    Py_InitModule("keywdarg",keywdarg_methods);
}

9   构造任意值

这个函数声明与 PyArg_ParseTuple() 很相似,如下:

PyObject* Py_BuildValue(char* format, ...);

接受一个格式字符串,与 PyArg_ParseTuple() 相同,但是参数必须是原变量的地址指针。最终返回一个Python对象适合于返回给Python代码。

一个与 PyArg_ParseTuple() 的不同是,后面可能需要的要求返回一个tuple,比如用于传递给其他Python函数以参数。 Py_BuildValue() 并不总是生成tuple,在多于1个参数时会生成tuple,而如果没有参数则返回None,一个参数则直接返回该参数的对象。如果要求强制生成一个长度为空的tuple,或包含一个元素的tuple,需要在格式字符串中加上括号。

例如:

代码 返回值
Py_BuildValue("") None
Py_BuildValue("i",123) 123
Py_BuildValue("iii",123,456,789) (123,456,789)
Py_BuildValue("s","hello") ‘hello’
Py_BuildValue("ss","hello","world") (‘hello’, ‘world’)
Py_BuildValue("s#","hello",4) ‘hell’
Py_BuildValue("()") ()
Py_BuildValue("(i)",123) (123,)
Py_BuildValue("(ii)",123,456) (123,456)
Py_BuildValue("(i,i)",123,456) (123,456)
Py_BuildValue("[i,i]",123,456) [123,456]
Py_BuildValue("{s:i,s:i}",'a',1,'b',2) {‘a':1,’b':2}
Py_BuildValue("((ii)(ii))(ii)",1,2,3,4,5,6) (((1,2),(3,4)),(5,6))

10   引用计数

在C/C++语言中,程序员负责动态分配和回收堆(heap)当中的内存。这意味着,我们在C中编程时必须面对这个问题。

每个由 malloc() 分配的内存块,最终都要由 free() 扔到可用内存池里面去。而调用 free() 的时机非常重要,如果一个内存块忘了 free() 则是内存泄漏,程序结束前将无法重新使用。而如果对同一内存块 free() 了以后,另外一个指针再次访问,则叫做野指针。这同样会导致严重的问题。

内存泄露往往发生在一些并不常见的程序流程上面,比如一个函数申请了资源以后,却提前返回了,返回之前没有做清理工作。人们经常忘记释放资源,尤其对于后加新加的代码,而且会长时间都无法发现。这些函数往往并不经常调用,而且现在大多数机器都有庞大的虚拟内存,所以内存泄漏往往在长时间运行的进程,或经常被调用的函数中才容易发现。所以最好有个好习惯加上代码约定来尽量避免内存泄露。

Python往往包含大量的内存分配和释放,同样需要避免内存泄漏和野指针。他选择的方法就是 引用计数 。其原理比较简单:每个对象都包含一个计数器,计数器的增减与引用的增减直接相关,当引用计数为0时,表示对象已经没有存在的意义了,就可以删除了。

一个叫法是 自动垃圾回收 ,引用计数是一种垃圾回收方法,用户必须要手动调用 free() 函数。优点是可以提高内存使用率,缺点是C语言至今也没有一个可移植的自动垃圾回收器。引用计数却可以很好的移植,有如C当中的 malloc() 和 free() 一样。也许某一天会出现C语言饿自动垃圾回收器,不过在此之前我们还得用引用计数。

Python使用传统的引用计数实现,不过他包含一个循环引用探测器。这允许应用不需要担心的直接或间接的创建循环引用,而这实际上是引用计数实现的自动垃圾回收的致命缺点。循环引用指对象经过几层引用后回到自己,导致了其引用计数总是不为0。传统的引用计数实现无法解决循环引用的问题,尽管已经没有其他外部引用了。

循环引用探测器可以检测出垃圾回收中的循环并释放其中的对象。只要Python对象有 __del__() 方法,Python就可以通过 gc module 模块来自动暴露出循环引用。gc模块还提供 collect() 函数来运行循环引用探测器,可以在配置文件或运行时禁用循环应用探测器。

循环引用探测器作为一个备选选项,默认是打开的,可以在构建时使用 –without-cycle-gc 选项加到 configure 上来配置,或者移除 pyconfig.h 文件中的 WITH_CYCLE_GC 宏定义。在循环引用探测器禁用后,gc模块将不可用。

10.1   Python中的引用计数

有两个宏 Py_INCREF(x) 和 Py_DECREF(x) 用于增减引用计数。 Py_DECREF() 同时会在引用计数为0时释放对象资源。为了灵活性,他并不是直接调用 free() 而是调用对象所在类型的析构函数。

一个大问题是何时调用 Py_INCREF(x) 和 Py_DECREF(x) 。首先介绍一些术语。没有任何人都不会 拥有 一个对象,只能拥有其引用。对一个对象的引用计数定义了引用数量。拥有的引用,在不再需要时负责调用 Py_DECREF() 来减少引用计数。传递引用计数有三种方式:传递、存储和调用 Py_DECREF() 。忘记减少拥有的引用计数会导致内存泄漏。

同样重要的一个概念是 借用 一个对象,借用的对象不能调用 Py_DECREF() 来减少引用计数。借用者在不需要借用时,不保留其引用就可以了。应该避免拥有者释放对象之后仍然访问对象,也就是野指针。

借用的优点是你无需管理引用计数,缺点是可能被野指针搞的头晕。借用导致的野指针问题常发生在看起来无比正确,但是事实上已经被释放的对象。

借用的引用也可以用 Py_INCREF() 来改造成拥有的引用。这对引用的对象本身没什么影响,但是拥有引用的程序有责任在适当的时候释放这个拥有。

10.2   拥有规则

一个对象的引用进出一个函数时,其引用计数也应该同时改变。

大多数函数会返回一个对对象拥有的引用。而且几乎所有的函数其实都会创建一个对象,例如 PyInt_FromLong() 和 Py_BuildValue() ,传递一个拥有的引用给接受者。即便不是刚创建的,你也需要接受一个新的拥有引用。一般来说, PyInt_FromLong() 会维护一个常用值缓存,并且返回缓存项的引用。

很多函数提取一些对象的子对象并传递拥有引用,例如 PyObject_GetAttrString() 。另外,小心一些函数,包括: PyTuple_GetItem() 、 PyList_GetItem() 、 PyDict_GetItem() 和 PyDict_GetItemString() ,他们返回的都是借用的引用。

函数 PyImport_AddModule() 也是返回借用的引用,尽管他实际上创建了对象,只不过其拥有的引用实际存储在了 sys.modules 中。

当你传递一个对象的引用到另外一个函数时,一般来说,函数是借用你的引用,如果他确实需要存储,则会使用 Py_INCREF() 来变为拥有引用。这个规则有两种可能的异常: PyTuple_SetItem() 和 PyList_SetItem() ,这两个函数获取传递给他的拥有引用,即便是他们执行出错了。不过 PyDict_SetItem() 却不是接收拥有的引用。

当一个C函数被py调用时,使用对参数的借用。调用者拥有参数对象的拥有引用。所以,借用的引用的寿命是函数返回。只有当这类参数必须存储时,才会使用 Py_INCREF() 变为拥有的引用。

从C函数返回的对象引用必须是拥有的引用,这时的拥有者是调用者。

10.3   危险的薄冰

有些使用借用的情况会出现问题。这是对解释器的盲目理解所导致的,因为拥有者往往提前释放了引用。

首先而最重要的情况是使用 Py_DECREF() 来释放一个本来是借用的对象,比如列表中的元素:

void
bug(PyObject* list) {
    PyObject* item=PyList_GetItem(list,0);
    PyList_SetItem(list,1,PyInt_FromLong(0L));
    PyObject_Print(item,stdout,0); /* BUG! */
}

这个函数首先借用了 list[0] ,然后把 list[1] 替换为值0,最后打印借用的引用。看起来正确么,不是!

我们来跟踪一下 PyList_SetItem() 的控制流,列表拥有所有元素的引用,所以当项目1被替换时,他就释放了原始项目1。而原始项目1是一个用户定义类的实例,假设这个类定义包含 __del__() 方法。如果这个类的实例引用计数为1,处理过程会调用 __del__() 方法。

因为使用python编写,所以 __del__() 中可以用任何python代码来完成释放工作。替换元素的过程会执行 del list[0] ,即减掉了对象的最后一个引用,然后就可以释放内存了。

知道问题后,解决方案就出来了:临时增加引用计数。正确的版本如下:

void
no_bug(PyObject* list) {
    PyObject* item=PyList_GetItem(list,0);
    Py_INCREF(item);
    PyList_SetItem(list,1,PyInt_FromLong(0L));
    PyObject_Print(item,stdout,0);
    Py_DECREF(item);
}

这是一个真实的故事,旧版本的Python中多处包含这个问题,让guido花费大量时间研究 __del__() 为什么失败了。

第二种情况的问题出现在多线程中的借用引用。一般来说,python中的多线程之间并不能互相影响对方,因为存在一个GIL。不过,这可能使用宏 Py_BEGIN_ALLOW_THREADS 来临时释放锁,最后通过宏 Py_END_ALLOW_THREADS 来再申请锁,这在IO调用时很常见,允许其他线程使用处理器而不是等待IO结束。很明显,下面的代码与前面的问题相同:

void
bug(PyObject* list) {
    PyObject* item=PyList_GetItem(list,0);
    Py_BEGIN_ALLOW_THREADS
    //一些IO阻塞调用
    Py_END_ALLOW_THREADS
    PyObject_Print(item,stdout,0); /*BUG*/
}

10.4   NULL指针

一般来说,函数接受的参数并不希望你传递一个NULL指针进来,这会出错的。函数的返回对象引用返回NULL则代表发生了异常。这是Python的机制,毕竟,一个函数如果执行出错了,那么也没有必要多解释了,浪费时间。(注:彪悍的异常也不需要解释)

最好的测试NULL的方法就是在代码里面,一个指针如果收到了NULL,例如 malloc() 或其他函数,则表示发生了异常。

宏 Py_INCREF() 和 Py_DECREF() 并不检查NULL指针,不过还好, Py_XINCREF() 和 Py_XDECREF() 会检查。

检查特定类型的宏,形如 Pytype_Check() 也不检查NULL指针,因为这个检查是多余的。

C函数的调用机制确保传递的参数列表(也就是args参数)用不为NULL,事实上,它总是一个tuple。

而把NULL扔到Python用户那里可就是一个非常严重的错误了。

11   使用C++编写扩展

有时候需要用C++编写Python扩展模块。不过有一些严格的限制。如果Python解释器的主函数是使用C编译器编译和连接的,那么全局和静态对象的构造函数将无法使用。而主函数使用C++编译器时则不会有这个问题。被Python调用的函数,特别是模块初始化函数,必须声明为 extern "C" 。没有必要在Python头文件中使用 extern "C" 因为在使用C++编译器时会自动加上 __cplusplus 这个定义,而一般的C++编译器一般都会设置这个符号。

12   提供给其他模块以C API

很多模块只是提供给Python使用的函数和新类型,但是偶尔也有可能被其他扩展模块所调用。例如一个模块实现了 “collection” 类型,可以像list一样工作而没有顺序。有如标准Python中的list类型一样,提供的C接口可以让扩展模块创建和管理list,这个新的类型也需要有C函数以供其他扩展模块直接管理。

初看这个功能可能以为很简单:只要写这些函数就行了(不需要声明为静态),提供适当的头文件,并注释C的API。当然,如果所有的扩展模块都是静态链接到Python解释器的话,这当然可以正常工作。但是当其他扩展模块是动态链接库时,定义在一个模块中的符号,可能对另外一个模块来说并不是可见的。而这个可见性又是依赖操作系统实现的,一些操作系统对Python解释器使用全局命名空间和所有的扩展模块(例如Windows),也有些系统则需要明确的声明模块的导出符号表(AIX就是个例子),或者提供一个不同策略的选择(大多数的Unices)。即便这些符号是全局可见的,拥有函数的模块,也可能尚未载入。

为了可移植性,不要奢望任何符号会对外可见。这意味着模块中所有的符号都声明为 static ,除了模块的初始化函数以外,这也是为了避免各个扩展模块之间的符号名称冲突。这也意味着必须以其他方式导出扩展模块的符号。

Python提供了一种特殊的机制,以便在扩展模块间传递C级别的信息(指针): CObject 。一个CObject是一个Python的数据类型,存储了任意类型指针(void*)。CObject可以只通过C API来创建和存取,但是却可以像其他Python对象那样来传递。在特别的情况下,他们可以被赋予一个扩展模块命名空间内的名字。其他扩展模块随后可以导入这个模块,获取这个名字的值,然后得到CObject中保存的指针。

通过CObject有很多种方式导出扩展模块的C API。每个名字都可以得到他自己的CObject,或者可以把所有的导出C API放在一个CObject指定的数组中来发布。所以可以有很多种方法导出C API。

如下的示例代码展示了把大部分的重负载任务交给扩展模块,作为一个很普通的扩展模块的例子。他保存了所有的C API的指针到一个数组中,而这个数组的指针存储在CObject中。对应的头文件提供了一个宏以管理导入模块和获取C API的指针,客户端模块只需要在存取C API之前执行这个宏就可以了。

这个导出模块是修改自1.1节的spam模块。函数 spam.system() 并不是直接调用C库的函数 system() ,而是调用 PySpam_System() ,提供了更加复杂的功能。这个函数 PySpam_System() 同样导出供其他扩展模块使用。

函数 PySpam_System() 是一个纯C函数,声明为static如下:

static int
PySpam_System(const char* command) {
    return system(command);
}

函数 spam_system() 做了细小的修改:

static PyObject*
spam_system(PyObject* self, PyObject* args) {
    const char* command;
    int sts;
    if (!PyArg_ParseTuple(args,"s",&command))
        return NULL;
    sts=PySpam_System(command);
    return Py_BuildValue("i",sts);
}

在模块的头部加上如下行:

#include "Python.h"

另外两行需要添加的是:

#define SPAM_MODULE
#include "spammodule.h"

这个宏定义是告诉头文件需要作为导出模块,而不是客户端模块。最终模块的初始化函数必须管理初始化C API指针数组的初始化:

PyMODINIT_FUNC
initspam(void)
{
    PyObject *m;
    static void *PySpam_API[PySpam_API_pointers];
    PyObject *c_api_object;

    m = Py_InitModule("spam", SpamMethods);
    if (m == NULL)
        return;

    /* Initialize the C API pointer array */
    PySpam_API[PySpam_System_NUM] = (void *)PySpam_System;

    /* Create a CObject containing the API pointer array's address */
    c_api_object = PyCObject_FromVoidPtr((void *)PySpam_API, NULL);

    if (c_api_object != NULL)
        PyModule_AddObject(m, "_C_API", c_api_object);
}

注意 PySpam_API 声明为static,否则 initspam() 函数执行之后,指针数组就消失了。

大部分的工作还是在头文件 spammodule.h 中,如下:

#ifndef Py_SPAMMODULE_H
#define Py_SPAMMODULE_H
#ifdef __cplusplus
extern "C" {
#endif

/* Header file for spammodule */

/* C API functions */
#define PySpam_System_NUM 0
#define PySpam_System_RETURN int
#define PySpam_System_PROTO (const char *command)

/* Total number of C API pointers */
#define PySpam_API_pointers 1

#ifdef SPAM_MODULE
/* This section is used when compiling spammodule.c */

static PySpam_System_RETURN PySpam_System PySpam_System_PROTO;

#else
/* This section is used in modules that use spammodule's API */

static void **PySpam_API;

#define PySpam_System \
 (*(PySpam_System_RETURN (*)PySpam_System_PROTO) PySpam_API[PySpam_System_NUM])

/* Return -1 and set exception on error, 0 on success. */
static int
import_spam(void)
{
    PyObject *module = PyImport_ImportModule("spam");

    if (module != NULL) {
        PyObject *c_api_object = PyObject_GetAttrString(module, "_C_API");
        if (c_api_object == NULL)
            return -1;
        if (PyCObject_Check(c_api_object))
            PySpam_API = (void **)PyCObject_AsVoidPtr(c_api_object);
        Py_DECREF(c_api_object);
    }
    return 0;
}

#endif

#ifdef __cplusplus
}
#endif

#endif /* !defined(Py_SPAMMODULE_H) */

想要调用 PySpam_System() 的客户端模块必须在初始化函数中调用 import_spam() 以初始化导出扩展模块:

PyMODINIT_FUNC
initclient(void) {
    PyObject* m;
    m=Py_InitModule("client",ClientMethods);
    if (m==NULL)
        return;
    if (import_spam()<0)
        return;
    /*其他初始化语句*/
}

这样做的缺点是 spammodule.h 有点复杂。不过这种结构却可以方便的用于其他导出函数,所以学着用一次也就好了。

最后需要提及的是CObject提供的一些附加函数,用于CObject指定的内存块的分配和释放。详细信息可以参考Python的C API参考手册的CObject一节,和CObject的实现,参考文件 Include/cobject.hObjects/cobject.c

基于memcache的队列设计

Wednesday, November 28th, 2007

基于memcache的队列设计

作者: gashero

1   需求

两个系统交互,使用memcache作为中间的接口,因为传送的数据可能类型很多,而且无法定义一个很好的键名,所以设计了memcache队列,以供数据传输用。

2   基本设计原理

memcache中实现了命令 incrdecr 分别对一个键进行自加和自减操作。执行命令的结果是返回自加或自减后的结果。因为是通用memcache命令,所以在各种语言的接口中都有实现,本例探讨基于Python的实现。

协议接口:

incr <key> <value>\r\n

decr <key> <value>\r\n

Python接口:

incr(key,delta=1)

decr(key,delta=1)

3   队列结构设计

基于memcache实现的队列,需要实现两个基本变量以控制整个队列。

3.1   队列指针

队列指针有两个变量,一个是 head 另一个是 tail 分别为队列的最后一个ID和最前一个ID。每次加入新的item都会使得 tail++ ,而每次取走一个item都会使得 head++

3.2   队列元素

每个队列元素都是按照 prefix+index 形式构造,即一个类别前缀加上索引数字。之后在value中存储串行化以后的数据。

3.3   队列的循环

经测试memcache中的自增变量的数据类型为long型,即有符号32位整数。最大可取值区间为-2199023255552至 2199023255551。所以,为了防止系统长时间运行出现问题,必须设置一个队列的循环机制。无论head还是tail变量,必须设计一个最大值, 当达到最大值时就清零循环。

经测试,在Python调用memcache测试最大允许自增变量值超过最大可取值范围时,发生了ValueError异常。而在memcache 协议中则表示根本没有验证数据溢出的情况。在应用中需要酌情考虑。比如设置为1048576,为队列的最大长度,并且每次在index达到这个数字时即循 环。

4   需要注意的问题

4.1   数据项丢失

memcache的设计目标是提供高速度的数据缓存服务,并不确保数据的完整性,在内存不足时,memcache甚至会主动删除一些存储的键值。所以,即便是通过正常的接口、head与tail指定的标志,获取键值时也要考虑到异常的可能。

4.2   及时删除过期变量

在接收端应用程序取走数据之后应该立即删除memcache中的对应键,否则时间长了可能会因为占用内存过多而影响尚未取走变量的存储。

如何嵌入pyrex

Tuesday, November 27th, 2007

 

如何嵌入pyrex

译者: gashero

 

简介

对于大多数Py程序员来说,pyrex已经是一个实现Python/C包装的成功解决方案,他消除了所有Python/C之间的杂七杂八的问题。

但是大部分的pyrex使用都是集中于构建扩展类型和模块,使得他们可以象一般的Python模块一样的使用。

这个小教程会引导你制作单独的可执行程序,其实是将pyrex嵌入了Python解释器来运行。

 

嵌入Python?怎么做到的?为什么这么做?

现代,操作系统、编程语言和开发工具都成为了一种信仰,Python也无法逃脱。

如果你尝试嵌入,那么会有一大堆Python程序员嘲笑你,他们更推崇扩展,而不是嵌入。

在大多数情况下,他们是对的。而且大多数时候,扩展Python也比嵌入Python到C程序更有意义。

但是仍然有一些情况嵌入是更方便的,例如:

  • 一个程序需要711权限(u+rwx,go-rw+x)
  • 一个程序需要在ps和top命令中按照名称显示,而不是python
  • 懒得区别用户的Python版本
  • 适合你的其他理由

 

这个教程会谈到哪些东西?

这个文档中会介绍使用pyrex构建单独的可执行程序的过程,或者说看起来像一个可执行程序,当然运行时会动态的链接libpython2.x.so文件。

 

处方

 

创建一个简单的.pyx文件

如下代码是完整的pyrex源码,可以直接编译成可执行文件:

"""

实验通过pyrex嵌入Python

"""

#从C头文件中获取我们需要的功能

cdef extern from "stdio.h":

    int printf(char* format,...)

cdef extern from "Python.h":

    #嵌入函数

    void Py_Initialize()

    void Py_Finalize()

    void PySys_SetArgv(int argc, char** argv)

    #声明其他的Python/C接口函数

    void Py_INCREF(object o)

    void Py_DECREF(object o)

    object PyString_FromStringAndSize(char *, int)

#注意:必须声明函数原型 'init<mymodulename>()' 而mymodulename就是这段

#代码的文件名,比如本例就叫做testpyx.pyx

cdef public void inittestpyx()

#这里可以定义一系列的Python扩展类型、类、方法等

cdef class Testclass:

    cdef public int someint

    cdef public char* somestring    def __init__(self):

        self.someint=43

        self.somestring="this is a string"

def hello(self):

        print "Hello, this is an instance of %s"%self.__class__.__name__

#现在可以声明C的main()函数了

cdef public int main(int argc, char** argv):

    Py_Initialize()

    PySys_SetArgv(argc,argv)

    printf("initialising testpyx\n")

    inittestpyx()

    #初始化完成,可以做Python的功能了

    printf("testmain: instantiating Testclass\n")

    testobj=Testclass()

    printf("testmain: created testobj\n")

print "testobj.someint=%s"%testobj.someint

    print "testobj.somestring=%s"%testobj.somestring

print "calling testobj.hello()"

    testobj.hello()

    #离开程序之前释放资源

    print "cleaning up"

    Py_Finalize()

 

编写Makefile

如下的Makefile对我工作的很好,希望对你有用:

#创建单独的Pyrex程序

PYVERSION=2.3

PYPREFIX=/usr

INCLUDES=-i$(PYPREFIX)/include/python$(PYVERSION)testpyx: testpyx.o

    gcc -o $@ $^ -lpython$(PYVERSION)

testpyx.o: testpyx.c

    gcc -c $^ $(INCLUDES)

testpyx.c: testpyx.pyx

    pyrexc testpyx.pyx

 

总结

想要做可以单独工作的pyrex可执行程序,必须要:

  1. 导入必须的几个API: Py_Initialize()Py_Finalize()PySys_SetArgv() ,还有就是你需要调用的其他函数

  2. 必须明确的声明pyrex生成的模块初始化函数的C原型,如本例的’inittestpyx’。

  3. 创建一个C可以调用的’main()’函数,需要注意:

    1. 初始化Python解释器
    2. 调用pyrex生成的模块初始化函数
    3. 随程序需要随便写Python语句
    4. 停止Python解释器
  4. 创建一个Makefile需要:

    1. 明确的编译.pyx到.c文件,使用单独的命令 pyrexc
    2. 编译生成的.c文件到.o或.obj文件,确保提供正确的include路径
    3. 链接结果到最终的可执行文件,并确保它可以找到Python运行时库

web.py的HTTP研究

Monday, November 26th, 2007

 

web.py的HTTP研究

作者: gashero 日期: 2007-11-21 版本: web.py-0.22

 

目录

1   简介

我想要实现使用twisted作为HTTP服务器,而web.py作为后端的动态引擎的部署方式。从而实现高性能,又易于学习的一种开发方式。

2   一个基本的例子

见web.py首页上提供的例子,如下:

import web
urls=(
    '/(.*)','hello',
)
class hello:
    def GET(self,name):
        i=web.input(times=1)
        if not name:
            name='world'
        for c in xrange(int(i.times)):
            print 'Hello,',name+'!'
if __name__=="__main__":
    web.run(urls,globals())

可见一个web.py编写的应用包含一个url映射表,一些映射处理类,类按照请求方法命名的方法实现了对应的逻辑,最后包含一个启动服务器的语句,只要传入URL映射即可。

3   web.py所属文件简介

3.1   __init__.py

导入所有的下属模块,并且有个主函数会运行doctest测试用例。导入的模块全部分为两种,例如:

import utils
from utils import *

3.2   cheetah.py

98行代码实现了cheetah的接口规范化,最终还是要导入真正的Cheetah,只是这里提供的接口比较好用。如:

def render(template,terms=None,asTemplate=False,base=None,isString=False)

3.3   db.py

自动导入DBUtils作为数据库连接池并提供web.py友好的接口。有703行代码,貌似大了一点。

3.4   debugerror.py

以Django的模板提供了调试服务器运行时的错误信息展示。316行代码。

3.5   form.py

提供了简单的表单代码生成,和貌似有数据验证功能。提供了面向对象的组织结构。215行代码的。

3.6   http.py

269行代码。实现了HTTP的一些机制,比如cookie、缓存过期、重定向等等。好像是与httpserver.py没有什么太大的耦合,但是也要小心,看到一些web.header之类的设置,这岂不是全局的?

3.7   httpserver.py

224行代码。实现了一个简单的HTTP服务器。还是基于Python自带的那个SimpleHTTPServer模块写的,所以也是单线程的。这里一共就2个函数,分别是启动不同的服务器,不过貌似都是使用wsgi方式实现的。

3.8   net.py

155行代码。IP地址验证,URL验证等等。

3.9   request.py

153行代码。好像就是请求处理的入口了。同时这里也有个 run() 函数,貌似就是启动服务器的入口。

3.10   template.py

878行代码,貌似实现了一个比较像Cheetah的模板系统吧,里面有明确的代码显示,这里面做了很多代码解析的工作,至少是有词法解析器。

3.11   utils.py

796行代码。看来就是我以前一直期望的那个具有很多超牛功能的东西,如函数的执行缓存等等。

3.12   webapi.py

369行代码。提供了一些常用功能的函数接口,如 badrequest() 、 notfound() 、 gone() 等等,这样可以加快解决过程。另外像 setcookie() 、 cookies() 、 debug() 等等也是编程所必需的。

3.13   wsgi.py

54行代码。提供了3个函数,分别是 runfcgi() 、 runscgi() 、 runwsgi() 。分别提供3种不同的发布方式。

3.14   wsgiserver/

仅内含一个 __init__.py 文件,其实是从CherryPy搞来的WSGI服务器的代码。

4   HTTP处理流程

4.1   启动服务器

启动服务器的代码:

web.run(urls,globals())

实际上是调用 web.request 模块中的 run() 函数。代码如下:

def run(inp,fvars,*middleware):
    autoreload=http.reloader in middleware
    return wsgi.runwsgi(webapi.wsgifunc(webpyfunc(inp,fvars,autoreload),*middleware))

其中首先确定是否需要动态重新装入功能,然后就是把 webpyfunc() 的调用结果,就是一个函数对象传入 webapi.wsgifunc() 函数。 webapi.wsgifunc() 函数接受两个参数,一个是 webpyfunc() 的结果,另一个是中间件列表。

4.2   webpyfunc() 生成调用请求处理器的函数

在模块 web.request 中,声明如下:

def webpyfunc(inp,fvars,autoreload=False)

参数解释:

  • inp :可以是函数或一个tuple,用于URL映射
  • fvars :一个dict,存储可用的变量
  • autoreload :是否自动重装

完全按照例子当中执行时webpyfunc()函数的调用参数为:

webpyfunc(urls,globals(),False)

这样最终是返回的结果是:

return lambda: handle(urls,globals())

即便 autoreload=True 估计也是同样的结果,只是可以自动重新加载而已。

5   启动的HTTP服务器

经分析,默认的服务器最终启动的是 web.wsgi 模块中 runwsgi() 函数中的如下语句:

return httpserver.runsimple(func,validip(listget(sys.argv,1,'')))

这个函数实际上内部居然还包含两个类的定义,一个作为静态文件服务的,叫 StaticApp ,另外一个用作动态的,叫 WSGIWrapper

6   总结

真不知道wsgi为什么这么重要,以至于我接触的几个框架基本上都是用wsgi来实现底层。每每让我无法下手修改其行为。django算是走的距离核心最近的一个了,而其他的基本上还没有摸到边。

总之,还是自己写那个htmid吧,自己写的终归了解。

 

Stackless Python并发式编程介绍[已校对版]

Sunday, November 18th, 2007

Stackless Python并发式编程介绍

作者: Grant Olson

作者:    Grant Olson
电子邮件:    olsongt@verizon.net
日期:    2006-07-07
译者:    刘禹呈
电子邮件:    lych77@gmail.com
日期:    2007-09-19
校对:    gashero
电子邮件:    harry.python@gmail.com
日期:    2007-09-20
原文地址:    http://members.verizon.net/olsongt/stackless/why_stackless.html
目录

1 介绍

1.1 为什么要使用Stackless

摘自 stackless 网站。

Note

Stackless Python 是Python编程语言的一个增强版本,它使程序员从基于线程的编程方式中获得好处,并避免传统线程所带来的性能与复杂度问题。Stackless为 Python带来的微线程扩展,是一种低开销、轻量级的便利工具,如果使用得当,可以获益如下:

  • 改进程序结构
  • 增进代码可读性
  • 提高编程人员生产力

以上是Stackless Python很简明的释义,但其对我们意义何在?——就在于Stackless提供的并发建模工具,比目前其它大多数传统编程语言所提供的,都更加易用: 不仅是Python自身,也包括Java、C++,以及其它。尽管还有其他一些语言提供并发特性,可它们要么是主要用于学术研究的(如 Mozart/Oz),要么是罕为使用、或用于特殊目的的专业语言(如Erlang)。而使用stackless,你将会在Python本身的所有优势之 上,在一个(但愿)你已经很熟悉的环境中,再获得并发的特性。

这自然引出了个问题:为什么要并发?

1.1.1 现实世界就是并发的

现实世界就是“并发”的,它是由一群事物(或“演员”)所组成,而这些事物以一种对彼此所知有限的、松散耦合的方式相互作用。传说中面向对象编程有 一个好处,就是对象能够对现实的世界进行模拟。这在一定程度上是正确的,面向对象编程很好地模拟了对象个体,但对于这些对象个体之间的交互,却无法以一种 理想的方式来表现。例如,如下代码实例,有什么问题?

def familyTacoNight():
    husband.eat(dinner)
    wife.eat(dinner)
    son.eat(dinner)
    daughter.eat(dinner)

第一印象,没问题。但是,上例中存在一个微妙的安排:所有事件是次序发生的,即:直到丈夫吃完饭,妻子才开始吃;儿子则一直等到母亲吃完才吃;而女 儿则是最后一个。在现实世界中,哪怕是丈夫还堵车在路上,妻子、儿子和女儿仍然可以该吃就吃,而要在上例中的话,他们只能饿死了——甚至更糟:永远没有人 会知道这件事,因为他们永远不会有机会抛出一个异常来通知这个世界!

1.1.2 并发可能是(仅仅可能是)下一个重要的编程范式

我个人相信,并发将是软件世界里的下一个重要范式。随着程序变得更加复杂和耗费资源,我们已经不能指望摩尔定律来每年给我们提供更快的CPU了,当 前,日常使用的个人计算机的性能提升来自于多核与多CPU机。一旦单个CPU的性能达到极限,软件开发者们将不得不转向分布式模型,靠多台计算机的互相协 作来建立强大的应用(想想GooglePlex)。为了取得多核机和分布式编程的优势,并发将很快成为做事情的方式的事实标准。

1.2 安装stackless

安装Stackless的细节可以在其网站上找到。现在Linux用户可以通过SubVersion取得源代码并编译;而对于Windows用户, 则有一个.zip文件供使用,需要将其解压到现有的Python安装目录中。接下来,本教程假设Stackless Python已经安装好了,可以工作,并且假设你对Python语言本身有基本的了解。

2 stackless起步

本章简要介绍了 stackless 的基本概念,后面章节将基于这些基础,来展示更加实用的功能。

2.1 微进程(tasklet)

微进程是stackless的基本构成单元,你可以通过提供任一个Python可调用对象(通常为函数或类的方法)来建立它,这将建立一个微进程并将其添加到调度器。这是一个快速演示:

Python 2.4.3 Stackless 3.1b3 060504 (#69, May  3 2006, 19:20:41) [MSC v.1310 32
bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import stackless
>>>
>>> def print_x(x):
...     print x
...
>>> stackless.tasklet(print_x)('one')
<stackless.tasklet object at 0x00A45870>
>>> stackless.tasklet(print_x)('two')
<stackless.tasklet object at 0x00A45A30>
>>> stackless.tasklet(print_x)('three')
<stackless.tasklet object at 0x00A45AB0>
>>>
>>> stackless.run()
one
two
three
>>>

注意,微进程将排起队来,并不运行,直到调用 stackless.run()

2.2 调度器(scheduler)

调度器控制各个微进程运行的顺序。如果刚刚建立了一组微进程,它们将按照建立的顺序来执行。在现实中,一般会建立一组可以再次被调度的微进程,好让每个都有轮次机会。一个快速演示:

Python 2.4.3 Stackless 3.1b3 060504 (#69, May  3 2006, 19:20:41) [MSC v.1310 32
bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import stackless
>>>
>>> def print_three_times(x):
...     print "1:", x
...     stackless.schedule()
...     print "2:", x
...     stackless.schedule()
...     print "3:", x
...     stackless.schedule()
...
>>>
>>> stackless.tasklet(print_three_times)('first')
<stackless.tasklet object at 0x00A45870>
>>> stackless.tasklet(print_three_times)('second')
<stackless.tasklet object at 0x00A45A30>
>>> stackless.tasklet(print_three_times)('third')
<stackless.tasklet object at 0x00A45AB0>
>>>
>>> stackless.run()
1: first
1: second
1: third
2: first
2: second
2: third
3: first
3: second
3: third
>>>

注意:当调用 stackless.schedule() 的时候,当前活动微进程将暂停执行,并将自身重新插入到调度器队列的末尾,好让下一个微进程被执行。一旦在它前面的所有其他微进程都运行过了,它将从上次 停止的地方继续开始运行。这个过程会持续,直到所有的活动微进程都完成了运行过程。这就是使用stackless达到合作式多任务的方式。

2.3 通道(channel)

通道使得微进程之间的信息传递成为可能。它做到了两件事:

  1. 能够在微进程之间交换信息。
  2. 能够控制运行的流程。

又一个快速演示:

C:>c:python24python
Python 2.4.3 Stackless 3.1b3 060504 (#69, May  3 2006, 19:20:41) [MSC v.1310 32
bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import stackless
>>>
>>> channel = stackless.channel()
>>>
>>> def receiving_tasklet():
...     print "Recieving tasklet started"
...     print channel.receive()
...     print "Receiving tasklet finished"
...
>>> def sending_tasklet():
...     print "Sending tasklet started"
...     channel.send("send from sending_tasklet")
...     print "sending tasklet finished"
...
>>> def another_tasklet():
...     print "Just another tasklet in the scheduler"
...
>>> stackless.tasklet(receiving_tasklet)()
<stackless.tasklet object at 0x00A45B30>
>>> stackless.tasklet(sending_tasklet)()
<stackless.tasklet object at 0x00A45B70>
>>> stackless.tasklet(another_tasklet)()
<stackless.tasklet object at 0x00A45BF0>
>>>
>>> stackless.run()
Recieving tasklet started
Sending tasklet started
send from sending_tasklet
Receiving tasklet finished
Just another tasklet in the scheduler
sending tasklet finished
>>>

接收的微进程调用 channel.receive() 的时候,便阻塞住,这意味着该微进程暂停执行,直到有信息从这个通道送过来。除了往这个通道发送信息以外,没有其他任何方式可以让这个微进程恢复运行。

若有其他微进程向这个通道发送了信息,则不管当前的调度到了哪里,这个接收的微进程都立即恢复执行;而发送信息的微进程则被转移到调度列表的末尾,就像调用了 stackless.schedule() 一样。

同样注意,发送信息的时候,若当时没有微进程正在这个通道上接收,也会使当前微进程阻塞:

>>> stackless.tasklet(sending_tasklet)()
<stackless.tasklet object at 0x00A45B70>
>>> stackless.tasklet(another_tasklet)()
<stackless.tasklet object at 0x00A45BF0>
>>>
>>> stackless.run()
Sending tasklet started
Just another tasklet in the scheduler
>>>
>>> stackless.tasklet(another_tasklet)()
<stackless.tasklet object at 0x00A45B30>
>>> stackless.run()
Just another tasklet in the scheduler
>>>
>>> #Finally adding the receiving tasklet
...
>>> stackless.tasklet(receiving_tasklet)()
<stackless.tasklet object at 0x00A45BF0>
>>>
>>> stackless.run()
Recieving tasklet started
send from sending_tasklet
Receiving tasklet finished
sending tasklet finished

发送信息的微进程,只有在成功地将数据发送到了另一个微进程之后,才会重新被插入到调度器中。

2.4 总结

以上涵盖了stackless的大部分功能。似乎不多是吧?——我们只使用了少许对象,和大约四五个函数调用,来进行操作。但是,使用这种简单的API作为基本建造单元,我们可以开始做一些真正有趣的事情。

3 协程(coroutine)

3.1 子例程的问题

大多数传统编程语言具有子例程的概念。一个子例程被另一个例程(可能还是其它某个例程的子例程)所调用,或返回一个结果,或不返回结果。从定义上说,一个子例程是从属于其调用者的。

见下例:

def ping():
    print "PING"
    pong()

def pong():
    print "PONG"
    ping()

ping()

有经验的编程者会看到这个程序的问题所在:它导致了堆栈溢出。如果运行这个程序,它将显示一大堆讨厌的跟踪信息,来指出堆栈空间已经耗尽。

3.1.1 堆栈

我仔细考虑了,自己对C语言堆栈的细节究竟了解多少,最终还是决定完全不去讲它。似乎,其他人对其所尝试的描述,以及图表,只有本身已经理解了的人才能看得懂。我将试着给出一个最简单的说明,而对其有更多兴趣的读者可以从网上查找更多信息。

每当一个子例程被调用,都有一个“栈帧”被建立,这是用来保存变量,以及其他子例程局部信息的区域。于是,当你调用 ping() ,则有一个栈帧被建立,来保存这次调用相关的信息。简言之,这个帧记载着 ping 被调用了。当再调用 pong() ,则又建立了一个栈帧,记载着 pong 也被调用了。这些栈帧是串联在一起的,每个子例程调用都是其中的一环。就这样,堆栈中显示: ping 被调用所以 pong 接下来被调用。显然,当 pong() 再调用 ping() ,则使堆栈再扩展。下面是个直观的表示:

堆栈
1 ping 被调用
2 ping 被调用,所以 pong 被调用
3 ping 被调用,所以 pong 被调用,所以 ping 被调用
4 ping 被调用,所以 pong 被调用,所以 ping 被调用,所以 pong 被调用
5 ping 被调用,所以 pong 被调用,所以 ping 被调用,所以 pong 被调用,所以 ping 被调用
6 ping 被调用,所以 pong 被调用,所以 ping 被调用,所以 pong 被调用,所以 ping 被调用……

现在假设,这个页面的宽度就表示系统为堆栈所分配的全部内存空间,当其顶到页面的边缘的时候,将会发生溢出,系统内存耗尽,即术语“堆栈溢出”。

3.1.2 那么,为什么要使用堆栈?

上例是有意设计的,用来体现堆栈的问题所在。在大多数情况下,当每个子例程返回的时候,其栈帧将被清除掉,就是说堆栈将会自行实现清理过程。这一般 来说是件好事,在C语言中,堆栈就是一个不需要编程者来手动进行内存管理的区域。很幸运,Python程序员也不需要直接来担心内存管理与堆栈。但是由于 Python解释器本身也是用C实现的,那些实现者们可是需要担心这个的。使用堆栈是会使事情方便,除非我们开始调用那种从不返回的函数,如上例中的,那 时候,堆栈的表现就开始和程序员别扭起来,并耗尽可用的内存。

3.2 走进协程

此时,将堆栈弄溢出是有点愚蠢的。 ping() 和 pong() 本不是真正意义的子例程,因为其中哪个也不从属于另一个,它们是“协程”,处于同等的地位,并可以彼此间进行无缝通信。

堆栈
1 ping 被调用
2 pong 被调用
3 ping 被调用
4 pong 被调用
5 ping 被调用
6 pong 被调用

在stackless中,我们使用通道来建立协程。还记得吗,通道所带来的两个好处中的一个,就是能够控制微进程之间运行的流程。使用通道,我们可以在 ping 和 pong 这两个协程之间自由来回,要多少次就多少次,都不会堆栈溢出:

#
# pingpong_stackless.py
#

import stackless

ping_channel = stackless.channel()
pong_channel = stackless.channel()

def ping():
    while ping_channel.receive(): #在此阻塞
        print "PING"
        pong_channel.send("from ping")

def pong():
    while pong_channel.receive():
        print "PONG"
        ping_channel.send("from pong")

stackless.tasklet(ping)()
stackless.tasklet(pong)()

# 我们需要发送一个消息来初始化这个游戏的状态
# 否则,两个微进程都会阻塞
stackless.tasklet(ping_channel.send)('startup')

stackless.run()

你可以运行这个程序要多久有多久,它都不会崩溃,且如果你检查其内存使用量(使用Windows的任务管理器或Linux的top命令),将会发现 使用量是恒定的。这个程序的协程版本,不管运行一分钟还是一天,使用的内存都是一样的。而如果你检查原先那个递归版本的内存用量,则会发现其迅速增长,直 到崩溃。

3.3 总结

是否还记得,先前我提到过,那个代码的递归版本,有经验的程序员会一眼看出毛病。但老实说,这里面并没有什么“计算机科学”方面的原因在阻碍它的正 常工作,有些让人坚信的东西,其实只是个与实现细节有关的小问题——只因为大多数传统编程语言都使用堆栈。某种意义上说,有经验的程序员都是被洗了脑,从 而相信这是个可以接受的问题。而stackless,则真正察觉了这个问题,并除掉了它。

4 轻量级线程

与当今的操作系统中内建的、和标准Python代码中所支持的普通线程相比,“微线程”要更为轻量级,正如其名称所暗示。它比传统线程占用更少的内存,并且微线程之间的切换,要比传统线程之间的切换更加节省资源。

为了准确说明微线程的效率究竟比传统线程高多少,我们用两者来写同一个程序。

4.1 hackysack模拟

Hackysack是一种游戏,就是一伙脏乎乎的小子围成一个圈,来回踢一个装满了豆粒的沙包,目标是不让这个沙包落地,当传球给别人的时候,可以耍各种把戏。踢沙包只可以用脚。

在我们的简易模拟中,我们假设一旦游戏开始,圈里人数就是恒定的,并且每个人都是如此厉害,以至于如果允许的话,这个游戏可以永远停不下来。

4.2 游戏的传统线程版本

import thread
import random
import sys
import Queue

class hackysacker:
    counter = 0
    def __init__(self,name,circle):
        self.name = name
        self.circle = circle
        circle.append(self)
        self.messageQueue = Queue.Queue()

        thread.start_new_thread(self.messageLoop,())

    def incrementCounter(self):
        hackysacker.counter += 1
        if hackysacker.counter >= turns:
            while self.circle:
                hs = self.circle.pop()
                if hs is not self:
                    hs.messageQueue.put('exit')
            sys.exit()

    def messageLoop(self):
        while 1:
            message = self.messageQueue.get()
            if message == "exit":
                debugPrint("%s is going home" % self.name)
                sys.exit()
            debugPrint("%s got hackeysack from %s" % (self.name, message.name))
            kickTo = self.circle[random.randint(0,len(self.circle)-1)]
            debugPrint("%s kicking hackeysack to %s" % (self.name, kickTo.name))
            self.incrementCounter()
            kickTo.messageQueue.put(self)

def debugPrint(x):
    if debug:
        print x

debug=1
hackysackers=5
turns = 5

def runit(hs=10,ts=10,dbg=1):
    global hackysackers,turns,debug
    hackysackers = hs
    turns = ts
    debug = dbg

    hackysacker.counter= 0
    circle = []
    one = hackysacker('1',circle)

    for i in range(hackysackers):
        hackysacker(`i`,circle)

    one.messageQueue.put(one)

    try:
        while circle:
            pass
    except:
        #有时我们在清理过程中会遇到诡异的错误。
        pass

if __name__ == "__main__":
    runit(dbg=1)

一个“玩者”类的初始化用到了其名字,和一个指向包含了所有玩者的全局列表 circle 的引用,还有一个继承自Python标准库中的Queue类的消息队列。

Queue这个类的作用,与stackless的通道类似。它包含 put() 和 get() 方法,在一个空的Queue上调用 put() 会阻塞,直到另一个线程调用 put() 将数据送入Queue中为止。Queue这个类被设计为能与操作系统级的线程高效合作。

__init__ 方法接下来使用Python标准库中的thread模块新建一个线程,并在新线程中开始了一个消息循环。此消息循环是个无限循环,不停地处理队列中的消息。如果其收到一个特殊的消息 ‘exit’ ,则结束这个线程。

如果收到了另一个消息——指定其收到了沙包,玩者则从圈中随机选取一个其他玩者,通过向其发送一条消息来指定,将沙包再踢给它。

由类成员变量 hackysacker.counter 进行计数,当沙包被踢够了指定的次数时,将会向圈中的所有玩者都发送一条特殊的 ‘exit’ 消息。

注意,当全局变量debug为非零的时候,还有个函数debugPrint可以输出信息。我们可以使这游戏输出到标准输出,但当计时的时候,这会影响精确度。

我们来运行这个程序,并检查其是否正常工作:

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:python24python.exe
hackysackthreaded.py

1 got hackeysack from 1
1 kicking hackeysack to 4
4 got hackeysack from 1
4 kicking hackeysack to 0
0 got hackeysack from 4
0 kicking hackeysack to 1
1 got hackeysack from 0
1 kicking hackeysack to 3
3 got hackeysack from 1
3 kicking hackeysack to 3
4 is going home
2 is going home
1 is going home
0 is going home
1 is going home

C:Documents and SettingsgrantDesktopwhy_stacklesscode>

如我们所见,所有玩者到了一起,并很快地进行了一场游戏。现在,我们对若干次实验运行过程进行计时。Python标准库中有一个 timeit.py 程序,可以用作此目的。那么,我们也同时关掉调试输出:

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:python24python.exe c:Python24libtimeit.py -s "import hackysackthreaded" hackysackthreaded.runit(10,1000,0)
10 loops, best of 3: 183 msec per loop

在我的机器上,十个玩者共进行1000次传球,共使用了183毫秒。我们来增加玩者的数量:

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:python24python.exe c:Python24libtimeit.py -s "import hackeysackthreaded" hackeysackthreaded.runit(100,1000,0)
10 loops, best of 3: 231 msec per loop

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:python24python.exe c:Python24libtimeit.py -s "import hackysackthreaded" hackysackthreaded.runit(1000,1000,0)
10 loops, best of 3: 681 msec per loop

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:python24python.exe c:Python24libtimeit.py -s "import hackysackthreaded" hackysackthreaded.runit(10000,1000,0)
Traceback (most recent call last):
  File "c:Python24libtimeit.py", line 255, in main
    x = t.timeit(number)
  File "c:Python24libtimeit.py", line 161, in timeit
    timing = self.inner(it, self.timer)
  File "<timeit-src>", line 6, in inner
  File ".hackeysackthreaded.py", line 58, in runit
    hackysacker(`i`,circle)
  File ".hackeysackthreaded.py", line 14, in __init__
    thread.start_new_thread(self.messageLoop,())
error: can't start new thread

在我的3GHz、1G内存的机器上,当尝试10,000个线程的时候出现了错误。就不想拿出这详细的输出内容来扰人了,只是通过若干实验与出错过程 得出,在我机器上,此程序从1100个线程左右开始出错。另请注意,1000个线程时候所耗用的时间,是10个线程时候的大约三倍。

4.3 stackless

import stackless
import random
import sys

class hackysacker:
    counter = 0
    def __init__(self,name,circle):
        self.name = name
        self.circle = circle
        circle.append(self)
        self.channel = stackless.channel()

        stackless.tasklet(self.messageLoop)()

    def incrementCounter(self):
        hackysacker.counter += 1
        if hackysacker.counter >= turns:
            while self.circle:
                self.circle.pop().channel.send('exit')

    def messageLoop(self):
        while 1:
            message = self.channel.receive()
            if message == 'exit':
                return
            debugPrint("%s got hackeysack from %s" % (self.name, message.name))
            kickTo = self.circle[random.randint(0,len(self.circle)-1)]
            while kickTo is self:
                kickTo = self.circle[random.randint(0,len(self.circle)-1)]
            debugPrint("%s kicking hackeysack to %s" % (self.name, kickTo.name))
            self.incrementCounter()
            kickTo.channel.send(self)

def debugPrint(x):
    if debug:print x

debug = 5
hackysackers = 5
turns = 1

def runit(hs=5,ts=5,dbg=1):
    global hackysackers,turns,debug
    hackysackers = hs
    turns = ts
    debug = dbg

    hackysacker.counter = 0
    circle = []
    one = hackysacker('1',circle)

    for i in range(hackysackers):
        hackysacker(`i`,circle)

    one.channel.send(one)

    try:
        stackless.run()
    except TaskletExit:
        pass

if __name__ == "__main__":
    runit()

以上代码实质上与线程版本是等价的,主要区别仅在于我们使用微进程来代替线程,并且使用通道代替Queue来进行切换。让我们运行它,并检查输出:

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:Python24python.exe hackysackstackless.py
1 got hackeysack from 1
1 kicking hackeysack to 1
1 got hackeysack from 1
1 kicking hackeysack to 4
4 got hackeysack from 1
4 kicking hackeysack to 1
1 got hackeysack from 4
1 kicking hackeysack to 4
4 got hackeysack from 1
4 kicking hackeysack to 0

工作情况确如所料。现在来计时:

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:Python24python.exe c:Python24libtimeit.py -s"import hackysackstackless" hackysackstackless.runit(10,1000,0)
100 loops, best of 3: 19.7 msec per loop

其仅用了19.7毫秒,速度几乎是线程版本的10倍。现在我们同样开始增加微线程的数量:

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:Python24python.exe c:Python24libtimeit.py -s"import hackysackstackless" hackysackstackless.runit(100,1000,0)
100 loops, best of 3: 19.7 msec per loop

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:Python24python.exe c:Python24libtimeit.py -s"import hackysackstackless" hackysackstackless.runit(1000,1000,0)
10 loops, best of 3: 26.9 msec per loop

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:Python24python.exe c:Python24libtimeit.py -s"import hackysackstackless" hackysackstackless.runit(10000,1000,0)
10 loops, best of 3: 109 msec per loop

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:Python24python.exe c:Python24libtimeit.py -s"import hackysackstackless" hackysackstackless.runit(100000,1000,0)
10 loops, best of 3: 1.07 sec per loop

甚至直到10,000个线程的时候,那时线程版本早已不能运行了,而这个仍然可以比线程版本在10个线程的时候运行的还快。

这里我在尽量保持代码的简洁,因此你可以相信我的话:计时时间的增长仅仅在于初始化游戏圈子的部分,而真正进行游戏的时间则是一直不变的,不管使用 10个微线程,还是10,000个。这归因于通道的工作方式:当它们收到消息的时候,是立即进行阻塞和恢复操作的。另一方面,各个操作系统线程则是轮番检 查自己的队列里是否有了东西,这意味着,跑着越多的线程,性能就变得越差。

4.4 总结

但愿我已经成功地演示了,微线程的运行至少比操作系统线程快一个数量级,并具备远高于后者的可伸缩性。关于操作系统线程的一般常识是:(1)尽量不要使用它,(2)如果非用不可,就能少用一点就少用一点。而stackless的微线程则使我们从这些限制中解放出来。

5 数据流

5.1 工厂

假设,我们要写程序来模拟一个生产玩具娃娃的工厂,具有如下的需求:

  • 一个仓库,装有用来塑造的塑料球。
  • 一个仓库,装有用来连接部件的铆钉。
  • 一台注塑机,可以在6秒内,用0.2磅塑料球来制造一双手臂。
  • 一台注塑机,可以在5秒内,用0.2磅塑料球来制造一双腿。
  • 一台注塑机,可以在4秒内,用0.1磅塑料球来制造一个头部。
  • 一台注塑机,可以在10秒内,用0.5磅塑料球来制造一个躯干。
  • 一个装配台,可以在2秒内,将一个现成的躯干和一双现成的腿,用一个铆钉装配在一起。
  • 一个装配台,可以在2秒内,将上面的半成品和一双现成的手臂,用一个铆钉装配在一起。
  • 一个装配台,可以在3秒内,将上面的半成品和一个现成的头部,用一个铆钉装配在一起。
  • 每台设备都一直不停地工作下去。

5.2 “普通”版本

如果不用stackless而用“普通”的方法来写这个,将会是很痛苦的事情。当我们经历了这个“普通”版示例之后,会再用stackless来做 一个,并比较两者的代码。如果你认为这个例子太不自然,并且有时间的话,可以稍为休息后,根据上面的需求,自己来做一个工厂的实现,再来将你写出的代码和 stackless版本做个比较。

代码如下:

class storeroom:
    def __init__(self,name,product,unit,count):
        self.product = product
        self.unit = unit
        self.count = count
        self.name = name

    def get(self,count):
        if count > self.count:
            raise RuntimeError("Not enough %s" % self.product)
        else:
            self.count -= count

        return count

    def put(self,count):
        self.count += count

    def run(self):
        pass

rivetStoreroom = storeroom("rivetStoreroom","rivets","#",1000)
plasticStoreroom = storeroom("plastic Storeroom","plastic pellets","lb",100)

class injectionMolder:
    def __init__(self,name,partName,plasticSource,plasticPerPart,timeToMold):
        self.partName = partName
        self.plasticSource = plasticSource
        self.plasticPerPart = plasticPerPart
        self.timeToMold = timeToMold
    self.items = 0
    self.plastic = 0
    self.time = -1
    self.name = name

def get(self,items):
    if items > self.items:
        return 0
    else:
        self.items -= items
        return items

def run(self):
    if self.time == 0:
        self.items += 1
        print "%s finished making part" % self.name
        self.time -= 1
    elif self.time < 0:
        print "%s starts making new part %s" % (self.name,self.partName)
        if self.plastic < self.plasticPerPart:
            print "%s getting more plastic"
            self.plastic += self.plasticSource.get(self.plasticPerPart * 10)
        self.time = self.timeToMold
    else:
        print "%s molding for %s more seconds" % (self.partName, self.time)
        self.time -= 1

armMolder = injectionMolder("arm Molder", "arms",plasticStoreroom,0.2,6)
legMolder = injectionMolder("leg Molder", "leg",plasticStoreroom,0.2,5)
headMolder = injectionMolder("head Molder","head",plasticStoreroom,0.1,4)
torsoMolder = injectionMolder("torso Molder","torso",plasticStoreroom,0.5,10)

class assembler:
    def __init__(self,name,partAsource,partBsource,rivetSource,timeToAssemble):
        self.partAsource = partAsource
        self.partBsource = partBsource
        self.rivetSource = rivetSource
        self.timeToAssemble = timeToAssemble
        self.itemA = 0
        self.itemB = 0
        self.items = 0
        self.rivets = 0
        self.time = -1
        self.name = name

    def get(self,items):
        if items > self.items:
            return 0
        else:
            self.items -= items
            return items

    def run(self):
        if self.time == 0:
            self.items += 1
            print "%s finished assembling part" % self.name
            self.time -= 1
        elif self.time < 0:
            print "%s starts assembling new part" % self.name
            if self.itemA < 1:
                print "%s Getting item A" % self.name
                self.itemA += self.partAsource.get(1)
                if self.itemA < 1:
                    print "%s waiting for item A" % self.name
            elif self.itemB < 1:
                print "%s Getting item B" % self.name
                self.itemB += self.partBsource.get(1)
                if self.itemB < 1:
                    print "%s waiting for item B" % self.name
            print "%s starting to assemble" % self.name
            self.time = self.timeToAssemble
        else:
            print "%s assembling for %s more seconds" % (self.name, self.time)
            self.time -= 1

legAssembler = assembler("leg Assembler",torsoMolder,legMolder,rivetStoreroom,2)
armAssembler = assembler("arm Assembler", armMolder,legAssembler,rivetStoreroom,2)
torsoAssembler = assembler("torso Assembler", headMolder,armAssembler,
                            rivetStoreroom,3)

components = [rivetStoreroom, plasticStoreroom, armMolder,
                legMolder, headMolder, torsoMolder,
              legAssembler, armAssembler, torsoAssembler]

def run():
    while 1:
        for component in components:
            component.run()
        raw_input("Press <ENTER> to continue...")
        print "nnn"

if __name__ == "__main__":
    run()

5.2.1 分析

我们从一个代表仓库的类开始,它的初始化需要一个其所储存的产品的名称、一个衡量单位(如磅,或部件数目)和一个初始存量作为参数。还有一个 run 方法什么也不做,其用途将会在稍后了解。基于这个类,我们建立了两个仓库示例。

接下来是一个注塑机类,它的初始化需要其产品的名称、一个作为塑料来源的仓库、制造一个部件所需要的原料量,和制造一个部件所需的时间作为参数。有 一个 get() 方法,在其内部已有完成的产品时,可将其取出,并调整内部记录。对于这个类, run() 方法是确实做了些事情的:

  • 在计时器大于0期间,塑造过程持续进行,并递减计时器。
  • 当塑造剩余时间达到0,则一个产品被建立,并把计时器设为-1。
  • 当计时器为-1时,注塑机检测是否还有足够的塑料来塑造下一个产品,如果有,则取来原料,并开始塑造。

用这个类,我们建立了四个注塑机实例。

再接下来是一个装配台类,它的初始化需要其产品的名字、部件1的来源、部件2的来源、一个铆钉的仓库,以及装配这些部件所需的时间作为参数。也有一个 get() 方法,在其内部已有完成的产品时,可将其取出,并调整内部记录。而这个类的 run() 方法是这样的:

  • 若计时器大于0,则已经具备原材料的装配台继续其装配过程。
  • 如果计时器等于0,则一个产品被完成,内部记录随之被调整。
  • 如果计时器小于0,则装配台试图取得新的各个部件,并再次开始装配。若其中某个部件还没有来得及塑造出来,则必须等待。

为了装配腿、手臂和头部,各有一个装配台实例被建立。

Note

你会注意到,仓库、注塑机和装配台类有很多相似之处。如果我是在写一个真正生产系统,则很可能先建立一个基类,并使用继承。但在这里,我觉得做出这种类层次关系的话只会使代码变得繁杂,所以有意保持了其简单。

由以上三个类所建立的所有实例,都被装进一个称为 components 的“设备”数组中。然后,我们建立一个事件循环,重复地调用每个设备的 run() 方法。

5.3 走进数据流

如果你熟悉 Unix 系统,那么不管你知不知道数据流技术,恐怕你都已经在使用它了。看下面的 shell 命令:

cat README | more

为了公平,也举出 Windows 中对应的:

type readme.txt | more

尽管,在 Windows 的世界中,数据流技术并不像在 Unix 世界中那么普遍深入。

顺便对还不熟悉 more 工具的读者:这个程序从一个外部来源接收输入,显示一页的内容后暂停,直到用户按下任意键,再显示下一页。这个“|”操作符获取一个程序的输出,并用管道 将其传送到另一个命令的输入。这样,不管 cat 还是 type ,都是将文档内容传送到标准输出,而 more 则接收这些输出。

这样,more 程序仅仅是坐在那里,等着来自另一个程序的数据来流向自己。只要流进的数据足够一定量,就在屏幕上显示一页并暂停;而用户击键时,more 则让后面的数据再流入,并开始再一次等待数据量足够,再显示,再暂停。这便是术语“数据流”。

使用通道,再使用stackless本身的轮转调度器,我们就可以使用数据流技术来写这个工厂的模拟。

5.4 代码的stackless版本

import stackless

#
# “休眠” 辅助函数
#

sleepingTasklets = []
sleepingTicks = 0

def Sleep(secondsToWait):
    channel = stackless.channel()
    endTime = sleepingTicks + secondsToWait
    sleepingTasklets.append((endTime, channel))
    sleepingTasklets.sort()
    # 阻塞,直到收到一个唤醒通知。
    channel.receive()

def ManageSleepingTasklets():
    global sleepingTicks
    while 1:
        if len(sleepingTasklets):
            endTime = sleepingTasklets[0][0]
            while endTime <= sleepingTicks:
                channel = sleepingTasklets[0][1]
                del sleepingTasklets[0]
                # 我们需要发送一些东西,但发什么无所谓,
                # 因为其内容是没用的。
                channel.send(None)
                endTime = sleepingTasklets[0][0] # 检查下一个
        sleepingTicks += 1
        print "1 second passed"
        stackless.schedule()

stackless.tasklet(ManageSleepingTasklets)()

#
# 工厂的实现
#

class storeroom:
    def __init__(self,name,product,unit,count):
        self.product = product
        self.unit = unit
        self.count = count
        self.name = name

    def get(self,count):
        while count > self.count: #重新调度,直到有了足够的原料
            print "%s doesn't have enough %s to deliver yet" % (self.name,
                                                                self.product)
            stackless.schedule()
        self.count -= count
        return count

        return count

    def put(self,count):
        self.count += count

    def run(self):
        pass

rivetStoreroom = storeroom("rivetStoreroom","rivets","#",1000)
plasticStoreroom = storeroom("plastic Storeroom","plastic pellets","lb",100)

class injectionMolder:
    def __init__(self,name,partName,plasticSource,plasticPerPart,timeToMold):
        self.partName = partName
        self.plasticSource = plasticSource
        self.plasticPerPart = plasticPerPart
        self.timeToMold = timeToMold
        self.plastic = 0
        self.items = 0
        self.name = name
        stackless.tasklet(self.run)()

    def get(self,items):
        while items > self.items: #重新调度,直到有了足够的产品
            print "%s doesn't have enough %s to deliver yet" % (self.name,
                                                                self.partName)
            stackless.schedule()
        self.items -= items
        return items

    def run(self):
        while 1:
            print "%s starts making new part %s" % (self.name,self.partName)
            if self.plastic < self.plasticPerPart:
                print "%s getting more plastic"
                self.plastic += self.plasticSource.get(self.plasticPerPart * 10)
            self.plastic -= self.plasticPerPart
            Sleep(self.timeToMold)
            print "%s done molding after %s seconds" % (self.partName,
                                                        self.timeToMold)
            self.items += 1
            print "%s finished making part" % self.name
            stackless.schedule()

armMolder = injectionMolder("arm Molder", "arms",plasticStoreroom,0.2,5)
legMolder = injectionMolder("leg Molder", "leg",plasticStoreroom,0.2,5)
headMolder = injectionMolder("head Molder","head",plasticStoreroom,0.1,5)
torsoMolder = injectionMolder("torso Molder","torso",plasticStoreroom,0.5,10)

class assembler:
    def __init__(self,name,partAsource,partBsource,rivetSource,timeToAssemble):
        self.partAsource = partAsource
        self.partBsource = partBsource
        self.rivetSource = rivetSource
        self.timeToAssemble = timeToAssemble
        self.itemA = 0
        self.itemB = 0
        self.items = 0
        self.rivets = 0
        self.name = name
        stackless.tasklet(self.run)()

    def get(self,items):
        while items > self.items: #重新调度,直到有了足够的产品
            print "Don't have a %s to deliver yet" % (self.name)
            stackless.schedule()
        self.items -= items
        return items

    def run(self):
        while 1:
            print "%s starts assembling new part" % self.name
            self.itemA += self.partAsource.get(1)
            self.itemB += self.partBsource.get(1)
            print "%s starting to assemble" % self.name
            Sleep(self.timeToAssemble)
            print "%s done assembling after %s" % (self.name, self.timeToAssemble)
            self.items += 1
            print "%s finished assembling part" % self.name
            stackless.schedule()

legAssembler = assembler("leg Assembler",torsoMolder,legMolder,rivetStoreroom,2)
armAssembler = assembler("arm Assembler", armMolder,legAssembler,rivetStoreroom,2)
torsoAssembler = assembler("torso Assembler", headMolder,armAssembler,
                            rivetStoreroom,3)

def pause():
    while 1:
        raw_input("Press <ENTER> to continue...")
        print "nnn"
        stackless.schedule()

stackless.tasklet(pause)()

def run():
    stackless.run()

if __name__ == "__main__":
    run()

5.4.1 分析

5.4.1.1 休眠功能

首先我们建立了一些辅助函数,好让我们的类可以进行“休眠”。一个微进程调用 Sleep() ,则先建立一个通道,再计算出将被唤醒的时间,并将这个时间信息添加到全局数组 sleepingTasklets 中。之后,将调用 channel.receive() ,这将使该微进程暂停运行,直到被再次唤醒。

接着我们建立另一个函数,来管理所有休眠的微进程。它检查全局数组 sleepingTasklets ,找出所有需要立即被唤醒的成员,并通过其通道来将其唤醒。这个函数也被添加到了微进程调度器中。

5.4.1.2 类

这些类与“普通”版本中的类相似,但也有一些显著不同:首先,在实例化的时候,他们的 run() 方法创建了微进程,这样我们不再需要手工建立一个设备数组,和一个外部的 run() 函数来处理事件循环,stackless本身就隐式地做了这些工作。其次的不同是,微进程可以通过休眠来等待一个产品被产出,而不用通过计数器来计时。第 三个不同,则是对 get() 的调用变得更自然了,如果某种原材料没有准备好,则这个微进程简单地重新进入调度循环,直到有了原材料。

5.5 那我们获得了什么?

OK,两个版本的程序都能运行,并得到同样的结果,那这里究竟有什么大不了的事情?——让我们查看一下普通版本的工厂的 run 方法:

def run(self):
    if self.time == 0:
        self.items += 1
        print "%s finished assembling part" % self.name
        self.time -= 1
    elif self.time < 0:
        print "%s starts assembling new part" % self.name
        if self.itemA < 1:
            print "%s Getting item A" % self.name
            self.itemA += self.partAsource.get(1)
            if self.itemA < 1:
                print "%s waiting for item A" % self.name
        elif self.itemB < 1:
            print "%s Getting item B" % self.name
            self.itemB += self.partBsource.get(1)
            if self.itemB < 1:
                print "%s waiting for item B" % self.name
        print "%s starting to assemble" % self.name
        self.time = self.timeToAssemble
    else:
        print "%s assembling for %s more seconds" % (self.name, self.time)
        self.time -= 1

再看 stackless 的版本:

def run(self):
    while 1:
        print "%s starts assembling new part" % self.name
        self.itemA += self.partAsource.get(1)
        self.itemB += self.partBsource.get(1)
        print "%s starting to assemble" % self.name
        Sleep(self.timeToAssemble)
        print "%s done assembling after %s" % (self.name, self.timeToAssemble)
        self.items += 1
        print "%s finished assembling part" % self.name
        stackless.schedule()

Stackless 的版本比普通的版本更加简单、清晰和直观,它不需要将事件循环的基础结构包装进 run 方法中,这个结构已经和 run() 方法解除了耦合。run() 方法仅仅描述了自己要做什么,而不需要关心具体究竟怎么做的。这就使软件开发者能集中精力于工厂的运作,而不是事件循环以及程序本身的运作。

5.6 推(push)数据

Note

本节的完整程序保存为 digitalCircuit.py ,在本文的末尾,和代码.zip文件中和都有。

在工厂的例子中,我们是在“拉”数据:每个部分都去请求其所需要的部件,并一直等待那些部件到来。我们也可以来“推”数据,这样,系统中的每个部分都将自身的变化向下传播到另一个部分。“拉”的方式,称为“ 懒惰数据流 ”,而“推”的方式则称为“ 急切数据流 ”。

为了演示“推”的方式,我们来建立一个数字电路的模拟器。这个模拟器由各种元件组成,元件具有0或1的状态,并可以各种方式互相连接起来。这里我们使用面向对象的方法,并定义一个 EventHandler 基类来实现其大部分功能:

class EventHandler:
    def __init__(self,*outputs):
        if outputs==None:
            self.outputs=[]
        else:
            self.outputs=list(outputs)

        self.channel = stackless.channel()
        stackless.tasklet(self.listen)()

    def listen(self):
        while 1:
            val = self.channel.receive()
            self.processMessage(val)
            for output in self.outputs:
                self.notify(output)

    def processMessage(self,val):
        pass

    def notify(self,output):
        pass

    def registerOutput(self,output):
        self.outputs.append(output)

    def __call__(self,val):
    self.channel.send(val)

EventHandler 类的核心功能,是做以下三件事:

  • 通过 listen 方法,持续地监听一个通道上传来的消息。
  • 之后,通过 processMessage 方法,处理所有收到的消息。
  • 最后,通过 notify 方法,将处理结果通知到所有注册的输出端。

还有两个附加的辅助方法:

  • registerOutput 用来在实例建立之后,再注册额外的输出端。
  • __call__ 被重载,作为一种便利,使我们可以以这种格式来发送消息:
    event(1)

从而无需这样:

event.channel.send(1)

使用 EventHandler 类作为基本构建单元,我们可以开始实现这个数字电路模拟器,由一个开关开始。下面描述的是一个可由用户控制的开关,可以向其发送0或1的值:

class Switch(EventHandler):
    def __init__(self,initialState=0,*outputs):
        EventHandler.__init__(self,*outputs)
        self.state = initialState

    def processMessage(self,val):
        debugPrint("Setting input to %s" % val)
        self.state = val

    def notify(self,output):
        output((self,self.state))

初始化之后,这个开关就保存着其初始的状态,而 processMessage 则被重载,用来将收到的消息保存起来,成为新的当前状态。其 notify 方法则被重载为发送一个元组,其中含有指向实例自身的引用,还有其状态。我们不久后会看到,我们需要顺便发送这个自身的引用,这样,那些具有多个输入端的 元件则可以判别,消息来自于哪个来源。

Note

若你正在随着我们的进度来调试代码,则别忘了我们还在使用 debugPrint() 函数来提供诊断信息,它最初是在“轻量级线程”这一节中定义的。

接下来我们要建立的,是“指示器”类,这个类的实例的作用仅仅是将其当前状态输出。我想我们可以认为,其相当于真正的数字电路中的发光二极管:

class Reporter(EventHandler):
    def __init__(self,msg="%(sender)s send message %(value)s"):
        EventHandler.__init__(self)
        self.msg = msg

    def processMessage(self,msg):
        sender,value=msg
        print self.msg % {'sender':sender,'value':value}

其初始化接受一个可选的格式字符串,来指定之后输出的样式。代码的其他部分意义自明。

现在我们有了一个足够好的框架,来测试这些最初的功能:

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:Python24python.ex
e
Python 2.4.3 Stackless 3.1b3 060516 (#69, May  3 2006, 11:46:11) [MSC v.1310 32
bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import stackless
>>> from digitalCircuit import *
>>>
>>> reporter = Reporter()
>>> switch = Switch(0,reporter) #创建一个开关,并连接到一个指示器做输出。
>>>
>>> switch(1)
<digitalCircuit.Switch instance at 0x00A46828> send message 1
>>>
>>> switch(0)
<digitalCircuit.Switch instance at 0x00A46828> send message 0
>>>
>>> switch(1)
<digitalCircuit.Switch instance at 0x00A46828> send message 1
>>>

与先前设计的工厂不同,对开关的操作会使结果立即被推至其输出端,并显示出来。

现在我们来建立一些数字逻辑部件,首先是反相器,它接受一个输入,并将其逻辑相反的值推出,就是说输入0会输出1,输入1会输出0:

class Inverter(EventHandler):
    def __init__(self,input,*outputs):
        EventHandler.__init__(self,*outputs)
        self.input = input
        input.registerOutput(self)
        self.state = 0

    def processMessage(self,msg):
        sender,value = msg
        debugPrint("Inverter received %s from %s" % (value,msg))
        if value:
            self.state = 0
        else:
            self.state = 1

反相器的初始化参数为一个输入端,即另外某个 EventHandler ,将它保存下来,并将自身注册为它的一个输出端。而 processMessage() 方法,则将自身的状态设为收到的消息的逻辑相反值。与 Switch 类类似,反相器类的 notify 事件也发送一个由其自身和其状态所组成的元组。

我们可以修改上面的例子,在开关和指示器之间串联入一个反相器。如有兴趣,尽可一试,但这个过程我认为已经没有必要列出了。

接下来是一个与门,这是我们遇到的第一个有多个输入端的类。它有两个输入端,如果都被置为1,则送出消息1,否则送出消息0:

class AndGate(EventHandler):
    def __init__(self,inputA,inputB,*outputs):
        EventHandler.__init__(self,*outputs)

        self.inputA = inputA
        self.inputAstate = inputA.state
        inputA.registerOutput(self)

        self.inputB = inputB
        self.inputBstate = inputB.state
        inputB.registerOutput(self)

        self.state = 0

    def processMessage(self,msg):
        sender, value = msg
        debugPrint("AndGate received %s from %s" % (value,sender))

        if sender is self.inputA:
            self.inputAstate = value
        elif sender is self.inputB:
            self.inputBstate = value
        else:
            raise RuntimeError("Didn't expect message from %s" % sender)

        if self.inputAstate and self.inputBstate:
            self.state = 1
        else:
            self.state = 0
        debugPrint("AndGate's new state => %s" % self.state)

    def notify(self,output):
        output((self,self.state))

在与门的 processMessage 方法中,我们需要判定,是哪个输入端送来了消息,并据此设置状态。这就是为什么别的部件送来的消息中需要含有其自身的引用。

最后我们做出或门。它和与门类似,只是,它只要有任一个输入端为1的时候就送出消息1,只有两个输入端都为0的时候才送出0:

class OrGate(EventHandler):
    def __init__(self,inputA,inputB,*outputs):
        EventHandler.__init__(self,*outputs)

        self.inputA = inputA
        self.inputAstate = inputA.state
        inputA.registerOutput(self)

        self.inputB = inputB
        self.inputBstate = inputB.state
        inputB.registerOutput(self)

        self.state = 0

    def processMessage(self,msg):
        sender, value = msg
        debugPrint("OrGate received %s from %s" % (value,sender))

        if sender is self.inputA:
            self.inputAstate = value
        elif sender is self.inputB:
            self.inputBstate = value
        else:
            raise RuntimeError("Didn't expect message from %s" % sender)

        if self.inputAstate or self.inputBstate:
            self.state = 1
        else:
            self.state = 0
        debugPrint("OrGate's new state => %s" % self.state)

    def notify(self,output):
        output((self,self.state))

5.6.1 半加器

作为结束,我们将使用我们已经建立的所有部件,来构建一个半加器。半加器实现两个比特的加法。我们将一些部件连接了起来,然后来“拨动”开关。开关的动作改变了其状态,并且把其变化,以数据流的方式,通过系统传播了下去:

if __name__ == "__main__":
    # 半加器
    inputA = Switch()
    inputB = Switch()
    result = Reporter("Result = %(value)s")
    carry = Reporter("Carry = %(value)s")
    andGateA = AndGate(inputA,inputB,carry)
    orGate = OrGate(inputA,inputB)
    inverter = Inverter(andGateA)
    andGateB = AndGate(orGate,inverter,result)
    inputA(1)
    inputB(1)
    inputB(0)
    inputA(0)

6 角色

在角色的模型里面,一切都是角色(废话!)。角色就是一个对象(一般意义上的对象,而不必是面向对象中的意义),它可以:

  • 从其他角色接收消息。
  • 对收到的消息中适合于自己的,进行处理。
  • 向其它角色发送消息。
  • 创建新的角色。

一个角色对其它的角色并不具有直接的访问渠道,所有的交流都通过消息传递来完成。这就提供了丰富的模型,来模拟现实世界中的对象——它们是彼此松散耦合的,并对彼此的内部所知有限。

如果我们要建立一个模拟过程的话,就来模拟一下……

6.1 杀手机器人

Note

本节的完整程序保存为actors.py,在本文的末尾,和代码zip文件中都有。

6.1.1 角色基类

在这个例子中,我们将配置出一个小小的世界,在其中有一些使用角色模型来移动和战斗的机器人。作为开始,我们来定义所有角色的基类:

class actor:
    def __init__(self):
        self.channel = stackless.channel()
        self.processMessageMethod = self.defaultMessageAction
        stackless.tasklet(self.processMessage)()

    def processMessage(self):
        while 1:
            self.processMessageMethod(self.channel.receive())

    def defaultMessageAction(self,args):
        print args

默认情况下,角色建立一个通道来接收消息,指定一个方法来处理这些消息,并启动一个循环来将接收的消息分派给处理方法。默认的处理过程只是把收到的消息显示出来。这些,已经是我们实现角色模型所需要的全部。

6.1.2 消息的格式

所有发送的消息都遵从一个格式:先是发送者的通道,接着一个字符串为消息的名称,再接下来是可选的参数。例如:

(self.channel, "JOIN", (1,1) )
(self.channel, "COLLISION")
等等……

注意,我们只将发送者的通道随消息送出,而不是整个发送者对象。在角色模型中,角色间的所有交流都必须通过消息传递来体现,如果将 self 都发送出去的话,则使得对方可以很容易地用非正常手段对发送者的内部未知信息进行访问。

事实上你会注意到,当我们将本节的大部分角色实例化的时候,甚至不需要将其赋值给能被别的角色访问到的变量。我们仅仅创建它们,并让它们独自漂浮在那里,对周围环境只有有限的了解。

6.1.3 世界(world)类

“世界”角色,扮演着其他所有角色相互作用的中央枢纽。其他角色发送 JOIN 消息给世界角色,后者则跟踪它们。周期性地,世界角色发出 WORLD_STATE 消息,其中包括关于所有可见的角色的信息,来供它们内部处理:

class world(actor):
    def __init__(self):
        actor.__init__(self)
        self.registeredActors = {}
        stackless.tasklet(self.sendStateToActors)()

    def testForCollision(self,x,y):
        if x < 0 or x > 496:
            return 1
        elif y < 0 or y > 496:
            return 1
        else:
            return 0

    def sendStateToActors(self):
        while 1:
            for actor in self.registeredActors.keys():
                actorInfo = self.registeredActors[actor]
                if self.registeredActors[actor][1] != (-1,-1):
                    VectorX,VectorY = (math.sin(math.radians(actorInfo[2])) * actorInfo[3],
                                       math.cos(math.radians(actorInfo[2])) * actorInfo[3])
                    x,y = actorInfo[1]
                    x += VectorX
                    y -= VectorY
                    if self.testForCollision(x,y):
                        actor.send((self.channel,"COLLISION"))
                    else:
                        self.registeredActors[actor] = tuple([actorInfo[0],
                                                          (x,y),
                                                              actorInfo[2],
                                                              actorInfo[3]])
            worldState = [self.channel, "WORLD_STATE"]
            for actor in self.registeredActors.keys():
                if self.registeredActors[actor][1] != (-1,-1):
                    worldState.append( (actor, self.registeredActors[actor]))
            message = tuple(worldState)
            for actor in self.registeredActors.keys():
                actor.send(message)
            stackless.schedule()

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "JOIN":
            print 'ADDING ' , msgArgs
            self.registeredActors[sentFrom] = msgArgs
        elif msg == "UPDATE_VECTOR":
            self.registeredActors[sentFrom] = tuple([self.registeredActors[sentFrom][0],
                                                     self.registeredActors[sentFrom][1],
                                                     msgArgs[0],msgArgs[1]])
        else:
            print '!!!! WORLD GOT UNKNOWN MESSAGE ' , args

World = world().channel

除了处理消息的微进程外,“世界”角色还建立了另一个独立的微进程,来执行 sendStateToActors() 方法。这个方法里有个循环,用于构建关于世界状态的信息,并发送给所有的角色。这是其它角色唯一可以指望接收到的消息。若有必要,它们可以回应这个消息, 即将某种 UPDATE 消息发回给世界。

作为 sendStateToActors() 方法的一部分,“世界”角色需要更新其内部的、对可移动的角色的位置的记录。它使用可移动角色的角度和速度来建立一个矢量,确保更新后的位置不会撞到世界的四面边界,并存下其新的位置。

defaultMessageAction() 方法处理以下已知信息,并忽略其他的:

JOIN
将一个角色添加到世界中的已知角色列表,其参数包括新角色的位置、角度和速度。位置-1, -1表示这个角色对其它角色不可见,比如后面将要详述的显示屏角色。
UPDATE_VECTOR
为发送这个消息的角色设置新的角度和速度。

最后,一个“世界”角色被实例化,其通道被保存进全局变量 World 中,使其它角色可以发送它们最初的 JOIN 消息。

6.1.4 一个简单机器人

我们将以一个简单的机器人开始,它以恒定的速度移动,每接到一个 WORLD_STATE 消息的时候,都顺时针旋转1度作为响应。当发生与世界边界碰撞的 COLLISION 事件时,它将旋转73度再尝试前进。所有其他消息将被忽略。

class basicRobot(actor):
    def __init__(self,location=(0,0),angle=135,velocity=1,world=World):
        actor.__init__(self)
        self.location = location
        self.angle = angle
        self.velocity = velocity
        self.world = world

        joinMsg =(self.channel,"JOIN",self.__class__.__name__,
                  self.location,self.angle,self.velocity)
        self.world.send(joinMsg)

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            self.location = (self.location[0] + 1, self.location[1] + 1)
            self.angle += 1
            if self.angle >= 360:
                self.angle -= 360

            updateMsg = (self.channel, "UPDATE_VECTOR",
                         self.angle,self.velocity)
            self.world.send(updateMsg)
        elif msg == "COLLISION":
            self.angle += 73
            if self.angle >= 360:
                self.angle -= 360
        else:
            print "UNKNOWN MESSAGE", args

basicRobot(angle=135,velocity=5)
basicRobot((464,0),angle=225,velocity=10)

stackless.run()

注意,机器人的构造方法发出了一个 JOIN 消息到“世界”对象,来注册自己。除此之外,代码应该还算易懂的。

6.1.5 蹊跷(Detour)的PyGame

至此,在示例程序中,我们都是使用调试输出语句来显示事情进行的过程。我试图以这种方式来保持代码的简单易懂,但有些时候,输出语句却变得不再直观,而是越发迷惑。在“数据流”一节中我们已经用得很勉强了,而在本节中,情况已经变得复杂到无法再尝试用打印输出来表示了。

Note

要运行本节的代码,需要安装 pyGame 的当前版本,可以从这里取得:http://www.pygame.org/

我决定使用 pyGame 来创建一个简单的可视化引擎。尽管对于 pyGame 内容的叙述已经超出了本教程的范围,但其操作本身还是相对简明的。当显示屏角色收到一个 WORLD_STATE 消息,就将相应的角色放置上去,并更新显示。很幸运,我们可以将所有的 pyGame 代码隔离在一个角色之内,因此代码的其它部分可以保持不被“污染”,依然可以被理解,哪怕不了解也不关心 pyGame 怎么进行的显示渲染:

class display(actor):
    def __init__(self,world=World):
        actor.__init__(self)

        self.world = World

        self.icons = {}
        pygame.init()

        window = pygame.display.set_mode((496,496))
        pygame.display.set_caption("Actor Demo")

        joinMsg = (self.channel,"JOIN",self.__class__.__name__, (-1,-1))
        self.world.send(joinMsg)

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            self.updateDisplay(msgArgs)
        else:
            print "UNKNOWN MESSAGE", args

    def getIcon(self, iconName):
        if self.icons.has_key(iconName):
            return self.icons[iconName]
        else:
            iconFile = os.path.join("data","%s.bmp" % iconName)
            surface = pygame.image.load(iconFile)
            surface.set_colorkey((0xf3,0x0a,0x0a))
            self.icons[iconName] = surface
            return surface

    def updateDisplay(self,actors):

        for event in pygame.event.get():
            if event.type == pygame.QUIT: sys.exit()

        screen = pygame.display.get_surface()

        background = pygame.Surface(screen.get_size())
        background = background.convert()
        background.fill((200, 200, 200))

        screen.blit(background, (0,0))
        for item in actors:
            screen.blit(pygame.transform.rotate(self.getIcon(item[1][0]),-item[1][2]), item[1][1])
        pygame.display.flip()

display()

这里获取了 WORLD_STATE ,并基于此创建了显示屏。

Note

为了使本节的示例得以运行,需要在Python下安装 pyGame 。你也可能需要下载我所建立的、可选的图标,并将其解压到你的代码目录中。

6.1.6 第一轮代码

现在我们做了足够的准备,来运行程序的第一个版本。运行后,两个简单机器人将会开始迂回移动,并在边界反弹。

6.2 又一蹊跷:机理的模拟

Note

本节的完整程序保存为 actors2.py ,在本文的末尾,和代码.zip文件中和都有。

作为另一条蹊径,我们需要实现某种游戏(呃……我是指,模拟)的机理。严格来说,这些机理与角色模型一点关系都没有。然而,为了建立一种丰富而逼真 的模拟,我们不能被它们所阻碍。本节将详述我们将要达到的目标,以及如何达到。在这之后,我们用来摆弄角色的这个环境将变得更加有用。

6.2.1 角色属性

随着“世界”角色需要跟踪的信息变得越发复杂,在初始的 JOIN 消息中带送一串参数的方式也变得越来越麻烦。为了使这更容易,我们建立一个属性集对象,来跟踪这些信息,这个将取代分立的参数,在 JOIN 消息中被传递。

class properties:
    def __init__(self,name,location=(-1,-1),angle=0,
                 velocity=0,height=-1,width=-1,hitpoints=1,physical=True,
                 public=True):
        self.name = name
        self.location = location
        self.angle = angle
        self.velocity = velocity
        self.height = height
        self.width = width
        self.public = public
        self.hitpoints = hitpoints

注意,属性集对象是为了在角色之间传递信息而建立的,我们不会在建立它的那个角色局部再存储一份,否则,“世界”角色就能够直接修改其它角色的内部内容了,而不是通过发送消息来修改。

6.2.2 碰撞检测

在上一个程序中,碰撞检测的过程是有点问题的,其中最明显的就是角色不会彼此相撞,两个弹来跳去的机器人只会彼此穿过,而不是发生碰撞。第二个问题 则是我们没有指明角色的尺寸,这在机器人撞到右边界或下边界的时候表现最为明显:在 COLLISION 消息起作用之前,它们看上去有部分越过了边界。我确信,碰撞检测这主题是可以写几本书的,但这里我们将试图坚持使用一个相当简单的版本,对我们的目的来说 这已经够好了。

首先,我们给每个角色加上高度与宽度属性,这就能够给角色建立一个“边框”。原来的“位置”属性已经确定了这个边框的左上角,而新加的高度和宽度属性则同时确定了其右下角,这就对角色的物理形状给出了一个合理的近似表示。

为了检测与世界边缘的碰撞,现在,我们检测角色边框的四角中任一个是否与边缘有碰撞;为了检测和其它物体的碰撞,我们将维护一个已被检测过碰撞的物体的列表。我们遍历这个列表,找出是否有哪个角色的四角之一处在了另一个角色的边框之内,若存在,则它们是碰撞的。

这些就是我们的基本碰撞检测系统的全部,下面是检测单个碰撞的函数:

def testForCollision(self,x,y,item,otherItems=[]):
    if x < 0 or x + item.width > 496:
        return self.channel
    elif y < 0 or y+ item.height > 496:
        return self.channel
    else:
        ax1,ax2,ay1,ay2 = x, x+item.width, y,y+item.height
        for item,bx1,bx2,by1,by2 in otherItems:
            if self.registeredActors[item].physical == False: continue
            for x,y in [(ax1,ay1),(ax1,ay2),(ax2,ay1),(ax2,ay2)]:
                if x >= bx1 and x <= bx2 and y >= by1 and y <= by2:
                    return item
            for x,y in [(bx1,by1),(bx1,by2),(bx2,by1),(bx2,by2)]:
                if x >= ax1 and x <= ax2 and y >= ay1 and y <= ay2:
                    return item
        return None

还有个方法,用来遍历所有角色并检测,它在 sendStateToActors() 微进程中被调用:

def updateActorPositions(self):
    actorPositions = []
    for actor in self.registeredActors.keys():
        actorInfo = self.registeredActors[actor]
        if actorInfo.public and actorInfo.physical:
            x,y = actorInfo.location
            angle = actorInfo.angle
            velocity = actorInfo.velocity
            VectorX,VectorY = (math.sin(math.radians(angle)) * velocity,
                               math.cos(math.radians(angle)) * velocity)
            x += VectorX/self.updateRate
            y -= VectorY/self.updateRate
            collision = self.testForCollision(x,y,actorInfo,actorPositions)
            if collision:
                #不移动
                actor.send((self.channel,"COLLISION",actor,collision))
                if collision and collision is not self.channel:
                    collision.send((self.channel,"COLLISION",actor,collision))
            else:
                actorInfo.location = (x,y)
            actorPositions.append( (actor,
                                    actorInfo.location[0],
                                    actorInfo.location[0] + actorInfo.height,
                                    actorInfo.location[1],
                                    actorInfo.location[1] + actorInfo.width))

6.2.3 恒定的时间

我们的模拟有另一个问题,就是它在不同的机器上运行,会消耗不同的时间。若你的机器比我的快,你可能基本看不清机器人了,或者比我的慢,则机器人好像在爬一样。

为了修正这个,我们将以恒定的速度来产生 WORLD_STATE 消息,默认情况下每1/30秒产生一次。如果这点可以标准化,事情就容易了。然而,在机器无法应付负载量的情况下,我们也要有能力进行修正,来维持这个刷 新率。如果产生一个帧的时间超过了1/30秒(不管是由于程序本身的复杂性,还是外部某个程序狂占资源),我们就需要调整刷新率。

在我们的例子中,如果完成一帧的计算的时间超过了刷新率所确定的周期时间,就将每秒刷新次数降低一次;在在当前刷新率下,若显示一帧的过程中的空闲时间占到了40%或更多,则将每秒刷新次数增加一次,最大不超过每秒30次刷新。

这样,我们就能够在不同的机器上以同样的速度运行了,但这引入了另一个有趣的问题:例如现在,在每次刷新的时候,我们都将机器人的角度调整1度,并 重设其速度。这样的话,我们让这程序在不同的机器上各运行10秒钟,其中一台机器跑到每秒20次刷新,另一台则跑到每秒30次,那么最后,两台机器上的机 器人的位置将不会相同,这显然是不应该的。我们需要将角色数据的更新也改成基于时间的。

在本例中,我们不再使用每帧调整1度角度并调整一次速度(例如5点)的做法,而是将这个调整变成基于流逝时间的,比如,可以在每个时间单位里调整角度30.0度,调整速度150.0点。这样就可以在任何刷新率下都得到一致的行为。

为了实现这点,我们将修改 WORLD_STATE 消息,使之既包含当前时间又包含刷新率。于是,收到这种消息的角色就可以计算出合适的更新结果。

实现刷新率的代码如下:

def runFrame(self):
    initialStartTime = time.clock()
    startTime = time.clock()
    while 1:
        self.killDeadActors()
        self.updateActorPositions()
        self.sendStateToActors(startTime)
        #等待
        calculatedEndTime = startTime + 1.0/self.updateRate

        doneProcessingTime = time.clock()
        percentUtilized =  (doneProcessingTime - startTime) / (1.0/self.updateRate)
        if percentUtilized >= 1:
            self.updateRate -= 1
            print "TOO MUCH LOWERING FRAME RATE: " , self.updateRate
        elif percentUtilized <= 0.6 and self.updateRate < self.maxupdateRate:
            self.updateRate += 1
            print "TOO MUCH FREETIME, RAISING FRAME RATE: " , self.updateRate

        while time.clock() < calculatedEndTime:
            stackless.schedule()
        startTime = calculatedEndTime

        stackless.schedule()

6.2.4 伤害值、生命值和死亡

现在,我们的机器人是无敌的,将会永远运行下去,这可不怎么好玩——它们应该只能承受有限的伤害,然后死亡才对。为了实现这个,我们得增加一些新型 的消息:DAMAGE 消息包含一个参数,指定其受到的伤害值,这个值将从机器人类的另一个新属性“生命值”(hitpoints)之中减去。如果生命值达到小于或等于0,则这 个角色将向“世界”角色发送一条 KILLME 消息。以下是从机器人的 defaultMessageAction() 方法中摘录的的代码,用来实现这一过程:

elif msg == "DAMAGE":
    self.hitpoints -= msgArgs[0]
    if self.hitpoints <= 0:
        self.world.send( (self.channel,"KILLME") )
else:
    print "UNKNOWN MESSAGE", args

另外,我们随手规定, COLLISION 消息也会使生命值降低一点,如果满足条件,也会导致发送 KILLME 消息。

当世界角色收到 KILLME 消息,便将其内部对这个角色的生命值的记录修改为0,接着,将删除那些生命值小于或等于0的角色,作为常规更新的一部分:

def killDeadActors(self):
    for actor in self.registeredActors.keys():
        if self.registeredActors[actor].hitpoints <= 0:
            print "ACTOR DIED", self.registeredActors[actor].hitpoints
            actor.send_exception(TaskletExit)
            del self.registeredActors[actor]

注意,在这里我们引入了通道的 send_exception() 方法。与普通的 send 不同,这将使接收的微进程所调用的 channel.receive() 方法抛出一个异常。此处我们抛出的是 stackless 中的 TaskletExit 异常,这将使一个微进程安静地结束。你也可以抛出其他任何异常,但如果不加以捕获的话,这个异常就会在主微进程中再被抛出一次。

6.2.5 第二轮代码

这个程序的完整版本,仍然算不上特别令人激动,但如果你运行它,你将看到,上面我们所添加的所有特性都起了作用:机器人在经过了足够数量的碰撞之后,最终会死亡,从屏幕上消失。

6.3 回到角色:一起抓狂

Note

本节的完整程序保存为 actors3.py ,在本文的末尾,和代码.zip文件中和都有。

现在对于机理的模拟已经不成问题,我们就可以开始用这个程序做一些有趣的事情了,首先……

6.3.1 爆炸

机器人死亡的时候,让它们简单地消失可没什么意思,至少应该让它们爆炸。机器人在死亡的时候将会创建一个“爆炸”角色,这个角色并不是物质化的,而是仅仅显示一个爆炸的图像。它会在3秒钟后将自己删除,爆炸图像也随之消失:

class explosion(actor):
    def __init__(self,location=(0,0),angle=0,world=World):
        actor.__init__(self)
        self.time = 0.0
        self.world = world
        self.world.send((self.channel,"JOIN",
                            properties(self.__class__.__name__,
                                       location = location,
                                       angle = angle,
                                       velocity=0,
                                       height=32.0,width=32.0,hitpoints=1,
                                       physical=False)))

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            WorldState = msgArgs[0]
            if self.time == 0.0:
                self.time = WorldState.time
            elif WorldState.time >= self.time + 3.0:
                self.world.send( (self.channel, "KILLME") )

6.3.2 埋雷机器人

现在我们来创建能埋地雷的机器人。在创建这种机器人的类之前,先得创建一个“地雷”类:

class mine(actor):
    def __init__(self,location=(0,0),world=World):
        actor.__init__(self)
        self.world = world
        self.world.send((self.channel,"JOIN",
                            properties(self.__class__.__name__,
                                       location=location,
                                       angle=0,
                                       velocity=0,
                                       height=2.0,width=2.0,hitpoints=1)))

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            pass
        elif msg == "COLLISION":
            if msgArgs[0] is self.channel:
                other = msgArgs[1]
            else:
                other = msgArgs[0]
            other.send( (self.channel,"DAMAGE",25) )
            self.world.send( (self.channel,"KILLME"))
            print "MINE COLLISION"
        else:
            print "UNKNOWN MESSAGE", args

这是个简单的角色,只是单纯地停在那里,直到有东西撞上它,就向这个撞它的东西送出25点伤害,并将自己删除。

埋雷机器人与普通机器人相似,除了几点不同:首先,为了使局面混杂,我将埋雷机器人设定为为曲折移动,而不是缓慢地转向一个方向。其次,它每一秒都创建一个地雷,立即放置在自己身后:

class minedropperRobot(actor):
    def __init__(self,location=(0,0),angle=135,velocity=1,
                 hitpoints=20,world=World):
        actor.__init__(self)
        self.location = location
        self.angle = angle
        self.delta = 0.0
        self.height=32.0
        self.width=32.0
        self.deltaDirection = "up"
        self.nextMine = 0.0
        self.velocity = velocity
        self.hitpoints = hitpoints
        self.world = world
        self.world.send((self.channel,"JOIN",
                            properties(self.__class__.__name__,
                                       location=self.location,
                                       angle=self.angle,
                                       velocity=self.velocity,
                                       height=self.height,width=self.width,
                                       hitpoints=self.hitpoints)))

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            for actor in msgArgs[0].actors:
                if actor[0] is self.channel:
                    break
            self.location = actor[1].location
            if self.deltaDirection == "up":
                self.delta += 60.0 * (1.0 / msgArgs[0].updateRate)
                if self.delta > 15.0:
                    self.delta = 15.0
                    self.deltaDirection = "down"
            else:
                self.delta -= 60.0 * (1.0 / msgArgs[0].updateRate)
                if self.delta < -15.0:
                    self.delta = -15.0
                    self.deltaDirection = "up"
            if self.nextMine <= msgArgs[0].time:
                self.nextMine = msgArgs[0].time + 1.0
                mineX,mineY = (self.location[0] + (self.width / 2.0) ,
                               self.location[1] + (self.width / 2.0))

                mineDistance = (self.width / 2.0 ) ** 2
                mineDistance += (self.height / 2.0) ** 2
                mineDistance = math.sqrt(mineDistance)

                VectorX,VectorY = (math.sin(math.radians(self.angle + self.delta)),
                                   math.cos(math.radians(self.angle + self.delta)))
                VectorX,VectorY = VectorX * mineDistance,VectorY * mineDistance
                x,y = self.location
                x += self.width / 2.0
                y += self.height / 2.0
                x -= VectorX
                y += VectorY
                mine( (x,y))

            updateMsg = (self.channel, "UPDATE_VECTOR",
                         self.angle + self.delta ,self.velocity)
            self.world.send(updateMsg)
        elif msg == "COLLISION":
            self.angle += 73.0
            if self.angle >= 360:
                self.angle -= 360
            self.hitpoints -= 1
            if self.hitpoints <= 0:
                explosion(self.location,self.angle)
                self.world.send((self.channel,"KILLME"))
        elif msg == "DAMAGE":
            self.hitpoints -= msgArgs[0]
            if self.hitpoints <= 0:
                explosion(self.location,self.angle)
                self.world.send((self.channel, "KILLME"))
        else:
            print "UNKNOWN MESSAGE", args

6.3.3 建造台

建造台每5秒钟就在其位置上简单地创建一个新的、具有随机的属性的机器人。在其构造函数中有一些猫腻:它并不是特意创建一个含有所有合法的机器人的 数组,而是使用自省,来查找所有的名称以“Robot”结尾的类,并将它们加入到一个列表。这样,如果你创建一个自己的机器人类,你将不需要在这个建造台 类上特意进行某种注册工作。这个类除此之外的部分还是相当易懂的:

class spawner(actor):
    def __init__(self,location=(0,0),world=World):
        actor.__init__(self)
        self.location = location
        self.time = 0.0
        self.world = world

        self.robots = []
        for name,klass in globals().iteritems():
            if name.endswith("Robot"):
                self.robots.append(klass)

        self.world.send((self.channel,"JOIN",
                            properties(self.__class__.__name__,
                                       location = location,
                                       angle=0,
                                       velocity=0,
                                       height=32.0,width=32.0,hitpoints=1,
                                       physical=False)))

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            WorldState = msgArgs[0]
            if self.time == 0.0:
                self.time = WorldState.time + 0.5 # 启动时等待1/2秒
            elif WorldState.time >= self.time: # 每5秒
                self.time = WorldState.time + 5.0
            angle = random.random() * 360.0
            velocity = random.random() * 1000.0
            newRobot = random.choice(self.robots)
            newRobot(self.location,angle,velocity)

6.3.4 最终的模拟

最后,我们在世界的四角和中央各放置一个建造台。现在我们的模拟系统能够一直运行,并不断造出新的机器人。我们可以添加新的机器人种类,来随便摆弄这个系统。

6.4 总结

我们已经设法用少量的代码,建立了一个相当复杂的模拟系统,更重要的是,甚至每个角色都在独立运行。如果你将我们已经在传递的各个消息看作一个API,那它还真不是很复杂:

  • WORLD_STATE
  • JOIN
  • UPDATE_VECTOR
  • COLLISION
  • KILLME
  • DAMAGE

除此以外,一个角色所必需的其他所有内容,都被封装在其内部。为了了解外部的世界,它只需要处理这六条消息,这种机制不仅简化了程序本身,也简化了我们对其的理解过程。

7 完整代码列表

7.1 pingpong.py递归的乒乓球示例

def ping():
    print "PING"
    pong()

def pong():
    print "PONG"
    ping()

ping()

7.2 pingpong_stackless.py无堆栈的乒乓球示例

#
# pingpong_stackless.py
#

import stackless

ping_channel = stackless.channel()
pong_channel = stackless.channel()

def ping():
    while ping_channel.receive(): #blocks here
        print "PING"
        pong_channel.send("from ping")

def pong():
    while pong_channel.receive():
        print "PONG"
        ping_channel.send("from pong")

stackless.tasklet(ping)()
stackless.tasklet(pong)()

# we need to 'prime' the game by sending a start message
# if not, both tasklets will block
stackless.tasklet(ping_channel.send)('startup')

stackless.run()

7.3 hackysackthreaded.py基于操作系统线程的hackysack示例

import thread
import random
import sys
import Queue

class hackysacker:
    counter = 0
    def __init__(self,name,circle):
        self.name = name
        self.circle = circle
        circle.append(self)
        self.messageQueue = Queue.Queue()

        thread.start_new_thread(self.messageLoop,())

    def incrementCounter(self):
        hackysacker.counter += 1
        if hackysacker.counter >= turns:
            while self.circle:
                hs = self.circle.pop()
                if hs is not self:
                    hs.messageQueue.put('exit')
            sys.exit()

    def messageLoop(self):
        while 1:
            message = self.messageQueue.get()
            if message == "exit":
                debugPrint("%s is going home" % self.name)
                sys.exit()
            debugPrint("%s got hackeysack from %s" % (self.name, message.name))
            kickTo = self.circle[random.randint(0,len(self.circle)-1)]
            debugPrint("%s kicking hackeysack to %s" % (self.name, kickTo.name))
            self.incrementCounter()
            kickTo.messageQueue.put(self)

def debugPrint(x):
    if debug:
        print x

debug=1
hackysackers=5
turns = 5

def runit(hs=10,ts=10,dbg=1):
    global hackysackers,turns,debug
    hackysackers = hs
    turns = ts
    debug = dbg

    hackysacker.counter= 0
    circle = []
    one = hackysacker('1',circle)

    for i in range(hackysackers):
        hackysacker(`i`,circle)

    one.messageQueue.put(one)

    try:
        while circle:
            pass
    except:
        #sometimes we get a phantom error on cleanup.
        pass

if __name__ == "__main__":
    runit(dbg=1)

7.4 hackysackstackless.py stackless的hackysack示例

import stackless
import random
import sys

class hackysacker:
    counter = 0
    def __init__(self,name,circle):
        self.name = name
        self.circle = circle
        circle.append(self)
        self.channel = stackless.channel()

        stackless.tasklet(self.messageLoop)()

    def incrementCounter(self):
        hackysacker.counter += 1
        if hackysacker.counter >= turns:
            while self.circle:
                self.circle.pop().channel.send('exit')

    def messageLoop(self):
        while 1:
            message = self.channel.receive()
            if message == 'exit':
                return
            debugPrint("%s got hackeysack from %s" % (self.name, message.name))
            kickTo = self.circle[random.randint(0,len(self.circle)-1)]
            while kickTo is self:
                kickTo = self.circle[random.randint(0,len(self.circle)-1)]
            debugPrint("%s kicking hackeysack to %s" % (self.name, kickTo.name))
            self.incrementCounter()
            kickTo.channel.send(self)

def debugPrint(x):
    if debug:print x

debug = 5
hackysackers = 5
turns = 1

def runit(hs=5,ts=5,dbg=1):
    global hackysackers,turns,debug
    hackysackers = hs
    turns = ts
    debug = dbg

    hackysacker.counter = 0
    circle = []
    one = hackysacker('1',circle)

    for i in range(hackysackers):
        hackysacker(`i`,circle)

    one.channel.send(one)

    try:
        stackless.run()
    except TaskletExit:
        pass

if __name__ == "__main__":
    runit()

7.5 assemblyline.py普通的生产线示例

class storeroom:
    def __init__(self,name,product,unit,count):
        self.product = product
        self.unit = unit
        self.count = count
        self.name = name

    def get(self,count):
        if count > self.count:
            raise RuntimeError("Not enough %s" % self.product)
        else:
            self.count -= count
        return count

    def put(self,count):
        self.count += count

    def run(self):
        pass

rivetStoreroom = storeroom("rivetStoreroom","rivets","#",1000)
plasticStoreroom = storeroom("plastic Storeroom","plastic pellets","lb",100)

class injectionMolder:
    def __init__(self,name,partName,plasticSource,plasticPerPart,timeToMold):
        self.partName = partName
        self.plasticSource = plasticSource
        self.plasticPerPart = plasticPerPart
        self.timeToMold = timeToMold
        self.items = 0
        self.plastic = 0
        self.time = -1
        self.name = name

    def get(self,items):
        if items > self.items:
            return 0
        else:
            self.items -= items
            return items

    def run(self):
        if self.time == 0:
            self.items += 1
            print "%s finished making part" % self.name
            self.time -= 1
        elif self.time < 0:
            print "%s starts making new part %s" % (self.name,self.partName)
            if self.plastic < self.plasticPerPart:
                print "%s getting more plastic"
                self.plastic += self.plasticSource.get(self.plasticPerPart * 10)
            self.time = self.timeToMold
        else:
            print "%s molding for %s more seconds" % (self.partName, self.time)
            self.time -= 1

armMolder = injectionMolder("arm Molder", "arms",plasticStoreroom,0.2,6)
legMolder = injectionMolder("leg Molder", "leg",plasticStoreroom,0.2,5)
headMolder = injectionMolder("head Molder","head",plasticStoreroom,0.1,4)
torsoMolder = injectionMolder("torso Molder","torso",plasticStoreroom,0.5,10)

class assembler:
    def __init__(self,name,partAsource,partBsource,rivetSource,timeToAssemble):
        self.partAsource = partAsource
        self.partBsource = partBsource
        self.rivetSource = rivetSource
        self.timeToAssemble = timeToAssemble
        self.itemA = 0
        self.itemB = 0
        self.items = 0
        self.rivets = 0
        self.time = -1
        self.name = name

    def get(self,items):
        if items > self.items:
            return 0
        else:
            self.items -= items
            return items

    def run(self):
        if self.time == 0:
            self.items += 1
            print "%s finished assembling part" % self.name
            self.time -= 1
        elif self.time < 0:
            print "%s starts assembling new part" % self.name
            if self.itemA < 1:
                print "%s Getting item A" % self.name
                self.itemA += self.partAsource.get(1)
                if self.itemA < 1:
                    print "%s waiting for item A" % self.name
            elif self.itemB < 1:
                print "%s Getting item B" % self.name
                self.itemB += self.partBsource.get(1)
                if self.itemB < 1:
                    print "%s waiting for item B" % self.name
            print "%s starting to assemble" % self.name
            self.time = self.timeToAssemble
        else:
            print "%s assembling for %s more seconds" % (self.name, self.time)
            self.time -= 1

legAssembler = assembler("leg Assembler",torsoMolder,legMolder,rivetStoreroom,2)
armAssembler = assembler("arm Assembler", armMolder,legAssembler,rivetStoreroom,2)
torsoAssembler = assembler("torso Assembler", headMolder,armAssembler,
                        rivetStoreroom,3)

components = [rivetStoreroom, plasticStoreroom, armMolder,
              legMolder, headMolder, torsoMolder,
              legAssembler, armAssembler, torsoAssembler]

def run():
    while 1:
        for component in components:
            component.run()
        raw_input("Press <ENTER> to continue...")
        print "nnn"

if __name__ == "__main__":
    run()

7.6 assemblyline-stackless.py stackless的生产线示例

class storeroom:
    def __init__(self,name,product,unit,count):
        self.product = product
        self.unit = unit
        self.count = count
        self.name = name

    def get(self,count):
        if count > self.count:
            raise RuntimeError("Not enough %s" % self.product)
        else:
            self.count -= count
        return count

    def put(self,count):
        self.count += count

    def run(self):
        pass

rivetStoreroom = storeroom("rivetStoreroom","rivets","#",1000)
plasticStoreroom = storeroom("plastic Storeroom","plastic pellets","lb",100)

class injectionMolder:
    def __init__(self,name,partName,plasticSource,plasticPerPart,timeToMold):
        self.partName = partName
        self.plasticSource = plasticSource
        self.plasticPerPart = plasticPerPart
        self.timeToMold = timeToMold
        self.items = 0
        self.plastic = 0
        self.time = -1
        self.name = name

    def get(self,items):
        if items > self.items:
            return 0
        else:
            self.items -= items
            return items

    def run(self):
        if self.time == 0:
            self.items += 1
            print "%s finished making part" % self.name
            self.time -= 1
        elif self.time < 0:
            print "%s starts making new part %s" % (self.name,self.partName)
            if self.plastic < self.plasticPerPart:
                print "%s getting more plastic"
                self.plastic += self.plasticSource.get(self.plasticPerPart * 10)
            self.time = self.timeToMold
        else:
            print "%s molding for %s more seconds" % (self.partName, self.time)
    self.time -= 1

armMolder = injectionMolder("arm Molder", "arms",plasticStoreroom,0.2,6)
legMolder = injectionMolder("leg Molder", "leg",plasticStoreroom,0.2,5)
headMolder = injectionMolder("head Molder","head",plasticStoreroom,0.1,4)
torsoMolder = injectionMolder("torso Molder","torso",plasticStoreroom,0.5,10)

class assembler:
    def __init__(self,name,partAsource,partBsource,rivetSource,timeToAssemble):
        self.partAsource = partAsource
        self.partBsource = partBsource
        self.rivetSource = rivetSource
        self.timeToAssemble = timeToAssemble
        self.itemA = 0
        self.itemB = 0
        self.items = 0
        self.rivets = 0
        self.time = -1
        self.name = name

    def get(self,items):
        if items > self.items:
            return 0
        else:
            self.items -= items
            return items

    def run(self):
        if self.time == 0:
            self.items += 1
            print "%s finished assembling part" % self.name
            self.time -= 1
        elif self.time < 0:
            print "%s starts assembling new part" % self.name
            if self.itemA < 1:
                print "%s Getting item A" % self.name
                self.itemA += self.partAsource.get(1)
                if self.itemA < 1:
                    print "%s waiting for item A" % self.name
            elif self.itemB < 1:
                print "%s Getting item B" % self.name
                self.itemB += self.partBsource.get(1)
                if self.itemB < 1:
                    print "%s waiting for item B" % self.name
            print "%s starting to assemble" % self.name
            self.time = self.timeToAssemble
        else:
            print "%s assembling for %s more seconds" % (self.name, self.time)
    self.time -= 1

legAssembler = assembler("leg Assembler",torsoMolder,legMolder,rivetStoreroom,2)
armAssembler = assembler("arm Assembler", armMolder,legAssembler,rivetStoreroom,2)
torsoAssembler = assembler("torso Assembler", headMolder,armAssembler,
                            rivetStoreroom,3)

components = [rivetStoreroom, plasticStoreroom, armMolder,
          legMolder, headMolder, torsoMolder,
          legAssembler, armAssembler, torsoAssembler]

def run():
    while 1:
        for component in components:
            component.run()
        raw_input("Press <ENTER> to continue...")
        print "nnn"

if __name__ == "__main__":
    run()

7.7 digitalCircuit.py stackless的数字电路

import stackless

debug=0
def debugPrint(x):
    if debug:print x

class EventHandler:
    def __init__(self,*outputs):
        if outputs==None:
            self.outputs=[]
        else:
            self.outputs=list(outputs)

        self.channel = stackless.channel()
        stackless.tasklet(self.listen)()

    def listen(self):
        while 1:
            val = self.channel.receive()
            self.processMessage(val)
            for output in self.outputs:
                self.notify(output)

    def processMessage(self,val):
        pass

    def notify(self,output):
        pass

    def registerOutput(self,output):
        self.outputs.append(output)

    def __call__(self,val):
        self.channel.send(val)

class Switch(EventHandler):
    def __init__(self,initialState=0,*outputs):
        EventHandler.__init__(self,*outputs)
        self.state = initialState

    def processMessage(self,val):
        debugPrint("Setting input to %s" % val)
        self.state = val

    def notify(self,output):
        output((self,self.state))

class Reporter(EventHandler):
    def __init__(self,msg="%(sender)s send message %(value)s"):
        EventHandler.__init__(self)
        self.msg = msg

    def processMessage(self,msg):
        sender,value=msg
        print self.msg % {'sender':sender,'value':value}

class Inverter(EventHandler):
    def __init__(self,input,*outputs):
        EventHandler.__init__(self,*outputs)
        self.input = input
        input.registerOutput(self)
        self.state = 0

    def processMessage(self,msg):
        sender,value = msg
        debugPrint("Inverter received %s from %s" % (value,msg))
        if value:
            self.state = 0
        else:
            self.state = 1

    def notify(self,output):
        output((self,self.state))

class AndGate(EventHandler):
    def __init__(self,inputA,inputB,*outputs):
        EventHandler.__init__(self,*outputs)

        self.inputA = inputA
        self.inputAstate = inputA.state
        inputA.registerOutput(self)

        self.inputB = inputB
        self.inputBstate = inputB.state
        inputB.registerOutput(self)

        self.state = 0

    def processMessage(self,msg):
        sender, value = msg
        debugPrint("AndGate received %s from %s" % (value,sender))

        if sender is self.inputA:
            self.inputAstate = value
        elif sender is self.inputB:
            self.inputBstate = value
        else:
            raise RuntimeError("Didn't expect message from %s" % sender)

        if self.inputAstate and self.inputBstate:
            self.state = 1
        else:
            self.state = 0
        debugPrint("AndGate's new state => %s" % self.state)

    def notify(self,output):
        output((self,self.state))

class OrGate(EventHandler):
    def __init__(self,inputA,inputB,*outputs):
        EventHandler.__init__(self,*outputs)

        self.inputA = inputA
        self.inputAstate = inputA.state
        inputA.registerOutput(self)

        self.inputB = inputB
        self.inputBstate = inputB.state
        inputB.registerOutput(self)

        self.state = 0

    def processMessage(self,msg):
        sender, value = msg
        debugPrint("OrGate received %s from %s" % (value,sender))

        if sender is self.inputA:
            self.inputAstate = value
        elif sender is self.inputB:
            self.inputBstate = value
        else:
            raise RuntimeError("Didn't expect message from %s" % sender)

        if self.inputAstate or self.inputBstate:
            self.state = 1
        else:
            self.state = 0
        debugPrint("OrGate's new state => %s" % self.state)

    def notify(self,output):
        output((self,self.state))

if __name__ == "__main__":
    # half adder
    inputA = Switch()
    inputB = Switch()
    result = Reporter("Result = %(value)s")
    carry = Reporter("Carry = %(value)s")
    andGateA = AndGate(inputA,inputB,carry)
    orGate = OrGate(inputA,inputB)
    inverter = Inverter(andGateA)
    andGateB = AndGate(orGate,inverter,result)
    inputA(1)
    inputB(1)
    inputB(0)
    inputA(0)

7.8 actors.py第一个角色示例

import pygame
import pygame.locals
import os, sys
import stackless
import math

class actor:
    def __init__(self):
        self.channel = stackless.channel()
        self.processMessageMethod = self.defaultMessageAction
        stackless.tasklet(self.processMessage)()

    def processMessage(self):
        while 1:
            self.processMessageMethod(self.channel.receive())

    def defaultMessageAction(self,args):
        print args

class world(actor):
    def __init__(self):
        actor.__init__(self)
        self.registeredActors = {}
        stackless.tasklet(self.sendStateToActors)()

    def testForCollision(self,x,y):
        if x < 0 or x > 496:
            return 1
        elif y < 0 or y > 496:
            return 1
        else:
            return 0

    def sendStateToActors(self):
        while 1:
            for actor in self.registeredActors.keys():
                actorInfo = self.registeredActors[actor]
                if self.registeredActors[actor][1] != (-1,-1):
                    VectorX,VectorY = (math.sin(math.radians(actorInfo[2])) * actorInfo[3],
                                       math.cos(math.radians(actorInfo[2])) * actorInfo[3])
                    x,y = actorInfo[1]
                    x += VectorX
                    y -= VectorY
                    if self.testForCollision(x,y):
                        actor.send((self.channel,"COLLISION"))
                    else:
                        self.registeredActors[actor] = tuple([actorInfo[0],
                                                          (x,y),
                                                          actorInfo[2],
                                                          actorInfo[3]])
            worldState = [self.channel, "WORLD_STATE"]
            for actor in self.registeredActors.keys():
                if self.registeredActors[actor][1] != (-1,-1):
                    worldState.append( (actor, self.registeredActors[actor]))
            message = tuple(worldState)
            for actor in self.registeredActors.keys():
                actor.send(message)
            stackless.schedule()

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "JOIN":
            print 'ADDING ' , msgArgs
            self.registeredActors[sentFrom] = msgArgs
        elif msg == "UPDATE_VECTOR":
            self.registeredActors[sentFrom] = tuple([self.registeredActors[sentFrom][0],
                                                     self.registeredActors[sentFrom][1],
                                                     msgArgs[0],msgArgs[1]])
        else:
            print '!!!! WORLD GOT UNKNOWN MESSAGE ' , args

World = world().channel

class display(actor):
    def __init__(self,world=World):
        actor.__init__(self)

        self.world = World

        self.icons = {}
        pygame.init()

        window = pygame.display.set_mode((496,496))
        pygame.display.set_caption("Actor Demo")

        joinMsg = (self.channel,"JOIN",self.__class__.__name__, (-1,-1))
        self.world.send(joinMsg)

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            self.updateDisplay(msgArgs)
        else:
            print "UNKNOWN MESSAGE", args

    def getIcon(self, iconName):
        if self.icons.has_key(iconName):
            return self.icons[iconName]
        else:
            iconFile = os.path.join("data","%s.bmp" % iconName)
            surface = pygame.image.load(iconFile)
            surface.set_colorkey((0xf3,0x0a,0x0a))
            self.icons[iconName] = surface
            return surface

    def updateDisplay(self,actors):
        for event in pygame.event.get():
            if event.type == pygame.QUIT: sys.exit()

        screen = pygame.display.get_surface()

        background = pygame.Surface(screen.get_size())
        background = background.convert()
        background.fill((200, 200, 200))

        screen.blit(background, (0,0))
        for item in actors:
            screen.blit(pygame.transform.rotate(self.getIcon(item[1][0]),-item[1][2]), item[1][1])
        pygame.display.flip()

display()

class basicRobot(actor):
    def __init__(self,location=(0,0),angle=135,velocity=1,world=World):
        actor.__init__(self)
        self.location = location
        self.angle = angle
        self.velocity = velocity
        self.world = world

        joinMsg =(self.channel,"JOIN",self.__class__.__name__,
                  self.location,self.angle,self.velocity)
        self.world.send(joinMsg)

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            self.location = (self.location[0] + 1, self.location[1] + 1)
            self.angle += 1
            if self.angle >= 360:
                self.angle -= 360

            updateMsg = (self.channel, "UPDATE_VECTOR",
                         self.angle,self.velocity)
            self.world.send(updateMsg)
        elif msg == "COLLISION":
            self.angle += 73
            if self.angle >= 360:
                self.angle -= 360
        else:
            print "UNKNOWN MESSAGE", args

basicRobot(angle=135,velocity=5)
basicRobot((464,0),angle=225,velocity=10)

stackless.run()

7.9 actors2.py第二个角色示例

import pygame
import pygame.locals
import os, sys
import stackless
import math
import time

class actor:
    def __init__(self):
        self.channel = stackless.channel()
        self.processMessageMethod = self.defaultMessageAction
        stackless.tasklet(self.processMessage)()

    def processMessage(self):
        while 1:
            self.processMessageMethod(self.channel.receive())

    def defaultMessageAction(self,args):
        print args

class properties:
    def __init__(self,name,location=(-1,-1),angle=0,
                 velocity=0,height=-1,width=-1,hitpoints=1,physical=True,
                 public=True):
        self.name = name
        self.location = location
        self.angle = angle
        self.velocity = velocity
        self.height = height
        self.width = width
        self.public = public
        self.hitpoints = hitpoints
        self.physical = physical

class worldState:
    def __init__(self,updateRate,time):
        self.updateRate = updateRate
        self.time = time
        self.actors = []

class world(actor):
    def __init__(self):
        actor.__init__(self)
        self.registeredActors = {}
        self.updateRate = 30
        self.maxupdateRate = 30
        stackless.tasklet(self.runFrame)()

    def testForCollision(self,x,y,item,otherItems=[]):
        if x < 0 or x + item.width > 496:
            return self.channel
        elif y < 0 or y+ item.height > 496:
            return self.channel
        else:
            ax1,ax2,ay1,ay2 = x, x+item.width, y,y+item.height
            for item,bx1,bx2,by1,by2 in otherItems:
                if self.registeredActors[item].physical == False: continue
                for x,y in [(ax1,ay1),(ax1,ay2),(ax2,ay1),(ax2,ay2)]:
                    if x >= bx1 and x <= bx2 and y >= by1 and y <= by2:
                        return item
                for x,y in [(bx1,by1),(bx1,by2),(bx2,by1),(bx2,by2)]:
                    if x >= ax1 and x <= ax2 and y >= ay1 and y <= ay2:
                        return item
            return None

    def killDeadActors(self):
        for actor in self.registeredActors.keys():
            if self.registeredActors[actor].hitpoints <= 0:
                print "ACTOR DIED", self.registeredActors[actor].hitpoints
                actor.send_exception(TaskletExit)
                del self.registeredActors[actor]

    def updateActorPositions(self):
        actorPositions = []
        for actor in self.registeredActors.keys():
            actorInfo = self.registeredActors[actor]
            if actorInfo.public and actorInfo.physical:
                x,y = actorInfo.location
                angle = actorInfo.angle
                velocity = actorInfo.velocity
                VectorX,VectorY = (math.sin(math.radians(angle)) * velocity,
                                   math.cos(math.radians(angle)) * velocity)
                x += VectorX/self.updateRate
                y -= VectorY/self.updateRate
                collision = self.testForCollision(x,y,actorInfo,actorPositions)
                if collision:
                    #don't move
                    actor.send((self.channel,"COLLISION",actor,collision))
                    if collision and collision is not self.channel:
                        collision.send((self.channel,"COLLISION",actor,collision))
                else:
                    actorInfo.location = (x,y)
                actorPositions.append( (actor,
                                        actorInfo.location[0],
                                        actorInfo.location[0] + actorInfo.height,
                                        actorInfo.location[1],
                                        actorInfo.location[1] + actorInfo.width))

    def sendStateToActors(self,starttime):
        WorldState = worldState(self.updateRate,starttime)
        for actor in self.registeredActors.keys():
            if self.registeredActors[actor].public:
                WorldState.actors.append( (actor, self.registeredActors[actor]) )
        for actor in self.registeredActors.keys():
            actor.send( (self.channel,"WORLD_STATE",WorldState) )

    def runFrame(self):
        initialStartTime = time.clock()
        startTime = time.clock()
        while 1:
            self.killDeadActors()
            self.updateActorPositions()
            self.sendStateToActors(startTime)
            #wait
            calculatedEndTime = startTime + 1.0/self.updateRate

            doneProcessingTime = time.clock()
            percentUtilized =  (doneProcessingTime - startTime) / (1.0/self.updateRate)
            if percentUtilized >= 1:
                self.updateRate -= 1
                print "TOO MUCH LOWERING FRAME RATE: " , self.updateRate
            elif percentUtilized <= 0.6 and self.updateRate < self.maxupdateRate:
                self.updateRate += 1
                print "TOO MUCH FREETIME, RAISING FRAME RATE: " , self.updateRate

            while time.clock() < calculatedEndTime:
                stackless.schedule()
            startTime = calculatedEndTime

            stackless.schedule()

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "JOIN":
            print 'ADDING ' , msgArgs
            self.registeredActors[sentFrom] = msgArgs[0]
        elif msg == "UPDATE_VECTOR":
            self.registeredActors[sentFrom].angle = msgArgs[0]
            self.registeredActors[sentFrom].velocity = msgArgs[1]
        elif msg == "COLLISION":
            pass # known, but we don't do anything
        elif msg == "KILLME":
            self.registeredActors[sentFrom].hitpoints = 0
        else:
            print '!!!! WORLD GOT UNKNOWN MESSAGE ' , args

World = world().channel

class display(actor):
    def __init__(self,world=World):
        actor.__init__(self)

        self.world = World
        self.icons = {}
        pygame.init()

        window = pygame.display.set_mode((496,496))
        pygame.display.set_caption("Actor Demo")

        self.world.send((self.channel,"JOIN",
                            properties(self.__class__.__name__,
                                       public=False)))

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            self.updateDisplay(msgArgs)
        else:
            print "UNKNOWN MESSAGE", args

    def getIcon(self, iconName):
        if self.icons.has_key(iconName):
            return self.icons[iconName]
        else:
            iconFile = os.path.join("data","%s.bmp" % iconName)
            surface = pygame.image.load(iconFile)
            surface.set_colorkey((0xf3,0x0a,0x0a))
            self.icons[iconName] = surface
            return surface

    def updateDisplay(self,msgArgs):

        for event in pygame.event.get():
            if event.type == pygame.QUIT: sys.exit()

        screen = pygame.display.get_surface()

        background = pygame.Surface(screen.get_size())
        background = background.convert()
        background.fill((200, 200, 200))

        screen.blit(background, (0,0))

        WorldState = msgArgs[0]

        for channel,item in WorldState.actors:
            screen.blit(pygame.transform.rotate(self.getIcon(item.name),-item.angle), item.location)
        pygame.display.flip()

display()

class basicRobot(actor):
    def __init__(self,location=(0,0),angle=135,velocity=1,
                 hitpoints=20,world=World):
        actor.__init__(self)
        self.location = location
        self.angle = angle
        self.velocity = velocity
        self.hitpoints = hitpoints
        self.world = world
        self.world.send((self.channel,"JOIN",
                            properties(self.__class__.__name__,
                                       location=self.location,
                                       angle=self.angle,
                                       velocity=self.velocity,
                                       height=32,width=32,
                                       hitpoints=self.hitpoints)))

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            for actor in msgArgs[0].actors:
                if actor[0] is self: break
            self.location = actor[1].location
            self.angle += 30.0 * (1.0 / msgArgs[0].updateRate)
            if self.angle >= 360:
                self.angle -= 360

            updateMsg = (self.channel, "UPDATE_VECTOR", self.angle,
                         self.velocity)
            self.world.send(updateMsg)
        elif msg == "COLLISION":
            self.angle += 73.0
            if self.angle >= 360:
                self.angle -= 360
            self.hitpoints -= 1
            if self.hitpoints <= 0:
                self.world.send((self.channel, "KILLME"))
        elif msg == "DAMAGE":
            self.hitpoints -= msgArgs[0]
            if self.hitpoints <= 0:
                self.world.send( (self.channel,"KILLME") )
        else:
            print "UNKNOWN MESSAGE", args

basicRobot(angle=135,velocity=150)
basicRobot((464,0),angle=225,velocity=300)
basicRobot((100,200),angle=78,velocity=500)
basicRobot((400,300),angle=298,velocity=5)
basicRobot((55,55),angle=135,velocity=150)
basicRobot((464,123),angle=225,velocity=300)
basicRobot((180,200),angle=78,velocity=500)
basicRobot((400,380),angle=298,velocity=5)

stackless.run()

7.10 actors3.py第三个角色示例

import pygame
import pygame.locals
import os, sys
import stackless
import math
import time
import random

class actor:
    def __init__(self):
        self.channel = stackless.channel()
        self.processMessageMethod = self.defaultMessageAction
        stackless.tasklet(self.processMessage)()

    def processMessage(self):
        while 1:
            self.processMessageMethod(self.channel.receive())

    def defaultMessageAction(self,args):
        print args

class properties:
    def __init__(self,name,location=(-1,-1),angle=0,
                 velocity=0,height=-1,width=-1,hitpoints=1,physical=True,
                 public=True):
        self.name = name
        self.location = location
        self.angle = angle
        self.velocity = velocity
        self.height = height
        self.width = width
        self.public = public
        self.hitpoints = hitpoints
        self.physical = physical

class worldState:
    def __init__(self,updateRate,time):
        self.updateRate = updateRate
        self.time = time
        self.actors = []

class world(actor):
    def __init__(self):
        actor.__init__(self)
        self.registeredActors = {}
        self.updateRate = 30
        self.maxupdateRate = 30
        stackless.tasklet(self.runFrame)()

    def testForCollision(self,x,y,item,otherItems=[]):
        if x < 0 or x + item.width > 496:
            return self.channel
        elif y < 0 or y+ item.height > 496:
            return self.channel
        else:
            ax1,ax2,ay1,ay2 = x, x+item.width, y,y+item.height
            for item,bx1,bx2,by1,by2 in otherItems:
                if self.registeredActors[item].physical == False: continue
                for x,y in [(ax1,ay1),(ax1,ay2),(ax2,ay1),(ax2,ay2)]:
                    if x >= bx1 and x <= bx2 and y >= by1 and y <= by2:
                        return item
                for x,y in [(bx1,by1),(bx1,by2),(bx2,by1),(bx2,by2)]:
                    if x >= ax1 and x <= ax2 and y >= ay1 and y <= ay2:
                        return item
            return None

    def killDeadActors(self):
        for actor in self.registeredActors.keys():
            if self.registeredActors[actor].hitpoints <= 0:
                print "ACTOR DIED", self.registeredActors[actor].hitpoints
                actor.send_exception(TaskletExit)
                del self.registeredActors[actor]

    def updateActorPositions(self):
        actorPositions = []
        for actor in self.registeredActors.keys():
            actorInfo = self.registeredActors[actor]
            if actorInfo.public and actorInfo.physical:
                x,y = actorInfo.location
                angle = actorInfo.angle
                velocity = actorInfo.velocity
                VectorX,VectorY = (math.sin(math.radians(angle)) * velocity,
                                   math.cos(math.radians(angle)) * velocity)
                x += VectorX/self.updateRate
                y -= VectorY/self.updateRate
                collision = self.testForCollision(x,y,actorInfo,actorPositions)
                if collision:
                    #don't move
                    actor.send((self.channel,"COLLISION",actor,collision))
                    if collision and collision is not self.channel:
                        collision.send((self.channel,"COLLISION",actor,collision))
                else:
                    actorInfo.location = (x,y)
                actorPositions.append( (actor,
                                        actorInfo.location[0],
                                        actorInfo.location[0] + actorInfo.height,
                                        actorInfo.location[1],
                                        actorInfo.location[1] + actorInfo.width))

    def sendStateToActors(self,starttime):
        WorldState = worldState(self.updateRate,starttime)
        for actor in self.registeredActors.keys():
            if self.registeredActors[actor].public:
                WorldState.actors.append( (actor, self.registeredActors[actor]) )
        for actor in self.registeredActors.keys():
            actor.send( (self.channel,"WORLD_STATE",WorldState) )

    def runFrame(self):
        initialStartTime = time.clock()
        startTime = time.clock()
        while 1:
            self.killDeadActors()
            self.updateActorPositions()
            self.sendStateToActors(startTime)
            #wait
            calculatedEndTime = startTime + 1.0/self.updateRate

            doneProcessingTime = time.clock()
            percentUtilized =  (doneProcessingTime - startTime) / (1.0/self.updateRate)
            if percentUtilized >= 1:
                self.updateRate -= 1
                print "TOO MUCH LOWERING FRAME RATE: " , self.updateRate
            elif percentUtilized <= 0.6 and self.updateRate < self.maxupdateRate:
                self.updateRate += 1
                print "TOO MUCH FREETIME, RAISING FRAME RATE: " , self.updateRate

            while time.clock() < calculatedEndTime:
                stackless.schedule()
            startTime = calculatedEndTime
            stackless.schedule()

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "JOIN":
            self.registeredActors[sentFrom] = msgArgs[0]
        elif msg == "UPDATE_VECTOR":
            self.registeredActors[sentFrom].angle = msgArgs[0]
            self.registeredActors[sentFrom].velocity = msgArgs[1]
        elif msg == "COLLISION":
            pass # known, but we don't do anything
        elif msg == "KILLME":
            self.registeredActors[sentFrom].hitpoints = 0
        else:
            print '!!!! WORLD GOT UNKNOWN MESSAGE ' , msg, msgArgs

World = world().channel

class display(actor):
    def __init__(self,world=World):
        actor.__init__(self)

        self.world = World
        self.icons = {}
        pygame.init()

        window = pygame.display.set_mode((496,496))
        pygame.display.set_caption("Actor Demo")

        self.world.send((self.channel,"JOIN",
                            properties(self.__class__.__name__,
                                       public=False)))

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            self.updateDisplay(msgArgs)
        else:
            print "DISPLAY UNKNOWN MESSAGE", args

    def getIcon(self, iconName):
        if self.icons.has_key(iconName):
            return self.icons[iconName]
        else:
            iconFile = os.path.join("data","%s.bmp" % iconName)
            surface = pygame.image.load(iconFile)
            surface.set_colorkey((0xf3,0x0a,0x0a))
            self.icons[iconName] = surface
            return surface

    def updateDisplay(self,msgArgs):

        for event in pygame.event.get():
            if event.type == pygame.QUIT: sys.exit()

        screen = pygame.display.get_surface()

        background = pygame.Surface(screen.get_size())
        background = background.convert()
        background.fill((200, 200, 200))

            screen.blit(background, (0,0))

            WorldState = msgArgs[0]

            for channel,item in WorldState.actors:
                itemImage = self.getIcon(item.name)
                itemImage = pygame.transform.rotate(itemImage,-item.angle)
                screen.blit(itemImage, item.location)
            pygame.display.flip()

    display()

    class basicRobot(actor):
        def __init__(self,location=(0,0),angle=135,velocity=1,
                     hitpoints=20,world=World):
            actor.__init__(self)
            self.location = location
            self.angle = angle
            self.velocity = velocity
            self.hitpoints = hitpoints
            self.world = world
            self.world.send((self.channel,"JOIN",
                                properties(self.__class__.__name__,
                                           location=self.location,
                                           angle=self.angle,
                                           velocity=self.velocity,
                                               height=32,width=32,hitpoints=self.hitpoints)))

            def defaultMessageAction(self,args):
                sentFrom, msg, msgArgs = args[0],args[1],args[2:]
                if msg == "WORLD_STATE":
                    for actor in msgArgs[0].actors:
                        if actor[0] is self: break
                    self.location = actor[1].location
                    self.angle += 30.0 * (1.0 / msgArgs[0].updateRate)
                    if self.angle >= 360:
                        self.angle -= 360

                    updateMsg = (self.channel, "UPDATE_VECTOR",
                                 self.angle,self.velocity)
                    self.world.send(updateMsg)
                elif msg == "COLLISION":
                    self.angle += 73.0
                    if self.angle >= 360:
                        self.angle -= 360
                    self.hitpoints -= 1
                    if self.hitpoints <= 0:
                        explosion(self.location,self.angle)
                        self.world.send((self.channel, "KILLME"))
                elif msg == "DAMAGE":
                    self.hitpoints -= msgArgs[0]
                    if self.hitpoints <= 0:
                        explosion(self.location,self.angle)
                        self.world.send( (self.channel,"KILLME") )

                else:
                    print "BASIC ROBOT UNKNOWN MESSAGE", args

        class explosion(actor):
            def __init__(self,location=(0,0),angle=0,world=World):
                actor.__init__(self)
                self.time = 0.0
                self.world = world
                self.world.send((self.channel,"JOIN",
                                    properties(self.__class__.__name__,
                                               location = location,
                                               angle = angle,
                                               velocity=0,
                                               height=32.0,width=32.0,hitpoints=1,
                                               physical=False)))

            def defaultMessageAction(self,args):
                sentFrom, msg, msgArgs = args[0],args[1],args[2:]
                if msg == "WORLD_STATE":
                    WorldState = msgArgs[0]
                    if self.time == 0.0:
                        self.time = WorldState.time
                    elif WorldState.time >= self.time + 3.0:
                        self.world.send( (self.channel, "KILLME") )

        class mine(actor):
            def __init__(self,location=(0,0),world=World):
                actor.__init__(self)
                self.world = world
                self.world.send((self.channel,"JOIN",
                                    properties(self.__class__.__name__,
                                               location=location,
                                               angle=0,
                                               velocity=0,
                                               height=2.0,width=2.0,hitpoints=1)))

            def defaultMessageAction(self,args):
                sentFrom, msg, msgArgs = args[0],args[1],args[2:]
                if msg == "WORLD_STATE":
                    pass
                elif msg == "COLLISION":
                    if msgArgs[0] is self.channel:
                        other = msgArgs[1]
                    else:
                        other = msgArgs[0]
                    other.send( (self.channel,"DAMAGE",25) )
                    self.world.send( (self.channel,"KILLME"))
                    print "MINE COLLISION"
                else:
                    print "UNKNOWN MESSAGE", args

        class minedropperRobot(actor):
            def __init__(self,location=(0,0),angle=135,velocity=1,
                         hitpoints=20,world=World):
                actor.__init__(self)
                self.location = location
                self.angle = angle
                self.delta = 0.0
                self.height=32.0
                self.width=32.0
                self.deltaDirection = "up"
                self.nextMine = 0.0
                self.velocity = velocity
                self.hitpoints = hitpoints
                self.world = world
                self.world.send((self.channel,"JOIN",
                                    properties(self.__class__.__name__,
                                               location=self.location,
                                               angle=self.angle,
                                               velocity=self.velocity,
                                               height=self.height,width=self.width,
                                               hitpoints=self.hitpoints)))

            def defaultMessageAction(self,args):
                sentFrom, msg, msgArgs = args[0],args[1],args[2:]
                if msg == "WORLD_STATE":
                    for actor in msgArgs[0].actors:
                        if actor[0] is self.channel:
                            break
                    self.location = actor[1].location
                    if self.deltaDirection == "up":
                        self.delta += 60.0 * (1.0 / msgArgs[0].updateRate)
                        if self.delta > 15.0:
                            self.delta = 15.0
                            self.deltaDirection = "down"
                    else:
                        self.delta -= 60.0 * (1.0 / msgArgs[0].updateRate)
                        if self.delta < -15.0:
                            self.delta = -15.0
                            self.deltaDirection = "up"
                    if self.nextMine <= msgArgs[0].time:
                        self.nextMine = msgArgs[0].time + 1.0
                        mineX,mineY = (self.location[0] + (self.width / 2.0) ,
                                       self.location[1] + (self.width / 2.0))

                        mineDistance = (self.width / 2.0 ) ** 2
                        mineDistance += (self.height / 2.0) ** 2
                        mineDistance = math.sqrt(mineDistance)

                        VectorX,VectorY = (math.sin(math.radians(self.angle + self.delta)),
                                           math.cos(math.radians(self.angle + self.delta)))
                        VectorX,VectorY = VectorX * mineDistance,VectorY * mineDistance
                        x,y = self.location
                        x += self.width / 2.0
                        y += self.height / 2.0
                        x -= VectorX
                        y += VectorY
                        mine( (x,y))
                    updateMsg = (self.channel, "UPDATE_VECTOR",
                                 self.angle + self.delta ,self.velocity)
                    self.world.send(updateMsg)
                elif msg == "COLLISION":
                    self.angle += 73.0
                    if self.angle >= 360:
                        self.angle -= 360
                    self.hitpoints -= 1
                    if self.hitpoints <= 0:
                        explosion(self.location,self.angle)
                        self.world.send((self.channel,"KILLME"))
                elif msg == "DAMAGE":
                    self.hitpoints -= msgArgs[0]
                    if self.hitpoints <= 0:
                        explosion(self.location,self.angle)
                        self.world.send((self.channel, "KILLME"))
                else:
                    print "UNKNOWN MESSAGE", args

        class spawner(actor):
            def __init__(self,location=(0,0),world=World):
                actor.__init__(self)
                self.location = location
                self.time = 0.0
                self.world = world

                self.robots = []
                for name,klass in globals().iteritems():
                    if name.endswith("Robot"):
                        self.robots.append(klass)

                self.world.send((self.channel,"JOIN",
                                    properties(self.__class__.__name__,
                                               location = location,
                                               angle=0,
                                               velocity=0,
                                               height=32.0,width=32.0,hitpoints=1,
                                               physical=False)))

            def defaultMessageAction(self,args):
                sentFrom, msg, msgArgs = args[0],args[1],args[2:]
                if msg == "WORLD_STATE":
                    WorldState = msgArgs[0]
                    if self.time == 0.0:
                        self.time = WorldState.time + 0.5 # wait 1/2 second on start
                    elif WorldState.time >= self.time: # every five seconds
                        self.time = WorldState.time + 5.0
                        angle = random.random() * 360.0
                        velocity = random.random() * 1000.0
                        newRobot = random.choice(self.robots)
                        newRobot(self.location,angle,velocity)

        spawner( (32,32) )
        spawner( (432,32) )
        spawner( (32,432) )
        spawner( (432,432) )
        spawner( (232,232) )

        stackless.run()

8 引用链接

stackless

9 给jorge的reStructuredText示例

一段文字

另外一段文字,注意段之间要有空行。

加重的字体使用 两个星号之间 ,当然星号与外界要留有空格。

引用的文字用两个那个符号,比如 我是引用文字 ,同样边界要有空格。这种引用一般用于代码引用,当然这是风格问题,希望你可以与我的风格相同。而下面的反引号引用可以由于函数定义等等非语句性质的引用。至于星号形成的斜体,我一般不用的。

斜体字就比较好说了,单个星号或单个的反引号都是,比如 斜体 或 斜体 。当然,比较规范的斜体是使用星号的。反引号一般用于引用。

链接么就比较简单了,比如 惊帆之静默 注意反引号结束之后有个下划线。然后定义链接地址在下一行。这里的反引号引用用于链接名称里面含有中文和空格的情况,如果没有这些,则可以省略反引号。

关于标题,只要拥有相同下划线的标题就属于同一个级别,当然,上面已经很多例子了。标题与文字段之间也有空行。

代码引用就很酷了,就是在一行的末尾用双冒号,如:

我是代码
def hello():
    print 'hello'
    return

当然,代码所在的块需要缩进表示,结束就是使用原缩进 即可。

图片的引用方式如下所示。

http://图片地址好了就这些吧,高级功能你用到时再问我,比如文章引用,表格等等。

SendPkt快速入门

Wednesday, October 3rd, 2007

SendPkt快速入门
作者:
gashero
电邮:
harry.python@gmail.com
原文地址:

http://gashero.yeax.com/?p=26

项目主页:

http://sendpkt.googlecode.com

日期:
2007-10-03
版本:
sendpkt 0.2
修订版:
1
目录
1 简介
2 使用SendPkt的流程
3 SendPkt发送数据包的简单例子
4 SendPkt发送ARP查询包的例子
5 参考
1 简介
摘自 SendPkt 项目wiki。
SendPkt是一套同时可以运行在Linux/Win32的发送链路层数据包的Python扩展库。
SendPkt在Linux和Win32下实现方式是不同的,但是提供对Python的相同接口,确保Python程序可以不经过修改就可以移植。
在Win32下SendPkt使用WinPcap?中的 pcap_sendpacket() 函数发送数据包。
在Linux下SendPkt使用libnet的 libnet_adv_write_link() 函数发送数据包。
同时SendPkt还提供了查找网络接口设备的功能,以供发送数据包时选择。在Linux下虽然可以使用libnet自动选择的网络接口设备,但是在Win32下,默认的网络设备总会导致发包失败。所以为了可移植性,你必须在发送数据包时指定使用哪个网络设备接口。
2 使用SendPkt的流程
选择合适的网络接口以发送数据包,因为Win32于Linux的不同,需要专门选择。
对于Win32,通过libpcap取得的第一个网络设备接口(网卡)是一个虚拟网卡,无法发送数据包,而一般选择第二个网口,可以通过os.name判断操作系统并选择合适的网口。
得到的网络设备接口列表,是一个字符串列表,最终提供给发送函数的网口标志也是字符串。
构造数据包。推荐使用dpkt。
dpkt是python界通用的数据包组包和解包库,在分层构造数据包时从高层(应用层)向底层依次构造数据包。并且把高层数据包的数据作为下一层数据包的data字段来使用。
对于高性能应用,手工构造数据包也未尝不可,因为哪种方式都需要对数据包格式的深入了解。
发送数据包,使用 sendpkt.sendpacket() 。
例子见下面的代码。调用方式为 sendpkt.sendpacket(packet,device) 。第一个参数为数据包,为字符串对象,以太网帧。第二个参数为网络设备名,通过第一步获得。
3 SendPkt发送数据包的简单例子
参见SendPkt项目的单元测试 test.py 。:
import sendpkt
#构造一个需要发送的数据包
packet=”a”*80
#查找所有可用网络设备接口
devlist=sendpkt.findalldevs()
#根据操作系统选择合适的网络设备
if devlist:
if os.name==”nt”:
device=devlist[-1] #\\NPF\\….
elif os.name==”posix”:
device=devlist[0] #eth0
else:
device=devlist[0]
else:
device=””
raise OSError,”You must run in root mode”
#发送数据包
sendpkt.sendpacket(packet,device)

4 SendPkt发送ARP查询包的例子
ARP查询包的op字段为1,表示查询一个IP地址对应的MAC地址。如下是查询过程。:
#! /usr/bin/env python
# -*- coding: UTF-8 -*-
# File: arpquery.py
# Date: 2007-09-21
# Author: gashero
# Copyright@1999-2007, Harry Gashero Liu.

“””
ARP查询的测试
“””

from socket import inet_aton,inet_ntoa

import dpkt
import sendpkt

LOCALMAC=”111111″
LOCALIP=inet_aton(“192.168.1.104″)
REMOTEMAC=””
REMOTEIP=inet_aton(“192.168.1.1″)

def main():
global LOCALMAC
global LOCALIP
device=sendpkt.findalldevs()[0] #for linux is eth0
arp=dpkt.arp.ARP(
hrd=1,
pro=2048,
hln=6,
pln=4,
op=1,
sha=LOCALMAC,
spa=LOCALIP,
tha=”\x00″*6,
tpa=inet_aton(“192.168.1.1″)
)
ether=dpkt.ethernet.Ethernet(
dst=”\xff”*6,
src=LOCALMAC,
type=0x0806,
data=arp
)
#print ether
#print repr(dpkt.ethernet.Ethernet(str(ether)))
sendpkt.sendpacket(str(ether),device)
return

if __name__==”__main__”:
main()

5 参考
详细问题请发邮件到 harry.python@gmail.com 询问。

在基于Pylons的服务器上测试使用DBUtils前后的性能对比

Sunday, September 30th, 2007

在基于Pylons的服务器上测试使用DBUtils前后的性能

目录

为了测试使用DBUtils实现的数据库连接池的性能,在Pylons写了一个测试服务,并将服务器架设在lab2服务器上,使用apache ab进行服务器压力测试。

1   测试环境

lab2服务器,MySQL 4.1.20,Pylons 0.9.4.1,apache ab 2.0.59。

为了确保测试的可靠性,每次测试之前都重启服务器。

在Pylons上假设的应用有3个URL用于测试,分别如下:

URL 说明
/testdb/test1 不使用连接池,每次访问都建立对数据库的连接
/testdb/test2 使用DBUtils.PersistentDB连接池,线程专用连接
/testdb/test3 使用DBUtils.PooledDB连接池,线程间共享连接

测试代码如下:

from helloworld.lib.base import *
import time
import random
import MySQLdb
import DBUtils.PersistentDB
import DBUtils.PooledDB

conn_kwargs={'host':'192.168.1.239','user':'ro','passwd':'','db':'test','port':3306}
sql="""SELECT * FROM test_table WHERE id=%d"""
persist=DBUtils.PersistentDB.PersistentDB(dbapi=MySQLdb,maxusage=1000,**conn_kwargs)
pooled=DBUtils.PooledDB.PooledDB(dbapi=MySQLdb,maxusage=1000,**conn_kwargs)

def query(conn):
    cur=conn.cursor()
    cur.execute(sql%(random.randint(1,1000)))
    data=cur.fetchall()
    cur.close()
    return data

class TestdbController(BaseController):
    def index(self):
        return Response('index')

    def test1(self):
        conn=MySQLdb.connect(**conn_kwargs)
        data=query(conn)
        conn.close()
        return Response(str(data))

    def test2(self):
        conn=persist.connection()
        data=query(conn)
        conn.close()
        return Response(str(data))

    def test3(self):
        conn=pooled.connection()
        data=query(conn)
        conn.close()
        return Response(str(data))

2   10线程并发

一共10000次测试,测试所用命令如下:

./ab -n 10000 -c 10 http://192.168.1.239:5000/testdb/test*

测试结果如下:

测试目标 总时间 请求处理速度 平均处理时间 错误率 100% 99% 90% 50%
/test1 32.764 305.22 32.764 ms 10.32% 776 237 40 29
/test2 27.895 358.49 27.895 ms 10.00% 3032 222 31 22
/test3 29.513 338.83 29.513 ms 10.46% 3037 58 36 27

3   50线程并发

一共10000次测试,测试所用命令如下:

./ab -n 10000 -c 50 http://192.168.1.239:5000/testdb/test*

测试结果如下:

测试目标 总时间 请求处理速度 平均处理时间 错误率 100% 99% 90% 50%
/test1 32.786 305.00 163.932 ms 9.48% 21031 3048 49 31
/test2 27.884 358.62 139.424 ms 9.65% 9227 3032 33 22
/test3 29.256 341.81 146.281 ms 9.88% 3654 328 151 136

4   远程10线程并发

一共10000次测试,测试所用命令如下:

./ab -n 10000 -c 10 http://192.168.1.241:5000/testdb/test*

测试结果如下:

测试目标 总时间 请求处理速度 平均处理时间 错误率 100% 99% 90% 50%
/test1 24.891 401.75 24.891 ms 9.07% 3035 44 31 22
/test2 21.652 461.85 21.652 ms 9.86% 256 59 26 19
/test3 23.952 432.99 23.095 ms 9.59% 239 38 28 22

5   远程50线程并发

一共10000次测试,测试命令如下:

./ab -n 10000 -c 50 http://192.168.1.241:5000/testdb/test*

测试结果如下:

测试目标 总时间 请求处理速度 平均处理时间 错误率 100% 99% 90% 50%
/test1 24.915 401.36 124.575 ms 9.82% 9280 3033 53 27
/test2 21.521 464.66 107.607 ms 9.47% 9621 3022 32 20
/test3 22.808 438.45 114.038 ms 9.11% 9107 145 114 95

6   干扰因素

测试过程中发现,MySQL服务器的同时并发连接数一直没有超过10,所以在进行50线程并发操作时可能会出现一些干扰。

7   单线程测试

使用代码如下:

import time
import random
import MySQLdb
import DBUtils.PersistentDB
import DBUtils.PooledDB

conn_kwargs={'host':'192.168.1.239','user':'ro','passwd':'','db':'test','port':3306}
sql="""SELECT * FROM test_table WHERE id=%d"""
persist=DBUtils.PersistentDB.PersistentDB(dbapi=MySQLdb,maxusage=1000,**conn_kwargs)
pooled=DBUtils.PooledDB.PooledDB(dbapi=MySQLdb,maxusage=1000,**conn_kwargs)

def query(conn):
    cur=conn.cursor()
    cur.execute(sql%(random.randint(1,1000)))
    data=cur.fetchall()
    cur.close()
    return data

def print_now():
    print time.strftime("%H:%M:%S")
    return

def test1(times):
    print_now()
    for i in range(0,times):
        conn=MySQLdb.connect(**conn_kwargs)
        query(conn)
        conn.close()
    print_now()
    return

def test2(times):
    print_now()
    for i in range(0,times):
        conn=persist.connection()
        query(conn)
        conn.close()
    print_now()
    return

def test3(times):
    print_now()
    for i in range(0,times):
        conn=pooled.connection()
        query(conn)
        conn.close()
    print_now()
    return

8   单线程测试

执行10000次查询,进入Python交互模式,调用各个函数并传递执行次数,每次执行过后重启MySQL服务器:

# python -i ttss.py
>>> test1(10000)
18:59:30
18:59:40
>>> test2(10000)
19:00:16
19:00:19
>>> test3(10000)
19:00:46
19:00:49

可见查询次数太少,以致难以精确测定时间,所以执行100000次查询,过程如下:

# python -i ttss.py
>>> test1(100000)
19:01:57
_mysql_exceptions.OperationalError: (2003, "Can't connect to MySQL server on '192.168.1.239' (99)")

连续两次都出现异常,之后改为30000也是如此。出现这个异常之后数据库服务器不经过重启就无法再使用了。经过测试发生这种连接异常之后,还是可以使用mysql客户端登录本机的MySQL服务器的。所以改为20000次查询,过程如下:

# python -i ttss.py
>>> test1(20000)
19:06:47
19:07:07
>>> test2(20000)
19:28:23
19:28:28
>>> test3(20000)
19:29:27
19:29:34

测试远程连接MySQL服务器:

# python -i ttss.py
>>> test1(10000)
20:25:23
20:25:57
>>> test2(10000)
20:27:18
20:27:26
>>> test3(10000)
20:27:46
20:27:56

9   结论

总体上来看,使用了DBUtils之后数据库的访问性能有了很大的提高。

Python并行编程之 Parallel Python文档页

Sunday, September 30th, 2007

Parallel Python文档页

原文: http://www.parallelpython.com/content/view/15/30/

目录

1   PP 1.4.2 模块API

class

Server

class Server

并行Python SMP执行服务器类

方法定义:

__del__(self)

__init__(self,ncpus=’autodetect’,ppservers=(),secret=None,loglevel=30,logstream=<open file ‘<stderr>’,mode ‘w’>)

创建服务器实例

ncpus – 本机要启动的工作进程数量,如果省略则为处理器数量

ppservers – 可用的PP服务器

secret – 网络连接密码,省略则使用缺省密码,简易在所有连接中使用一个自定义密码

loglevel – logging模块的日志级别

logstream – 日志目标

使用ncpus=1则所有任务串行执行。要获取最好性能则使用’autodetect’值,或手动设置CPU数量。

get_ncpus(self)

获取工作进程数量

get_stats(self)

返回所有工作执行状态字典

print_stats(self)

打印工作执行状态,用于集群测试标准。

set_ncpus(self,ncpus=’autodetect’)

设置工作进程数量

ncpus – 工作进程数量,如果省略则设置为系统处理器数量

submit(self,func,args=(),depfuncs=(),modules=(),callback=None,callbackargs=(),group=’default’,globals=None)

提交函数到执行队列

func – 需要执行的函数

args – func函数的执行参数tuple

depfuncs – 待执行函数需要调用的函数tuple

modules – 需要被import导入的模块名字tuple

callback – 在函数执行完成之后的回调函数,参数为callbackargs+(result,)

callbackargs – 附加的回调函数参数

group – 工作组,用于当 wait(group) 被调用,等待工作组全部工作结束时

globals – 字典,所有模块和函数共享与导入的,例如 globals=globals()

wait(self,group=None)

等待工作组中所有工作结束,如果忽略group参数则等待所有工作结束

数据和其他属性:

default_port = 6000

default_secret = ‘epo20pdosl;dksldkmm’

Data

copyright = ‘Copyright (c) 2005-2007 Vitalii Vanovschi. All rights reserved’

version = ‘1.4.2’

2   快速开始,SMP

  1. 导入pp模块:

    import pp
  2. 启动PP执行服务器,按照处理器数量指定工作进程数:

    job_server=pp.Server()
  3. 提交需要进行并行计算的任务:

    f1=job_server.submit(func1,args1,depfuncs1,modules1)
    f2=job_server.submit(func2,args2,depfuncs2,modules2)
    f3=job_server.submit(func3,args3,depfuncs3,modules3)
    # ...etc...
  4. 取回结果:

    r1=f1()
    r2=f2()
    r3=f3()
    # ...etc...

查找如何使任务并行化,参考例子 例子

3   快速开始,集群

  1. 在各个计算节点上启动并行Python执行服务器:

    node-1> ./ppserver.py
    node-2> ./ppserver.py
    node-3> ./ppserver.py
  2. 导入pp模块:

    import pp
  3. 创建集群中计算节点列表(就是运行ppserver.py的机器):

    ppservers=("node-1","node-2","node-3")
  4. 使用计算节点列表和其他参数来启动执行服务器:

    job_server=pp.Server(ppservers=ppservers)
  5. 提交并行执行任务,同SMP的例子

  6. 取回结果,同SMP的例子

4   高级指南,集群

  1. 在各个计算节点上启动并行Python计算服务器,监听本地35000端口,仅接受密码正确的连接:

    node-1> ./ppserver.py -p 35000 -i 192.168.0.101 -s "mysecret"
    node-2> ./ppserver.py -p 35000 -i 192.168.0.102 -s "mysecret"
    node-3> ./ppserver.py -p 35000 -i 192.168.0.103 -s "mysecret"
  2. 导入pp模块,同SMP例子

  3. 创建集群中计算节点列表,就是运行ppserver.py的机器:

    ppservers=("node-1:35000","node-2:35000","node-3:35000")
  4. 启动pp执行服务器,同时指定集群列表和验证密码:

    job_server=pp.Server(ppservers=ppservers,secret="mysecret")
  5. 提交并行计算任务,同SMP例子

  6. 取回结果,同SMP例子

  7. 显示执行状态:

    job_server.print_stats()

5   命令行参数,ppserver.py

Usage: ppserver.py [-hd] [-i interface] [-p port] [-w nworkers] [-s secret]
Options:
-h                 : 显示这个帮助信息
-d                 : 调试
-i interface       : 监听所用的网口
-p port            : 监听端口
-w nworkers        : 工作进程数量
-s secret          : 认证密码

Python并行编程之 Parallel Python首页

Sunday, September 30th, 2007

Parallel Python首页

原文: http://www.parallelpython.com/

目录

1   简介

PP 是一个Python模块,提供了在SMP(多CPU或多核)和集群(通过网络连接的多台计算机)上并行执行Python代码的机制。轻量级,易于安装,并集成了其他软件。PP也是一个用纯Python代码实现的跨平台,开放源码模块。

2   功能

  • 在SMP和集群上并行执行Python代码
  • 易于理解和实现的基于工作的并行机制,便于把穿行应用转换成并行的
  • 自动构造最佳配置(默认时工作进程数量等同于系统处理器数量)
  • 动态处理器分配(允许运行时改变工作处理器数量)
  • 函数的工作缓存(透明的缓存机制确保后续调用降低负载)
  • 动态负载均衡(任务被动态的分配到各个处理器上)
  • 基于SHA的连接加密认证
  • 跨平台移植(Windows/Linux/Unix)
  • 开放源代码

3   开发动机

现代Python程序已经广泛的应用在商业逻辑,数据分析和科学计算等方面。其中广泛应用着SMP(多处理器或多核)和集群(通过网络连接的多台计算机),市场需要并行的执行的Python代码。

在SMP计算机上编写并行程序最简单的方法是使用多线程。尽管如此,使用 ‘thread’ 和 ‘threading’ 模块仍然无法在字节码一级实现并行。因为Python解释器使用GIL(全局解释器锁)来在内部禁止并行执行。这个GIL限制你在SMP机器上同一时间也只能执行一条字节码指令。

PP 模块正是为了解决这个问题而来,提供简单的方式实现并行Python应用。 ppsmp 在内部使用 进程IPC (进程间通信)来组织并行计算。并处理了所有内部的细节和复杂性,你的应用程序只需要提交工作任务并取回结果就可以了。这也是编写并行程序的最简单的方法。

为了更好的实现,所有使用 PP 的软件通过网络来连接和协作。跨平台和动态负载均衡使得 PP 可以轻松组织多平台、异构的集群计算环境。

4   安装

任何平台:下子阿模块压缩包,解压,运行setup脚本:

python setup.py install

Windows:下载和执行安装包。

5   文档

模块API

快速开始,SMP

快速开始,集群

高级指南,集群

命令行参数,ppserver.py

6   例子

并行Python的使用方法

7   下载

并行Python下载

8   技术支持

并行Python讨论区 提供帮助和支持。

9   帮助我们扩展,链接我们

<a href='http://www.parallelpython.com'>Parallel Python</a>

Twisted的WEB开发

Sunday, September 30th, 2007

Twisted的WEB开发

作者: gashero <harry.python@gmail.com>

目录

1   简介

在WEB开发中,偶尔需要对HTTP协议更多底层细节进行控制,这时的django/web.py等等显然无法满足要求,所以只好求助于Twisted了。使用Twisted进行WEB开发,其实更合适的叫法应该是基于HTTP服务器的开发,因为Twisted相对底层,所以可以控制的东西也比较底层。

在Twisted的技术体系中,这个WEB开发实际上要涉及到HTTPChannel、HTTPFactory、Request三个层次的开发,以下详诉。

HTTP协议参考 RFC2616

2   Twisted技术体系

Twisted技术体系包含2个层次:协议和工厂。协议负责连接成功以后对交互的处理,而工厂则是负责连接过程。在HTTP协议中,连接之后还有个生成HTTP请求报文的过程,所以构造出了一个Request对象来处理具体的一个HTTP请求的报文。

在HTTP中的请求报文处理对象是 twisted.web.http.Request 类;HTTP的协议类是 twisted.web.http.HTTPChannel ;HTTP工厂是 twisted.web.http.HTTPFactory 。

3   一个简单的例子

节选自《Twisted网络编程必备》:

from twisted.web import http

class MyRequestHandler(http.Request):
    pages={
        '/':'<h1>Home</h1>Home Page',
        '/test':'<h1>Test</h1>Test Page',
        }
    def process(self):
        if self.pages.has_key(self.path):
            self.write(self.pages[self.path])
        else:
            self.setResponseCode(http.NOT_FOUND)
            self.write("<h1>Not Found</h1>Sorry, no such page.")
        self.finish()

class MyHttp(http.HTTPChannel):
    requestFactory=MyRequestHandler

class MyHttpFactory(http.HTTPFactory):
    protocol=MyHttp

if __name__=="__main__":
    from twisted.internet import reactor
    reactor.listenTCP(8000,MyHttpFactory())
    reactor.run()

与其他很多框架不同,TwistedWEB只有一个核心的请求处理类Request,各个针对不同的URL的请求也要通过这里来分发。而这个类只要重载 process() 方法就可以了,期间的很多数据都可以通过self来引用。

请求的处理流程也就是判断对不同URL的不同处理,然后向客户端写入响应信息,并在最后调用关闭请求。步骤如下:

  1. 过滤URL, self.path
  2. self.write(data) 向客户端写入数据
  3. self.finish() 关闭响应

4   Twisted WEB Request参考

来自分析 twisted.web.http.http.py 源代码。

4.1   请求

包含请求的数据,这里都是指Request类的成员。

channel :包含上级的HTTP协议对象。

transport :通信对象。

method :HTTP方法,如GET和POST。

uri :全部请求的URI。

path :具体的请求路径,不含参数。

args :请求参数,包括URL参数和POST参数。格式如 {'key':['val1','val2'],}

received_headers :请求报文的头字段。

received_cookies :请求报文的cookie。

content :请求报文的实体主体,文件对象。

clientproto :发出请求的客户端的HTTP版本。

client :?

host :?

getHeader(key) :获取请求的头字段。

getCookie(key) :获取请求的cookie。

getAllHeaders() :所有请求的头字段字典,就是返回received_headers。

getRequestHostname() :请求的host字段,不含端口号。

getHost() :原始请求的通信地址,返回host。

getClientIP() :获取客户端IP。

getUser() :获取basic验证中的用户名。

getPassword() :获取basic验证中的密码。

getClient() :?

4.2   响应

包含响应的数据,这里都是Request类的成员。

headers :字典,包含响应报文的头字段。

cookies :字典,包含响应报文的cookie。

finish() :结束响应报文。

write(data) :向客户端发送数据,经过了HTTP包装了。

addCookie(k,v,expires=None,domain=None,path=None,max_age=None,comment=None,secure=None) :为响应报文添加一个cookie。

setResponseCode(code,message=None) :设置响应代码,code参考常量定义。

setHeader(k,v) :设置头字段。

redirect(url) :HTTP重定向。

setLastModified(when) :设置缓存超时,when的值为长整型的那个时间。

setETag(etag) :设置缓存标志,用于在内容更改时让用户有所发觉。

setHost(host,port,ssl=0) :设置请求地址。用于代理服务器的重定向。

4.3   常量

没有响应主体的code:

NO_BODY_CODES=(204,304)

responses=RESPONSES :字典,保存了各个响应码的对应提示信息。

响应报文中的响应码:

OK=200 :请求处理成功,最常见的响应代码,但是正因为常见,所以默认就是这个了,也无须设置到setResponseCode。

NOT_MODIFIED=304 :请求的资源没有没有修改过,用于浏览器缓存。

BAD_REQUEST=400 :请求报文有语法错误。

UNAUTHORIZED=401 :尚未认证,要求用户输入认证信息。

FORBIDDEN=403 :禁止访问。

NOT_FOUND=404 :请求的资源不存在。

INTERNAL_SERVER_ERROR=500 :服务器内部错误。

NOT_IMPLEMENTED=501 :该功能尚未实现。

BAD_GATEWAY=502 :请求路径错误。

4.4   HTTPChannel

构造函数无参数,处理HTTP报文。

requestFactory=Request :指定了请求报文处理工厂。

4.5   HTTPFactory

__ini__(logPath=None,timeout=60*60*12) :构造函数可以设置日志和超时。

buildProtocol(addr) :内部的构造协议对象的方法,不要调用。

protocol=HTTPChannel :设置协议对象。

5   比较完善的开发模式

建立一个Request类的子类作为请求工厂,或者说请求发布器,其中有识别不同的URL并的能力,通过字典找到该URL对应的函数,调用这个函数并传递self参数。每个具体的请求处理函数也只有1个request参数,返回数据都是直接写入request.write()中。

一般来说请求工厂的process()中需要设置响应类型,如网页的:

self.setHeader("Content-Type","text/html; charset=GB2312")

同时也要在没有对应的URL时告知客户端找不到:

self.setResponseCode(http.NOT_FOUND)
self.write("<h1>Not Found</h1>Sorry, no such page.")
self.finish()

至于self.finish()放在各个请求处理函数中还是放在process(),就是个人爱好问题了。比较推荐放在process()中。

提取请求参数的重点在request.args字典,每个键都是映射到一个列表,为了适应HTTP提交中一个键对应多个值的情况,当然,你也可以只取第一个值。

6   以resource方式提供WEB资源

  1. 每个资源都是 twisted.web.resource.Resource 的子类
  2. 可以自己定义构造函数
  3. 要重载 render(self,request) 方法来响应请求
  4. render 方法中的request对象实际就是Request的实例

一个例子:

from twisted.web import resource,static,server

class HomePage(resource.Resource):

    def render(self,request):
        request.write("Home Page")
        return

    def getChild(self,path,request):
        return AnotherPage() #另外一个Resource的子类

if __name__=="__main__":
    from twisted.internet import reactor
    root=resource.Resource()
    root.putChild('',HomePage())
    root.putChild('color',ColorRoot())
    root.putChild('style.css',static.File('style.css'))
    site=server.Site(root)
    reactor.listenTCP(8000,site)
    reactor.run()

可以通过各个Resource的构造参数传入path参数,用以后来寻找下级Resource的参数。

Note

关于Resource还有很多细节,但是对本文意义不大,所以略。

7   总结

总的来说,用Twisted来开发更适合于开发个框架,而不是直接做WEB应用,有如一直都很少有人直接用 mod_python 来开发WEB应用一样。

函数式编程-考验人品

Friday, September 28th, 2007

函数式编程-考验人品

好友jorge(附加形容词:牛人,超牛,巨牛无比,变态,下流)推荐我学函数式编程语言scheme,以前倒是考虑过erlang,不过实在没空所以暂时没有动手。不过Python中对函数式编程倒是有支持,所以最近尝试了一下。

问题如下,一个字符串形式的IP地址,比如”12.24.0.9″,需要转换成32bit无符号整数形式表示的数字。这个其实可以用socket.inet_aton来实现,但是问题是,我拿到的这些IP地址都是这个形式”012.024.000.009″,来自珊瑚虫IP数据库的导出结果,而inet_aton会把这些一段段的以0开头的数字识别为八进制,所以,只能自己写转换函数。

最终呢,就是用函数式编程来解决了问题,实际代码只有一行。代码如下:

ipstr=”012.024.000.009″
numset=reduce(lambda x,y:x*256+y,map(lambda numlist:reduce(lambda x,y:x*10+y,numlist),map(lambda strlist:map(int,strlist),map(list,list(ipstr.split(‘.’,3))))))
from socket import inet_ntoa
import struct
print inet_ntoa(struct.pack(‘!L’,numset))

可以看出函数式编程的代码可读性确实河蟹的可以,我午饭前写的,现在就已经基本看不懂了。不过在TDD(测试驱动开发)的指导下,所有测试用例通过了,就算过了。

函数式编程看起来确实很酷,写这么长的语句一看就是人品很河蟹,所以有如国外的牛说的那样,他们一直希望取缔Python中的函数式编程支持。读者么如果有希望考验自己人品的,也可以多用用函数式编程。呵呵,先这样吧。

Pytho WEB 编程的学习方法

Wednesday, September 12th, 2007

Pytho WEB 编程的学习方法

日期: 2007-09-12

目录

Python是一种开发效率较高的语言,因此比较适合需求快速变化并且要求快速开发的WEB开发领域。在此对Python的WEB开发做一些简要介绍并对已有的PythonWEB编程技术的学习方法进行一些分析。

1   Python WEB开发现状

Python的快速开发能力使得其在WEB开发中应用广泛,因此也诞生了很多Python的WEB开发框架。Python的WEB开发框架较多一方面依赖于Python的方便性,另一方面也导致了入门时选择框架的麻烦。先进比较流行的几种Python WEB开发框架有Django、TurboGears、Zope等。

同时,由于很多Python WEB开发框架都是基于MTV模式(区别于MVC),所以也同时产生了很多种模板系统,比较流行的有Cheetah等几种。

为了快速的实现数据库的定义与关联,也产生了几种数据库操作框架,流行的几种包括SQLObject、SQLAlchemy、DBUtils等等。

同时又有一些WEB框架本身使用自己专用的模板系统和ORM系统,比如django、Zope等。

虽然面对林林总总的这么多选择,让人总是不知所措,但是实际上各类框架之间有很多相似之处,也存在很多重复实现。只要学通了其中一套框架体系(包含WEB框架、模板和ORM),其他的框架体系也就都可以触类旁通了。

另外,现在的Python WEB开发也正在逐渐向着以django为主的集中靠拢,所以,本文郑重推荐初学者学习django。

2   WEB开发基础-HTTP

2.1   GET请求

撰写中……

2.2   POST表单

撰写中……

3   WEB开发的基本工具

3.1   重定向

撰写中……

3.2   内部重定向

撰写中……

3.3   cookie

撰写中……

3.4   session

撰写中……

4   几种Python WEB开发框架的对比

这里仅对比如下几种比较流行的WEB开发框架,不求全,但求对比。数据取自2007年9月12日。

框架/功能 django TurboGears Zope CherryPy web.py Twisted mod_python
开始时间 2005.7 2005.9 199x.x 2004.11 2006.1 2001.7 2000.10
书籍数量 1 2 N 1 0 1 1+
单一/组合 单一 组合 单一 不完善 单一 不完善 单一
开发难度 一般 一般 较难 简单 超简单 较难 一般
google搜索结果 2.6M 2.12M 2.48M 1.53M 0.78M 2.11M 2.49M
google中文结果 0.68M 0.12M 0.63M 58.4K 25.7K 0.31M 78.6K

4.1   django

比较推荐,详见下面介绍。

4.2   TurboGears

一套组合框架,HTTP处理使用cherrypy,ORM可以使用SQLObject或SQLAlchemy,模板系统默认为kid,可以更换。作为一套组合框架,有着很多个不同的组件。从TG的哲学上讲这叫不重新发明轮子。但是从我Pylons的经验来看,使得各个组件可以和谐的工作在一起也是很困难的。至少我就曾经因为差不多的Pylons使用的某个组件更新版本的差异最终放弃了Pylons。

TurboGears的优点很明显,有大部头的书,文档丰富。另外,就是其版本已经到了1.0.3了,稳定性值得信赖,另外对于开源软家来说,1.0以上的版本意味着接口的稳定。从表格中可以看出,国内用TG的人很少。我最初对TG的好感也仅限于用过CherryPy一段时间。

4.3   Zope

Python中的巨无霸WEB开发框架,曾经是绝对的选择,后来么,因为学习成本的问题而使用略少了一些。对于初学者不太推荐,因为稍微重量级了一点,可以对比J2EE。至于稳定性,据说很多国外政府和军队的网站在用。而且基于Zope开发的Plone是世界排名第三位的CMS系统。文档么,有中文的书籍支持,英文的更不必说。当然,Zope也是一套组合的框架,一些在Python WEB开发方面的机制当初都是Zope开创的,另外在面向对象数据库等方面,Zope也拥有领先的优势。

如果将来要做Python的企业级WEB开发,绝对推荐。

4.4   CherryPy

一套比较简单的WEB开发框架,只含有WEB开发框架。开发很简单,是并不算是方便,因为ORM和模板等等需要自己来选择和配置。比较适合开发实验系统或为其他应用提供一个WEB管理界面。

如果需要学习TurboGears也推荐先从CherryPy学起,这样很多内部机制就好理解了。

4.5   web.py

一套非常简单的框架,不过并不是非常简洁。只提供WEB开发框架而没有其他东西,但是内部做了对流行的ORM和模板系统的导入引用。使用方式在WEB框架里面是比较独特的,只需要把web.py这一个文件放到工程目录里面带走即可,到其他机器上运行时也无需安装,直接可以使用。因此更加适合做其他系统的WEB管理界面。

如果有兴趣推荐读一下web.py的代码,里面有很多先进的思想对未来的编程很有帮助。

4.6   Twisted

Twisted是一套巨无霸的网络编程框架,其WEB开发不过是HTTP协议实现的一个附带功能,如果希望了解足够深的底层知识并控制这些底层细节,那么推荐。因为连HTTP协议的某些细节你都要照顾到。

所以呢,如果你希望提供一些超高性能的WEB接口,比如AJAX应用,XMLRPC服务器等等,那么强烈推荐,因为这个东东真的是太快了。在我的测试中AMD64 x2的机器用apache ab测试,100并发时每秒可以处理1500个HTTP请求。这速度已经没得挑了。

4.7   mod_python

apache-httpd服务器的Python解释器插件,允许直接控制apache服务器来提供WEB功能。速度比较快,但是速度不是强项。强项是稳定性,拖apache的福,在多进程中使用的mod_python稳定性巨牛无比,即便是死掉一个子进程对整体服务器也没什么影响。但是mod_python一般作为如上多种WEB开发框架的Apache-httpd的API接口来使用,提供高可靠性的服务器前端。至于直接用mod_python编程呢,提供了PSP、publisher、自定义模块等几种方式。总体来说也不难,但是大家习惯了把它当底层了,所以也就没人愿意学了。

文档方面,虽然google中文结果很少,但是却又两份官方文档翻译,这在这些框架里面文档的权威性仅次于Zope。反倒是其他框架的中文资料里面hello world满天飞。国内现在仅有的两家提供Python虚拟主机的,有一家就是专门做mod_python的,当然,另外一家专门做django。

5   几种模板系统的对比

撰写中……

6   几种ORM对比

撰写中……

7   Python WEB 快速入门

有如上面所说的,Python的WEB编程,各个框架之间相似度很大,可以按照一定的步骤了解几个重要的知识点,就可以迅速的过渡到另一个框架了。推荐步骤如下:

  1. 开发服务器的启动和端口配置
  2. hello world程序
  3. 返回字符串的方法
  4. 调用模板的方法(对单一框架则没有)
  5. URL组织
  6. 重定向
  7. 提交参数处理
  8. cookie & session
  9. 调用ORM的方法(对单一框架则没有)
  10. 附加功能,如表单生成与自动验证(TurboGears、Django)、通用模板(django)、中间件支持(Pylons…)、内置认证、AJAX支持等等
  11. 服务器部署

8   django

撰写中……

在Python独立访问django数据模型

Tuesday, September 11th, 2007

在Python独立访问django数据模型

在需要使用其他独立运行的程序调用django的数据模型时往往会提示导入数据模型时的EnvironmentError异常。其实这都是因为没有正确的找到django的配置文件所造成的。这其实也难怪django,连数据库的连接信息都在settings.py中,没有指定这个文件当然没法使用模型了。

好了谈谈解决方法吧。按照django官方手册上的讲解,就是如下:

$ ./manage.py shell

这样就可以直接进入Python交互模式了,并且可以访问很多东东。

但是问题时,有时候需要使用其他独立运行的Python脚本来访问数据模型,也就不是在交互模式下。这时可以自己设置环境变量DJANGO_SETTINGS_MODULE来达到同样的效果。

对于交互模式可以这样运行:

$ DJANGO_SETTINGS_MODULE=settings python

这样的解释器就可以直接使用其他环境变量了。

也可以先设置好环境变量,然后再使用Python,不过这时如果系统当中有多个django工程时就比较麻烦了,总之自己考量吧。方法如下:

$ export -p DJANGO_SETTINGS_MODULE=settings
$ python

另外,有如前面说的,在有多个工程时,如果需要启动另外一个,则可以清除环境变量,如下:

$ export -n DJANGO_SETTINGS_MODULE

这样就又恢复到原来的样子了。

祝大家django愉快。

gashero的Pylons笔记

Tuesday, September 11th, 2007

gashero的Pylons笔记

目录

1   《Pylons初探》笔记

1.1   安装

  1. 下载安装工具ez_setup.py

  2. 命令行运行如下:

    python ez_setup.py Pylons
  3. 设置环境变量

    pyth -> C:Python24Scripts

  4. 运行命令paster,如果运行成功会出现如下结果:

    Usage: C:\Python24\Scripts\paster-script.py COMMAND
    ... ...

1.2   开始动手

  1. 新建一个Pylons工程:

    paster create --template=pylons helloworld

    运行成功后产生如下结果:

    Selected and implied templates::
    pylons#pylons  Pylons application template
    ... ...
  2. 运行这个新建的工程

    1. 启动服务器:

      cd helloworld
      paster serve --reload development.ini
    2. 访问 http://127.0.0.1:5000/ 将会看到欢迎页面

    3. helloworld/public 目录下创建test.html文件,内容如下:

      <html>
          <body>
              Hello world!
          </body>
      </html>
    4. 访问 http://127.0.0.1:5000/test.html 看到结果。

  3. 禁用调试功能

    将development.ini文件中的如下行的注释删除:

    # set debug=false
  4. 创建一个控制器,修改Routes

    1. 命令行运行:

      paster controller hello
    2. 修改 helloworld/controllers/hello.py ,代码如下:

      from helloworld.lib.base import *
      
      class HelloController(BaseController):
          def index(self):
              return Response('hello world')
    3. 修改 helloworld/config/routing.py ,代码如下:

      """
      Setup your Routes options here
      """
      import sys,os
      from routes import Mapper
      
      def make_map(global_conf={}, app_conf={}):
          root_path=os.path.dirname(os.path.dirname(\
                  os.path.abspath(__file__)))
          map=Mapper(directory=os.path.join(root_path,'controllers'))
          # 这个route handle用来显示错误页面
          map.connect('error/:action/:id', controller='error')
          #定义自己的route,可以定义很多细节
          #更多有用的信息参考http://routes.groovie.org/docs/
          map.connect('',controller='hello',action='index')
          map.connect(':controller/:action/:id')
          map.connect('*url',controller='template',action='view')
          return map
    4. 删除 public/index.html 。访问 http://127.0.0.1:5000/hellohttp://127.0.0.1:5000/ 将会看到 Hello world。

  5. 模板和请求周期

    1. 创建模板文件 helloworld/templates/serverinfo.myt ,代码如下:

      <p>Hi, here's the server environment: <br />
      <% str(request.environ) %></p>
      <p>
      and here's the URL you called: <% h.url_for() %>
      </p>
    2. 修改 helloworld/controllers/hello.py ,代码如下:

      from helloworld.lib.base import *
      class HelloController(BaseController):
          def index(self):
              return Response('hello world')
          def serverinfo(self):
              return render_response('/serverinfo.myt')
    3. 访问 http://127.0.0.1:5000/hello/serverinfo ,可以看到如下结果:

      Hi, here's the server environment:
      ... ...
      and here's the URL you called: /hello/serverinfo
  6. 使用Session,代码如下:

    def serverinfo(self):
        session['name']='George'
        session.save()
        return render_response('/serverinfo.myt')
  7. 控制器变量和模板全局变量

    控制器变量

    1. 修改 helloworld/controllers/hello.py ,代码如下:

      from helloworld.lib.base import *
      class HelloController(BaseController):
          def index(self):
              return Response('hello world')
          def serverinfo(self):
              c.value=2
              return render_response('/serverinfo.myt')
    2. 修改 helloworld/templates/serverinfo.myt ,代码如下:

      <p> The value of <tt>c.value</tt> is:
      <% c.value %>
    3. 访问 http://127.0.0.1:5000/hello/serverinfo ,可以看到 The value of c.value is: 2

    模板全局变量

    1. 修改 lib/app_globals.py ,代码如下:

      class Globals(object):
          def __init__(self,defaults,app,**extra):
              self.message='Hello'
          def __del__(self):
              """
              将清空代码放在这里
              """
              pass
    2. 修改 helloworld/controllers/hello.py ,代码如下:

      from helloworld.lib.base import *
      class HelloController(BaseController):
          def index(self):
              return Response('hello world')
          def serverinfo(self):
              c.value=2
              return render_response('/serverinfo.myt')
          def app_globals_test(self):
              resp=Response()
              if g.message=='Hello':
                  resp.write(g.message)
                  g.message='Hello world!'
              else:
                  resp.write(g.message)
              return resp
    3. 访问 http://127.0.0.1:5000/hello/app_globals_test/ ,可以看到 Hello world!

  8. 原文网址 http://pylonshq.com/docs/0.9.3/getting_started.html

2   Pylons首页信息

Pylons的最新版本为0.9.4.1,发布于2007-01-06。这是一个bugfix版本,原版本0.9.4发布于2006-12-29。上面那篇文章的对应版本为0.9.3,看来差的 还不是太远。

Pylons是一个轻量级的WEB开发框架,强调灵活性和快速开发。

2.1   为什么使用Pylons

Pylons结合了Ruby、Python和Perl的优点,提供了结构化的,但是却非常灵活的Python WEB开发框架。他也是首个支持新出现的WSGI标准的项目,在需要时允许广泛的重用和移植。Pylons的目标是使得WEB开发更加快速、灵活和简单。

更多信息

安装

开始学习

2.2   与其他组件一起使用

Pylons基于 Paste 并支持其他常见的Python组件:

2.3   最新的入门教程

2.4   有用的资源

你可能对 使用了Pylons的站点 感兴趣。如果你需要的信息在 文档 中是没有的,则可以参考 wiki ,这里的信息会有更快的更新,并且有更加全面的资源。当然,你也可以自己 添加文章

2.5   顶部导航栏链接

主页 文档 wiki 社区 FAQ 安装 Pastebin 查找文档

3   安装Pylons

首先,Windows上的Python2.3用户需要先安装 subprocess.exe (下载失效),而Python2.4用户则不需要。所有的Windows用户在安装完成后需要研读 安装后必读 ,而Ubuntu和Debian用户则需要安装 python-dev 包。

3.1   系统级安装

如果需要安装好的Pylons被所有用户所使用,就需要使用root帐户。如果你已经拥有了easy install,则可以执行如下命令:

$ easy_install Pylons

Note

偶尔python.org会挂掉。这时仍然可以用本地的文件来安装Pylons及其以来的包。:

easy_install -f http://pylonshq.com/download/ Pylons

这样会自动安装最新版本的包。如果你正在使用一个旧版本,则可以指定版本:

easy_install -f http://pylonshq.com/download/0.8.2/ Pylons

否则,先下载 easy install ,地址为 http://peak.telecommunity.com/dist/ez_setup.py 。然后运行:

$ python ez_setup.py Pylons

Note

你可以选择安装一个其他的扩展(extra),如下:

$ easy_install Pylons[extra]

pudge :支持构建构建文档,查看 为应用添加文档 。需要注意的是很多文档工具还处于开发中,并不是很稳定。

genshi :支持使用 genshi 模板,查看 使用其他模板语言

cheetah :支持使用 cheetah 模板,查看 使用其他模板语言

kid :支持使用 kid 模板,查看 使用其他模板语言

full :如上所有的

3.2   自定义安装位置

如果需要自定义安装位置,则可以参考:

http://peak.telecommunity.com/DevCenter/EasyInstall#custom-installation-locations

查看Easy Install的文档可以了解Pylons是否是安装为系统级。

http://peak.telecommunity.com/DevCenter/EasyInstall

3.3   升级安装

升级到最新版本使用:

$ easy_install -U Pylons

3.4   工作于前沿(Living on the Cutting Edge)

如果不满意于最新的发行版,或者希望使用源代码工作,可以使用最新的开发版本:

http://pylonshq.com/docs/0.9.4.1/pylonsdev.html

3.5   Easy Install 的问题

有时候如果Pylons依赖的包安装失败会导致Easy Install出错。这时候确保 setuptools 是最新版本,使用如下命令:

$ easy_install -U setuptools

然后再次尝试安装Pylons,如果再次安装失败,就先手动安装Pylons依赖的软件包,然后再安装Pylons。例如,如果你安装Myghty时,可以指定最新版本的位置:

$ easy_install -U -f http://pylonshq.com/download/ Myghty

或者你也可以直接使用URL上的发行版来安装。这时我们将会从cheeseshop安装Myghty 1.1,但是你必须确保安装当前Pylons版本所使用的文件:

$ easy_install -U http://cheeseshop.python.org/packages/source/M/Myghty/Myghty-1.1.tar.gz#md5=5865361811dca4054f1ec60ac32ee965

3.6   软件包注释

Pylons使用的 Cheetah、docutils、elementtree、nose等等软件包多多少少的被 James Gardner 修改过,所以可以被setuptools和Python2.3方便的安装和使用。这些软件包的某些安装指令需要手动调整。如果你不打算使用Pylons提供的这些软件包版本,Pylons开发组并不确保可以工作。

4   启动Pylons之后,主页上的例子

helloworld/public/ 目录在运行之前搜索需要显示的静态文件。删除文件 helloworld/public/index.html 然后修改路由(Route) helloworld/config/routing.py 成如下的样子:

map.connect('',controller='hello',action='index')

新建一个hello.py文件,内容如下:

# helloworld/controllers/hello.py
from helloworld.lib.base import *
class HelloController(BaseController):
    def index(self):
        return Response('Hello world')

然后访问 http://127.0.0.1:5000/hello 可以看到结果。

使用模板的例子,Myghty模板:

# helloworld/templates/serverinfo.myt
<p>Hi, here's the server environment: <br />
<% str(request.environ) %></p>
<p>here's the URL you called: <% h.url_for() %> </p>
<p>and here's the name you set: <% c.name %></p>

然后将这些添加到你的hello控制器类当中:

def serverinfo(self):
    c.name='The Black Knight'
    return render_response('/serverinfo.myt')

然后通过 http://127.0.0.1:5000/hello/serverinfo 来访问。

5   《Getting Started》比《Pylons初探》增加的内容

尽管《Pylons初探》就是翻译自《Getting Started》,但是版本并不相同。现在版本比原版本增加了一些内容。

http://pylonshq.com/docs/0.9.4.1/getting_started.html

5.1   简介

如果尚未完成安装,阅读 安装过程 。这个文档引导用户快速入门。

5.2   创建一个Pylons工程

Pylons使用Paste创建和部署工程,当然还可以创建控制器及其单元测试。

创建一个新工程叫做 helloworld 使用如下命令:

paster create --template=pylons helloworld

Note

Windows用户必须先配置 PATH 环境变量在 安装后必读 。至少应该可以在控制台中得到 paster 的路径。

这将会创建一个基本的工程目录,目录具有如下结构:

-helloworld
    -data
    -helloworld
    -helloworld.egg-info
        -Various files including paste_deploy_config.ini_tmpl
    -development.ini
    -setup.cfg
    -setup.py

文件 setup.py 用于创建自安装包,叫做 eggegg 可以看作等同于Java中的 .jar 文件。而 setup.cfg 包含了工程的其他信息,目录 helloworld.egg-info 包含关于egg的信息,包括一个 paste_deploy_config.ini_tmpl 文件用作工程使用的配置模板,可以用 paster make-config 命令来通过这个模板创建配置文件。发行和部署工程可以参考 发行工程 ,而最终用户可以参考 安装应用

注意 data 目录,这个目录可以被 development.ini 文件所配置,并用于保存缓存数据和会话。

helloworld 中的 helloworld 目录用于放置应用程序的特定文件。目录结构如下:

-helloworld
    -helloworld
        -config
        -controllers
        -docs
        -i18n
        -lib
        -models
        -public
        -templates
        -tests
        -__init__.py
        -websetup.py

config 目录包含了WEB应用的配置选项。

controllers 目录放置控制器。控制器是程序的核心,决定了装入哪些数据,和如何显示。

docs 目录用于放置程序的文档。你可以使用 setup.py pudge 来将他们转换为HTML。

i18n 目录用来放置多语言信息编目。

lib 目录用来放置各个控制器之间的共享模块,第三方代码,或者其他合适的代码。

models 目录用来放置模型对象,比如ORM使用的类。在 models/__init__.py 中的对象定义将会被装入并作为模型 model.YourObject 。数据库的配置字符串保存在 development.ini 文件。

public 目录用来放置HTML、图像、JavaScript、CSS等等静态文件。有如apache的htdocs目录。

tests 目录用来放置控制器等等的单元测试。控制器的单元测试使用Nose和 paste.fixture 。

templates 目录来保存模板。模板包含混合的静态文本和Python代码,用来生成HTML文档。Pylons使用Myghty模板作为缺省,但是同时支持Cheetah、Kid等等其他多种模板,这些需要通过Buffet来调用。查看 如何使用其他模板

__init__.py 文集那将 helloworld 目录作为一个Python包来发布。

websetup.py 文件包含了用户在运行 paster setup-app 时运行的代码,参考 安装应用 。如果希望在应用执行之前运行一些代码,这里比较好。

5.3   尝试使用模板

我们可以尝试一个模板工程如下:

$ cd helloworld
$ paster serve --reload development.ini

这些命令会使用配置development.ini进行启动并提供Pylons服务。

选项 –reload 会在Python代码或 development.ini 文件发生改变时自动重启服务器。这在开发中是非常有用的。

如果访问 http://127.0.0.1:5000/ 你可以看到欢迎页面( 127.0.0.1 是本机IP),但是可以通过 development.ini 文件来修改。

尝试创建一个新文件 test.html 在目录 helloworld/public 中,内容如下:

<html>
    <body>
        Hello world!
    </body>
</html>

如果访问 http://127.0.0.1:5000/test.html 可以看到 Hello World! 这个信息。任何在 public 目录下的文件都回按照相同的名字对外提供服务,Pylons有个选项可以决定从 public 目录返回,还是从代码返回。这个行为可以通过修改 config/middleware.py 文件中的 Cascade 的顺序来改变。

5.4   交互调试器

交互调试器是开发中的强大工具。他在使用 development.ini 作为配置时是默认启用的。当启用时,他允许通过一个WEB页面寻找错误发生的位置。在产品环境中调试器将会降低安全级别,所以通过 paster make-config 生成的产品环境配置文件需要关闭调试器。

关闭调试器,取消如下的注释,在 development.ini 文件的 [app:main] 节中:

# set debug=false

改为:

set debug=false

然后。将debug设置为false就是为了在产品环境提高安全系数。

更多相关信息参考 交互调试器 文档。

5.5   创建控制器并修改URL路由

你现在可以创建一个自己的WEB应用了。首先给hello world创建一个控制器:

$ paster controller hello

命令 paster 会创建 controllers/hello.py 文件,并同时创建其单元测试文件 helloworld/tests/functional/test_hello.py 用来运行这个控制器的单元测试。

如下是用于在 http://127.0.0.1:5000/hello 打印 ‘Hello world’ 的简单控制器代码。将这些代码放入 helloworld/controllers/hello.py 文件中:

from helloworld.lib.base import *
class HelloController(BaseController):
    def index(self):
        return Response('Hello world')

Pylons使用一个强大的并且灵活的URL路由来控制代码和URL的映射。

我们希望同时在 http://127.0.0.1:5000/hellohttp://127.0.0.1:5000/ 下显示,可以通过如下路由。将如下的行添加到配置文件 helloworld/config/routing.py ,如下:

map.connect('',controller='hello',action='index')
map.connect(':controller/:action/:id')
map.connect('*url',controller='template',action='view')

这样就可以将空URL匹配到 hello 控制器的 index 行为上,否则就按照 controller/action/id 的方式匹配,当然要确保这些可以匹配到。如果确实匹配不到了,就转到 templates 控制器的 view 行为,显示404错误。

改变了URL的路由之后必须重启服务器。但是如果你有开启 --reload 选项则会自动重启服务器,要不就要手动关闭和启动。

Note

Myghty模板的改变不需要重启服务器,也不需要 --reload 就可以起效。

访问 http://127.0.0.1:5000/hellohttp://127.0.0.1:5000/ 可以看到第一个显示 Hello world ,而第二个显示从前的欢迎页面。这是因为在 public 目录下的静态文件要优先于代码被显示。

删除文件 public/index.html 可以得到预期效果。更多信息参考 URL路由手册

5.6   模板和请求周期

当你的控制器的方法被WSGI应用所请求并返回Response对象时,如果希望使用模板返回,可以使用 command 命令,或者 render_response 命令。随之会处理模板并生成响应(Response)对象。

Note

如果希望获得更多关于 renderrender_reponse 的信息,参考 Pylons模板API

下面的例子模板,使用Myghty,打印一些信息。

创建一个模板文件 helloworld/templates/serverinfo.myt ,包含如下内容:

<p>Hi, here's the server environment: <br />
<% str(request.environ) %></p>
<p>
and here's the URL you called: <% h.url_for() %>
</p>

使用这个模板,在 helloworld 的HelloController控制器添加新方法如下:

def serverinfo(self):
    return render_response('/serverinfo.myt')

函数 render_response('/serverinfo.myt') 会使用默认引擎(Myghty)处理模板。

如果服务器仍然在运行,可以查看 http://127.0.0.1:5000/hello/serverinfo

也可以简单的重启服务器,在helloworld目录执行 paster serve --reload development.ini

5.7   使用Sessions

会话的处理来自应用程序的 Beaker middleware ,其包含Myghty的容器API。提供了健壮和强大的Session和缓存能力。

使用Session是很容易的,如下是保存Session的例子:

def serverinfo(self):
    session['name']='George'
    session.save()
    return render_response('/serverinfo.myt')

Session的选项可以通过 development.ini 来定制,并且使用相同的名字,详见 Myghty docs for sessions

Note

记住每次调用 session.save() 在返回相应之前来确保Session的保存。

5.8   控制器变量和模板全局变量

5.8.1   Pylons全局变量

为了方便,有少数几个通过导入 lib.base 就可以使用的全局变量,你可以在控制器中随意使用:

sessioin :存储Session数据,参考 Myghty Session docs

request :请求对象。

Response :响应类,控制器一般来说应该返回这个类的实例。

abort :用于中断请求,并发送 HTTPException ,可以附带状态码。

redirect_to :通过HTTP 302状态码来重定向浏览器到其他地址的函数,通过HTTPException实现。

render :用来使用模板生成字符串的函数。

render_response :用来使用模板生成 Response 对象的函数,相当于 Response(render(...))

h :用于引用Pylons的其他工具函数。缺省时,Pylons会将工具函数装入自 Web Helper 包。阅读文档时应该记住这些函数都可以通过 h 来引用。在Pylons下面通过这个作为命名空间。

c :用来向模板传递变量。

g :应用程序全局变量对象,可持续的。

5.8.2   传递变量到模板

Pylons控制器被每个请求创建一次。这意味着你可以在 self 上面绑定需要使用的变量。当然,这样做不太好跟踪,除非你需要传递到模板。如果希望传递传递数据到模板,可以使用变量 c 。因为Myghty模板默认就是使用 c 作为全局变量的。下面是使用的例子:

def serverinfo(self):
    c.value=2
    return render_response('/serverinfo.myt')

然后修改 templates 目录中的模板 serverinfo.myt 文件成如下:

<p>The value of <tt>c.value</tt> is:
<% c.value %>

Note

c对象在其他模板语言中也是适用的。

你可以看到打印出来的 2 。当访问c的属性不存在时,不会发生错误,而是返回空字符串。这对控制响应行为很有用。例如:

<p>Hi there <% c.name or c.full_name or "Joe Smith" %>

Warning

不要随意设置以 “_” 开头的c的属性。c和其他很多全局变量都是 StackedObjectProxies 类的成员。这可能会与已有的内置方法发生冲突,导致一些难于调试的问题。

c对象在每次请求时都回初始化,所以不需要担心控制器使用了先前请求的旧值。

5.8.3   应用程序全局对象和持续对象

某些场合需要在各个控制器之间共享一些信息而不会在每个请求中重启。例如在应用程序启动时开启一个TCP连接。你可以通过 g 变量。

g 变量是一个 Globals 类的实例在文件 lib/app_globals.py 中。在 __init__() 中设置的属性对 g 来说都是应用级别的全局属性。在任何一个请求中对g的某个属性的修改都会保持下来。在请求中设置全局变量时必须小心。

下例是如何使用 g 变量。首先修改 lib/app_globals.py Globals 类,把 __init__() 方法如下:

def __init__(self,global_conf,app_conf,**extra):
    self.message='Hello'

然后添加一个新方法到 helloworld/controllers/hello.py 如下:

def app_globals_test(self):
    resp=Response()
    if g.message=='Hello':
        resp.write(g.message)
        g.message='Hello World!'
    else:
        resp.write(g.message)
    return resp

这时如果运行服务器在 http://127.0.0.1:5000/hello/app_globals_test ,你可以看到信息 Hello 。如果再次访问,将会改变到 Hello World! ,然后就保持这样的改变了,因为在首次请求时,变量就已经改变了。

__init__() 方法需要参数 global_confapp_conf ,这些配置保存在文件 development.ini 中。这意味着可以在这里就设置全局变量。注意在第一个请求没有建立之前不可以设置变量c和h。

可以给全局变量添加 __del__() 方法,确保在程序退出时执行。

6   《Windows用户安装后必读》笔记

安装后必读

7   调试中发现的问题

7.1   URL映射的定义

必须在一开始就进行URL映射的定义,否则无法从URL上访问到自己的模块。在文件 config/routing.py 中定义。定义格式如下:

map.connect('urlpath',controller='controller_name',action='index')

其中的action是干什么是不知道的,重要的只是指定URL和控制器。而且要注意,这个定义必须写在 make_map() 函数中,并且必须在如下的两个语句之前定义,因为这两个语句会尽可能匹配多的URL,放在这后面不会起作用:

map.connect(':controller/:action/:id')
map.connect('*url',controller='template',action='view')

当然,这种使用URL映射的方式,可以很好自定义URL的样式。

另外,只要手动重启服务器,不去 routing.py 定义URL映射,也是可以直接使用模块名来访问的。当然,这个所谓模块名是不包含 .py 的。

7.2   参数传递,方法参数传递

Pylons用于暴露的方法是允许接受参数的,当然,如果硬是不传递,也没有设置默认值的时候会由Pylons好心的传递一个None过来。这里有个例子:

class HappyController(...):
    def test(self,id):
        return Response('test'+id)

当然,顶层类是使用 paster controller happy 自动生成的,所以在URL中也是使用 happy 这个词的。而 test 则是其暴露出来供访问的方法。这时有几个URL可供调用,结果分别为:

URL 解释
/happy/test 返回异常,报告id为None不可以连接字符串
/happy/test/ 返回异常,报告id为None不可以连接字符串
/happy/test/50 id得到参数50,返回 test50

当然这里注意,这个暴露方法参数只是通过URL的分级方式提供的,而问号之后的查询参数是不会传递到这里的。

还要注意routing.py中的一句:

map.connect(':controller/:action/:id')

这句的意思是允许以 控制器名/方法名/id 的方法来调用,作为一种默认的调用方法。而注意这个 :id ,就是说,方法的参数名只能是 id ,如果使用其他的则会发生错误。

7.3   共享模块代码

工程目录中的lib目录可以用于放置一些共享模块。调用的方法么,举个例子,比如有个模块的文件名是helloworld/lib/testunit.py,那么导入语句就可以写作:

import helloworld.lib.testunit

这样,可以在lib目录中放置数据库访问模块等等,多种东东。

Python的logging模块

Tuesday, September 11th, 2007

Python的logging模块

翻译: gashero <harry.python@gmail.com>

目录

1   简介-Python文档

从Python2.3版本中开始引入的logging模块为应用提供了灵活的日志系统。

logging的行为依靠调用 Logger 类的方法来实现,实例一般叫做logger。每个实例都拥有自己的名字,并且可以通过点来分割具备层次的名字。例如,一个logger叫做”scan”是一个叫做”scan.text”的logger的顶层,当然也包括”scan.html”和”scan.pdf”。logger的名字指示了他属于应用程序的哪个位置。

日志信息也拥有级别指定其重要程度。缺省提供的级别包括DEBUG,INFO,WARNING,ERROR,CRITICAL(严重)。为了方便,你可以调用特定的方法来指定日志的重要性,对应的方法为 debug()info()warning()error()critical() 。你也不必拘泥于这些级别,你可以使用更为通用的方法 log() 并且手动指定其级别。

日志级别对应值如下表所示。了解这些有助于你定义自己的日志级别的相对关系。如果你定义的级别与已有的数值相同,则原有的级别会被覆盖。

级别 数值
CRITICAL 50
ERROR 40
WARNING 30
INFO 20
DEBUG 10
NOTSET 0

日志级别也可以在指定logger时定义,可以在开发时装入和存储配置。当调用logger的日志方法时,logger对比日志级别和自己的被指定的级别。如果本身的级别高于调用方法的级别,则不会记录这个日志信息。这是一个方便的方法用于忽略过多细节的日志。

日志信息会被编码为 LogRecord 类,当logger决定记录一个事件时,就会为日志信息创建一个LogRecord实例。

日志信息会被发布(dispatch)机制通过handler来发布出去,这些handler继承自 Handler 类。Handler负责把日志信息(LogRecord的实例)发布到对应的位置供处理这些信息。Handler传递LogRecord实例并指定特定目标。每个logger都可以拥有0个或多个handler,通过 addHandler() 方法添加。除了handler与logger的组合之外,所有的handler还会被所组合的祖先logger所调用来发布信息。

有如logger,handler也可以有自己的级别过滤器,用于过滤日志级别。如果一个handler决定发布一个事件,可以调用 emit() 来发送信息到目的。大多数用户继承的Handler的子类需要重载这个emit()方法。

除了基本的Handler类,比较有用的子类如:

  1. StreamHandler实例发送错误到流(类似文件的对象)。
  2. FileHandler实例发送错误到磁盘文件。
  3. BaseRotatingHandler是所有轮徇日志的基类,不能直接使用。但是可以使用RotatingFileHandler和TimeRotatingFileHandler。
  4. RotatingFileHandler实例发送信息到磁盘文件,并且限制最大的日志文件大小,并适时轮徇。
  5. TimeRotatingFileHandler实例发送错误信息到磁盘,并在适当的事件间隔进行轮徇。
  6. SocketHandler实例发送日志到TCP/IP socket。
  7. DatagramHandler实例发送错误信息通过UDP协议。
  8. SMTPHandler实例发送错误信息到特定的email地址。
  9. SysLogHandler实例发送日志到UNIX syslog服务,并支持远程syslog服务。
  10. NTEventLogHandler实例发送日志到WindowsNT/2000/XP事件日志。
  11. MemoryHandler实例发送日志到内存中的缓冲区,并在达到特定条件时清空。
  12. HTTPHandler实例发送错误信息到HTTP服务器,通过GET或POST方法。

StreamHandler和FileHandler类都是在核心logging模块中定义的。其他handler定义在各个子模块中,叫做logging.handlers。当然还有一个logging.config模块提供了配置功能。

日志信息在输出之前会经过 Formatter 类的格式化。他们在最开始使用%操作符来设置。

批量格式化信息,需要 BufferingFormatter 类。除了格式化字符串之外,它还会在输出信息之前加上其他信息。

当基于logger和handler的级别过滤器仍然不够时,可以使用 Filter 类的实例来添加到logger和handler实例中,通过他们的 addFilter() 方法。当决定输出日志之前,logger和handler会询问过滤器,如果过滤器返回False,则不会显示这条日志。

最简单的过滤器功能用于指定日志的名字。如果使用了这个过滤器,则发送到命名logger和其子logger的日志将会通过,其他的会被丢弃。

除了上面所描述的,还有很多模块级别的函数。如下:

getLogger([name])

返回一个logger,可以指定名字,如果没有指定名字则返回根logger。指定的名字典型的为以点分隔的分层次的名字。选择一个恰当的名字,让别人知道,谁在输出日志。所有使用相同名字调用这个函数都会返回相同的logger实例。这意味着logger实例不需要在应用中到处传递。

getLoggerClass()

返回一个标准的Logger类,或者上次传递到 setLoggerClass() 的类。这个函数用于定义一个新的日志类,但是之前的代码不会受到影响。例如:

class MyLogger(logging.getLoggerClass()):
    #重载一些行为

debug(msg[,*args[,**kwargs]])

记录一个DEBUG级别的日志信息到根logger。 msg 参数就是用于输出的日志, args 参数是用于填入msg字符串的参数,当然kwargs也是差不多的角色。这说明msg可以是一个包含%d、%s等等的待格式化字符串。

在kwargs中有2个参数是必须的: exc_info 如果不为False,则异常产生的信息也会被记录;如果提供了一个异常tuple(以 sys.exc_info() 格式),则会使用它;否则会调用 sys.exc_info() 来获取异常信息。

另外一个参数为 extra 用于传递一个字典表示 LogRecord 中的 __dict__ 信息,可以加入用户自定义属性。这些传递的参数可以随你喜欢的。例如可能需要传递的:

FORMAT="%(asctime)-15s %(clientip)s %(user)-8s %(message)s"
logging.basicConfig(format=FORMAT)
dict={'clientip':'192.168.1.1','user':'fbloggs'}
logging.warning("Protocol problem: %s","connection reset",extra=d)

将会有如下的显示:

2006-02-08 22:20:02,165 192.168.1.1 fbloggs Protocol problem: connection reset

extra字典中的键不会与已有的键相冲突,具体可以参考Formatter的文档中关于使用字典的部分。

在选择字典中的键名时,必须小心一些情况。上面的例子中,已经在待格式化字符串中有’clientip’和’user’了,这时如果没有传递这两个键,则不会输出日志,因为出现了异常了。这种情况时,你必须要传递这2个键。

出于这种复杂性,这种功能往往用在很特殊的地方,比如多线程服务器,或关心多种信息的上下文环境,例如远程IP和登录用户。在这些情况中,需要先指定Formatter实例。 extra 参数从2.5版本开始加入。

info(msg[,*args[,**kwargs]])

记录INFO级别的日志信息,其他参数同debug()。

warning(msg[,*args[,**kwargs]])

记录WARNING级别的日志信息,其他参数同debug()。

error(msg[,*args[,**kwargs]])

记录ERROR级别的日志信息,其他参数同debug()。

critical(msg[,*args[,**kwargs]])

记录CRITICAL级别的日志信息,其他参数同debug()。

exception(msg[,*args])

记录ERROR级别的日志信息,其他参数同debug()。一般用在异常处理中。

log(level,msg[,*args[,**kwargs]])

记录level级别的日志信息,其他参数同debug()。

disable(lvl)

提供一个在logger中禁用某个级别日志的方法,用于应用临时切换可显示的日志。

addLevelName(lvl,levelName)

给级别lvl指定一个名字levelName到内部字典。以后用于映射数字的级别到特定的名字,供给Formatter用来格式化字符串。也可以用于定义自己的级别。这里也是唯一可以注册级别用的地方,级别必须是数字,并且在按照一定的排序要求。

getLevelName(lvl)

返回lvl级别的文字说明。如果级别是CRITICAL、ERROR、WARNING、INFO、DEBUG中的某一个,也会返回对应的名字。如果你自己通过addLevelName()自定义过级别,那么也会返回对应的名字。如果对应级别不存在,则返回 "Level %s"%lvl 这个字符串。

makeLogRecord(attrdict)

创建并返回一个LogRecord实例,并使用attrdict赋值过。这个函数用于把一个LogRecord给pickle过后通过socket发送,并方便接收方重组这个信息。

basicConfig([**kwargs])

对日志系统进行基本配置,使用缺省的StreamHandler和Formatter并添加根logger。一些函数如debug()、info()、warning()、error()、critical()会自动使用basicConfig()配置好的根logger进行日志输出。在2.4版之前,basicConfig()不接受字典参数。

如下是支持的字典参数:

格式 描述
filename 指定FileHandler的文件名,而不是StreamHandler
filemode 打开文件的模式,同open函数中的同名参数,默认为’a’
format 输出格式字符串
datefmt 日期格式
level 设置根logger的日志级别
stream 指定StreamHandler。这个参数与filename冲突,忽略stream

shutdown()

告知日志系统准备关闭日志并将所有信息写入磁盘。

setLoggerClass(klass)

告知日志系统使用类klass作为示例的Logger类。这个类应该定义 __init__() 方法并接受一个参数表示名字,而且 __init__() 方法应该在内部调用 Logger.__init__() 方法。这个函数一般在应用自定义Logger的行为时使用。

PEP 282, A Logging System

Original Python logging package

2   各种Handler的子类

2.1   TimedRotatingFileHandler

TimedRotatingFileHandler 类是在logging.handler包中,支持对写入到磁盘的日志按照时间间隔来轮询。

class TimedRotatingFileHandler(filename[,when[,interval[,backupCount]]])

返回一个新的TimedRotatingFileHandler类实例,在指定文件名时会自动使用词缀。轮询发生的时间取决于参数 when 和 interval 。

你可以使用 when 来指定 interval 的类型。可选值如下,注意区分大小写:

interval的类型
S
M 分钟
H 小时
D
W
midnight 在午夜

如果 backupCount 为非零,则系统会在旧日志文件名的末尾添加词缀。词缀格式为日期和时间,使用 strftime 格式如 %Y-%m-%d %H:%M:%S 或者依赖于之前的规则。最多会保留 backupCount 个日志文件,如果继续增加,会持续删除最旧的的日志。

doRollover()

进行一次回转,有如上面描述的。

emit(record)

把日志输出到文件,按照如上的回转规则。

DBUtils超快速入门指南

Tuesday, September 11th, 2007

DBUtils超快速入门指南

版本: 0.9.2

目录

1   简介

DBUtils是一套Python数据库连接池包,并允许对非线程安全的数据库接口进行线程安全包装。DBUtils来自Webware for Python。

DBUtils提供两种外部接口:

  • PersistentDB :提供线程专用的数据库连接,并自动管理连接。
  • PooledDB :提供线程间可共享的数据库连接,并自动管理连接。

实测证明 PersistentDB 的速度是最高的,但是在某些特殊情况下,数据库的连接过程可能异常缓慢,而此时的PooledDB则可以提供相对来说平均连接时间比较短的管理方式。

另外,实际使用的数据库驱动也有所依赖,比如SQLite数据库只能使用PersistentDB作连接池。

下载地址:

http://www.webwareforpython.org/downloads/DBUtils/

2   使用方法

连接池对象只初始化一次,一般可以作为模块级代码来确保。

PersistentDB的连接例子:

import DBUtils.PersistentDB
persist=DBUtils.PersistentDB.PersistentDB(dbpai=MySQLdb,maxusage=1000,**kwargs)

这里的参数dbpai指使用的底层数据库模块,兼容DB-API的。maxusage则为一个连接最大使用次数,参考了官方例子。后面的**kwargs则为实际传递给MySQLdb的参数。

获取连接:

conn=persist.connection()

实际编程中用过的连接直接关闭 conn.close() 即可将连接交还给连接池。

PooledDB使用方法同PersistentDB,只是参数有所不同。

  • dbapi :数据库接口
  • mincached :启动时开启的空连接数量
  • maxcached :连接池最大可用连接数量
  • maxshared :连接池最大可共享连接数量
  • maxconnections :最大允许连接数量
  • blocking :达到最大数量时是否阻塞
  • maxusage :单个连接最大复用次数
  • setsession :用于传递到数据库的准备会话,如 ["set name UTF-8"]

一个使用过程:

db=pooled.connection()
cur=db.cursor()
cur.execute(...)
res=cur.fetchone()
cur.close() # or del cur
db.close() # or del db

DBUtils 用户指南(更新至0.9.3)

Tuesday, September 11th, 2007

DBUtils 用户指南

版本: 0.9.3

目录

摘要

DBUtils 是一套允许线程化 Python 程序可以安全和有效的访问数据库的模块。DBUtils已经作为 Webware for Python 一部分用来结合 PyGreSQL 访问 PostgreSQL 数据库,当然他也可以用在其他Python应用程序中来访问 DB-API 2 兼容的数据库接口。

模块

DBUtils实际上是一个包含两个子模块的Python包,一个用于连接DB-API 2模块,另一个用于连接典型的PyGreSQL模块。

全局的DB-API 2变量
SteadyDB.py 用于稳定数据库连接
PooledDB.py 连接池
PersistentDB.py 维持持续的数据库连接
SimplePooledDB.py 简单连接池
典型的 PyGreSQL 变量
SteadyPg.py 稳定PyGreSQL连接
PooledPg.py PyGreSQL连接池
PersistentPg.py 维持持续的PyGreSQL连接
SimplePooledPg.py 简单的PyGreSQL连接池

对标准DB-API 2模块的依赖如下图所示:

dbdep.gif对典型的PyGreSQL模块依赖如下图所示:

pgdep.gif

下载

你可以从 Webware 的网站下载最新版本:

http://www.webwareforpython.org/downloads/DBUtils/

也可以从Python Package Index来下载:

http://www.python.org/pypi/DBUtils/

安装

安装为顶层模块

如果你打算在除了Webware之外的程序中使用,推荐安装为顶层模块:

python setup.py install

安装为Webware的子模块(插件)

如果你只是打算在Webware中使用,则可以按照如下安装:

python setup.py install --install-lib=/path/to/Webware

替换 /path/to/Webware 为Webware安装的根路径。你还需要运行Webware的安装程序来同时包含DBUtils的文档:

cd path/to/Webware
python install.py

功能

这一节的主要例子面向DB-API 2,但是也适用于典型的PyGreSQL模块。

SimplePooledDB

DBUtils.SimplePooledDB 是一个非常简单的数据库连接池实现。他比完善的 PooledDB 模块缺少很多功能。 DBUtils.SimplePooledDB 本质上类似于 MiscUtils.DBPool 这个Webware的组成部分。你可以把它看作一种演示程序。

SteadyDB

DBUtils.SteadyDB 是一个模块实现了”强硬”的数据库连接,基于DB-API 2建立的原始连接。一个”强硬”的连接意味着在连接关闭之后,或者使用次数操作限制时会重新连接。

一个典型的例子是数据库重启时,而你的程序仍然在运行并需要访问数据库,或者当你的程序连接了一个防火墙后面的远程数据库,而防火墙重启时丢失了状态时。

一般来说你不需要直接使用 SteadyDB 它只是给接下来的两个模块提供基本服务, PersistentDBPooledDB

PersistentDB

DBUtils.PersistentDB 实现了强硬的、线程安全的、顽固的数据库连接,使用DB-API 2模块。如下图展示了使用 PersistentDB 时的连接层步骤:

persist.gif当一个线程首次打开一个数据库连接时,一个连接会打开并仅供这个线程使用。当线程关闭连接时,连接仍然持续打开供这个线程下次请求时使用这个已经打开的连接。连接在线程死亡时自动关闭。

简单的来说 PersistentDB 尝试重用数据库连接来提高线程化程序的数据库访问性能,并且他确保连接不会被线程之间共享。

因此, PersistentDB 可以在底层DB-API模块并非线程安全的时候同样工作的很好,并且他会在其他线程改变数据库会话或者使用多语句事务时同样避免问题的发生。

PooledDB

DBUtils.PooledDB 实现了一个强硬的、线程安全的、有缓存的、可复用的数据库连接,使用任何DB-API 2模块。如下图展示了使用 PooledDB 时的工作流程:

pool.gif如图所示 PooledDB 可以在不同线程之间共享打开的数据库连接。这在你连接并指定 maxshared 参数,并且底层的DB-API 2接口是线程安全才可以,但是你仍然可以使用专用数据库连接而不在线程之间共享连接。除了共享连接以外,还可以设立一个至少 mincached 的连接池,并且最多允许使用 maxcached 个连接,这可以同时用于专用和共享连接池。当一个线程关闭了一个非共享连接,则会返还到空闲连接池中等待下次使用。

如果底层DB-API模块是非线程安全的,线程锁会确保使用 PooledDB 是线程安全的。所以你并不需要为此担心,但是你在使用专用连接来改变数据库会话或执行多命令事务时必须小心。

该选择哪一个?

PersistentDBPooledDB 都是为了重用数据库连接来提高性能,并保持数据库的稳定性。

所以选择何种模块,可以参考上面的解释。 PersistentDB 将会保持一定数量的连接供频繁使用。在这种情况下你总是保持固定数量的连接。如果你的程序频繁的启动和关闭线程,最好使用 PooledDB 。后面将会提到更好的调整,尤其在使用线程安全的DB-API 2模块时。

当然,这两个模块的接口是很相似的,你可以方便的在他们之间转换,并查看哪个更好一些。

使用方法

所有模块的使用方法都很相似,但是在初始化 “Pooled” 和 “Persistent” 时还有有些不同,尤其是DB-API和PyGreSQL之间。

这里只讲解 PersistentDB 和更复杂的 PooledDB 模块。其他模块的细节请参与其文档。使用Python解释器控制台,你可以显示 PooledDB 的文档,如下:

help(PooledDB)

PersistentDB

为了使用 PersistentDB 你首先需要通过创建 PersistentDB 的实例来设置一个特定数据库连接的生成器,床底如下参数:

  • dbapi: 需要使用的DB-API 2兼容的数据库模块
  • maxusage: 一个连接最大允许复用次数(缺省为 0False 意味着无限制的重用),当达到限制时,将会重新连接数据库
  • setsession: 一个可选的SQL命令列表可以用于准备会话,如 ["set datestyle to german", ...]
  • 其他的,你还可以传递用于传递给真实的DB-API 2模块的参数,例如主机名、数据库、用户名、密码等。

举个例子,如果你正在使用 pgdb 作为数据库模块并想要连接本机数据库 mydb ,允许重用1000次:

import pgdb # import used DB-API 2 module
from PersistentDB import PersistentDB
persist = PersistentDB(pgdb, 1000, database='mydb')

按照如上设置完成了连接生成器之后,你可以按照如下来请求一个连接:

db = persist.connection()

你可以使用这些连接就像使用原始的DB-API 2连接一样。实际上你得到的是一个通过SteadyDB得到的强硬的连接,基于DB-API 2。

关闭一个强硬的连接使用 db.close() ,这在内部实际上被忽略掉了,并且供下次使用。在线程关闭时,也会自动关闭数据库连接。你可以改变这个行为通过 persist._closeableTrue

PooledDB

为了使用 PooledDB 模块,你首先需要通过创建 PooledDB 来设置数据库连接池,传递如下参数:

  • dbapi: 需要使用的DB-API 2模块
  • mincached : 启动时开启的空连接数量(缺省值 0 意味着开始时不创建连接)
  • maxcached: 连接池使用的最多连接数量(缺省值 0 代表不限制连接池大小)
  • maxshared: 最大允许的共享连接数量(缺省值 0 代表所有连接都是专用的)如果达到了最大数量,被请求为共享的连接将会被共享使用。
  • maxconnections: 最大允许连接数量(缺省值 0 代表不限制)
  • blocking: 设置在达到最大数量时的行为(缺省值 0False 代表返回一个错误;其他代表阻塞直到连接数减少)
  • maxusage: 单个连接的最大允许复用次数(缺省值 0False 代表不限制的复用)。当达到最大数值时,连接会自动重新连接(关闭和重新打开)
  • setsession: 一个可选的SQL命令列表用于准备每个会话,如 ["set datestyle to german", ...]
  • 其他,你可以设置用于传递到真正的DB-API 2的参数,例如主机名、数据库、用户名、密码等。

举个例子,如果你正在使用 pgdb 作为DB-API模块,并希望连接池中至少有5个连接到数据库 mydb

import pgdb # import used DB-API 2 module
from PooledDB import PooledDB
pool = PooledPg(pgdb, 5, database='mydb')

一旦设置好了连接池,你就可以按照如下请求一个连接:

db = pool.connection()

你可以使用这些连接有如原始的DB-API 2一样。而实际使用的是SteadyDB版本的强硬连接。

请注意连接可以与其他线程共享,只要你设置 maxshared 参数为非零,并且DB-API 2模块也允许。如果你想要使用专用连接则使用:

db = pool.connection(0)

如果你不再需要这个连接了,则可以返回给连接池使用 db.close() 。你也可以使用相同的方法获取另一个连接。

警告: 在一个多线程环境,不要使用下面的方法:

pool.connection().cursor().execute(...)

这将会导致过早的释放连接以供复用,而且如果是非线程安全还会出错。确保连接对象在你的使用过程中是一直存在的,例如:

db = pool.connection()
cur = db.cursor()
cur.execute(...)
res = cur.fetchone()
cur.close() # or del cur
db.close() # or del db

在Webware中使用

如果你正在 Webware for Python 的 servlets 中使用DBUtils来存取数据库,你要确保数据库连接生成器只被应用启动一次,而不是每个servlet启动时都创建一个。为了达到这个目的,你可以在模块或类的初始化代码中添加这些代码,或者使用 __init__.py 中的 contextInitialize() 函数。

目录 Examples 是DBUtils发行包的一部分,包含了一个使用示例数据库的Webware的例子,用来跟踪演讲会的出席者(这个例子的主意来自Andrew Kuchling的 “The Python DB-API“)。

例子的正文可以通过创建配置文件 Configs/Database.config 来配置,改变例子 Examples/DBUtilsExample.py 的缺省参数。这种方式可以设置一个专用数据库的用户名和密码,你也可以选择底层的数据库模块。如果设置了 maxcached ,则例子会使用 “Pooled” 模块,否则会使用 “Persistent” 模块。

注意

如果你正在使用流行的ORM SQLObject ,你并不需要使用DBUtiils,因为他已经内含连接池了。 SQLObject 2 (SQL-API) 事实上还从DBUtils这里借用了连接池分层的代码。

未来功能

一些未来会使用的方法:

  • 一个连接最大被使用的次数,或一个连接最大活动时间。
  • 创建模块 MonitorDBMonitorPg 运行在单独的线程中,监控连接池中各个共享连接的状态。如果检测到一个损坏的连接,则会自动恢复这个连接。这在很多网站中是很实用的,因为晚上往往要重启数据库服务器。如果不使用监控线程,则用户要等到第二天早上才可以使用。正是因为如此,检测损坏的连接并自动恢复是很有用的。使用了监控线程之后,间断时间在晚上,而且很短。监控线程同样可以配置连接生成器的线程池,并且确保用户到达之前完成。
  • 可选的日志,记录损坏的连接和最大限制。

错误报告与回馈

请将错误报告、补丁、回馈直接发送给作者(使用下面给出的邮件地址)。

如果有Webware相关的问题,可以到邮件列表讨论 Webware for Python mailing list

链接

一些相关软件的链接:

作者列表

作者: Christoph Zwerschke <cito@online.de>

版权与许可

Copyright @ 2005-2006 by Christoph Zwerschke. All Rights Reserved.

DBUtils是一个自由开源软件,使用 Open Software License version 2.1 许可。

pysqlite使用指南

Tuesday, September 11th, 2007

pysqlite使用指南

最后更新: pysqlite 2.3.0

目录

1 简介

本使用指南并非Python、SQL、SQLite的入门指南;而是pysqlite功能的介绍,并且包含了使用模式的示例代码。这个指南将会指导你使用Python的数据库接口和SQLite的文档。

2 Python数据库接口2.0兼容

2.1 不兼容的选项

pysqlite包含不兼容的DB API 2.0的功能,如下:

  • 在cursor.description中并不包含类型信息

    cursor.description是一个元组包含如下字段(name,type_code,display_size,internal_size,precision,scale,null_ok)来定义每个列。特定的数据库接口需要至少name和type_code,但是有时cursor.description是内置的,pysqlite不只能检测到任何类型。所以,pysqlite只在cursor.description中填入name字段。其他的字段全部设置为None。

  • 没有类型对象

    同样的,模块级并没有STRING、BINARY、NUMBER、DATETIME、ROWID,并且也没什么用。

2.2 不支持的可选功能

Cursor
不支持nextset方法。这个方法没有实现是因为数据库引擎也没有实现单个游标返回多个结果集。

2.3 有名无实支持的可选功能

Cursor

  • arraysize 属性

    作为最基本的需求,这个属性的值用于支持 fetchmany 方法。不过改变这个值也没什么关系,因为数据库引擎根本就只支持一次获取一行。

  • setinputsizes 方法

    尽管提供了这个方法,但是什么都不做。

  • setoutputsize 方法

    什么活都不干的方法。

2.4 扩展与警告

pysqlite提供了大量的功能来实现对Python DB API的基本支持。大部分扩展在已经在本节的Native Database Engine Features and Extensions Beyond the Python DB API中有所介绍。

  • connect 函数

    参数 database 指定SQLite数据库的文件。一般指定文件系统的绝对或相对路径。

    这个链接函数支持如下可选参数:

    • timeout 当多个连接同时存取数据库时,并且其中一个进程修改了数据库,SQLite数据库会锁定(locked)直到事务被提交。这个超时参数指定连接会等待这个锁定多长时间,并抛出异常。缺省的超时参数是5.0秒。例如:
      sqlite.connect(database="mydb",timeout=10.0)
    • isolation_level pysqlite将会缺省的以”BEGIN”语句开始一个事务,当使用DML语句如INSERT/UPDATE/DELETE/REPLACE。一些用户并不想pysqlite暗自打开事务,而希望使用自动提交模式。其他用户想要pysqlite打开不同类型的事务,例如”BEGIN IMMEDIATE”。查看 控制事务 了解更多细节。注意你通过设置isolation_level属性可以选择不同的isolation级别。例如:
      # 打开自动提交模式
      con=sqlite.connect("mydb",isolation_level=None)
      # 将isolation_level设置到"IMMEDIATE"
      con.isolation_level="IMMEDIATE"
    • detect_types SQLite本来支持的类型包括TEXT、INTEGER、FLOAT、BLOB、NULL。如果你想使用其他类型,你需要自己添加支持。 detect_types 参数和使用自定义的 converters 来使用 register_converter 函数允许你做到这些。 detect_types 缺省是0,就是关闭,你可以设置 PARSE_DECLTYPES 和 PARSE_COLNAMES 的组合来改变设置的类型。参见 SQLite与Python类型 来了解更多。
      • sqlite.PARSE_DECLTYPES 这会

Warning

pause

3 简要入门

这个教会你如何初步的使用pysqlite。而并非是一个Python Database API入门,也不是覆盖其他使用的方面。

3.1 连接到数据库

例子1

连接到数据库文件 mydb

from pysqlite2 import dbapi2 as sqlite
con=sqlite.connect("mydb")

例子2

创建一个内存数据库:

from pysqlite2 import dbapi2 as sqlite
con=sqlite.connect(":memory:")

3.2 执行SQL语句

为了执行这一节,我们需要一个数据库按照如下定义:

CREATE TABLE people
(
    name_last   varchar(20),
    age         integer
);
INSERT INTO people (name_last,age) VALUES ('Yeltsin',72);
INSERT INTO people (name_last,age) VALUES ('Putin',51);

例子1

这个例子显示了一个打印 people 表格内容的最简单的例子:

from pysqlite2 import dbapi2 as sqlite
# 创建数据库连接到文件"mydb"
con=sqlite.connect("mydb")
# 获取游标对象
cur=con.cursor()
# 执行SELECT语句
cur.execute("SELECT * FROM people ORDER BY age")
# 获取所有行并显示
print cur.fetchall()

输出:

[(u'Putin', 51), (u'Yeltsin', 72)]

例子2

如下是另一个小例子展示了如何单行显示记录:

from pysqlite2 import dbapi2 as sqlite
con=sqlite.connect("mydb")
cur=con.cursor()
SELECT="SELECT name_last,age FROM people ORDER BY age, name_last"
# 1. 第一种显示记录的方法
cur.execute(SELECT)
for (name_last,age) in cur:
    print '%s is %d years old.'%(name_last,age)
# 2. 第二种显示记录的方法
cur.execute(SELECT)
for row in cur:
    print '%s is %d years old.'%(row[0], row[1])

输出:

Putin is 51 years old.
Yeltsin is 72 years old.
Putin is 51 years old.
Yeltsin is 72 years old.

例子3

如下的程序以表格的方式打印表格内容:

from pysqlite2 import dbapi2 as sqlite
FIELD_MAX_WIDTH=20
TABLE_NAME='people'
SELECT="SELECT * FROM %s ORDER BY age,name_last"%TABLE_NAME
con=sqlite.connect("mydb")
cur=con.cursor()
cur.execute(SELECT)
#打印表头
for fieldDesc in cur.description:
    print fieldDesc[0].ljust(FIELD_MAX_WIDTH),
print #结束表头行
print '-'*78

#每行打印一次值
fieldIndices=range(len(cur.description))
for row in cur:
    for fieldIndex in fieldIndices:
        fieldValue=str(row[fieldIndex])
        print fieldValue.ljust(FIELD_MAX_WIDTH),
    print

输出:

name_last               age
---------------------------------------------
Putin                   51
Yeltsin                 72

例子4

插入人员信息到 people 表格:

from pysqlite2 import dbapi2 as sqlite
con=sqlite.connect("mydb")
cur=con.cursor()
newPeople=(
    ('Lebed',53),
    ('Zhirinovsky',57),
)
for person in newPeople:
    cur.execute("INSERT INTO people (name_last,age) VALUES (?,?)",person)
#修改之后必须明确的提交
con.commit()

注意参数化的SQL语句。当处理重复语句时,这会更快并产生更少的错误,相对于手动生成SQL语句。

而上面的核心语句:

for person in newPeople:
    cur.execute("INSERT INTO people (name_last,age) VALUES (?,?)",person)

可以被重写为:

cur.executemany("INSERT INTO people (name_last,age) VALUES (?,?)",newPeople)

在例子4的后面的打印结果为:

name_last                 age
-------------------------------------------
Putin                     51
Lebed                     53
Zhirinovsky               57
Yeltsin                   72

4 数据库引擎的本地功能和Python DB API的扩展功能

4.1 创建用户自定义函数

SQLite支持用户自定义函数。使用pysqlite,你使用连接的 create_function 方法来创建新函数:

def create_function(self,name,numparams,func)

如下是参数说明:

  • name

    SQL函数的名字

  • numparams

    函数接收的参数数量,-1是接收不限制数量的参数

  • func

    Python函数

函数可以返回任何pysqlite支持的SQLite类型:unicode、str、int、long、float、buffer和None。任何用户自定义函数中的异常都会导致SQL语句的执行中断。

例子:

from pysqlite2 import dbapi2 as sqlite
import md5
def md5sum(t):
    return md5.md5(t).hexdigest()
con=sqlite.connect(":memory:")
con.create_function("md5",1,md5sum)
cur=con.cursor()
cur.execute("SELECT md5(?)", ("foo",))
print cur.fetchone()[0]

4.2 创建用户自定义聚合体类型

SQLite支持用户自定义聚合类型函数。使用

Warning

pause

4.3 创建和使用对比函数

Warning

pause

4.4 检查语句完整性

Warning

pause

4.5 启用SQLite的共享缓存

Warning

pause

4.6 设置认证回调函数

Warning

pause

5 SQLite与Python类型

5.1 介绍

详见 http://sqlite.org/datatype3.html

Warning

pause

5.2 使用Python类型的SQLite数据库存储适配器

Warning

pause

5.3 转换SQLite值到Python类型

Warning

pause

5.4 缺省的pysqlite适配器和转换器

Warning

pause

6 控制事务

缺省时,pysqlite在DML语句(INSERT/UPDATE/DELETE/REPLACE)语句之前自动开启一个事务,并且在一个非DML语句或非DQL语句之前自动提交(除了SELECT/INSERT/UPDATE/DELETE/REPLACE)语句之外。

所以如果你在一个事务当中,要执行类似 CREATE TABLE … , VACUUM , PRAGMA 之类的命令,则pysqlite会在执行这些命令之前提交。有两个原因需要这么做。首先,大多数此类命令并不是工作在事务当中的。第二个原因是pysqlite需要保持对事务状态的跟踪,无论事务处于活跃状态与否。

你可以自己控制何种类型的”BEGIN”语句会自动执行(没有,或者所有),通过设置 connect 的 isolation_level 参数,或者连接对象的 isolation_level 属性。

如果你想要开启 autocommit 模式,设置 isolation_level 为None。

否则让这个值为缺省,将会使用平坦的”BEGIN”语句。或者设置一个SQLite支持的隔离(isolation)级别:DEFERRED(延时)、IMMEDIATE(立即)或EXCLUSIVE(独占)。

因为pysqlite需要对事务状态进行跟踪,所以你将不能使用 OR ROLLBACK 或 ON CONFLICT ROLLBACK 。作为交换,捕捉 IntegrityError 异常并手动调用连接的 rollback 方法。

7 高效的使用pysqlite

7.1 使用命令的捷径

使用非标准的连接的 execute() 、 executemany() 、 executescript() 方法,你的代码可以更加简明,因为你不需要再创建多余的游标对象了。作为交换,游标对象会隐含的创建并使用。这种方法,你可以直接在对象之上使用SELECT语句并使用序列方法。:

from pysqlite2 import dbapi2 as sqlite
persons=[
    ("Hugo","Boss"),
    ("Calvin","Klein")
    ]
con=sqlite.connect(":memory:")
#创建表格
con.execute("CREATE TABLE person (firstname,lastname)")
#填写表格
con.executemany("INSERT INTO person(firstname,lastname) VALUES (?,?)",
    persons)
#打印表格内容
for row in con.execute("SELECT firstname,lastname FROM person"):
    print row
#使用一个哑WHERE子句来让SQLite删除表格
print "I just deleted",con.execute("DELETE FROM person WHERE 1=1").rowcount,"rows"

7.2 使用列名而不是序号存取字段

一个pysqlite2.1.0的新功能是新的内建的sqlite.Row类设计作为记录工厂。 Rows包装了这个类并允许同时使用列序号(类似元组)和大小写不敏感的列名:

from pysqlite2 import dbapi2 as sqlite
con=sqlite.connect("mydb")
con.row_factory=sqlite.Row
cur=con.cursor()
cur.execute("SELECT name_last,age FROM people")
for row in cur:
    assert row[0] == row["name_last"]
    assert row["name_last"] == row["nAmE_lAsT"]
    assert row[1] == row["age"]
    assert row[1] == row["Age"]

Firebird笔记

Monday, September 10th, 2007

Firebird笔记

目录

1   选用理由

我正在开发一个爬虫,原来使用的SQLite数据库的多线程并发操作很麻烦,我一直都没能找到比较好的方法来避免数据库锁定,所以在尝试了一个多月之后我最终放弃了。放弃之后使用了BerkeleyDB作为数据库,自己写接口。运行过程尽管还算很快,但是也有问题,就是多线程操作BerkeleyDB时会让Python解释器出现段错误而退出,这也是无法忍受的错误。

所以在权衡之后我最终还是决定捡起一度放弃的Firebird数据库。决定未来将其集成到我的爬虫当中。另外,我的另外一个产品也需要使用足够健壮的数据库服务器。

关于MySQL,我还是很信赖其稳定性的,使用感受也不错,但是在我的程序中并不需要复杂的权限控制,而且通过网络连接数据库的安全性和速度也让我很讨厌。所以选用了Firebird的嵌入式版本。

2   与Python的结合使用

Firebird的Python接口叫做KinterbasDB,支持所有版本的Firebird和部分版本的Interbase。尽管支持,但是对1.0、1.5、2.0版本的Firebird是使用不同的安装包来支持的。

在Windows 2000系统之下我使用了 kinterbasdb-3.2.win32-FB-2.0-py2.4.exe 来进行安装,版本为3.2,对应Firebird版本为2.0。双击然后一路继续就完成了。

3   安装Firebird的嵌入式版本

因为考虑到未来程序的可移植性,所以我坚决避免使用Firebird的安装包进行安装。在安装完成 KinterbasDB 之后尝试在Python中导入:

>>> import kinterbasdb

发生了错误,提示找不到DLL文件,然后我使用了压缩包 Firebird-2.0.0.12748-0_embed_win32.zip 。对缺少的DLL文件依次从压缩包中解压出来并放置到目录 C:Python24Libsite-packageskinterbasdb 目录中。因为我发现这个目录是可以被路径识别的,并且跟 KinterbasDB 的关系密切。如下几个文件是导入语句所必须的:

  • fbclient.dll :最初叫做 fbembeb.dll 需要复制过来以后该名为新名字的,否则 KinterbasDB 是不认的。
  • icuuc30.dll :不知道干什么的,反正必须要有。
  • icudt30.dll :不知道干什么的。
  • icuin30.dll :不知道干什么的。

这四个动态链接库的大小就已经达到了 3.68MB ,不过面前还是可以接受的。

在这样一番折腾之后就可以成功的导入了,做了一下dir,结果超长,此处也就略掉吧。FB支持的功能非常多,包括触发器、事务、存储过程之类的,这些东西在开源数据库当中是非常少有的。

4   《Firebird嵌入版本开发方案》笔记

Firebird的官方网站( http://www.ibphoenix.com )。这里还推荐了一个叫UIB的东西,网站是( http://www.progdigy.com/UIB/ )。原来用于Interbase的可视化管理工具IBExpert ( http://www.ibexpert.com )从某种程度上来说也可以用于Firebird。

拒作者说IBExpert好像是有限制的,所以推荐去 ftp://hdkej.8800.org 下载一个修改过的版本,没有限制。运行IBExpert新建数据库时服务器选择Local,Client Library选择fbclient.dll。

看来UIB是给Delphi使用的一套接口,Python的就免了。作者的调试过程发现不能让两个进程同时访问数据库。

5   博客 http://tb.blog.csdn.net 的Firebird使用 笔记

推荐下载IBExpert而已,另外就是FB内置了isql.exe在控制台进行数据库的基本操作。

6   《python2.4 连接firebird1.5 一个连接数据库的文件》笔记

作者使用Python2.4和KinterbasDB连接了FB数据库。首先是导入:

import kinterbasdb as kdb

初始化连接并返回连接对象:

conn=kdb.connect(dsn='lq:D:\\data\\aaa.FDB',user='sysdba',password='masterkey')
conn.text_factory=str

连接选项的dsn用来指定数据源,用户名密码sysdba:masterkey是默认的。第二行是用来避免 “Could not decode to UTF-8 column” 错误的。

因为很多时候程序需要随身携带数据库,所以需要使用相对路径,文中给出了获取当前路径的方法:

import os
ypath=os.path.abspath(os.path.dirname(sys.argv[0]))

如果需要通过列名来返回值,则可以作如下设置:

conn.row_factory=kdb.Row

通过ConfigParser模块支持INI格式的配置文件,如下是某配置文件:

[SQLFbDB]
dbname=data\aaa.FDB

如下是处理配置文件的代码:

import ConfigParser
config=ConfirParser.ConfigParser()
config.read('config.txt')
dbname=config.get('SQLFbDB','dbname')

7   《本地数据源:使用firebird数据库》笔记

Windows下的Firebird有classical和superserver两种版本。配置文件aliases.conf配置数据库别名,firebird.conf配置数据库参数如rootpath。

嵌入的FB数据库不再支持多用户访问和用户安全控制,不过对于本机程序来说很正常。

在.net下使用时有个连接选项叫server type,需要设置为1才是嵌入FB数据库。

8   Python-Chinese邮件列表上朋友的回复

  1. 安装KInterbasDB,会产生一个 lib\site-package\kinterbasdb 目录。

  2. 在kinterbasdb目录下新建一个embedded目录,然后将嵌入式的Firebird中的 fbembed.dllfirebird.msgib_util.dll 这3个文件放入embedded目录下。如果需要国际化支持,则同时复制intl子目录,这个目录包含 fbintl.dll 文件。

  3. fbembed.dll 文件改名为 fbclient.dll

  4. 这时就可以使用FB数据库了。

  5. KInterbasDB为了向后兼容,默认使用 mx.DateTime 模块,但是在Python2.4下并不需要而且多余。在Python标准库中的datetime模块更好用。需要用datetime替代mx.DateTime则按照如下修改:

    import kinterbasdb
    kinterbasdb.init(type_conv=200)

实际测试发现如果把几个动态链接库放在 embedded 目录中,则会提示动态链接库找不到。

使用twisted.python.log日志

Monday, September 10th, 2007

使用twisted.python.log日志

目录

版本: 2.4.0

1   简单使用

Twisted提供了一个简单而且可移植的日志系统叫做 twisted.python.log 。它包含3个函数:

msg

记录一条信息,例如:

from twisted.python import log
log.msg("hello, world")

err

把错误写入日志,包括traceback信息。你可以传递一个 failure 或者异常的实例,甚至什么都没有。如果传递其他的东西则会通过 repr() 函数获得字符串来显示。如果什么都不传递则会自动构造一个 Failure 实例,一般用于 except 子句:

try:
    x=1/0
except:
    log.err() #会自动记录ZeroDivisionError

startLogging

通过一个类似文件对象来开始日志,例如:

log.startLogging(open("/var/log/foo.log",'w'))

或者:

log.startLogging(sys.stdout)

缺省条件下,startLogging会同时将输出重定向到sys.stdout和sys.stderr。你可以在startLogging中设置setStdout=False来禁用这个功能。

1.1   日志与twistd

如果你使用twistd来运行你的程序作为后台进程,则他会自动托管startLogging,甚至还会自动轮训日志。查看 twistd and tac 一节或twistd的man手册了解更多。

1.2   日志文件

twisted.python.logfile 模块提供了一些可以同startLogging共同使用的类,例如 DailyLogFile ,提供了以天为单位的日志轮询。

2   编写日志监视器

日志监视器是Twisted日志系统的基础。一个日志监视器的例子是供startLogging使用的 FileLogObserver ,可以把事件写入文件中。一个日志监视器是可调用的,并且只接受一次字典作为参数。随后你可以用它来接收所有的日志事件(当然也会给其他日志监视器):

twisted.python.log.addObserver(yourCallable)

字典至少有2个项目:

message

日志信息,一个列表或字符串,被log.msg或log.err传递过来的。

isError

一个布尔值,如果为True时就是从log.err过来的。如果为True说明字典当中还会有个Failure对象。

其他项目是自动被加入的:

printed

这条信息是从sys.stdout中捕获的,例如这条信息是从print输出的。如果isError同样为True,则是从sys.stderr来的。

你可以通过 log.msg 或 log.err 传递附加项目到事件字典。标准的日志监视器将会忽略他们不用的字典参数。

Note

注意

  • 不要在日志监视器中抛出异常,否则会挂掉。
  • 不要在日志监视器中阻塞,尤其是在主线程中。这将会导致很多问题。
  • 日志监视器需要线程安全。

memcached简介

Monday, September 10th, 2007

memcached简介

翻译: gashero

目录

主要取自 memcached 的主页。

memcached的Python客户端下载地址为 python-memcached

1   什么是memcached

memcached是一个高性能的、分布式内存对象缓存系统,尽管很通用,但是用来加速WEB应用、降低数据库负载时比较多。

Danga Interactive 开发了memcached用来提高 LiveJournal.com 的速度,这个站点每天处理2000万以上的动态页面请求,独立用户有100万左右。memcached把数据库的负载降到了几乎没什么事可干的地步,并为用户提供很快的页面响应速度,更好的资源利用率和更快的数据库存取操作。

2   它如何工作

首先启动一个memcached监护进程,监护进程不需要配置文件,只要在命令行里面加三四个参数就可以了:

# ./memcached -d -m 2048 -l 10.0.0.40 -p 11211

这里指定使用2GB内存,监听10.0.0.40的11211端口来启动memcached。因为32位处理器最多只能有4GB的地址空间。如果你确实需要32位的服务器处理4-64GB的内存(使用PAE),则每个使用2-3GB内存。

3   移植应用

Warning

pass

4   数据库不能做这些么

数据库为了在关系数据库管理系统上实现ACID,使用了锁,导致很多读写阻塞。而memcached从不会阻塞。

5   那共享内存呢

首先使用缓存的很多用户都是用于WEB。这意味着要分配时间出来做多请求处理给不同的系统,如mod_perl、PHP等等,这是对全局缓存的一种浪费。如果你使用多线程语言和共享内存API,如IPC::Shareable,你的每个线程都拥有独立的缓存。这个也是共享内存的局限性。

memcached服务器和客户端实现了全局缓存。实际上,很推荐你在同一台机器上同时运行WEB结点和memcached结点。这会节省一点网络开销。

6   MySQL 4.x的查询缓存如何

Warning

pass

7   数据库复制的对比

Warning

pass

8   memcached够快么

非常快,它使用 libevent 来处理任意数量的连接。在Linux上,尽可能使用 epoll 来实现。使用非阻塞的网络IO和计算,所以对象在不同的客户端上可能有不同的状态。还使用内置的内存块分配和哈希表算法,确保虚拟内存不会过来捣乱。

9   竞争情况

你可能感兴趣用户是否会获取一个过期的值。

服务器API只有一种获取数据的方法,并有3种提交数据的方法:

  • set 无条件的设置指定的键为给定的值。
  • add 添加到缓存,如果不存在。
  • replace 当存在已有值时,替换一下。

另外,这3个函数都可以设置过期时间。

10   memcached的Python客户端

10.1   基本使用

import memcache
mc=memcache.Client(['127.0.0.1:11211'],debug=0)
mc.set("some_key","Some value")
value=mc.get("some_key")
mc.set("another_key",3)
mc.delete("another_key)
mc.set("key","1") #用于自动增量/减量的必须是字符串
mc.incr("key")
mc.decr("key")

标准的使用memcache作为数据库缓存的方法如下:

key=derive_key(obj)
obj=mc.get(key)
if not obj:
    obj=backend_api.get(...)
    mc.set(obj)
#现在可以操作obj了

10.2   Client类

__init__(servers,debug=0,pickleProtocol=0,pickler=pickle.Pickler,unpickler=pickle.Unpickler)

构造函数

delete(key,time=0)

删除某个键。time的单位是秒,确保特定时间内的set/update操作会失败。返回1成功,0失败。

incr(key,delta=1)

给自增量变量加上delta,默认为1。

decr(key,delta=1)

给自减量变量减去delta,默认为1。

add(key,val,time=0,min_compress_len=0)

添加一个键值对,内部调用_set()方法。

replace(key,val,time=0,min_compress_len=0)

替换值,内部调用_set()方法。

set(key,val,time=0,min_compress_len=0)

无条件的设置键值对。time设置超时,单位是秒。min_compress_len用于设置zlib压缩。内部调用_set()方法。

set_multi(mapping,time=0,key_prefix=”,min_compress_len=0)

设置多个键值对。

get(key)

获取值。出错则返回None。

get_multi(keys,key_prefix=”)

获取多个键的值,返回字典。keys为健明列表。key_prefix是键名前缀,可以最终构成key_prefix+key的完整键名。与set_multi中一样。

在Windows下用MinGW编译Python扩展模块

Thursday, September 6th, 2007

在Windows下用MinGW编译Python扩展模块

作者: gashero

测试使用VC++ 2003 Toolkit并不顺利,所以先用这个MinGW,似乎还不错。

MinGW的安装

我的是gcc 3.2.3的解压缩包,解压后设置PATH就可以用了。

构建

可以直接在构建命令上指定编译器来运行:

python setup.py build -c mingw32
python setup.py build -compiler=mingw32

两种命令都相同。编译完成后可以在 \build\lib.win32-2.4 看到得到的*.pyd文件。把这个文件复制到site-packages就可以用了。

还可以在setup.py所在目录下新建一个setup.cfg文件,指定默认的编译器为MinGW。同时在构建Windows二进制安装包时也需要用这种方法来指定默认编译器。内容如下:

[build]
compiler=mingw32

制作二进制安装包

如上方法指定默认编译器为MinGW之后,使用命令:

python setup.py bdist_wininst

来生成Windows二进制安装包,新生成的二进制安装包在 dist\ 目录下。

一般来说这个安装包在各类Windows系统上双击安装,一路Next下去就可以了。最终会在Python安装目录生成一个卸载用的可执行文件,并且把软件包安装到site-packages目录下。

SQLAlchemy指南(tutorial)

Thursday, September 6th, 2007

SQLAlchemy指南(tutorial)

对应版本: 0.3.4

目录

这个入门指导用于SQLAlchemy的快速入门,并便利SQLAlchemy的简单功能。如果你可以跳过这一部分进入 主文档 会涉及更多内容。如下的例子全部是在Python交互模式下完成了,并且全部通过了 doctest 测试。

1 安装

1.1 安装SQLAlchemy

setuptools 安装是非常简单的,只要运行如下命令即可:

# easy_install SQLAlchemy

这将会在Python Cheese Shop获取SQLAlchemy的最新版本并安装。或者你也可以使用setup.py安装一个发行包:

# python setup.py install

1.2 安装一个数据库API

SQLAlchemy被设计用于操作一个 DBAPI 实现,包括大多数常见的数据库。如果你有一个支持DBAPI的实现,那么可以跳入下一节。另外SQLite是一个易于使用的数据库,可以快速开始,并且他可以使用内存数据库。如果要使用SQLite,你将会需要:

  • pysqlite – SQLite的Python接口
  • SQLite函数库

注意在Windows下并不需要SQLite函数库,因为Windows版的pysqlite已经内含了。pysqlite和SQLite可以被安装到Linux或FreeBSD,通过预编译或从源码安装。

预编译包的地址为:

http://initd.org/tracker/pysqlite/wiki/PysqlitePackages

2 快速开始

2.1 导入

SQLAlchemy提供了完整的命名空间,只要导入sqlalchemy即可,无需其子包。为了方便使用本教程,我们导入所有命名到本地命名空间:

>>> from sqlalchemy import *

2.2 连接到数据库

导入之后,下一步是连接到需要的数据库,表现为(represent)为一个Engine对象。这个对象处理了连接的管理和特定数据库的操作。下面,我们连接SQLite基于文件的数据库”tutorial.db”

>>> db=create_engine("sqlite:///tutorial.db")

创建数据库引擎的更多信息查看”Database Engines”。

3 SQLAlchemy是两个库的包装

现在已经完成了安装和连接数据库,可以开始做点实际的事情了。但首先需要有些解释。

SQLAlchemy的核心有两个完全不同的功能,一个在另一个之上工作。一个是 SQL语言构造器 ,另一个是 ORM 。SQL语言构造器允许调用 ClauseElements 来构造SQL表达式。这些 ClauseElements 可以在编译成字符串并绑定到数据库后用于执行,并返回一个叫做 ResultProxy 的对象,类似于一个结果集对象,但是更象dbapi高版本的 cursor 对象。

ORM是建立在SQL语言构造器之上的工具集,用于将Python对象映射到数据库的行,提供了一系列接口用于从数据库中存取对象(行)。在ORM工作时,在底层调用SQL语言构造器的API,这些通用的操作有些许的不同。不同的是,你不再使用行,而是使用自定义类的对象来操作。另外,数据库的查询方式也不同,ORM的可以生成大多数的SQL查询,除此之外还可以在类中定义更多操作。

SA功能强大,无与伦比,只是有两个混合在一起的方法有些复杂。有效的使用SA的方法是先了解这两种不同的工具集,这是两个不同的概念,而大家常常混交SQL语言构造器和ORM。关键的不同是,使用cursor形式的结果集时使用的是SQL语言构造器;而使用类实例进行管理时使用的是ORM。

本指南首先介绍SQL语言构造器,首先需要声明的数据库信息叫做 table metadata 。本指南包含了一些SQL构造的例子,包括如何有效的使用SQL语言构造器的例子。

4 操作数据库对象

在SQLAlchemy的核心哲学中表格和类是不同的。因为如此,SQLAlchemy提供了构造表格的方法(使用表格的元信息table metadata)。所以我们从构造表格的元信息对象和定制操作他们的对象开始。稍后我们可以看到SQLAlchemy的ORM,提供了表格元信息的高层封装,允许我们随心所欲的装载和保存Python类。

4.1 定义元信息,绑定到引擎

首先,你的表格必须已经在MetaData集合中。我们将要创建简单(handy)表格的MetaData,并自动连接到引擎(将一个模式(schema)对象连接到引擎成为绑定binding):

>>> metadata=BoundMetaData(db)

一个构造BoundMetaData对象的等同方法是直接使用引擎URL,这将会帮我们调用 create_engine

>>> metadata=BoundMetaData("sqlite:///tutorial.db")

现在,我们告知metadata关于数据库中的表格,我们可以使用(issue)CREATE语句来创建表格,并且通过他们来创建和执行SQL语句,除非需要打开和关闭任何连接。这都是自动完成的。注意这个功能是推荐使用的。SQLAlchemy包含了使用模式进行连接管理和SQL构造的全部功能,并可以在任何引擎上进行操作。

本教程的目的,是教会大家使用”bound”对象,他可以使得代码简单和易读。

4.2 创建表格

使用metadata作为基本连接,我们可以创建表格:

>>> users_table=Table('user',metadata,
...     Column('user_id',Integer,primary_key=True),
...     Column('user_name',String(40)),
...     Column('password',String(10))
... )

有如你看到的,我们刚刚定义了一个叫做users的表格并拥有3个列:user_id作为主键,user_name和password。它现在只是一个对象而与数据库中的表格没有必然联系。为了让表格生效,我们使用create()方法。有趣的是,我们可以让SQLAlchemy发送SQL语句到数据库时同时显示SQL语句,只要设置BoundMetaData关联的Engine的echo选项即可:

>>> metadata.engine.echo=True
>>> users_table.create()
CREATE TABLE users (
    user_id INTEGER NOT NULL,
    user_name VARCHAR(40),
    password VARCHAR(10),
    PRIMARY KEY (user_id)
)
...

或者,如果users表格已经存在了(比如你第二次运行这些例子),在这种情况下你可以跳过create()方法的调用。你设置可以跳过列定义,而是让SQLAlchemy自动从数据库装入定义:

>>> users_table=Table('users',metadata,autoload=True)
>>> list(users_table.columns)[0].name
'user_id'

关于表格元信息的文档在”Database Meda Data”中。

4.3 插入记录

插入记录是通过表格对象的insert()方法实现的,这将会定义一个子句对象(clause object)(就是CluseElement)来代理INSERT语句:

>>> i=users_table.insert()
>>> i
<sqlalchemy.sql._Insert object at 0x...>
>>> print i
INSERT INTO users (user_id,user_name,password) VALUES (?,?,?)

当我们创建这个插入语句对象时,语句本身也绑定到了Engine,并已经可以执行了。子句对象的execute()方法将会将对象编译为特定引擎的SQL方言,并且执行语句:

>>> i.execute(user_name='Mary',password='secure')
INSERT INTO users (user_name,password) VALUES (?,?)
['Mary','secure']
COMMIT
<sqlalchemy.engine.base.ResultProxy object at 0x...>
>>> i.execute({'user_name':'Tom'},{'user_name':'Fred'},{'user_name':'Harry'})
INSERT INTO users (user_name) VALUES (?)
[['Tom'],['Fred'],['Harry']]
COMMIT
<sqlalchemy.engine.base.ResultProxy object at 0x...>

注意VALUES子句会自动调整参数数量。这是因为ClauseElement的编译步骤并不依赖于特定的数据库,执行的参数也是如此。

当构造子句对象时,SQLAlchemy会绑定所有的值到参数。在构造时,参数绑定总是依靠键值对。在编译时,SQLAlchemy会转换他们到适当的格式,基于DBAPI的参数风格。这在DBAPI中描述的参数位置绑定中同样工作的很好。

这些文档继承于”Inserts”。

4.4 查询

我们可以检查users表中已经存在的数据。方法同插入的例子,只是你需要调用表格的select()方法:

>>> s=users_table.select()
>>> print s
SELECT users.user_id,users.user_name,users.password
FROM users
>>> r=s.execute()
SELECT users.user_id,users.user_name,users.password
FROM users
[]

这时,我们并没有忽略execute()的返回值。他是一个ResultProxy实例,保存了结果,而行为非常类似于DBAPI中的cursor对象:

>>> r
<sqlalchemy.engine.base.ResultProxy object at 0x...>
>>> r.fetchone()
(1,u'Mary',u'secure')
>>> r.fetchall()
[(2,u'Tom',None),(3,u'Fred',None),(4,u'Harry',None)]

查询条件同Python表达式,使用Column对象。所有表达式中的Column对象都是ClauseElements的实例,例如Select、Insert和Table对象本身:

>>> r=users_table.select(users_table.c.user_name=='Harry').execute()
SELECT users.user_id,users.user_name,users.password
FROM users
WHERE users.user_name=?
['Harry']
>>> row=r.fetchone()
>>> print row
(4,u'Harry',None)

所幸的是所有标准SQL操作都可以用Python表达式来构造,包括连接(join)、排序(order)、分组(group)、函数(function)、子查询(correlated subquery)、联合(union)等等。关于查询的文档”Simple Select”。

4.5 操作记录

你可以看到,当我们打印记录时返回的可执行对象,它以元组打印记录。这些记录实际上同时支持列表(list)和字典(dict)接口。字典接口允许通过字符串的列名定位字段,或者通过Column对象:

>>> row.keys()
['user_id','user_name','password']
>>> row['user_id'],row[1],row[users_table.c.password]
(4,u'Harry',None)

通过Column对象来定位是很方便的,因为这样避免了使用列名的方式。

结果集也是支持序列操作的。但是相对于select还有微小的差别,就是允许指定所选的列:

>>> for row in select([user_table.c.user_id,users_table.c.user_name]).execute():
...     print row
SELECT users.user_id,users.user_name
FROM users
[]
(1,u'Mary')
... ...

4.6 表间关系

我们可以创建第二个表格,email_addresses,这会引用users表。定义表间的关联,使用ForeignKey构造。我们将来也会考虑使用表格的CREATE语句:

>>> email_addresses_table=Table('email_addresses',metadata,
...     Column('address_id',Integer,primary_key=True),
...     Column('email_address',String(100),nullable=False),
...     Column('user_id',Integer,ForeignKey('users.user_id')))
>>> email_addresses_table.create()
CREATE TABLE email_addresses (
    address_id INTEGER NOT NULL,
    email_address VARCHAR(100) NOT NULL,
    user_id INTEGER,
    PRIMARY KEY (address_id),
    FOREIGN KEY(user_id) REFERENCES users (user_id)
)
...

上面的email_addresses表与表users通过ForeignKey(‘users.user_id’)相联系。ForeignKey的构造器需要一个Column对象,或一个字符串代表表明和列名。当使用了字符串参数时,引用的表必须已经存在于相同的MetaData对象中,当然,可以是另外一个表。

下面可以尝试插入数据:

>>> email_addresses_table.insert().execute(
...     {'email_address':'tom@tom.com','user_id':2},
...     {'email_address':'mary@mary.com','user_id':1})
INSERT INTO email_addresses (email_address,user_id) VALUES (?,?)
[['tom@tom.com',2],['mary@mary.com',1]]
COMMIT
<sqlalchemy.engine.base.ResultProxy object at 0x...>

在两个表之间,我们可以使用 join 方法构造一个连接:

>>> r=users_table.join(email_addresses_table).select().execute()
SELECT users.user_id, users.user_name, users.password,
email_addresses.address_id, email_addresses.email_address,
email_addresses.user_id
FROM users JOIN email_addresses ON users.user_id=email_addresses.user_id
[]
>>> print [row for row in r]
[(1, u'Mary', u'secure', 2, u'mary@mary.com', 1),
(2,u'Tom', None, 1, u'tom@tom.com', 2)]

join 方法同时也是 sqlalchemy 命名空间中的一个独立的函数。连接条件指明了表对象给定的外键。条件(condition),也可以叫做 “ON 子句” ,可以被明确的指定,例如这个例子我们查询所有使用邮件地址作为密码的用户:

>>> print join(users_table, email_addresses_table,
...     and_(users_table.c.user_id==email_addresses_table.c.user_id,
...     users_table.c.password==email_addresses_table.c.email_address)
...     )
users JOIN email_addresses ON users.user_id=email_addresses.user_id AND
users.password=email_address.email_address

5 使用ORM工作

现在我们已经有了一些表格和SQL操作的知识了,让我们来看看SQLAlchemy的ORM (Object Relational Mapper) 。使用ORM,你可以将表格(和其他可以查询的对象)同Python联系起来,放入映射集(Mappers)当中。然后你可以执行查询并返回 对象实例 列表,而不是结果集。 对象实例 也被联系到一个叫做 Session 的对象,确保自动跟踪对象的改变,并可以使用 flush 立即保存结果。

5.1 创建一个映射

一个映射通常对应一个Python类,其核心意图是,“这个类的对象是用作这个表格的行来存储的”。让我们创建一个类叫做 User ,描述了一个用户对象,并保存到 users 表格。:

>>> class User(object):
...     def __repr__(self):
...         return "%s(%r,%r)"%(
...             self.__class__.__name__,self.user_name,self.password)

这个类是一个新形式(new style)的类(继承自 object )并且不需要构造器(在需要时默认提供)。我们只实现了一个 __repr__ 方法,用于显示 User 对象的基本信息。注意 __repr__ 方法应用了实例变量 user_namepassword ,这是还没定义的。我们可选的定义这些属性,并可以进行处理;SQLAlchemy的 Mapper 的构造器会自动管理这些,而且会自动协调到 users 表格的列名。让我们创建映射,并观察这些属性的定义:

>>> usermapper=mapper(User,users_table)
>>> ul=User()
>>> print ul.user_name
None
>>> print ul.password
None

函数 mapper 返回新建的 Mapper 实例。这也是我们为 User 类创建的第一个映射,也就是类的 主映射 。一般来说,不需要保存 usermapper 变量;SA的ORM会自动管理这个映射。

5.2 获取会话(Session)

创建了一个映射之后,所有对映射的操作都需要一个重要的对象叫做 Session 。所有对象通过映射的载入和保存都 必须 通过 Session 对象,有如对象的工作空间一样被加载到内存。特定对象在特定时间只能关联到一个 Session

缺省时,需要在载入和保存对象之前明确的创建 Session 对象。有多种方法来管理会话,但最简明的方法是调用 create_session()

>>> session=create_session()
>>> session
<sqlalchemy.orm.session.Session object at 0x...>

5.3 查询对象

会话对象拥有载入和存储对象的所有方法,同时也可以查看他们的状态。会话还提供了查询数据库的方便接口,你可以获取一个 Query 对象:

>>> query=session.query(User)
>>> print query.select_by(user_name='Harry')
SELECT users.user_name AS users_user_name, users.password AS users_password,
users.user_id AS users_user_id
FROM users
WHERE users.user_name=? ORDER BY users.oid
['Harry']
[User(u'Harry',None)]

对象所有的查询操作实际上都是通过 Query 的。 Mapper 对象的多种 select 方法也是偷偷的在使用 Query 对象来执行操作。一个 Query 总是联系到一个特定的会话上。

让我们暂时关闭数据库回显,并尝试 Query 的几个方法。结尾是 _by 的方法主要用于对象的键参数。其他的方法允许接受 ClauseElement 对象,使用 Column 对象的Python表达式产生,同样的方法我们在上一节使用过。使用 ClauseElement 结构来查询更加冗长,但是更加灵活:

>>> metadata.engine.echo=False
>>> print query.select(User.c.user_id==3)
[User(u'Fred',None)]
>>> print query.get(2)
User(u'Tom',None)
>>> print query.get_by(user_name='Mary')
User(u'Mary',u'secure')
>>> print query.selectfirst(User.c.password==None)
User(u'Tom',None)
>>> print query.count()
4

Note

User类有一个特别的属性 c ,这个属性描述了User映射表格对象的列。

User.c.user_name 等同于 users_table.c.user_name ,记得 User 是Python对象,而 usersTable 对象。

5.4 修改数据

作为小经验,我们看看如何做出修改。首先,创建一个新的用户”Ed”,然后加入会话:

>>> ed=User()
>>> ed.user_name='Ed'
>>> ed.password='edspassword'
>>> session.save(ed)
>>> ed in session
True

也可以修改数据库中的其他对象。使用 Query 对象载入,然后改变:

>>> mary=query.get_by(user_name='Mary')
>>> harry=query.get_by(user_name='Harry')
>>> mary.password='marysnewpassword'
>>> harry.password='harrysnewpassword'

这时,什么东西都没有保存到数据库;我们所有的修改都在内存中。如果这时程序其他部分尝试修改’Mary’会发生什么呢?因为使用相同的会话,所以再次载入’Mary’实际上定位到相同的主键’Mary’,并且 返回同一对象实例 。这个行为用在会话中确保数据库的一致性:

>>> mary2=query.get_by(user_name='Mary')
>>> mary is mary2
True

在唯一映射中,单一的会话可以确保安全的操作对象。

如果两个不同的会话同时操作一个对象,会检测到并发;SA会使用简单的并发控制来保存对象,可以选择使用拥有更强的使用ids的检查。参考 Mapper Arguments 了解更多细节。

5.5 保存

在新建了用户”ed”并对”Mary”和”Harry”作修改之后,我们再删除”Fred”

>>> fred=query.get_by(user_name='Fred')
>>> session.delete(fred)

然后发送更改到数据库,使用会话的 flush() 方法。开启回显来查看过程:

>>> metadata.engine.echo=True
>>> session.flush()
BEGIN
UPDATE users SET password=? WHERE users.user_id=?
['marysnewpassword',1]
UPDATE users SET password=? WHERE users.user_id=?
['harrysnewpassword',4]
INSERT INTO users (user_name,password) VALUES (?,?)
['Ed','edspassword']
DELETE FROM users WHERE users.user_id=?
[3]
COMMIT

5.6 关系

如果一个关系包含其他信息时,例如包含邮件地址的列表,我们可以在使用 relation() 创建 Mapper 时声明。当然,你还可以对这个关系作很多事情,我们举几个简单的例子。首先使用 users 表,它拥有一个外键关系连接到 email_addresses 表。 email_addresses 表中的每一行都有列 user_id 用来引用 users 表中的一行;而且 email_addresses 表中的多行可以引用 users 表中的同一行,这叫一对多关系。

首先,处理 email_addresses 表。我们创建一个新的类 Address 描述了 email_addresses 表中的一行,并且也创建了用于 Address 类的映射对象:

>>> class Address(object):
...     def __init__(self,email_address):
...         self.email_address=email_address
...     def __repr__(self):
...         return "%s(%r)"%(
...             self.__class__.__name__,self.email_address)
>>> mapper(Address, email_addresses_table)
<sqlalchemy.orm.mapper.Mapper object at 0x...>

然后,我们通过使用 relation() 创建一个关系连接 UserAddress 类,并且添加关系到 User 映射,使用 add_property 函数:

>>> usermapper.add_property('addresses',relation(Address))

函数 relation() 需要一个类或映射作为首参数,并且还有很多选项来控制行为。 User 映射现在给每一个 User 实例添加了一个属性叫 addresses 。SA将会自动检测这个一对多关系。并且随后创建 addresses 列表。当新的 User 对象创建时,这个列表为空 。

让我们看看数据库做了什么。当我们修改映射的配置时,最好清理一下会话,让所有载入的 User 对象可以重新载入:

>>> session.clear()

我们随之可以使用 User 对象的 addresses 属性来象列表一样处理:

>>> mary=query.get_by(user_name='Mary')
SELECT users.user_name AS users_user_name, users.password AS users_password,
users.user_id AS users_user_id
FROM users
WHERE users.user_name=? ORDER BY users.oid
LIMIT 1 OFFSET 0
['Mary']
>>> print [a for a in mary.address]
SELECT email_addresses.user_id AS email_address_user_id,
email_addresses.address_id AS email_addresses_address_id,
email_addresses.email_address AS email_addresses_email_address
FROM email_addresses
WHERE ?= email_addresses.user_id ORDER BY email_addresses.oid
[1]
[Address(u'mary@mary.com')]

向列表添加元素也很简单。新的 Address 对象将会在调用会话的flush()时保存:

>>> mary.addresses.append(Address('mary2@gmail.com'))
>>> session.flush()
BEGIN
INSERT INTO email_addresses (email_address,user_id) VALUEs (?,?)
['mary2@gmail.com',1]
COMMIT

主文档中关于使用映射的部分在如下地址:

http://www.sqlalchemy.org/docs/datamapping.myt#datamapping

5.7 事务

你可能已经注意到在上面例子中的 session.flush() ,SQLAlchemy使用 BEGINCOMMIT 来使用数据库事务。 flush() 方法使用事务来对一些记录执行一系列的指令。如果希望在 flush() 之外使用更大规模的事务,可以通过 SessionTransaction 对象,其生成使用 session.create_transaction() 。下面将会执行一个非常复杂的 SELECT 语句,进行大量的修改,然后再创建一个有两个邮箱的用户,这些都在事务中完成。而且将会在中间使用 flush() 来保存,然后在执行最后的 commit() 时将所有改变写入数据库。我们把事务封装在一个 try/except 语句块当中确保资源的安全释放:

>>> transaction=session.create_transaction()
>>> try:
...     (ed,harry,mary)=session.query(User).select(
...         User.c.user_name.in_('Ed','Harry','Mary'),
...         order_by=User.c.user_name
...     )
...     del mary.address[1]
...     harry.addresses.append(Address('harry2@gmail.com'))
...     session.flush()
...     print "***flushed the session***"
...     fred=User()
...     fred.user_name='fred_again'
...     fred.addresses.append(Address('fred@fred.com'))
...     fred.addresses.append(Address('fredsnewemail@fred.com'))
...     session.save(fred)
...     transaction.commit()
... except:
...     transaction.rollback()
...     raise
BEGIN
SELECT users.user_name AS users_user_name,
users.password AS users_password,
users.user_id AS users_user_id
FROM users
WHERE users.user_name IN (?, ?, ?) ORDER BY users.user_name
['Ed', 'Harry', 'Mary']
SELECT email_addresses.user_id AS email_addresses_user_id,
email_addresses.address_id AS email_addresses_address_id,
email_addresses.email_address AS email_addresses_email_address
FROM email_addresses
WHERE ? = email_addresses.user_id ORDER BY email_addresses.oid
[4]
UPDATE email_addresses SET user_id=? WHERE email_addresses.address_id = ?
[None, 3]
INSERT INTO email_addresses (email_address, user_id) VALUES (?, ?)
['harry2@gmail.com', 4]
***flushed the session***
INSERT INTO users (user_name, password) VALUES (?, ?)
['fred_again', None]
INSERT INTO email_addresses (email_address, user_id) VALUES (?, ?)
['fred@fred.com', 6]
INSERT INTO email_addresses (email_address, user_id) VALUES (?, ?)
['fredsnewemail@fred.com', 6]
COMMIT

对应的主文档:

http://www.sqlalchemy.org/docs/unitofwork.myt#unitofwork

5.8 下一步

如上已经介绍了一下SQLAlchemy。但是不同的人可能有不同的做事方法,比如定义不同风格的映射的关系,所以还是允许使用原始的SQL来定义表格,还有Engine、SQL语句、数据库连接等。

Berkeley DB 3.x & 4.x Python扩展包

Thursday, September 6th, 2007

Berkeley DB 3.x & 4.x Python扩展包

翻译: gashero <harry.python@gmail.com>

目录

1 简介

这里介绍了一点关于bsddb3.db的Python扩展模块的东西,它包装了Berkeley DB 3.x和4.x的C库。这里的扩展模块指部分纯Python模块。

本模块希望可以应用到如下情况中。这个模块用于确保作事情不要太复杂,而在需要复杂的时候也可以提供相关功能。

  1. 向后兼容。本模块一直希望可以适应各种版本的BDB接口,甚至于1.85接口。这意味着需要创建具备相同接口的,比如btopen()、hashopen()、rnopen()和他们返回的对象接口,特别是first()、last()、next()、prev()这些经常用于避免使用游标的接口。这些都是在 bsddb3.__init__.py 中以Python代码实现的。
  2. 简单的持续字典。在前面实现的前进了一小步。程序员可能希望直接使用新的DB对象,但是可能仅仅在单一进程或线程中使用。这时程序员并不需要被DBEnv所骚扰,应该尽可能的表现的像一个字典。
  3. 并发存取字典。这需要具备处理单一写者和多读者的DB对象,常见于多线程或多进程环境。这时需要使用适当的参数创建DBEnv对象。这时并不需要其他附加的操作。
  4. 高级的基于事务的数据存储。这将会发挥BerkeleyDB的所有能力。程序员在这时可能并非使用字典活其他类似的DB接口,而是传递一个事务对象。另外,这类功能大部分可以简单的通过设置DBEnv的参数来实现,这时可以使用事务,并且在发现死锁时产生异常,等等。

2 提供的类型

bsddb3.db 扩展模块提供如下对象类型:

  • DB :简单的数据库对象,支持Hash/BTree/Recno/Queue的存取方法。
  • DBEnv :提供数据库环境以进行更多高级设置,如事务、日志、并发存取等。
  • DBCursor :一个类似指针的对象,用于遍历数据库。
  • DBTxn :一个数据库事务。支持多文件提交,中断和数据库修改的检查点。
  • DBLock :一个锁的不透明句柄。查看 DBEnv.lock_get()DBEnv.lock_put() 。锁对于数据库中的内容并不重要,但是对多线程或多进程的并发异步访问是很重要的,需要DBEnv的支持。

3 提供的异常

BerkeleyDB的C API使用函数返回码表示错误。而bsddb3.db模块把这些转换成了Python异常,允许使用try-except来处理。

每个错误代码都转换成了一种异常,如下表所示。如果你使用C API文档,也会很容易的关联这些异常。

所有的异常都是继承自DBError异常,如果你希望处理所有异常,可以直接使用DBError。DBNotFoundError在找不到对应的键名时抛出,DBNotFoundError同时也继承自标准的KeyError异常,用来把DB模拟的像一个字典一样。

当抛出任何一个异常时,都会同时包含一个整数值指定错误代码,和一个字符串指定错误信息。

DBError 基类,以下所有都是从这里继承的
DBIncompleteError DB_INCOMPLETE
DBKeyEmptyError DB_KEYEMPTY
DBKeyExistError DB_KEYEXIST
DBLockDeadlockError DB_LOCK_DEADLOCK
DBLockNotGrantedError DB_LOCK_NOTGRANTED
DBNotFoundError DB_NOTFOUND(同时继承自KeyError)
DBOldVersionError DB_OLD_VERSION
DBRunRecoveryError DB_RUNRECOVERY
DBVerifyBadError DB_VERIFY_BAD
DBNoServerError DB_NOSERVER
DBNoServerHomeError DB_NOSERVER_HOME
DBNoServerIDError DB_NOSERVER_ID
DBInvalidArgError EINVAL
DBAccessError EACCESS
DBNoSpaceError ENOSPC
DBNoMemoryError ENOMEM
DBAgainError EAGAIN
DBBusyError EBUSY
DBFileExistsError EEXIST
DBNoSuchFileError ENOENT
DBPermissionsError EPERM

4 其他包模块

  • dbshelve.py :使用Python标准实现的shelve方式存取对象,同时也提供一些高层方法隐藏bdb底层细节。
  • dbtables.py :这个模块是由Gregory Smith实现的一个简单的表格结构。
  • dbutils.py :一个简单的DB接口提供字典的操作方法。
  • dbobj.py :包含DB和DBEnv的子类。
  • dbrecio.py :包含DBRecIO类,允许像读写文件一样存取DB的记录。

5 测试

一个完整的测试套件测试了所有类型的功能,现在使用了 PyUnit 进行自动化测试,已经包含了超过150个测试用例了。

6 参考

查看C API的 在线文档 ,或 这份文档的本地副本 了解更多功能的细节。Python方法的名称与C API中的相似。