2004年04月25日

在NewEdit运用了这两个概念,那么它们有什么区别呢?下面我谈谈我个人对它们的区分。

Mixin,我称它为混入。好象它不是一个真正的单词,应该是Mix和in的结合吧。就象plugin也好象没有这个词一样。那么它主要是指结构上发生了变化,比如在NewEdit中,Mixin用在扩展一个类的类变量、修改类变量、增加新的成员函数。它不需要你在代码中预先定义。

Plugin,称为插件,可以指将某些东西插入到代码中的某个地方,但对代码结构没有影响。它需要在插入点加入特殊的调用语句才可以,也就是说你需要知道什么时候调用一个插件。而且现在NewEdit用到的插件类型只是函数,现在还没有别的什么类型。从预定义与处理的类型上就可以看出plugin与Mixin的差别。

因此,Mixin更适合大规模的程序扩展,而plugin旨在对软件提供一定程序的扩展,更主要的是不想对软件结构有影响。在NewEdit中,Mixin用在增加软件的功能,而plugin用在对功能的调用上。

关于Mixin方面的内容可以去我的主布上看以前写过的文章:

Mix-in技术介绍
Mix-in技术与分布类编程

在进行NewEdit项目中,需要调用保存的函数对象,但因为参数的个数是未知的,因此想到使用apply函数。在我的记忆中,apply是一个内置函数,它可以调用一个函数,同时可以传入多个参数包括关键字参数。它适合处理通用的函数调用。不过,在最新的 Python 2.3.3文档中我发现现在已经不赞成使用它了,因为你可以使用更新的扩展调用语法(Extended Call Syntax)来处理。它的形式为:function(*args, **keywords)。因此在NewEdit中用到了这一特性。


在NewEdit中的Mixin.py模块的Mixin类中有一个函数是这样定义的:



def callplugin(self, name, *argv, **argkv):
    …
    for f in self.__plugins__[name]:
        f(*argv, **argkv)


也就是说在__plugins__中保存着许多的函数,有些同名函数组成一个列表。由于callplugin是一个通用的函数调用结构,因此它不知道具体某个函数类别的参数有几个,都是什么,但调用者是一定要知道的。因此它的参数列表使用了*argv和**argkv的形式。*argv表示无名参数(相当于只是传入了一个值),**argkv表示有名参数(相当于key=value形式,既有参数名又有值)。这种表示方法是允许callplugin接收任意多个无名参数和有名参数,因此是非常通用过。这样当调用某个名字的参数时,f为某个函数对象的引用,直接运用扩展调用语法即可。这比起用apply函数要更直观与简单。

2004年04月24日

Radio MenuItem是在菜单上实现多选一效果的菜单项。向一个菜单增加Radio效果的菜单项有两个函数可以用。它们都是Menu类的方法:Append和AppendRadioItem。

其中Append是一个通用方法,它可以用来增加普通的菜单项,也可以增加特殊菜单项,如分隔线、Check菜单项和Radio菜单项。而AppendRadioItem则只可以增加Radio菜单项。在wxWindow的参考手册上有关于AppendRadio有这么一句话:

“All consequent radio items form a group and when an item in the group is checked, all the others are automatically unchecked.”

这也就是说当我们增加一个Radio菜单项时自动创建了一个Radio组,如果后续加入的菜单项均为Radio菜单项,则它们在一个组里。当这个组中某一个菜单项被选中,则其它项会自动被设置为未选中状态。那么我可以联想到,如果后面不是Radio菜单项而是别的类型的菜单,那么会结束Radio组。如果再加入新的Radio菜单项,则后续的Radio菜单项又编为一组。简单地讲就是,接在一起的Radio菜单项会在同一个组里。那么我利用NewEdit框架对我这一联想进行了试验,如增加一个下接菜单:

radio menu1
radio menu2
radio menu3
other menu
radio menu4
radio menu5
radio menu6

