Mix-in技术与分布类编程
作者:limodou
大家一看到这个题目,看到“分布类编程”可能会认为是一种什么新技术,其实只不过是我个人所创,是指一个类的实现由多个文件(或模块)组成。至于它如何构成,有何作用,及相应的实例且听我慢慢道来。
Mix-in技术简介
关于Mix-in技术本人有专门的文章讲述,这里就不再赘述,而只进行简单地介绍。[1]
如果我们在运行时改变一个类的基类或类的方法、属性等就叫做Mix-in。那么它与类派生和重载有什么区别呢?根本的区别就是它的动态性。派生和重载是在程序运行前就已经对类进行了修改,它的改变是确切存在于某个文件中的,这种改变在运行时是稳定的。而Mix-in是在运行时才对相应的类发生作用,其运行时的表现与文件中所描述可能有所区别,而且随着运行环境的不同其表现也可以发生变化。另外,对于派生后的类,在运行时创建其一个实例就可以使用了。而使用Mix-in技术,我们需要将新的基类或方法先加入到原来的类中,然后再创建实例进行应用。
Mix-in技术的主要实现方法有两种:加入基类和加入方法。加入基类则相当于从基类进行派生,从而使原来的类具有基类的方法和属性。加入方法使来的类具有新的方法,如果存在加入的方法与原类中的某个方法同名,则新的方法将替换原来的方法(这个规则不是必然的,因为Mix-in的实现是你自已编写的,而不是固定不变的)。加入基类可以修改类的__bases__属性。加入方法可以使用内置函数getattr()和setattr()来实现。那么对于这种Mix-in技术在实现时还要考虑当新的方法与原方法重名的处理。
之所以会有这种伟大的Mix-in技术的存在,完全要得益于Python语言的动态性(当然可能不止一种语言能够实现这种技术),它允许你在运行时修改类所有的属性,也可以增加新的属性。通过setattr()我们就可以向一个类增加新的方法,并且使用它。
分布类的构成
类是对象的定义和描述,通过类我们就可以生成实例,即对象,用它进行处理。分布类,即表明类的定义不是一次性定义出来,而是分布在不同的文件或模块(以后我们就叫它们为分布类文件或分布类模块)中定义的。可能的一种类结构的分布如下:
+----原始类
+----基类1
+...
+----基类n
+----新功能1
+...
+----新功能2
+----Mix-in模块
上面的意思就是在一个目录下,有一个原始类,可能有n个基类模块,有n个新功能模块。最后为了实现Mix-in技术还需要一个Mix-in模块,而这个模块可以是你自已编写的。更复杂的结构可能是原始类下还有子目录,但处理方法都是一样的。
在进行基类与新功能的编程时要考虑重名方法的情况。在新功能模块中,方法定义应按照类的方法进行定义,即第一个参数应为对象实例参数,如self。
在运行时,应先使用Mix-in模块中的处理函数将基类与新功能加入到原始类中,然后再生成类的实例,进行使用。
大家可以看到,将类分成多个文件进行编程并不复杂,而Mix-in的处理是比较复杂的,应根据实际情况进行处理。
分布类的作用
为什么要使用分布类呢?就我个人来说,主要有以下的好处:
清晰。把一个类的不同功能分组放在不同的模块中,可以清晰地看出类的构成,容易理解,维护起来相对容易。我们在写程序时,可以将最核心的功能写在一个文件中,在其中定义类的属性,类的初始化,及一些最基本的功能。然后对于类的扩展,我们可以根据功能分组放到不同的文件中。这样使得每个文件所描述的功能相对集中,而不再是拥肿的代码。
灵活。如果我们的类的功能耦合性非常小,那么增加、卸载功能都是相当的容易,只要把相应的文件加入或去掉就可以了。在调试程序和定位错误时可能会很方便。如果我们想使某些功能有效,就加入相应的文件,反之,去掉相应的文件。如果发现有错误,可以通过一个文件一个文件地减少,从而进行一个文件一个文件地进行排除。
那么可能带来的不便是:文件变多,没有一个完整的静态描述。那么如果喜欢静态类,我们完全可以在类的功能测试完全之后再合并成静态类。至于需不需要这样做完全看你自已了。
使用实例
下面举一个本人所写的FlyEdit程序中所用到的例子。FlyEdit是一个编辑器,它的基本结构就是建立在分布类的基础之上,其中还有一些特别的处理。FlyEdit的分布类模块放在两个目录下,一个为modules,另一个在plugin中。modules为相对核心的部分;plugin中为可由用户自行编写的插件功能模块。在FlyEdit中,最主要的一个类是编辑器类(Editor),它实现了编辑器的基本功能,同时规定了分布类模块某些功能的执行位置(如分布类模块的初始化就放在Editor类的初始化函数内,而且放在所有的可视控件初始化完成之后)。这个类放在主程序 flyedit.pyw中。同时flyedit.pyw中在生成编辑器类时先进行Mix-in的处理,然后再生成类的实例。
1 import register
2 register.registerpackage(Editor, 'modules')
3 register.registerpackage(Editor, 'plugin')
4 root=Tk()
5
6 from windowlist import windowlist
7 windowlist=windowlist(root)
8 Editor(root, file, windowlist)
9 root.mainloop()
第1行导入register模块。它是我写的实现Mix-in功能的模块。
第2,3行,对modules子目录和plugin子目录下的文件进行Mix-in处理,将新功能加到Editor类上。在分布类模块中不仅有新的方法,还有一些新增的数据,如菜单等。这样为了使分布类的加入顺序不变,将modules和plugin目录做成python包的形式进行处理(即加入 __init__.py文件);同时在包的__init__.py文件中定义了__all__列表,用来说明哪些模块应该Mix-in,并且加入的顺序如何。
第8行,使用新的类创建实例。
下面举一个这样的包结构。
+----modules
+----__init__.py
+----a.py
+----b.py
对于__init__.py文件,内容可能为:
__all__=['a', 'b']
在modules目录下共有三个文件,其中__init__.py文件记录了哪些模块将Mix-in到待处理的类中去。它通过定义一个全局变量 __all__来实现。__all__为一个列表,每一项记录了要Mix-in到类中去的模块文件名(没有.py的后缀);同时列表的顺序表明每个模块 Mix-in的顺序。a.py和b.py即为真正的分布类模块。从__all__的内容上看,先处理a.py再处理b.py。如果想改变处理顺序或去掉哪些模块,则只要修改__all__的列表值即可,而不用将相应的文件删除。
对于分布类模块,如a.py中应定义要Mix-in到类中去的方法或属性。同时也应定义一个__all__列表,其中列出所有要放入到类中去的方法、属性的字符串名称。那么在进行Mix-in处理时,只有在__all__中定义的方法或属性才会被处理。
那么Mix-in过程是如何实现的呢?下面将FlyEdit中所用到的register模块简述一下。
01 #register functions to a class
02 def registerpackage(class_, modulename):
03 module=__import__(modulename)
04 if hasattr(module, '__all__'):
05 for i in module.__all__:
06 register(class_, modulename+'.'+i)
07
08 def register(class_, modulename):
09 import types
10 module=my_import(modulename)
11 if hasattr(module, 'init'):
12 value=getattr(module, 'init')
13 class_.initlist.append(value)
14 if not hasattr(module, '__all__'): return
15 for name in module.__all__:
16 property=getattr(module, name)
17 t=type(property)
18 if t in (types.DictType, types.TupleType, types.ListType):
19 if hasattr(class_, name):
20 value=getattr(class_, name)
21 if t == types.DictType :
22 value.update(property)
23 else:
24 value=value+property
25 else:
26 value=property
27 setattr(class_, name, value)
28 else:
29 if hasattr(class_, name): #exist a func
30 delattr(class_, name)
31 setattr(class_, name, property)
32
33 def my_import(name):
34 mod = __import__(name)
35 components = name.split('.')
36 for comp in components[1:]:
37 mod = getattr(mod, comp)
38 return mod
02行,定义函数registerpackage,用于将一个包结构Mix-in到类上。
03行,导入包。由于这里使用字符串名字作为包名,故应使用__import__内置函数。
4行到6行,判断如果包中含有__all__属性(变量),则依次取出__all__的值(每个值为一个模块名),然后把每个模块名与包名相接,生成“包名.模块名”的可调用的形式,再调用register函数Mix-in到类中。
08行,定义将某个模块Mix-in到指定类的函数。
10行,导入真正的模块。使用__import__函数时,对形如“包名.模块名”的模块,只会返回包对象,而不是最后的模块对象。故调用一个可以真正返回模块对象的函数my_import。
11行到13行,判断模块是否有init属性,如果有则取出放到类的初始化列表中(这个处理是针对FlyEdit所特有的,主要是想解决某些模块需初始的问题,在你的应用中可以不使用)。
14行,判断模块如果没有__all__属性,则不会进行Mix-in处理。这样,只有定义在__all__列表中的内容才会被Mix-in,而其它的内容会被看到“私有”内容对待而不会Mix-in。
从15行到31行,是真正的Mix-in的处理。处理的对象是__all__列表中所有的元素。
16行,根据属性的字符串名,取出分布类模块中相应的属性。
17行,得到属性相应的类型。
18行到27行,如果属性类型为字典,元组(tuple),列表时,当类已经存在同名的值时,将新值添加到原值中;当不存在同名的值时,将新值加入到类中即可。
28行到31行,如果属性为其它类型,则先删除原值,再增加新值。
33行到38行,实现将“包名.模块名”形式的模块导入,返回模块对象。此处不详说了。
结论
分布类编程在软件开发阶段可以作为一种调试与功能编写的有力方法。同时在软件应用阶段可以作为一种插件技术而使用。大家要注意的是,本文实例中所介绍的具体的分布类的构成与Mix-in方法的实现都是基于FlyEdit的实现。大家在实现自已的项目时应使用自已的方式。
--------------------------------------------------------------------------------
[1] 关于Mix-in技术的文章见本人写的《Mix-in技术介绍》。
原文见我的主页,可能有格式问题。