动 态 语 言 - bobseadream的专栏 - CSDNBlog

来源:百度文库 编辑:神马文学网 时间:2024/06/03 07:49:28
1  引 言
语言是交流的工具,计算机语言是人与机器交流的工具。从广义上讲,任何描述算法和数据结构的符号都可以构成一种程序语言,但计算机语言是指那些在计算机上实现、能被计算机所理解的语言。自从20世纪50年代最早的高级语言出现以来,随着计算机硬件价格廉价化和工作高速化,新思想和新技术不断涌现,特别是Internet的出现,对计算机语言的发展产生了深远的影响。从早期二进制机器代码,到增加了助记符的汇编语言,再到面向特定应用领域的语言,以及现在功能越来越强大的系统语言。
纵观程序设计语言的发展,一个很显著的特点的是以机器的性能换取人的效率,以提高开发者的工作效率和满足人的需求为目标,在所有的资源中人是最昂贵的资源,因而程序的编写从面向机器逐渐向面向人过渡。早期二进制代码是完全面向机器的,机器可以直接执行,不需要任何编译或解释环节,因而执行效率极高,但用二进制编写程序对于人来说,不但枯燥无味而且工作效率极其低下,一连串的杂乱无章01代码要人去理解和记忆是不可能的。为了提高人的工作效率和程序的可读性,于是对二进制代码表示的机器指令换成便于人们识别记忆的符号,便形成了带有助记符的汇编语言,人们用汇编语言编写汇编指令操纵机器,具体操作可直接作用到底层硬件的某个存储单元、寄存器和设备接口,程序的执行速度并没有受到很大的影响,较之机器代码,人的工作效率显然得到了一定的改善,人们只需要识记相对简单的汇编指令集,但是汇编指令仍然是面向机器的,不同机器上有不同的指令集,机器的体系结构对汇编指令源程序的影响很大,在一台机器上能够运行的程序往往在另一台机器上不能正常运行,开发大型程序的工作量极大,后期的维护工作也很困难。
20世纪60年代中期,随着半导体技术的发展和成熟,机器硬件代价不断下降,而编程的代价在上升,同时也出现各种各样的软件需求,不同应用领域提出了不同的应用需求,开发的软件不但要便于移植还要便于后期的维护,现有的软件技术是无法满足人们对软件提出的要求,为了能够解决这些问题,迫切需要产生新的程序设计语言,从根本上改变人们编写程序的方式。这个时期,编译器技术进一步走向成熟,高级语言应运而生。高级语言类似自然语言又区别与自然语言,对人来说很容易理解和学习,在语法、句法和文法方面都有严格的要求,比起汇编语言,高级语言不仅容易记忆、掌握和使用,而且是面向问题的,为特定应用领域的某些问题提供了良好的解决方案。二进制代码是机器可以执行的代码,因此机器要理解并执行一种高级语言就必须要有一个“中介”充当语言与机器的桥梁作用,这个“中介”根据语言的性质分为编译器和解释器,其用途就是将高级语言翻译为机器可以理解的语言。
软件技术的发展史就是程序设计语言的发展史。高级语言的产生促进了软件技术的革新和发展。自从有了高级语言,软件界发生了日新月异的变化,各种新的软件技术层出不穷。面向过程语言的产生使得结构化程序设计成为软件开发最基本的方法,程序的流程控制是分析程序必不可少要素之一,许多技术创新为后来的软件开发技术发展铺垫了一条宽阔的道路,如模块化设计思想、结构化设计、信息隐藏,并成为软件工程的早期发展,为面向对象语言的产生提供了宝贵的技术来源。当然,从软件工程的角度看,面向过程语言难以开发出可复用性、可扩展性、易于维护的软件,面向对象语言克服了面向过程语言的缺点,将面向对象技术都融合到语言中,在语言一级支持不同层次的模块化设计,更好的实现了数据抽象和信息隐藏,继承实现了软件复用,多态便于动态重构,基本可以应付一些简单的需求变更,为开发出高质量的软件提供了一把利器,所谓“工欲善其事,必先利其器”,有了锋锐的工具,做起事来当然会又快又好。然而,“金无足金,人无完人”,面向对象语言中的对象技术也存在与生俱来的缺陷,因此提出了面向方面的编程语言,将软件关注点模块化,弥补了面向对象编程的不足,提高了程序模块的内聚度,更利于软件的维护和复用。正是这些新技术将开发者从与机器打交道的繁杂中不断解放出来,使开发者在有更多的时间和精力集中、专注分析和解决应用领域某些问题的同时,又可以开发出高质量的软件。
每一次软件技术的飞跃发展都与新型程序设计语言的出现分不开,新的软件技术构建了一个新的环境,新的环境对软件技术又提出了更高的要求。早期受到计算机硬件性能和价格的限制,人们主要关注点是如何充分利用硬件提高软件的性能,如今,硬件技术在“摩尔定律”的支配下飞跃发展,计算机运行速度和性能大大提高,人们对软件的要求发生了根本的转变。随着计算机技术深入各个应用领域,软件应用的领域不断扩大,特定应用领域涌现出来的、需要借助计算机才能解决的问题千奇百怪,面对的问题也越来越复杂,对软件的需求也越来越高,需要有更多、更好、更新的技术来解决遇到的问题。一个高质量的软件要求能够适应需求变更,不能适应需求变更的软件所付出的返工代价是惨痛的。现在越来越多的应用甚至要求系统能够在运行中修正和改变,所做出的改变都是开发人员和用户难以预测的,需要软件本身具备这种处理能力,能够支持运行时的动态重构。以C++、Java、C#为代表的典型的高级语言虽然在一定程度上支持运行时动态性,比如虚函数实现的多态性,可以在运行时确定调用的成员函数是父类还是子类的,以及RTTI具有的在运行时识别、加载和管理类的能力,但是在运行时并不能改变类或变量的类型,引用变量前必须声明变量的类型,它们都属于静态语言,在解决某些问题受到了极大的限制,很多技术难以实现,尤其是实现运行时动态重构。
近几年来,以Python、Perl、Tcl、Ruby为代表的动态语言(又称脚本语言)越来越受到开发者的青睐,使用动态语言群体、社区不断壮大,许多开发团队不再单纯使用一种语言编程开发软件,而是混合式编程,动态语言作为胶水语言专为应用程序而设计,在今后的混合式应用中变的越来越重要,甚至有人认为是“21世纪更高级的编程语言”[1]。为什么动态语言一族在新型语言层出不穷的今天可以异军突起,受到如此青睐和厚爱?动态语言是一些什么样的语言?具有什么优势?究竟能够解决哪些静态语言所不能解决的问题?动态语言和静态语言二者有哪些区别?程序设计语言的未来发展又将是如何?本文接下来主要讨论的就是这些问题,首先从程序设计语言的基本原理和概念入手,采用与静态语言对比的方式逐步深入介绍动态语言的语言特征,最后给出了动态语言实例Python作为研究对象,简要的介绍Python的语言特性,并尝试用Python实现了GoF的几种设计模式。
2  基本概念
2.1虚拟机、编译器和解释器
实现一种计算机程序设计语言时,运行时的数据结构和程序执行中的算法定义了一台虚拟机。该虚拟机的机器语言是由语言翻译器所产生的可执行代码,如果语言是编译型的,则它的形式就是实际的计算机二进制代码,实际的机器可以直接执行;如果语言是解释型的,则结构和形式可以是任意的。该虚拟机的数据结构就是程序执行时的数据结构,基本操作是那些在执行时实际执行的操作。编译型语言C所定义的虚拟机的机器代码是二进制代码。C源程序要生成可执行程序需要经过C编译器编译成目标机器代码,不同的平台要编译成不同的目标代码。Java虚拟机是Java程序的运行环境,Java虚拟机是一个虚拟的计算机,它有自己的处理器、堆栈、寄存器,还有相应的指令系统,同时也有支持的数据结构和基本操作。要运行任何Java程序首先必须通过Java编译程序翻译成Java虚拟机可以理解的目标码(Java虚拟机将这种机器码称为字节码byte code),执行Java程序时实际上是在Java虚拟机上执行字节码,虚拟机负责将字节码解释称特定平台上的机器指令,Java程序运行时的数据结构就是虚拟机所定义的数据结构。虚拟机在平台和程序之间充当了纽带作用,承上启下,进一步抽象了系统平台,使得Java程序与具体机器的体系结构、操作系统平台无关,因而成为Java语言的一个重要的特性。
在具体计算机上实现一种语言,首先要确定的是表示该语言语义解释的虚拟计算机,一个关键的问题是程序执行时的基本表示是实际计算机上的机器语言还是虚拟机的机器语言。这个问题决定了语言的实现。根据这个问题的回答,可以将程序设计语言划分为两大类:编译型语言和解释型语言。
由编译型语言编写的源程序需要经过编译、汇编和链接才能输出目标代码,然后机器执行目标代码,得出运行结果,目标代码由机器指令组成,一般不能独立运行,因为源程序中可能使用了某些汇编程序不能解释引用的库函数,而库函数代码又不在源程序中,此时还需要链接程序完成外部引用和目标模块调用的链接任务,最后输出可执行代码。C、C++、Fortran、Pascal、Ada都是编译实现的。高级语言转换为可执行代码的过程如下图:
解释型语言的实现中,翻译器并不产生目标机器代码,而是产生易于执行的中间代码,这种中间代码与机器代码是不同的,中间代码的解释是由软件支持的,不能直接使用硬件,软件解释器通常会导致执行效率较低。用解释型语言编写的程序是由另一个可以理解中间代码的解释程序执行的。与编译程序不同的是,解释程序的任务是逐一将源程序的语句解释成可执行的机器指令,不需要将源程序翻译成目标代码后再执行。对于解释型Basic语言,需要一个专门的解释器解释执行Basic程序,每条语言只有在执行才被翻译。这种解释型语言每执行一次就翻译一次,因而效率低下。Java很特殊,Java程序也需要编译,但是没有直接编译称为机器语言,而是编译称为字节码,然后在Java虚拟机上用解释方式执行字节码。Python的也采用了类似Java的编译模式,先将Python程序编译成Python字节码,然后由一个专门的Python字节码解释器负责解释执行字节码。一般地,动态语言都是解释型的,如Tcl、Perl、Ruby、VBScript、 JavaScript等。
2.2  绑定和绑定时间
绑定和绑定时间是程序设计语言中的十分重要的概念。在设计一门新的语言时,首先要确定的就是这种语言的绑定以及绑定发生的时间,为语言的特性和实现定下了一个基调。一段程序实际上是由不同元素组成的,这些元素包括实体和属性,有时一个元素既可作为实体,也可作为属性。在c语言中,语句int count = 5;中,int本身可以被认为是实体—整数类型,它的属性则是该类型所表示的整数范围,16位机器上为−32768~32767;同时int也是变量count的属性,表明count是一个整型变量。为了保证程序元素的语义完整性和可理解性,对于程序中的每个实体都要求有相关的语义信息,即要从一个可能的性质集合中选择一个性质作为实体的性质或属性值,确定实体和属性的约束关系。这种约束关系的确定称为绑定,绑定相对于程序翻译和处理的时间称为绑定时间,绑定时间决定了语言的特性。
绑定可以发生从语言的设计和实现到程序的翻译、连接和执行的各个阶段。在语言定义时,首先构造了一个支撑语言的虚拟机,确定了一个语言的大部分结构,例如,选择语句的形式、数据类型及其所能表示的范围和程序结构。在语言实现时,确定语言定义的细节在具体计算机上的实现方式,不同的实现存在着很大的差别。比如Microsoft C和Turbo C都是C语言的实现,两者实现的C语言大部分是相同的,但在一些方面存在着不同,因此会出现在Microsoft C可以顺利运行的程序在Turbo C中不能运行。我们知道,要使一个以静态文本方式存放的程序成为一个可执行的程序要经过编译、连接和执行步骤。在C/C++语言中,.c文件经过编译后,程序中变量的类型、数据成员的偏移量、数组的存储方式以及静态变量的存储位置都确定下来,并且完成了类型检查和转换工作。对于文件作用域中尚未定义的外部符号,在连接步骤中连接程序负责在其他的编译单元查找,并将找的符号名所表示的内容绑定到目标代码中的符号生成可执行代码。因此,库函数调用时函数名与函数体的绑定关系发生在连接过程中。一个已经顺利的通过编译和连接的程序,一定可以顺利地在计算机上运行吗?答案是否定的。因为程序中还有一部分绑定需要在程序执行时才能确定下来,如果在确定绑定关系时发生了错误,那么程序的运行也会发生异常。大部分语言中,这些绑定包括数据对象和其存储单元(临时存储单元)、动态内存分配、调用函数时的形参和实参的连接、形参和实际存储单元的对应关系、调用子程序的返回结果等等。
一般地,我们将绑定时间分为两类:一类是在运行前确定绑定关系,称之为“早绑定”;另一类是在运行时确定绑定关系,称之为“晚绑定”。“早绑定”在程序运行前就把大部分程序元素的属性确定下来,运行时只需要完成少部分的绑定,因而执行效率高,但不够灵活,要求程序员在程序运行之前尽可能确定程序元素的属性,一旦确定下来修改起来就十分困难,需要重新编写、编译、连接整个程序。而“晚绑定”恰恰与之相反,它把程序中大部分绑定关系推迟到执行时确定下来,程序执行时要完成大部分的绑定工作,所以灵活但效率不高。“早绑定”和“晚绑定”的优缺点实际上是语言的灵活性和效率的冲突。所谓“鱼和熊掌不可兼得”,关键要根据语言设计的目标在两者之间寻找一个最佳的平衡点。优先考虑执行效率、以执行效率为主要目标的语言通常设计为绑定尽可能地在程序运行之前确定下来的“早绑定”,如Fortran、C、Pascal。而对于追求灵活性的语言来说,如Smalltalk、LISP、Python,ML,采用的则是“晚绑定”。有些语言(Ada)提供了对绑定时间的选择机制,允许程序员自己根据实际情况选择合适的绑定时间。
弄清了“早绑定”和“晚绑定”的概念及其优缺点后,我们可以很容易地回答诸如“为什么Fortran语言在处理字符串方面的能力并不强”、“为什么C语言的执行效率比Lisp语言要高”等问题。当然,比较语言是为了更好的分析语言,不能盲目地评判不同语言之间孰好孰坏,每种语言都有自己的用武之地,都有自己的优势。
基于绑定时间的概念可以定义和比较语言的特性。在程序设计语言中,有两个使用广泛的、相对的术语——静态和动态——实际上是根据绑定时间划分的。一般提到“静态”指的是在执行前所发生的动作,“动态”是指在执行时发生的动作。由此,“早绑定”也可称为静态绑定,“晚绑定”称为动态绑定。一般来讲,“静态”具有执行效率高而灵活性不够的特点,相反,“动态”具有执行效率低但灵活性高的特点,两者是一对相对的术语,是一组对立统一的辨证关系。程序设计语言中有许多概念都涉及到这两者,如静态类型检查和动态类型检查、静态内存分配和动态内存分配、静态作用域和动态作用域,以及本文所阐述的静态语言和动态语言。
3  动态语言
3.1  语言的动态特性
介绍动态语言之前,首先介绍一下语言的动态特性,并引出动态语言的定义。
语言的动态特性表示语言具有在运行时确定绑定关系的性质。实际上,“静态”是一个相对的概念,任何程序设计语言都可以看作具有某种程度的动态特性。一个变量能够在运行时改变自身的值,可以说它具有动态特性,比如最简单的赋值语句:
char ch;
int  m;
ch = cin.get();//从输入流中接收一个字符
m  =  func();  //将func的返回结果赋给m
对字符型变量ch与值的绑定关系要推迟到程序运行时检测到标准I/O流的输入数据才能确定下来,整型变量m的值也要到程序运行时执行了函数func后才能确定。在运行时进行的动态类型检查、动态内存分配都是语言的动态特性的具体表现。变量的寻址方式也是一个动态的过程。程序经编译后,获得了变量的相对于程序代码段(CS)或数据段(DS)的偏移量信息,在程序执行时,操作系统首先将程序装载到内存中足够大小的某个单元中,这个单元的首地址加上变量的偏移量就形成了变量的存储单元地址。此外,面向对象语言中运行时多态性是一个重要的动态特性。从这个意义上说,早期Fortran语言、C、Pascal都具备一定程度的动态特性,但比较弱,而Smalltalk、Lisp、Python等语言在运行时可以改变变量的类型甚至时自身的程序结构,我们就认为这样的语言具有更强的动态特性。
不同的语言具有不同程度的动态特性。纵观程序设计语言的发展史,如果将Fortran和Lisp语言看作是语言在动态特性上的两个极端的话,那么现代大多数语言都是介于二者之间的折中,综合考虑了开发效率和执行效率以及其他众多因素。各种语言也在相互借鉴,不断演变。下图是以Fortran和Lisp为两头,比较了几种语言的动态性程度:
在上图中,从左到右语言的动态特性逐渐加强。处于最左端的Fortran语言不支持堆栈,所有的变量和子程序都是在编译时分配好内存的,不能进行动态内存分配,因而不能进行函数递归调用,许多问题的解决方式受到极大的限制。这主要是由早期Fortran语言的设计目标决定的,早期Fortran语言主要是为了解决科学和工程中的计算问题,优先考虑的是语言的执行效率。虽然Fortran语言被无数次认定为过时,然而它仍然继续发展着。Fortran90大大扩展了传统Fortran的功能,增加了现代高级语言的数据和控制特征,允许内存的动态分配,使得它具有和C和Pascal语言相当的能力。
比Fortran动态性更强的C/C++语言提供了指针,支持堆栈,提供了malloc/new和free/delete操作,运行时可以动态分配和释放内存,可以比较灵活地动态生成对象并分配存储空间。此外,C++语言中的RTTI(RunTime Type Identification)机制可以在只有指向某个对象的基类指针的情况下,根据驻留在虚函数表中的类型信息,在运行时确定该对象的准确类型。然而,C/C++程序中的变量类型仍然需要在编译时确定下来,大部分类型检查也是在编译时完成的,执行前完成了大部分的绑定工作。
现在流行的Java和C#语言的动态特性之所以要比C/C++强,是因为Java/C#提供了更强的反射Reflection机制,可以在运行时通过Reflection APIs取得任何一个已知名称的class的内部信息,包括其modifiers(诸如public, static 等等)、superclass(例如Object)、实现的interfaces(例如Cloneable),也包括fields和methods的所有信息,并可于运行时改变fields内容或唤醒methods。如在C#中,可以通过System.Type类在运行时获取任何给定数据的类型信息,包括构造函数、属性、方法和事件,也可以唤醒(Invoke)相关的方法(具体的使用可参考Microsoft出版的《Inside C#》的“Querying Metadata with Reflection”一章)。
而Smalltalk以右的语言,变量的类型绑定和类型检查都是在运行时进行的。Perl、Python和Ruby语言可以在运行时修改类的结构或定义,变量的类型可以按需改变,编写程序时无须声明变量类型。Lisp语言甚至可以在运行时动态地改变自身的代码, Lisp语言对程序代码和数据一视同仁,都看作是存储在内存中的对象,这样数据结构既可作为程序执行,程序也可作为数据进行修改。
上面介绍的几种语言都具备一定程度的动态特性,那么应该如何定义动态语言呢?目前在许多开发社群中,提及“动态语言”,一种普遍的观点是认为“动态语言是指能够在运行时改变程序结构和变量类型的语言”,有时也称作“动态类型语言”(Dynamic Type Language),本文采用的就是这种观点,在下文中的动态语言都是指具有这种性质的语言,与之相对的概念“静态语言”指的是在编译阶段确定变量或数据对象类型的语言。按照这种说法,上图中Smalltalk/Perl以左的为静态语言,以右的语言都为动态语言。这里需要说明的是,许多技术文章和语言研究中都涉及到“脚本语言”的概念,实际上脚本语言就是动态语言,因为脚本语言也具备在运行时动态改变类型的性质,符合动态语言的定义,本文不区分这两个概念,一律使用“动态语言”,强调该类语言的最明显的特征。另外,有的人也将“静态语言”称之为“系统程序设计语言”,因为大多数静态语言都是系统程序设计语言,这两个概念只是从不同的角度描述了同一类语言,各自的侧重点不同,“静态语言”的“静态”是基于绑定时间的,强调了语言中大部分绑定关系是发生在运行前,而“系统程序设计语言”强调语言所能完成的功能是侧重于编写系统程序。
3.2  动态语言的特点
近几十年来,传统的软件开发模式和工具、编程语言已难以适应软件需求的变化,软件的混合式开发逐渐增加。随着动态语言的兴起,人们的编程方式和观念发生了转变。在诸如C/C++、Java、C#之类的静态语言中,语法规则迫使程序员在使用一个变量前首先要声明它的类型,变量的类型一旦确定下来,那么在整个程序中都不能改变,只能是一种类型。而在象Smalltalk、Python、Perl、Tcl、Ruby、Visual Basic和Unix Shells的动态语言中,无类型化定义使程序员从庞大的类型系统解脱出来,变量不经声明就可以直接使用,变量的类型可以按需改变,编写程序变得轻松自由。本节主要介绍动态语言的特点和编程方式,以及与静态语言的主要区别和联系。
动态语言,顾名思义,最显著的特点就是在于它的“动态性”,即运行时可以按需改变程序结构和变量类型、函数的定义等,这也是与静态语言的根本区别。除此之外,动态语言还有许多特点,这些特点深受开发人员的喜爱,使得动态语言渐入人心。
动态类型
类型系统影响了语言的诸多特性。在静态语言中,声明的目的是为了告诉编译器所使用的变量“是什么”,使类型检查可以在编译阶段静态地进行,尽量减少执行时的类型检查,提高执行效率,但缺乏灵活性。而在动态语言中,没有声明语句,赋值语句将名称绑定在数据对象上,如果名称赋予数据对象一种类型,稍后也可以给对象赋予另外一种类型。变量被设计成无类型的,变量的类型可以按需改变,同一个变量既可作为整型的,也可作为字符串,还可用来定义函数。
我们知道,程序中定义的操作一般需要特定类型的参数作为操作的输入,操作只有在接收到类型正确的参数时才能正确无误的执行,最典型的实例就是函数的定义,函数的原型包括函数的参数列表和返回值类型,参数列表提供输入参数的全部信息,执行函数前首先要进行参数类型的检查。在动态语言中,变量是无类型的,那么如何保证所执行的操作是否接收到类型正确的参数呢?在运行时进行动态类型检查机制解决了类型安全这一问题。动态类型检查通过在每个数据对象中保存一个类型标签表明该数据对象的类型,比如在表达式C=A+B中,A和B的类型在程序运行时确定,也可以在运行时改变,所以每次执行 + 操作时都要根据类型标签对A和B的类型进行检查,只有在类型正确的情况下才能执行,否则发出错误信号。操作正确执行后,确定了变量C的类型,并记下C的类型标签以备随后可能的操作进行类型检查。
显然,动态类型检查不能静态地检测到程序代码中类型不匹配的错误,并不意味着动态类型容易在程序中引入类型安全的错误。事实上,在诸如C++、Java之类的静态类型语言中虽然能够在编译时尽可能多的检测到程序中类型失配的错误,但类型仅仅是数据的一小部分信息,类型正确并不能保证程序中不存在其他的错误。在大规模的程序中,要为类型上编写大量的语句,这就使得程序员专注于程序中的类型正确性而容易忽视程序其他部分的正确性。有些问题用静态类型很难实现,例如对于一个不支持泛型编程的语言来说实现一个可以支持多种类型的容器类就比较困难,假如编写了一个可以存放objects的容器类,在具体应用时要通过向上或向上转换成所需的类型,而这种转换往往是不安全的。而动态类型语言不需要这种转换,它所实现的容器类完全是泛型的,在语言一级就提供了良好的支持[3]。裘宗燕教授则认为“很多人之所以在最初使用具有丰富动态特征的语言编程时容易犯错误,主要是因为他们习惯了诸如C、Pascal等语言,不了解用这些动态特征丰富的语言中编程的一些必要的风格和习惯。如果熟悉了这些风格和习惯,犯错误这件事情同样是可能很好地避免的。使用弱类型的语言同样可以开发很好的系统,而且实际上已经开发了很多很好的系统。”[6]。
动态类型检查的主要优点在于程序设计的灵活性,不需要声明语句,一个变量名绑定的数据对象的类型可以在程序执行时按需改变,使程序员从数据类型摆脱出来,同时也可以编写更少的程序代码行完成同样的功能。动态类型是动态语言的最显著的优点,但也是动态语言的弱点根源所在。运行时进行的类型检查也存在几点重大不足:
ü 程序难以调试。因为动态类型检查只在程序运行到某一条操作时才对其进行类型检查,而ü 从来不ü 检查没有被执行的执行路径上的操作。在软件测试时是不ü 能遍历程序中所有的执行路径,ü 这样没有被执行的路径仍有可能存在bugs。这一点可能是动态语言致命的缺点,ü 它导致了动态语言对开发大型软件项目支持力度不ü 够。
ü 保存大量的类型信息。运行时需要相当大的额外存储空间。
ü 执行效率低。动态类型检查要靠软件模拟实现,ü 主要是在运行时完成的,ü 所以在执行速度上降低了不ü 少。
静态类型和动态类型都有各自的优点和缺点,不能简单地认为静态类型比动态类型要好或者动态类型比静态类型要好[3],关键要看具体应用的场合。
开发效率高
在动态语言社区里,值得开发人员津津乐道的是动态语言具有比静态语言高数倍的开发效率。虽然静态语言在执行效率上比动态语言略胜一筹,但在开发效率上只能甘拜下风。正如前文所提到,程序设计语言逐步从面向机器到面向人过渡,以牺牲运行效率来换取开发效率。如今,计算机硬件性能的大大提升,我们能够承受起动态语言在执行效率上的损失,实际上,只有很少的应用才需要尽可能的利用机器的性能。目前,普遍认为Python的开发效率要比Java高出5-10倍。那么动态语言为什么会在开发效率方面具有天独厚的优势?下面就从几个方面进行说明:
1)易学易用,编程人员可快速上手
Python、Perl、Ruby等动态语言具有简洁的语法规则,交互式的编程环境,比C++、Java等静态语言更容易学习和掌握。比如Python语言,语言的语法结构、控制结构和C/C++十分类似,对于一个熟悉C、C++语言的编程人员来说,花3-5个小时完全可以掌握Python的语法特性。软件的复杂性在于整合了千丝万缕的关系,交互式环境把程序分解成一块块,对于不熟悉的语言特性,可以方便输入交互式命令进行测试。动态语言不需要声明语句的编程方式使得动态语言更加接近于自然语言,简单明了,易学易读,许多优秀的语言特性都是语言大师深思熟虑之后的产物。对于一个软件项目来说,项目经理更愿意选择一门易于上手又同样可以完成任务的编程语言作为软件的开发语言,一则可以减少员工的学习成本,二则有利于缩短项目完工的时间。
动态语言的易学易用性最终导致了编程群体的改变。二十年前大多数编程者是大型项目的专业编程人员.那个时代的编程人员需要花几个月的时间掌握一门语言和它的编程环境,系统程序设计语言(静态语言)就是为这些人设计的.然而,自从个人电脑出现以后,越来越多的非专业编程者加入到编程者的行列.对这些人来说,编程不是他们的主要工作,而只是他们偶尔用来帮助他们工作的工具。偶尔编程的例子是编写简单的数据库查询或者是电子数据表的宏.非专业编程者不希望花几个月的时间学习一门专为系统程序而设计的静态语言,但他们可以花几个小时的时间学到足够的动态语言知识来写出有用的脚本代码。由于动态语言有简单的句法并且省略了诸如对象和线程的复杂特性,因而它比静态语言要更容易学习和掌握。例如,非专业编程人员很少会选择Visual C++,而大部分会用Visual Basic编写有用的应用程序[1]。
2)内置丰富的数据结构和操作
实际上,动态语言是建立系统程序设计语言之上的语言,大多数动态语言解释器的内核都是用某种静态语言实现的,Python是用C实现的,JVM上的Jython是用纯Java实现的,.Net CLR上的IronPython是用C# 实现的。动态语言将灵活性作为其设计的目标,优先考虑了语言的灵活性。因此,动态语言在语言层次上集成了许多操作方便、性能良好、高度抽象的数据类型,为编程人员提供了高效的数据结构实现,有助于提高开发效率。以Python语言为例。Python语言提供了强大的数学运算,可以处理超大的整数和浮点数,支持复数的基本运算。Python内置了功能强大、使用方便的数组类型,程序员不需要预先声明使用数组的大小就可以直接使用数组,也不必担心数组大小不够,这些解释器会自动根据具体地操作动态地分配数组的存储空间,不象C/C++中使用数组首先必须声明数组的大小,这就使得很多人在使用数组时不知道究竟应将大小设置成多少,大了觉得浪费,小了又担心不够,想改成指针方式动态分配又有可能给程序带来新的问题。对数组类型还有一系列灵活的操作,比如可以在数组任何位置插入元素,可以对数组分片,可以将数组的某一片作为参数传递给某个函数而并不需要将数组从头到尾的都传递。在Python的具体应用中使用可能最广当属字典类型(Dictionary Type)。字典类型是Python内置的高效强大的数据类型,是一种类似于关联表和哈希表的结构,Python的实现者花费了大量的时间对字典类型做了优化,以尽可能保证语言的高效性。Python内部实现也使用了大量的字典类型。如果没有提供这些内置的数据类型,开发人员可能需要独立开发出支持所需的数据类型的库程序,而开发这样库不但费时,而且在性能优化和接口通用性方面欠佳。正是因为动态语言内置了丰富的数据类型,节省了开发人员独立实现这些数据类型的时间,从而提高了程序的开发效率[4]。
3)无类型化,用更少的代码可完成同样的工作
动态语言的动态类型使得编写程序时不需要声明变量的类型,变量无类型化省去了程序代码中大量的编译器编译时所需的类型信息语句,使程序看上去简洁明了。
比如在Tcl语言中,如命令:
button .b -text Hello! -font {Times 16} -command {puts hello}
创建了一个新的按钮来显示16点Times字体,当用户敲击控制键时显示一段小信息。它把六种不同事件类型混合成一条语句:一个命令名(button)、一个按钮控制(.b)、所有权名字(-text, -font, 和-command),简单字符串(Hello! 和hello),包含字样名(Times)及字点大小(16)的字体名(Times 16)和Tcl脚本(puts hello)。Tcl统一用字符串表示了这些类型。
上述例子在Java中要调用两个方法完成,需要7行代码。用C++和微软基本类(MFC)需要三个过程,25行代码,在微软基本类中仅仅设置字体就需要几行代码:
CFont *fontPtr = new CFont();
fontPtr->CreateFont(16, 0, 0,0,700, 0, 0, 0, ANSI_CHARSET,
OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY,
DEFAULT_PITCH|FF_DONTCARE, Times New Roman);
buttonPtr->SetFont(fontPtr);
大部分代码是由静态类型造成的。为了设置按钮字体,必须运用SetFont方法,但这个方法必须通过指针传给CFont对象,这反过来需要声明和初始化一个新的对象.为了初始化CFont对象必须唤醒它的CreateFont 方法,但CreateFont有一个需要14个特殊化引数的固定接口。在Tcl中字体(Times铅字,16点)的基本特征不用声明或转换就可以立即使用。另外,Tcl允许在创建按钮的命令中直接包含按钮行为,而C++和Java中需要把它放在单独声明的方法中。
高级程序设计语言的高级性在于它的每一条代码可以完成更多的工作,完成同样的工作,使用的代码愈少,表明语言的等级愈高。每行汇编代码平均可以翻译成1-3条机器指令,与此相比,每行C/C++代码平均翻译成5条机器指令,而对于Python、Perl、Tcl这样的动态语言,每行代码可翻译成100-1000行机器指令。汇编程序中,基本上是没有类型,所有的数据看起来都是一样,机器的每个细节都暴露在代码中,程序员必须显式地管理寄存器和内存分配、栈调用等等,所有的操作都涉及到最底层的设备操作,对程序员的专业素质要求高,用汇编语言开发程序效率低下,因此难以编写和维护大型程序。到高级语言出现后,编译器隐藏了机器的底层细节,能够自动管理存储分配、目标代码生成和优化、过程调用顺序等操作,有了抽象数据类型,将程序员从面向机器的复杂中解脱出来,提高了程序的开发效率。
动态语言把许多工作都交给解释器去完成,程序员专注于自己需要解决的问题,建立问题解决方案的逻辑就够了,因此更接近于自然语言。比起静态语言,动态语言的一条语句可以完成更多的功能。从这个意义上说,动态语言是一种更高级的语言。因此,动态语言具有更高的开发效率。
支持动态重构
随着软件应用面临的问题多样化和复杂化,有些应用领域中逐渐出现这样一类需求:要求软件在运行时还能够改变,即动态重构。比如在一个银行系统中,要求软件一旦运行,就不能停下来,否则会对银行造成灾难性的损失,那么这样的系统中如果在运行时发现某个错误怎样修正呢?显然,用静态语言很难实现,因为静态语言在运行时不能修改自身代码,用静态语言实现的系统执行的是经过翻译处理后的可执行代码,要修正软件必须要经过修改源代码->编译->连接->生成可执行程序的步骤。而动态语言比较容易实现,动态语言是解释执行的,运行时还能够修改自身的代码。国外有人用Smalltalk实现了这样一个分布式的银行系统,银行系统留有一个接口,开发好的系统发现bug后,维护人员可以通过该接口远程登录后,可以在系统不停止运行的情况下把bug消除掉。
胶水语言(Glue Language)
动态语言一个广泛的应用就是作为“胶水语言”胶合用静态语言编写的组件,从而整合成一个应用程序,这种应用称之为混合式应用。RedHat Linux的安装程序就是通过Python胶合各个组件模块实现的。静态语言开发的程序运行速度快,但开发周期较长,而动态语言灵活简洁,开发效率可提高5-10倍,但程序运行慢,混合式应用合理结合各自的优点,充分利用不同语言的各自优势,取长补短,可以快速高效地构建应用程序并具有相当的性能。[8]
这里介绍动态语言具有静态语言所没有的优点并不是意味着动态语言要替代或推翻静态语言,而是作为静态语言有力的补充。当然,动态语言不能回避的就是它与生俱来的缺陷,比如执行效率低下、对大型软件开发的支持力度不够等等,使得动态语言难以成为主流的开发语言。现代软件面临的问题越来越复杂,软件工程领域中没有“银弹”可用,同样,也没有一种语言可以成为软件开发的“万金油”,可以很好地解决各类问题,每种语言都各有所长,也各有所短。动态语言和静态语言相互补充,相得益彰,扬长避短,才能充分发挥不同语言的优势,在软件开发中做到“动中有静,静中有动,动静结合,各司其职”。
3.3  动态语言的发展
近几年来,动态语言的发展势头良好,受到越来越多的关注,开发社区显得十分活跃。尤其是Internet的发展,为动态语言提供一个良好的展现个性的活动舞台。Perl因为编写CGI而变得流行,VBScript和JavaScript因为能编写网页而在互联网上广泛使用。
目前,动态语言已经进军当前主流的Java VM/.Net平台。Jython是Python语言在Java VM上的完全实现,是用纯Java语言编写的,可以轻松的访问所有的Java类库。Groovy也是Java VM上一款非常流行的动态语言,并作为JSR草案正式提交给标准化组织审议。IronPython是.Net平台上Python的高效实现。
动态语言的应用已经深入到系统脚本、服务器程序、游戏开发、数据库引擎和CAD系统的开发中,我们有理由相信,随着Internet的发展趋势和混合式应用的扩展,计算机硬件的快速发展,动态语言将会展现蓬勃的生命力,与静态语言一同构建强劲的编程环境,在软件开发中扮演越来越重要的角色。
4  Python语言简介
Python是一种解释型编程语言,类似Java,源代码必须首先由编译器转换成字节码(byte code),然后再由解释器来执行字节码。与Java不同的是,Python的编译器和解释器都是一个程序。因此,源代码也可以直接交给这个编译器/解释器来执行。实际上,源代码编译成了字节码,只是没有存在硬盘上,而是直接执行了。某些情况下,这种方式要比Java的“编辑-编译-修改-再编译-执行”方式效率要高,尤其是在编写写一些小规模的程序时。Python程序的结构、设计与使用方法和“主流”的计算机编程语言相当接近。这个特点降低了门槛,易于初学者上手。
Python是一种面向对象的编程语言。所有的内置(built-in)数据类型都是类:整数(int)、浮点(float)、串(string)、布尔(boolean)、元组(tuple),字典(dictionary)等等。Python的字符串常量也是其类型的实例,也就是对象,因此可以出现类似这样的代码:idx = "book.txt".find("txt")。和常规的面向对象语言一样,Python支持封装和继承。Python的类机制是C++ 和Modula-3 类机制的混合。在Python里,类的成员和方法都是默认是公有的,所有方法都是虚拟的,子类可以声明同名的方法重载父类中的方法。如果变量名是由连续两个下划线起头的,比如:__count,__copy_by_size等等,编译器在生成字节码时会把类型名加上,比如:__Employee_count。这通常被叫做“mangle”。其实,Python的“私有”成员更多是防止与子类的成员产生名字冲突,而不是试图控制对类成员和类方法的存取。在实际使用中,这种控制是很弱的,可以被轻易绕过。
Python没有“被保护”(protected)成员的概念。在实际使用中,程序员通常在变量名前加上一个下划线作为一种提示。这只是一种惯例,语言规则本身和编译器并不强制执行。
Python的很多地方都把下划线放在特殊位置上。在子类中可以定义非私有成员和方法重载父类中同名的成员和方法。不同的是,在定义类方法时,对象实例(C++中的this)必须被显式地放在方法形参表的第一个,Python程序的惯例是用self。在C++中,这个对象实例是被编译器自动加上去的。
在Python类定义中,类构造器必须用 __init__命名,析构器必须用 __del__命名。Python类的构造器不会自动掉用父类的构造器,子类必须在其构造器中显式地调用父类的构造器。子类可以选择调用父类构造器的时机,比C++和Java要灵活一些。相应地,子类的析构器也需要显式地调用父类的析构器。
Python程序中的变量是动态类型,没有固定的类型。Python程序中的变量也不用事先声明,变量的类型可以按需改变。这在写小程序的时候很方便,但是对于大一些的项目,特别是需要多个人合作完成的软件,就会带来麻烦,因为没有编译器来帮你发现使用未声明的变量,或是变量类型不符。通常的做法是在使用变量前检查其是否为“空”(None),因为第一次使用的变量总是指向“空”这个特殊的对象,直到被赋值。当然,这种检查对性能有一定的影响。
Python使用引用计数来简化内存管理,程序员基本上不用关心内存管理的问题,但是要注意避免循环引用。变量生成时,其所指对象的引用计数为一。每次变量出现在等号的右边,或者出现在方法或函数调用实参表里,其所指对象的引用计数加一。当一个变量的生命周期结束时,其所指对象的引用计数会被减一。如果计数值为零,就会调用对象的析构器清除对象的存储空间。
Python有限度地支持运算符重载。比如说,可以为一个用户自定义的矩阵类型重载加、减、乘、除,那么使用这个矩阵类型的代码就可以用更加直观的+、-、*、/等操作符来直接进行矩阵的算术运算。
Python还支持结构化的编程方式,有条件判断、循环、函数等常见的控制结构。一个程序可以写出来完全没有类的定义,从头到尾都是free functions和函数调用。这点上,Python和C++类似。Python的函数和类方法都支持重载。因为变量是没有类型的,只能靠区分形参数目来分辨重载的操作。
Python支持异常。和C++、Java一样,Python的异常使得当前栈帧中所有的变量退出作用域,同时中断程序的执行,将异常升级。这点上和C++及Java类似。
最有争议性的是Python对源代码格式的要求。不像C家族的编程语言,Python不是自由格式的。Python的作用域是靠行首缩进来界定的,而不是匹配的括号。比方说,如果一个类的定义起始于第一列,那么,类中所有成员及方法必须出现在第一列以后,并且处于同一层次的语句必须出现在相同的列上。这个特点的初衷是为了维护程序的可读性,也确实达成了目的。大部分的Python源代码都是排列得整整齐齐的,风格基本接近。
每一个Python源代码文件可以包含一个或多个的类、自由函数。多个源文件在一个文件系统目录下可以成为一个模块,只要这个目录中有一个名为__init__.py的文件存在。这个文件甚至可以是空的。模块可以被其他Python代码导入,用类似于Java的“import graphic.2D.text”。模块也是Python最常见的代码重用形式。Python的编译器和解释器会在缺省的和指定的路径中搜索被引入的模块。
除了可以用Python语言自身外,其他语言也可以用来写模块,这体现了Python很强的可扩展性。最常见的是C和C++。其实,Python的很多基本模块就是用C写成的。通常来说,C写成的模块要比Python写的快很多,通常是几个数量级的区别。另外,很多的已有的C/C++动态库也可以通过这种办法成为Python的模块。这样,既增加了Python的应用范围,又把Python的易于使用和C/C++的高效、高性能就很好地结合起来了,这种混合式的编程方式越来越广泛。这大概就是Python日益流行的原因之一。
随着Python编译器/解释器一起发行的有上百个模块,涵盖了从字符串匹配,xml parsing,操作系统功能到电子邮件处理等等各个领域。这些模块大部分是由来自世界各地的Python使用者贡献的,在早期也没有比较正式的命名规范,每个人都有自己的风格。对于习惯了Java严谨命名规范的人来说,Python看起来就太不“专业”了。
此外,Python还可以被用作嵌入式的解释器。Python的运行时环境有很清晰的C接口,整个编译器/解释器可以被很容易地嵌入到C或者C++的程序中,加上上面所说的由Python到C的接口,Python就具有了类似微软的VBA的能力。这对大型的软件系统是很有吸引力的。OpenOffice就有一个Python接口。
在2.2版本发布后,Python语言本身的发展逐渐转向了对大型软件项目的支持,添加了一些新的语言特性,比如generator、类方法、静态方法、property、method decorator等等。Python community同时也非常注意借鉴其他编程语言的优点。
5、动态语言与设计模式
设计模式是一个抽象层次,描述了在一个特定的环境中用来解决一般设计问题的对象和类之间的交互关系,其主要目的是充分利用语言的特性,设计可复用的、能够适应需求变更的软件[9]。设计模式是一种设计思想,语言是实现思想的工具。因此,不同语言的特性影响了设计模式的实现,有些语言更容易实现设计模式,而有些语言则比较难。GoF在设计模式一书中选用了两种面向对象语言—C++和Smalltalk实现软件开发中常用的23种设计模式,其中C++为主,Smalltalk为辅,重点突出了两种语言不同的语言特性对实现设计模式的影响。C++语言的运行时多态性的基础是虚函数机制,指向基类的指针可以指向它的任何派生类,在实现设计模式时充分利用了C++这一特性,结合继承机制,建立类和对象的层次关系,使C++最大程度的具有动态特性,将绑定关系尽可能推迟到运行时确定。
在GoF的23种模式中,部分设计模式是专门为静态语言提出的,有些模式在动态语言中语言一级就提供直接的支持,如Command模式,动态语言提供的函数式编程将函数本身看作是类对象。
Python是一种完全面向对象的动态语言,提供了与传统面向对象语言截然不同的对象模型,影响了设计模式的实现和使用。Python中类也是对象,类和类的对象都有可供操作的特殊属性,在运行时还可以修改类的结构和定义,这些特性使Python具有强大的“内省”能力,利用这种能力程序员可以创建高级的、动态的和灵活的应用程序,可以更容易实现设计模式。本部分选取了几种常见的设计模式,尝试用Python语言实现,并与C++的实现方式进行比较,进一步体现动态语言中的“动态性”及其具体应用。
5.1 抽象工厂(Abstract Factory)
抽象工厂模式提供了一个不需要指定具体类就可以创建一系列相互关联或相互依赖的对象的接口。抽象工厂隔离了具体类,客户代码只需通过抽象接口创建对象,不需要访问具体的类。参考GoF的设计模式一书,对书中实现迷宫工厂的C++代码用Python实现如下:
class MazeFactory:
def MakeMaze(self):
return Maze()
def MakeWall(self):
return Wall()
def MakeRoom(self, n):
return Room(n)
def MakeDoor(self, r1, r2):
return Door(r1, r2)
上述代码定义了一个可以创建Maze、Wall、Room和Door的MazeFactory接口,接下来创建一个魔法迷宫工厂EnchantedFactory,EnchantedFactory继承于MazeFactory,并通过MakeRoom和MakeDoor接口创建了具有富有个性的EnchantedRoom和EnchantedDoor。
class EnchantedFactory(MazeFactory):
def MakeRoom(self, n):
return EnchantedRoom(n)
def MakeDoor(self, r1, r2):
return EnchantedDoor(r1, r2)
这段代码只是对C++代码的简单翻译,没有运用Python的语言特色。从上述的代码中可以看出,抽象工厂难以向MazeFactory中添加新的产品,假如迷宫中还需要创建陷阱(Trap),就必须在MazeFactory接口中增加MakeTrap方法,这样就造成了MazeFactory接口的不稳定,继承MazeFactory的所有子类的接口也随着基类的接口改变而改变。
工厂方法(Factory Method)解决了通过引入一个的Make操作将创建所有产品类型的操作统一化,Make操作中有一个参数可以唯一标识创建对象的类型。然而,用C++语言实现的工厂方法仍然存在局限性,这种局限性不利于构建可复用的软件。因为创建所有的产品类型都是通过Make接口的,为了保持Make接口的返回值对所有产品的兼容性,就不得不迫使所有产品类型必须继承于一个公共的基类,然后Make接口返回该基类,这样保证了Make返回的类型都可以转换成特定的产品类型。但是,同一系列不同类型的产品在逻辑上可能不存在明确的公共基类,比如MazeFactory中的Maze和Wall,而且,使用公共基类导致了大量的向下强制转换,这种转换往往是不安全的,有时还不可行。[9]Pyhon语言的动态类型特性为解决该问题提供良好的方案,Python允许一个变量在运行时绑定到不同类型的对象上,所以不必要求不同类型的产品具有公共基类,Make接口不必声明其返回类型,调用时具体的返回值类型在运行时交给解释器去完成。Python实现工厂方法的代码如下:
class Maze:…
class Wall:…