上面可以看出radio menu1-3为一组,radio menu4-6为另一组,因为中间有一个别的类型的菜单项。测试很简单:每个菜单项对应一个处理函数,每个处理函数都将自已设为选中状态。测试表明,的确是存在两个Radio组。其实只要看到显示出来的菜单就应该知道存在两个Radio组,因为有两个黑点(选中标识),所以一定是存在两个Radio组。

如果选中其个菜单项呢?可以使用Menu类的Check方法,注意这里要的是Menu类。因为NewEdit是通过一个列表结构自动生成的菜单,因此中间生成的Menu类没有保存起来,那么怎么办呢?可以使用MenuBar类的FindMenu方法先得到某个Menu的索引值,再使用GetMenu方法得到真正的Menu对象。在FindMenu方法中要求指定Menu的标题,可以使用标识有快捷键的写法也可以只是内容,如’&File’, ‘File’都是可以的。

NewEdit的项目框架已经基本完成,主要完成了:

  1. Mixin结构框架
  2. 主窗体
  3. 二个测试性子窗体
  4. 菜单

因为本项目为测试性项目,现在还未考虑作为正式项目进行发布,因此现在只能通过cvs进行访问。当功能达到一定要求时,会增加相关的下载。

项目源码cvs下载介绍:

本项目位于共创软件联盟,借用了flyedit项目的空间(现flyedit项目已经改名为NewEdit,不过cvs仓库已经改不过来了),上面多了一个newedit模块。

从CVS取得源码分为二步:

cvs -d:pserver:anonymous@cvs.cosoft.org.cn:/sfroot/cvs/flyedit login

当需要输入口令时回车即可

cvs -z3 -d:pserver:anonymous@cvs.cosoft.org.cn:/sfroot/cvs/flyedit co newedit

关于在windows上使用cvs和ssh可以看我以前的贴子:从SF.cvs上获取DrPython的源码

运行NewEdit.py 或NewEdit.pyw即可。因为现在只是测试框架的可行性和正确性,因此只能看到测试性结果。

运行要求:wxPython 2.4以上,Python 2.3.3。

2004年04月20日

SubVersion 已经有了 Python 语言的绑定,不错。可以去 http://pysvn.tigris.org/ 访问。

2004年04月19日

今天在CZug上浏览偶然看到 SubVersion 是新一代的CVS,于是很感兴趣。因为我平时都用CVS,因此对CVS的感情很深,不过有些地方CVS还是不尽如人意,这些地方在我找到一篇Blog文章上已经说了:


http://www.blogbus.com/blogbus/blog/diary.php?diaryid=121157


而且还找到 SubVersion 的英文文档:http://svnbook.red-bean.com/svnbook/book.html


我们的台湾兄弟已经将其翻译成中文了:http://freebsd.sinica.edu.tw/~plasma/svnbook/book.html


这下学起来方便了。不过看到 SubVersion 要使用数据库,可能安装要麻烦吧。不过我还没有试过,还不清楚。

2004年04月18日

做编辑器是一件很有趣的事情。以前我就做过 flyedit 项目,它是基于 Python + Tkinter 开发的,是从 Python 的IDLE修改而来,不过增加了许多功能,最主要的就是想要实现一种灵活的插件技术。我的想法是提供一个稳定而易于扩充的核心,新的功能可以全动态地增加,体现在:菜单,弹出菜单,热键,及相应的处理。增加某种功能,只要编写一个插件即可,从而不用再在原来的代码上修修补补。用户也可以根据自已的需要使用或不使用某些插件,从而实现完全自已的风格。不过这个项目现在已经停止了,最主要的原因就是没有一个很好的编辑控件,还有就是Tkinter功能的限制。flyedit 这个项目最早是在 2001 年做的,当时想用 wxPython 的,但那个时候 wxStyledTextContral 控件很不稳定,打开两个就出问题,因此转而使用 Tkinter 开发,而且基本的功能已经完全实现(因为大部分的代码是从 IDLE来的,如语法识别、替换等,但效率上有些低[1]),而且还可以将 StructText 文本转换成 HTML 文件(加入了 StructText 模块),还支持可供用户使用的简单插件,其实平时也够用了。不过,由于使用的是 Tkinter 因此在 windows 上还是功能有限,特别是想实现多文档时很困难,还有就是中文的支持,那时我也不是很懂。再加上后来在 Linux 上测试失败了,就从此停了下来。


