最近由于老板需要做一个基于ArcGIS的地理分析工具。
经过分析权衡,最终选用了Python作为开发语言,开发出的工具将在ArcToolbox中运行。
由于需要存在比较复杂的用户交互,ArcToolbox自带的界面无法满足需求,因此使用了PyQt做用户界面。
这差不多还是我头一次用脚本开发一个完整的应用。麻雀虽小(就数千行代码),但也五脏俱全。
此帖就来总结一下做这个工具的某些经验教训。
一方面是自我总结,一方面也希望给各位迄今为止没用脚本干过大程序的静态语言拥趸的筒子们以微不足道的启发。
先来讨论一下设计上的差异。
说到设计,便会讨论到设计模式。
对于设计模式的经典图书《Design Patterns》而言,它所提出的模式基本是针对静态语言,有关设计模式的实现方案的讨论,也多半是针对C++一类的强类型静态语言。在Python的开发过程中,会发现很多模式(或者其它的惯用手法)在原有实现的基础上有不小的变化,甚至一些原则也有所变动。
最典型的例子是,在C++中,会提倡使用接口继承而不是实现继承;实现继承改由委托来完成等一系列原则。
原先在C++中用接口继承来分割接口与实现,在Python中,可以完全不使用继承,而采用动态类型的特性,以类似于运行期Concept的方式达到接口的目的;
相对的,继承尽管也用于设计中,但是更主要的是以extend这样的方式对原有的类进行功能扩展,使得行为类似于decorator模式;减少扩展所需要的代码量。
同时,python不支持重载(因为都是动态类型,也确实没法重载),如果想重载只能在函数体中用instance of这样的函数进行判断并手工分派(你拿字典分派也是手工分派),至少我目前只知道这个办法。为了避免不必要的错误,建议大家还是用命名对多个重载函数显示的区分,对于需要重载的构造函数,还是用Factory Method比较好。
关于重构,如果脚本没有对应的测试,一定不要重构。对于静态语言,这一条件要稍显宽松,但是在Python这样的动态语言上,就连Rename这样的小型重构,都需要测试的保证,因为几乎所有的错误只有在运行期才能被检定出来。
其次,说一下代码编写的问题。
和C++相比,Python是很节省代码的。一方面要归功于语言机制,另一方面,Python丰富多样的标准库也为我们节省了不少的代码量。
先来说说语言机制。
python一个很好的地方,就在于它将list、tuple、set、map/dict作为了build-in的要素,python的语法为这些要素提供了first class的支持。这使得我们在编写容器相关操作时可以非常的方便。通常程序中大量存在类似的操作,在静态语言中大量的语句可以在python中一句概括;在实际的编码过程中,一定要灵活运用Python容器操作,写出干净利落的代码。
其二就是动态类型也让我们不需要为类型约束填写过多的代码,比如不必要的继承与接口定义。这些代码的节省其实是很可观的。
其三,lambda、可调用体(其实就是仿函数)被语言机制直接支持,也是能节省大量代码的重要因素。凭借仿函数,可以写出大量在C++中难以编写出的简洁优雅的代码。虽然boost费劲心思提供了functor与lambda,但是这些库编译之慢,调试之辛苦,相比大家都是有感触的。
个人体会:
首先,灵活运用语法优势,特别是一些通用的初始化格式,以及一些特殊的写法,比如list的构造格式,slice等。这点往往也是脚本和静态语言相比最大的优势。
其次灵活运用内建函数。内建函数往往最能发挥语法优势,甚至可以填补一些语法上的空缺。个人印象最深的,要数map/reduce/zip/lambda这几个函数/语言机制。这些东西运用好了,能很大程度上简化本来复杂的循环代码。当然,对于多层循环而言,个人不太建议用嵌套的map一类的函数,外层的还是展开写可读性比较强,内层则保留以简化结构;
同时我也不太愿意用大量的lambda函数,因为lambda函数本身很占版面,用多了代码不那么好读。必要的时候,还是用def定义出去比较好了。可调用体用恰当了,能简化代码,但是用的太多或者用法不好,也会影响可读性。
同时,python对于内建类型的模拟做的很好,它提供了一系列buildin function的重写方法,可以达到完全乱真的目的,这一点做的比C++还要好。
常规的内建函数就不讨论了,Python的Ref上都有。
讨论一下__getattr__, __setattr__这两个函数。如果我们用getattr函数,或者XXX.xxx这样的方法取得对象的一个成员,python首先会到对象内建的属性字典中查找。如果找不到,要么raise一个exception出来,如果重写了__getattr__,那就调用这个函数。因此这两个函数实际上是实现了属性get/set的挂钩。一般来说,我用get/set都不是为了单纯的get/set,实际上是为了保持对象内部的一致性,访问的安全性,细节的封装性等等目的,不同的属性不是那么容易就用同样的逻辑代码。
本来像C#那样为每个属性提供独立的get/set其实挺好的。Python则把所有的成员的get/set都拢到一起了。如果存取的附加代码稍有差异,就容易写出if...elif...elif...else这样的分支代码。
对这个问题,我是这样做,属性对应的成员变量放入字典中;每个需要做复杂存取操作的属性,有一个存取函数,存取操作的区别用if分开,属性与存取函数,则用一个字典来关联。这样,attr函数首先访问存取函数的字典,按照需要执行存取操作。如果属性不对应存取函数,那么就直接访问属性字典。
对于查找不到的情况,建议先捕获异常,然后仿照的内建的属性访问异常抛出。
当然,有时候我也完全不用属性,直接使用get/set函数的形式。不过这样的话还是不如属性来的方便。况且有时候括号漏写了,代码直接就来个运行期异常,也是挺郁闷的。
最后讨论下私有函数。对于python来说不存在真正的私有函数,一般来讲,要表达“私有实现”都是用“_”开头的命名约定。
最后,讨论一下调试和测试。
恐怕保证程序的正确性上,脚本还是要比静态语言难。缺乏静态检测的脚本,连拼写错误都要延期到运行期才能被检测出来。因此大量脚本的调试,还是相当痛苦的。
在这里,确保有有效的单元测试对脚本比对静态语言要重要许多。这次做的工具,一开始并没在意,但是到了中期以后,发现调试占用了大量的时间(因为ArcGIS本身启动速度就比较慢,执行一个调试周期很长),才开始给代码部分地方补充了一些单元测试。有了单元测试以后,很大程度上缩短了调试周期。
单元测试也是脚本重构的必要条件。
对于脚本而言,由于代码量比传统语言少很多,因此利用TDD一类测试先行的方法恐怕比在静态语言上的收益要大得多。
在脚本中,偶尔要写一些防卫代码。在工程的早期,我在防卫代码,特别是类型约束的代码上做了大量的工作。但是后来发现,这些防卫代码本身引入的错误不必原始工程来的少。因此中期之后,对于内部的类,撤消了大部分的类型防卫代码,而改用测试保证内部逻辑的一致性。这样减轻了代码量,提高了可读性。
--------------------------------
其实脚本对于Web开发来说一点都不陌生。上次跟老李说这事,他说他至少写了万行的jsp和vbs代码,调试不难,但是要保证正确性很难。测试很大程度上成为了脚本的救命稻草。
一般来说,脚本比静态语言省代码,比静态语言方便,比静态语言XX,但是如果没有测试,这些,都是镜花水月而已。