class MazeFactory(object):
def make(self, typename, *args):
if typename == ‘maze‘: return Maze()
elif typename == ‘wall‘: return Wall()
elif typename == ‘room‘:
return Room(args[0])
elif typename == ‘door‘: ]
return Door(args[0], args[1])
self是MazeFactory实例对象的引用参数,typename标识创建对象的类型,*args是创建具体对象时所需的参数列表。魔法迷宫的代码
class EnchantedFactory(MazeFactory):
def make(self, typename, *args):
if typename == ‘room‘: return EnchantedRoom(args[0] )
elif typename == ‘door‘: return EnchantedDoor(args[0],args[1])
else: return super(EnchantedFactory, self).make(typename, args)
make方法中的return super(EnchantedFactory, self).make(typename, args)表示调用父类的操作创建其它类型的对象。
那么创建一个具体的EnchantedFactory实例的代码:
mf = EnchantedFactory()
mz = mf.make(‘maze‘)
r1 = mf.make(‘room‘, 1)
r2 = mf.make(‘room‘, 2)
dr = mf.make(‘door‘, r1, r2)
当需要在MazeFactory添加一个Trap新类型时,只需要在Make方法中添加标示新类型的参数即可:
elif typename == “trap”: return Trap()
这种做法不但保持了MazeFactory对外接口的稳定性,而且不需要类型的向下转换。但这里同样存在一个问题:每添加一个新类型,都要修改Make的实现代码。能不能不用修改Make的代码即可添加一个新类型呢?原型模式(Prototype)提供了一种更好的解决方案——编制产品字典。
5.2 原型模式(Prototype)
原型模式使用一个原型实例指定创建对象的类型,并且通过复制原型创建新的对象。原型模式的优点是可以在运行时动态的增加和删除产品类型,减少了子类化,还可以动态的配置应用程序。使用原型管理器(Prototype Manager)可以方便实现运行时类型的增加和删除,管理器中有个类型的注册表,注册表是个关联存储结构的表,对于给定类型的键值可以唯一确定一个类型,增加一个新类型时就是在表中注册该类型,客户程序在使用一个类型前先访问注册表检索它的原型。实现迷宫MazeFactory原型的Python代码如下:
class MazeFactory:
def __init__(self):
self.index = {‘maze‘: Maze,
‘wall‘: Wall,
‘room‘: Room,
‘door‘: Door}
def make(self, typename, *args):
return apply(self.index[typename], args)
def registtype(self, typename, instance):
self.index[typename] = instance
def unregisttype(self, typename):
del self.index[typename]
在MazeFactory中,数据成员self.index={…}是个字典类型,存放MazeFactory产品类型,方法registtype和unregisttype实现了产品类型的动态增加和删除,参数instance表示需要添加或删除类型的实例名。假如创建了一个MazeFactory实例mf=MazeFactory(),实例Trap的定义如下:
class Trap:
def __init__(self, radius, height):
self.radius = radius
self.height = height