现在机会终于来了,wxPython 已经到了 2.5.X,wxStyledTextContral 控件也已经非常稳定,还有就是我参加了 DrPython 项目,有许多已经实现的功能可以直接使用。因此,我打算使用 wxPython 来重新实现 flyedit 的思想。不过只想作为试验性质,如果成熟的化,可以推出正式的版本。而且本版本也不打算替换现有的 DrPython 项目,一方面那个项目不是我的,另一方面DrPython本身也开始庞大,并不容易修改。因此,我可以一边做 DrPython ,一边做这个试验试的东西,可以把一些好的东西带过来。如果有可能,希望可以和 DrPython 进行融合。


那么我想在做这个项目的过程中,一方面可以把我的想法变成现实,另一方面可以学习和积累许多关于 wxPython 编程的经验。而且通过Blog,我可以把我的想法变成文字与大家进行交流,真是让人激动呀。


项目正在策划中,很快就开始了。嗬嗬!


[1]为什么效率低呢?因为IDLE的语法识别全部是在 Python 中实现的,这些处理是很费资源的,所以效率低。而wxStyledTextContral是Scintilla的封装速度非常快。

2004年04月17日

这几天在看 DrPython 的代码的时候,看到了 drPreferences.py 是用来保存参数配置信息的。 DrPython 中可以配置的参数非常多,因此它使用 drPreferences 类来进行保存,而且为了使用户的修改能够保存起来,那么需要将类的信息保存起来。这里我用类的信息并不精确,应该说是将 drPreferences 类的某个实例的成员属性保存起来。只需要保存属性值就可以了。而且在启动 DrPython 时,还需要将信息从保存的文件中读出来。 DrPython 使用了 drPrefsFile.py 模块来作这件事,不过代码好长,而且语句相似,因此我就想到有没有简单的方法实现这一功能。


一种是可以使用pickle或cpickle模块。 pickle 在英语中可以译为“腌制”,不过翻译成中文可不怎么好听,还是保留原样吧。它的作用就是将对象或模块保存到文件中,有些面向对象技术把这种处理叫作“serialization(序列化)”,而且可以从文件中恢复对象的值。cpickle与pickle功能基本一样,但cpickle是C语言实现的,速度要比pickle快1000倍(这句话不是我说的,是 Python 文档中说的),而且cpickle不支持子类化,而pickle可以。那么哪些数据可以被pickle呢?文档中说:



  • None, True, and False
  • integers, long integers, floating point numbers(浮点数), complex numbers (复数)
  • normal and Unicode strings (正常和Unicode字符串)
  • tuples, lists, and dictionaries containing only picklable objects (仅包含可以pickle对象的元组,列表和字典)
  • functions defined at the top level of a module (定义在模块顶层的函数)
  • built-in functions defined at the top level of a module (定义在模块顶层的内置函数)
  • classes that are defined at the top level of a module (定义在模块顶层的类)
  • instances of such classes whose __dict__ or __setstate__() is picklable(这样的类实例,它们的__dict__ 或 __setstate__() 是可以pickle的)

这样,使用pickle模块提供的方法,dump()进行pickle操作,load()进行unpickle操作。还可以先生成Pickler或Unpickler类,再调用相应的dump()或load()方法进行pickle操作。保存的结果是用ascii表示的字符序列。有兴趣的可以看一看,乱七八糟的:)不过,虽然可以保存,但不直观。pickle还有一点要注意的是,在你从文件中恢复对象时,对象的类所在的模块应该是可以导入的,也就是说pickle在unpickle时,因为要生成原来类的对象,因此首先要找到类对象所在的模块,然后将其导入再生成类的实例。如果在unpickle时,类模块无法导入,unpickle会失败。


第二种就是自已来做。当然,如果数据类型很复杂,自已做的确麻烦。不过,就 DrPython 项目来说,保存到文件中的每个参数都不复杂,作起来不成问题。这里我们讨论一下最简单的情况。


如我们有一个类:



class a:
    def __init__(self):
        self.a=1
        self.b=’a’
    def pp(self):
        print self.a, self.b


b=a()


枚举对象的所有成员变量


下面我分析一下对象b,因为a是类,只有b才有真正的数据,保存它才有意义。



>>> print b.__dict__
{‘a’: 1, ‘b’: ‘a’}


因此对象的__dict__中存放着所有的成员变量。这样,我们可以对__dict__进行循环处理,即可以把它们保存到文件中去。我们可以一个模块做这件事。



from types import*


def _defaultsavefunc(k, v):
    if type(k) == IntType:
        return “%s i %d\n” % (k, v)
    elif type(k) == StringType:
        return “%s s %s\n” % (k, v)
    else:
        raise SaveException()


def saveobj(obj, file, savefunc=_defaultsavefunc):
    for k, v in obj.__dict__.items():
        file.write(savefunc(k, v))
       
class SaveException(Exception):
    pass


这样,我们使用saveobj就可以将一个对象保存到文件中去。例如:



if __name__ == ‘__main__’:
    class a:
        def __init__(self):
            self.a=1
            self.b=’a’
        def pp(self):
            print self.a, self.b
           
    b=a()
    saveobj(b, open(“mysave.p”, “w”))


当我们执行完毕后,查看文件mysave.p,会看到:



a i 1
b s a


一个变量占一行,变量名后面为一个空格,再后面是类型:i为整数,s为字符串,再后面是空格,然后是它的值。当然这个例子很简单,它只实现了两个类型:int和string。


保存做完后,下面就可以做恢复了。



def _defaultreadfunc(line):
    k, t, v = line.split(‘ ‘, 2)
    if t == ‘i’:
        v=int(v)
    return k, v


def readobj(file, readfunc=_defaultreadfunc):
    obj=_emptyclass()
   
    line=file.readline()[:-1]
    while line:
        k, v = readfunc(line)
        setattr(obj, k, v)
        line=file.readline()
    return obj
   
class _emptyclass:
    pass


这里为了返回一个对象,我们先建立了一个空类,用它生成一个对象,然后接收我们保存在文件中的数据。测试一下:



>>> c=readobj(open(“mysave.p”, “r”))
>>> print c.a, c.b
1 a


但这里面其实是存在一些问题的,我列在下面,大家可以思考一下:



  1. 为什么要用空类,不能直接生成a的对象吗?
  2. 可以使用c.pp()来输出吗?

回答一:使用空类是因为我们没有在文件中保存b对象的类信息,如:类a在哪个模块中,类名是什么。因些通过文件生成类a的对象是不可能的,因此只能用一个空类。这也就是为什么pickle需要能够导入对象类的原因,这样它就可以真正生成原来类的对象了,而不是象我们生成了另外一个类。


回答二:因为c是空类(_emptyclass)的对象因此不存在pp()函数,调用会失败。


如果想解决这些问题,首先要在保存文件中保存原来的类所在的模块和类名,使用类的__module__和__name__值即可。然后在生成类时,根据文件中的模块名和类名,将类导入,然后修改对象的__class__值即可。说起来容易作起来难。下面我从pickle中找到部分代码贴在下面:



    def find_class(self, module, name):
        # Subclasses may override this
        __import__(module)
        mod = sys.modules[module]
        klass = getattr(mod, name)
        return klass