向mf中添加实例Trap的代码:mf.registtype(‘trap’,Trap),而相应的删除代码为mf.unregisttype(‘trap’,Trap)。
显然,这种实现方式便于动态管理类型,具有良好的可扩展性。
5.3 单件模式(Singleton)
单件模式提供了一种将系统中类的实例个数限制为一个的机制,保证了一个类只有一个实例,并提供了该实例的一个全局访问点。程序的不同模块通常会共享同一个对象。单件模式隐藏了实际的全局变量,对外提供了访问的接口,是一种很好的访问全局变量的方法。首先我们来看一下C++是怎样实现单件模式的。Singleton类的定义如下:
class Singleton {
public:
static Singleton* Instance();
protected:
Singleton();
private:
static Singleton* _instance;
};
//对应的实现:
Singleton* Singleton::_instance = 0;
Singleton* Singleton::Instance ()
{
if (_instance == 0)
{
_instance = new Singleton;
}
return _instance;
}
在单件类中,静态数据成员_instance指向已经创建的实例,静态成员函数Instance()为单件类提供了全局访问点,客户程序只能通过Instance()接口创建单件类,如果_instance不为0,则直接返回已创建的实例,注意Singleton类的构造函数声明为protected属性防止客户程序不通过Instance()接口创建它,保证了单件类的唯一性。
Python语言中的类和函数的定义可以在运行时改变,借助这一语言特性给出实现Singleton模式Python版本:
class Singleton(object):
def __new__(cls):
cls.instance = object.__new__(cls)
cls.__new__ = cls.Instance
cls.instance.init()
return cls.instance
def Instance(cls,type):
return cls.instance
Instance = classmethod(Instance)
def init(self):
pass
Singleton类重载了object的内置方法__new__,object是Python中所有数据类型的基类,包括内置数据类型和用户自定义类型,所有的数据类型都继承了object的属性和方法。在Python中,类方法和静态方法是两个不同的概念。类的静态方法相当于C++中的静态成员函数,调用方式也类似。而类方法隐式地将类本身作为第一个参数,在声明和调用的格式方面与静态方法都不同。而__new__方法是类的一个静态方法,不是一个类方法,它在调用类的初始化方法__init__之前调用,创建对象的第一步就是调用__new__方法。__new__方法的第一个参数必须是一个类,并返回该类的一个新的实例,其余的参数是调用__init__所需的参数。[11] getInstance是Singleton类的类方法,定义了Singleton实例的全局访问点。在重载的__new__方法中,首先调用了父类object的__new__方法返回一个新的Singleton的实例cls.instance,参数cls实际是对Singleton类本省的引用,instance是类的一个数据成员,保存了当前的单件对象。语句:
cls.__new__ = cls.getInstance是将getInstance赋给__new__方法,执行后,Singleton类的__new__方法变成了getInstance。第一次创建Singleton实例对象时,调用__new__方法生成Singleton的一个新的实例,试图再次创建Singleton实例对象时,调用的__new__的方法实际上被“偷偷的“调包成getInstance,__new__方法的代码不再被执行,而是执行getInstance方法返回已经创建的实例对象cls.instance,从而保证了只存在一个Singleton实例对象。cls.instance.init()说明了__new__方法在__init__之前调用,为了进一步初始化Singleton子类。
Singleton的子类继承了Singleton的__new__方法,每个子类也是单件的,只能有一个实例对象,无论调用多少次构造函数,子类重载了init方法,例如:
>>> class MySingleton(Singleton):
def init(self):
print "call init..."
def __init__(self):
print "call __init__..."
print "Initilizing My singleton"
>>> a = MySingleton()       #创建一个新的MySingleton实例对象
call init...                  #调用父类Singleton的__new__方法
call __init__...             #调用自己的__init__方法
Initilizing My singleton
>>> b = MySingleton()        #试图再创建一个MySingleton对象
call __init__...              #没有调用__init__方法,说明__new__被“调包”
Initilizing My singleton
>>> a == b                     #a和b实际上指向同一个实例对象
True
在C++中子类化Singleton相对比较麻烦,不能直接继承Singleton将子类定义为单件类,有三种方法创建Singleton的子类:在Singleton的Instance操作中设置一个参数指定需要创建的单件;将Instance操作从父类分离到它的各个子类中;使用单件注册表。这些方法都可以子类化单件类,但没有Python版本来得直观。Python中利用了在运行时可以改变类定义的动态特性,在运行时将自身的静态方法__new__改成另外一个方法getInstance,这种“偷梁换柱”的做法确实为某些应用带来了便利。
5.3 代理模式(Proxy)
从面向对象设计的角度看,限制访问属性给一些旧问题提供了一种新的解决办法。代理模式就是一个很好的例子。代理模式用于隔离对象和访问它的客户,比如引用计数、不同等级的授权访问以及对象的惰性赋值等。代理模式的结构如下:
客户程序不需要直接访问实际的对象,换句话说,代理替代了实际的对象,客户通过代理去访问实际的对象。在C++中,这就意味着Proxy和RealSubject必须要有一个公共的基类。在Python中,通过提供相同的方法接口,Proxy可以达到冒充Subject的效果。以下Python代码中Proxy类是基于小型的通用包装类,它的主要功能就是为多个特定代理的实现提供一个基类,在Proxy类中可以重载__gettattr__方法处理不同的方法。
#Proxy Base Class
class Proxy:
def __init__( self, subject ):
self.__subject = subject
def __getattr__( self, name ):
return getattr( self.__subject, name )
#Subject class
class RGB:
def __init__( self, red, green, blue ):
self.__red = red
self.__green = green
self.__blue = blue
def Red( self ):
return self.__red
def Green( self ):
return self.__green
def Blue( self ):
return self.__blue
# More specific proxy implementation
class NoBlueProxy( Proxy ):
def Blue( self ):
return 0
考虑以下情况:首先我们需要直接访问RGB类的实例对象,然后使用一个通用的代理实例作为一个包装类,最后传递给NoBlueProxy类:
>>> rgb = RGB( 100, 192, 240 )
>>> rgb.Red()
100
>>> proxy = Proxy( rgb )
>>> proxy.Green()
192
>>> noblue = NoBlueProxy( rgb )
>>> noblue.Green()
192
>>> noblue.Blue()
0
代理模式在Python中应用很广泛,Python语言提供的机制中有一些就是代理模式实现的,比如垃圾收集中的简单引用计数。[10]
5.4 命令模式(Command)
用过集成开发环境的人都知道,开发基于窗口图形界面的应用程序时,一般要用到按钮和菜单等控件对象响应用户的输入,但在集成环境的工具箱提供的按钮和菜单并没有显式地实现该请求,也就是按钮和菜单不知道关于请求的操作和请求的接受者的任何信息,这些请求特定于具体应用,只用控件的使用者才知道该由哪个对象响应哪个操作,工具箱的设计者无法知道请求的接受者和执行的操作。那么工具箱的设计者是如何实现按钮和菜单的这种功能的呢?用Command模式。
Command模式解耦了调用操作的对象(如按钮、菜单)和实现该操作的对象(如文档)。C++利用继承组合机制实现Command模式,通过定义一个带有Execute接口的Command抽象类,特定应用相关的Command派生于此类,在Command类的子类显式定义接受者的对象。Command类在调用操作的对象(Invoker)和实现该操作的对象(Receiver)之间充当了桥梁作用, Invoker请求某个Command类,由Command类的Execute接口执行Receiver的具体操作并将返回结果告诉Invoker,对于Invoker根本不知道是谁执行了该操作,也不需要知道,从而实现了两者的解耦。而Python具有运行时可以改变类的结构、函数的定义的动态特性,很简单地就实现了Command模式:
class Button:
def click(self):pass
class document:
def open(self):
print "open document..."
btn = Button()
doc = document()
btn.click = doc.open
执行btn.click()时实际上相当于调用了doc.open()的方法,实现了Button和document两者的解耦,比C++的继承组合机制要简单。
有时一个按钮要求执行一系列命令,这种宏命令(MacroCommand)在应用中也是很常见的。Python支持lamda匿名函数定义,运用函数式编程方式也可以很简便的实现MacroCommand模式。我们先来看看用C++是怎样实现的。C++用一个命令列表管理命令系列,执行宏命令实际上是遍历一次命令列表:
class Command {
public:
virtual ~Command();
virtual void Execute() = 0;
protected:
Command();
};
class MacroCommand : public Command {
public:
MacroCommand();
virtual ~MacroCommand();
virtual void Add(Command*);
virtual void Remove(Command*);
virtual void Execute();
private:
List* _cmds;//命令列表
};
void MacroCommand::Execute ()
{
ListIterator i(_cmds);
for (i.First(); !i.IsDone(); i.Next())  //遍历命令列表
{
Command* c = i.CurrentItem();
c->Execute();
}
}
在Python中,假设已经定义了一个Button类和一个Document类,Button类有一个Click方法,Document包含Paste和Replace方法,以下代码实现了点击按钮后同时执行Paste和Replace操作:
doc = Document()
btn = Button()
macrcocmd = lambda cmds : map(lambda cmd:cmd(),cmds)
btn.Click = lambda: macrcocmd([doc.Paste, doc.Replace])
函数map是个Python内置函数,其作用是将列表cmds作为参数传递给匿名函数lambda cmd : cmd()执行并返回一个元组作为匿名函数macrocmd的参数,调用btn.Click()执行了匿名函数macrocmd(),该函数接受元组[doc.Paste, doc.Replace] 作为它的输入参数,执行doc.Paste()和doc.Replace()命令。
以上我们只选取了GoF的23种模式的4种模式,展示了如何利用Python语言的动态特性实现设计模式。Python的灵活性和动态性为实现一些不同的、优美的解决方案提供了一个良好的基础。Python的无类型化解决了静态语言实现工厂方法中需要不安全的强制类型转换等问题,既减少了程序中类的设计,又避免了静态语言中的向下转换问题。原型模式为创建不同类型的对象提供一种更好的方法,利用了Python 可以运行时改变类的定义的特性,可以动态地增删类型,提高了程序的可扩展性。而单件模式则利用了类的静态方法__new__以及动态改变类的结构和定义的特点,巧妙地实现了限制了单件类的实例对象个数的功能。在Proxy模式中,充分体现了Python中同一个类的多个实例对象之间可以拥有不同的结构以及可以监视和限制属性访问的语言机制,这些机制为个实现多个通用类提供了一个相当完美的解决方法。在C++中不支持匿名函数,命令模式初步展现了Python中lamda匿名函数在实现某些问题的简便性。其他的模式,如职责链(Chain Of Responsibility)、策略模式(Strategy)、装饰模式(Decorator),利用Python语言特性,也可以写出Python的实现版本。
6  小 结
动态语言最主要的特点就是将大部分绑定关系推迟到程序运行时确定,更强的动态特征是动态语言与静态语言的根本区别。动态语言的这一特性提供了一种解决问题的新思路,实现了许多在静态语言中比较难甚至难以实现的技术,但动态语言在执行时要耗费大量的时间在确定类型的绑定关系上,因此执行效率不如静态语言。虽然动态语言在执行效率方面是“先天的不足”,但它的灵活性和动态性大大提高了软件的开发效率,对于一些小型的应用程序,动态语言更具有得天独厚的优势。但动态语言替代不了静态语言,只有合理地结合静态语言,才能够发挥更强劲的威力。Python是一款个性鲜明的、面向对象的交互式动态语言,利用Python语言的动态特性,可以巧妙的实现某些应用,提供一种优美的解决方案,使编写的软件具有更高的复用性、更强的扩展性,本文选取的几种设计模式的实现就是一个例子。在面向Internet的应用程序中,动态语言具有更广阔的前景。动态语言今后的一个发展方向就是混合式应用,充分发挥静态语言和动态语言各自的优势,扬长避短。