这是根据模块名和类名找到真正的类对象,没错,是类这个对象,不是类的对象这个对象。糊涂了吗?类也是对象啊!


    def _instantiate(self, klass, k):
        args = tuple(self.stack[k+1:])
        del self.stack[k:]
        instantiated = 0
        if (not args and
                type(klass) is ClassType and
                not hasattr(klass, “__getinitargs__”)):
            try:
                value = _EmptyClass()
                value.__class__ = klass
                instantiated = 1
            except RuntimeError:
                # In restricted execution, assignment to inst.__class__ is
                # prohibited
                pass
        if not instantiated:
            try:
                value = klass(*args)
            except TypeError, err:
                raise TypeError, “in constructor for %s: %s” % (
                    klass.__name__, str(err)), sys.exc_info()[2]
        self.append(value)

上面生成对象采用了两种方法,一种可以称为MixIn的方法(上面红色字),即先生成一个空类,然后根据导入的类将这个空类变成为导入的类,即把一个原来是类A的对象改成了类B的对象。有趣吧,体会到 Python 动态性的强大了吧。另一种是直接由导入的类生成实例(上面兰色字)。这两种生成策略是由环境不同造成的,这就不细说了。


因此要实现比较完善的保存机制的确还有一些工作要做,有兴趣的话,可以做一个。


那么我们生成的这个对象到底算什么呢?它只是将原来对象的数据恢复了过来。不过,我们还可以使用一个变通的方法使我们的恢复工作做得更好。我们可以这样设想,我们已经存在了一个类的对象,现在只是想将它上次的状态恢复,而不是重新生成一个新的对象,如果这样的话,可以做得简单一些。修改readobj函数:



def readobj(file, obj=None, readfunc=_defaultreadfunc):
    if obj == None:
        obj=_emptyclass()
   
    line=file.readline()[:-1]
    while line:
        k, v = readfunc(line)
        setattr(obj, k, v)
        line=file.readline()
    return obj


这样,我们将obj做为一个参数,如果传入一个对象了,就直接将值保存在对象中,否则再生成一个新的空对象。这样的效果要好一些。测试一下:



>>> c=a()
>>> readobj(open(“mysave.p”, “r”), c)
>>> c.pp()
1 a


看见了,一切OK。


只不过它的前提是我们已经有了一个对象了。


还有问题吗?我想可能就是对于复杂数据的表示,如tuple, list, dictionary,更复杂的如子对象,嗬嗬!有兴趣的自已实现吧。我想可能使用xml格式可能是一个好方法。

2004年04月16日

GrassLand 搜索的结果会生成一个RSS,很有趣。也就是说,你可以把这个RSS存起来,以后不需要总是访问GrassLand站点提交相同的搜索内容。这样,如果你的目的很固定,那么把RSS保存起来会非常方便。不过也发现 GrassLand返回的结果好象只是按日期顺序排列,其它的选项:相关性、Blog级别好象没什么用。而且它的匹配不是先整词匹配,再包含匹配,而只是包含就行,从而造成命中不精确。还是Google命中高。

2004年04月15日

有时在写文章时,需要引用一段代码。为了说清楚那些行的语句的作用,需要给语句加上行号。于是我利用 DrPython 中的客户化脚本的功能写了一段小程序可以完成这一工作。下面是代码:



#drscript
#This drScript can add line number to selected text base on 1
#limodou(chatme@263.net) 2004/04/16
#version 1.0
text=DrDocument.GetSelectedText()
lines=text.split(‘\n’)
endtext=”
if lines[-1]==”:
    del lines[-1]
    endtext=’\n’
text=”\n”.join([ "%4d  %s" % (i+1, lines[i]) for i in range(len(lines))])
text += endtext
DrDocument.SetSelectedText(text)


大家有兴趣可以试一试。先把它保存在一个文件中,然后用Add DrScript加进去。使用时,先选中要加行号的文本,再调用菜单功能就行了。关于如何生成客户化脚本,可以参考我以前写的文章:


DrPython客户化脚本介绍