Perl 和Python 的比较研究
随着系统管理的复杂化和网络运用的扩展,脚本语言在实际编程中的应用越
来越广泛。传统观念是:一般的高级语言如C/C++,Java,Delphi 等用来编写主要
的功能组件,如java 的类和beans,用C/C++写的动态连接库,用VisualBasic 写
的控件等,而脚本语言如javascript,Vbscript,perl,python,sh 等通常认为脚
本语言是介于HTML 应用的发展和脚本语言本身的发展,脚本语言的应用早就超
出仅仅作为常规编程语言的辅助角色而用来直接开发应用系统,著名的网络流控
制系统mrgt 就是用perl 开发的。现在的脚本语言,其功能甚至强大到可以和一
般高级语言相媲美,而且引入较新的程序机制和技术(如OO 和异常处理),加
上固有的简单易用,逐渐发展为程序开发的一支主流。脚本语言的基本特征是语
法简单,跨平台,类型宽松,解释执行。早期的脚本语言?本文选择现今在自由
软件开发里很具代表性和广泛使用的两种脚本语言perl 和python 进行比较研究,
以期使读者能对这两种脚本语言的基本特点和新发展有一定的了解。
一、 两者初识
Perl(可以看作Practical Extraction And Reporting Language 的首字母)语言
最早由Larry Wall 开发,原始动机即作为一个文本提取和报告的实用语言,本来
是基于UNIX 系统,后来发展成能运行于几乎所有的机器体系和操作系统。Larry
Wall 是坚实的免费软件拥护者,所以perl 也成为免费软件之一(但不属GNU),
按自由免费软件的一般模式发展和传播(perl 中的源代码、标准库文件、可选模
块及所有文档都免费提供,并被用户团体所支持)。从1988 年的最初诞生,到现
在的perl 6 系列版本,perl 能够如此稳健蓬勃的发展是和它自由免费、简单高效
(语法上象C 和Unix 的sh,解释执行前会简单编译,有点象java)、强可扩展性、
数据类型灵活、面向对象以及有强大规范的用户团体交流(CPAN, Comprehensive
Perl Archive Network)和幕后支持组织(CPAST, Comprehensive Perl Arcana Society
Tapestry)分不开的。
Python 最初出现在2000 年前后,名字来源一喜剧团体Monty Python,并无
实际代表意义。Python 最初由Guido van Rossum 及其合作者设计开发,后来
python 开发小组多次重组,最终到Digital Creations。Python 和perl 一样是在迅
速稳定发展,目前的一个著名成功业绩是Digital Creations 开发的zope,下一代
开放源码应用服务器以及门户工具。从抽象的功能和特点来说,python 是和perl
最相像的语言,可能和perl 的成功和python 的较晚出现有关。和perl 一样,python
也是自由免费、简单高效、强可扩展性、数据类型灵活、面向对象的。并且python
的功能相对更强大,更易于编写大程序,核心又较小。尽管从抽象的角度,perl
和python 两者有很大的相似,但作为不同的语言,他们却是又有许多差别,下
文从几个主要的方面对两者进行深入的比较研究,尽量能找出它们的异同并对它
们一些进行原理和模型层次的探讨。
下面我们先来看一下如何用这两个语言实现最简单的“hello!”程序,先对
它们有个大概的印象。在perl 情形,先选择一个你比较喜欢的文本编辑器编写
hello.pl 程序如下:
#This is a hello saying demo
print “what is your name?\n”;
$name=;
print “hello $name!”;
在终端或命令行输入perl hello.pl 或直接输入hello.pl 程序将被解释执行,提示输
入你的名字,输入名字xiao rong 后程序在屏幕输出“hello xiao rong!”。
Python 脚本运行有两种方式,一种是交互式的,一种是自动运行脚本。交互
式是在命令行输入python,然后进入交互状态(>>>为主提示符,…为从提示符):
ActivePython 2.4.1 Build 245 (ActiveState Corp.) based on
Python 2.4.1 (#65, Mar 30 2005, 09:33:37) [MSC v.1310 32 bit (Intel)] on
win32
Type "help", "copyright", "credits" or "license" for more information.
>>> name=raw_input("what is your name?\n")
what is your name?
xiao rong
>>> print "hello, ",name,"!"
hello, xiao rong !
>>>
或者编好脚本文件hello.py 如下:
name=raw_input(“what is your name?\n”)
print "hello, ", name, "!"
然后在命令行输入python hello.py 或hello.py 发生的情形和perl 一样。
所以从顶层模型来看,perl 是一个完整的perl 脚本输入自动执行。一个perl
脚本文件和C/C++源程序类似,由一些语句和语句块组成,语句由“;”分隔,
语句块由一对“{}”包括。而python 脚本既可自动执行,又能交互式运行。而
且python 特别的一个地方使它采取缩进来标示分界和层次,不同于一般语言采
用”;”和”{}”。这种缩进方式的程序在运行时依赖一个栈来记录逻辑行的INDENT
(缩进)和DEDENT(抽出)。在读入脚本文件第一行之前,有个数值0 压入栈
底,这个0 直到脚本运行完是不会被弹出的。压进栈中的的数值是从底向顶严格
增加的。每次读到一行新的逻辑行,先比较它的缩进量和栈顶纪录的缩进量大小,
大则表示进入下一层,此时把新的值压入栈并产生一个INDENT 记号,小则表
示此缩进量和前面某处一样(否则错误,这一点很严格),这时把栈中比它大的
量都弹出,产生一个DEDENT 记号。如此可以获知每一行所在的层次,并逐行
解释执行。
二、 数据类型
脚本语言特点之一就是数据类型灵活,变量无需先声明,类型是靠值来动态
确定的。一个变量在一个程序中可以存储不同类型的值。
Perl 支持的内置数据类型有数值(包括整数和浮点)、字符串(包括单个字
符)、引用、列表和数组、关联数组(字典),其中前3 类属标量,在变量名前面
由”$”标示,列表和数组变量名由”@”标示,关联数组名由”%”标示。即变量类型
分三类,标量($varname),列表(@varname),关联数组(%varname)。之所以采
取这样的标示,很可能是和perl 要支持引用相关,因为,要高效支持引用,就得
很方便的知道引用的类型。内置数据类型有内置方法支持,如数值的+-*/,字符
串的连接、比较和匹配(详见下文的正则表达式讨论),列表的排序和翻转。当然
除了内置的类型,还支持新类型,面向对象的自定义对象,在面向对象分析小节
使我们详细讨论。Perl 的大部分数据对象采取值模型,赋值时复制对象。所以它
把引用类型引入(类似C 里的指针,但作为引用类型不能进任何算术运算),以
方便数据的传递,在子程序小节将讨论引用的应用。引用类型属标量,通过在普
通变量前加”\”来获得引用的值,如$rarr=\@array;取值时用类型标示符作用在引
用变量前即可,如@$rar 得到数组@array 的值,而$$rarr[i]取到@array 第i 元素
(数组下表从0 开始)。
Perl 的文件管道句柄用标量来存储,这样很方便对文件和管道的操作。另外,
perl 支持别名(alias),即在变量前加“*”,如:
@array=(1,2,3);
*arr=*array;
$arr[0]=2;
print "\@array:@array\n\@arr:@arr\n";
这里有的一个问题是,如果同时有@array,$array,%array 变量则解释器无法区
分它们的别名。别名实际是指向符号表项的指针,可以用来方便文件句柄和列表
的操作。别名在perl 中又叫类型标识。
Python 支持的内置基本类型有数值、字符串、链表、元组、字典。由于设计
时把python 定位为完全的面向对象(包括数值类型,直接支持复数类型),对对
象操作大多都是靠调用对象的方法来实现。Python 的不同类型变量定义无需特殊
标示。和perl 不一样,python 大部分对象采用引用模型,但是python 不支持引
用型变量。象这类采用引用模型的语言,对对象一般分成两类,可以修改(mutable)
和不可修改(immutable),不可修改的对象如果发生修改操作报错(但python 返
回” NotImplemented ”型值)。
两者的列表数组和链表意义一样,是一个可以同时存多种标量值(整数,字
符串等等)的一个线性表。Python 的链表允许插入、删除、倒置、排序、切片等
操作,长度动态变化;perl 的数组只允许在切片和首尾增加删除元素操作。两者
的关联数组和字典大致一样,存储的是健值对,允许检索,插入,删除。Python
比perl 多的元组是不可修改型(不能在其中增加、删除、重新赋值)可以当作列
表常量( 在这一点上和perl 的列表更象, 它们的输出形式都是
“(item1,item2,…,itemn)”,python 的链表数组形式为[item1,item2,…,itemn]),而
且Python 的元组允许嵌套,相当于广义表(但不允许原地插入删除)。
三、 控制流
perl 支持灵活多样的控制流。有多种条件判断和循环并且支持循环控制和
goto 跳转。首先说一下perl 里的条件和条件表达式。同C 语言一样,perl 没有另
外的boolean 类型,零和空等表示false,其余为ture。条件可能来自比较,函数
返回(如读取),状态和环境的判断(如文件是否存在)。特别注意在perl 中的有
些比较操作,返回3 真值1,0,-1。如数值比较的”<=>”运算符和字符串比较的
cmp 操作。逻辑运算有与(&&或and 连接)、或(||或or 连接)、非(!或not)、
异或(xor)。
Perl 除提供传统的if () { } elsif ( …)
{ } … else{ }条件控制外,还提供
了特别的单行条件语句,语法为statement keyword condexpr。其中keyword 可为
if、unless、while 或until,如: print ("This is zero.\n") if ($var == 0) 和 print ("This
is zero.\n") unless ($var != 0),虽然条件判断写在后面,但却是先执行的,只有满
足条件才执行前面的语句。
Perl 支持while 循环 while ( ) { },until 循
环 until ( ) { },类C 的for 循环,针对列表(数
组) 每个元素的循环foreach , 语法为: foreach localvar (listexpr)
{ statement_block; }(此处的循环变量localvar 是个局部变量,如果在此之前它
已有值,则循环后仍恢复该值。在循环中改变局部变量,相应的数组变量也会改
变),do 循环 do { statement_block } while_or_until (condexpr); do 循环至少执行一
次循环。
退出循环为last,与C 中的break 作用相同;执行下一个循环为next,与C
中的continue 作用相同;Perl 特有的一个命令是redo,其含义是重复此次循环,
即循环变量不变,回到循环起始点,但要注意,redo 命令在do 循环中不起作用。
Goto 语句和C 完全一样。
Python 的控制结构相比方式少一些,但能满足通常的需要(因为支持while
型的循环)。Python 的条件表达式值有Booleans 类型值True 和False 表示,但是
Booleans 是作为Integers 即整数的子类型,其中Ture 就是1,False 就是0。Python
的条件值同样由比较,函数,环境和状态产生。有与(and)、或(or)、非(not)运
算。Pyhton 支持多分支的if…elif…else 条件判断,但书写时必须注意缩排对齐。
Python 支持所谓的对序列中(包括链表和元组)的元素的for 循环“for elements
in Sequence:”,此时要特别注意在循环过程中不应改变列表形状;python 还支持
while 循环, while expression : block [ else : block ]。Python 循环时除支持类似C
中break,continue 等跳转控制语句外,还有一种pass 语句,它什么都不做。
四、 解释和环境
很多脚本语言的运行是需要脚本解释器或专门的虚拟机(类似Java 的
JVM)。而且一般脚本语言不能直接生成可执行文件,所以与环境和操作系统的
交互必须通过一些间接的方式,如更多的通过环境变量和特殊变量。这一节我们
来看一下perl 和python 的解释执行与环境交互。
Perl 和perl 函数的执行可以被某些环境变量影响,许多环境变量在安装perl
时被操作系统或shell 自动设置,有些变量可以在脚本运行时被修改和配置。Perl
通过内置变量哈希表%ENV 提供到当前解释器的环境变量的接口,常用的环境
变量包括HOME,PATH,PWD,HOST,USER 等,环境变量在不同的操作系
统下是有差别的。除了环境变量外,还有一些特殊变量可以用来控制perl 运行或
为perl 运行提供外部信息,如@ARGV 存储传递给程序的参数,@_存储传递给
子程序的参数,$]表保存perl 版本,$@保存最后一次调用eval 产生的错误,$?
保存最后一个子进程返回状态,@INC 包含包搜索目录列表,%INC 保存通过do
和require 得到的程序中每一个文件的列表,%SIG 表示信号句柄(在第十节会详
细介绍),STDERR,STDIN,STDOUT 分别是标准错误、输入、输出句柄。Perl
可以通过system 函数运行程序或外部命令,通过fork 创建子进程。
Perl 在解释执行前实际有一个粗略的编译,解释执行的是操作码(由C 语言
编写的一些基本操作指令,类似与Java 的虚拟机)。尽管perl 是以脚本语言,可
以使用perlcc 命令或其它工具将perl 脚本编译成可执行文件(在windows 下.exe
文件)。但是,编译成可执行文件不会进一步提高perl 代码的执行效率,因为它
只是进一步把操作码翻译成机器指令,而不会进一步的优化机器码。
Python 通过sys 和os 两个模块来和系统与环境进行交互,其中sys 模块处理
一些与具体系统无关的,而os 是针对具体系统的。在sys 模块的argv 数组中可
以获得命令行参数,stdin、stdout、stderr 分别获得标准输入输出错误的句柄,可
以通过sys 里的函数exit 终止脚本的运行。另外sys 模块还提供了一些解释器的
信息,如modules 是当前被加载的模块,path 是个字典,是模块和路径对。os
模块的主要功能是确认用户和进程环境,对诸如文件和文件系统这样的外部系统
的进行控制和通信。os 中的字典environ 提供对当前进程的环境变量的访问,函
数system 和exec 用于启动其它程序或外部命令。关于进程的控制和通信在第十
节并发控制将进一步讨论。
Python 在解释前也是会进行编译的,这一点在使用import 语句导入一个的
模块时看得更清楚,因为它将源代码”.py”编译成”.pyc”再导入,每次导入都会比
较”.py”和”.pyc”文件的时间戳,确定是否需要重新编译。Python 的解释器构架中
有一部分就叫做PVM(python 虚拟机);Python 脚本解释器核心比较小,很容易
用其他高级语言实现,如现在比较流行的Jython,可以看成java 和python 的一
个结合。
五、 正则表达式
本来正则表达式一般不在的程序语言设计时考虑。但考虑到脚本语言,尤其
是早期的脚本语言如awk,很大程度上是为加强在Unix Shell 上的报文处理而设
计。从这个程度上来说,perl 的内置强大正则表达式处理能力更有传统脚本语言
的风格。正则表达式处理一般包括模式串的匹配、提取、替换、分割。而模式串
用一个正则表达式来描述。如一般语言里的标示符用
[a..z]|[A..Z]( [a..z]|[A..Z]|[0..9])*表示以字母开头任意个字母或数字的字符串。
Perl 语言支持对字符串进行模式匹配、替换等丰富的正则表达式相关操作,
使用非常方便。和通常郑泽表达式记号差不多:在一对”/”里表示模式串,即
/pattern/;”.”代表任一字符,”*”表示前一字符的任意重复,”+” 表示前一字符的
至少一次重复;[…]中表示字符可选集,[^…]表示禁止字符;特殊字符用”\”转
义,”|”表示选择项,{n}指定n 次重复等等。“=~”是模式匹配符,表达式$str=~/pat/
意思是判断$str 里面是否存在”pat”串,返回第一个匹配的串(有的话为非零值即
真),可以用它给一个数组赋值,@match=$str=~/pat/g(”g”选项表示匹配所有可
能)。下面举一个复杂点例子来说明正则表达式的使用,在一个文本文件里找出
所有的emial 地址(xxx@xxx.xxx.xxx)输出:
#prntemail.pl 从email.data 文件中找出所有的email 地址
$file="email.data";
if (open($dfile,"$file")){ #打开文件
@data=<$dfile>; #读数据
foreach $line (@data){
print"@email\n" if(@email=$line=~/[^ \\\.]+\@[^ \\\.\@]+\.[^ \\\.\@]+\.[^ \\\.\@]+/g);
#上一行语句为从每行中找出所有的xxx@xxx.xxx.xxx 形式的串并输出
}
close($file);
}
else{print "error in open file $file\n"}
另一常见用法是用split(/pat/, $str) 将字符串用某模式分成多个单词。此外,=~
运算还可以带许多选项来提高使用的灵活性,这里不进一步细说。
Python 语言本身不支持正则表达式操作。进行文本处理时,除内置的字符串
基本操作外,可能还需使用string 模块。由于字符串对象的方法很丰富,没有正
则表达式,也能实现模式的查找、替换,但不如正则表达式方便自然。而要进行
正则表达式的操作,则需要re 模块的支持。
六、 子程序和作用域
每一个语言都会提供不同层次的抽象机制,常见的有语句块、子程序、自定
义数据结构、模块等等。
Perl 提供了子程序、包、模块三种抽象。作用域控制有语句块、子程序、包
三种作用域。Perl 里的基本名字空间是包。包和模块详细在下一节再讨论,在这
里,我们先说一下子程序和语句块。
Perl 子程序涵盖了函数,这两个概念大部分时候不做区分。函数是输入到输
出的映射的一段代码,而子程序是完成一项任务的一个子功能的代码,很自然的,
所有的子程序可以看成函数。Perl 中没有专门的函数声明,在perl 中用sub
[subname] block 来定义子程序。例:
sub hello{
my $name=”xiao rong”; #局部变量$name,$msg 用来传递参数,设定了默认值
my $msg=”hello”;
$name=$_[0]; #待传递的参数自动存储在列表@_中
$msg=$_[1];
print “$msg, $name!”;
}
这定义了一个名字为hello 的子程序。可以通过&hello(name, msg)、hello(name,
msg)、do hello(name, msg)或hello name, msg 来调用(如果调用处在定义前,必
须用&型)。
默认情况下,所有建立的变量都会确认为在当前包范围内的全局变量。Perl
也提供作用域限定符,my 表示局部私有变量,our 表示共享全局变量(即所有
包可见),local 建立全局变量副本(对全局变量访问,访问完之后全局变量值复
原)。
现在再回过来说一下子程序相关的。可以在任何地方定义子程序,这一点容
易从子程序的实际意思理解,这意味着允许子程序任意嵌套。因为函数即是子程
序,所以函数调用时参数不必用扩号限定,而可以象在命令行输入一个命令或运
行一个程序。
子程序可以被引用,即子程序可以看成一种数据类型,用&标示,可以定义
一个引用指向子程序。如上例子程序hello,我们可以如下:
$fun=\&hello;
&$fun(“Mr. Zhang”, “hello”);
定义引用$fun 指向它,并通过解析它调用函数。到此我们可以很好的理解函数的
多种调用方式了。
函数参数传递时,所有的参数看成一个列表,都存在系统变量@_里。因为
系统会自动把参数变成数组,所以在传递参数是要特别小心。比如2 个数组
@array1=(1,2),@array2=(3,4),把这两个参数传给一个子程序,实际子程序得到
列表(1,2,3,4),或者说在子程序内部可见的是一个列表@_=(1,2,3,4)。传哈希表给
子程序,会将哈希表转成键值对顺序列表。如果要传递哈希表或多个数组则可以
通过引用来实现:
sub withref{
my ($rarr, $rhash)=@_; #@_中第一个元素是数组引用,第二个为hash 引用
foreach (@$rarr){…}
foreach $key (%$rhash){…}
}
另外一个用来克服传值缺陷的做法是对列表使用别名,类似与C 语言的指
针,传的是列表的起始地址,而不是复制整个列表的元素。用“*”运算符来标
明,如:
sub alias{
my (*arr)=*_;
foreach (@arr){…}
}
@array=(1,2,4,3);
alias(*array);
因为perl 参数的类型和数目一般不在声明时指定(函数原型的概念是在最近
的perl 版本中才引入),所以默认子程序是处理不定个数和类型的,当然通过值
@_可以得知参数的个数,但一般更常用的是shift 函数。shft 函数去列表的第一
个值并从中删除,默认对@_操作。
在perl 新近版本可以使用函数原型来限定参数的个数和类型。用相应的类型
标示符表示接受的参数的类型,多个参数中间以“;”间隔。另外子程序可以接
受一些诸如locked、lvalue、method 等选项。
Perl 预定义了三个子程序,分别在特定的时间执行,它们是:BEGIN 子程序
在程序启动时被调用;END 子程序在程序结束时被调用;AUTOLOAD 子程序在
找不到某个子程序时被调用。你可以自己定义它们,以在特定时间执行所需要的
动作。若同一个预定义子程序定义了多个,则BEGIN 顺序执行,END 逆序执行。
除了子程序外,perl 还支持语句块结构(有时也成为非限定块),可以给语句
块命名。块类似与一段循环,不过它值运行一次,可以用last,next,redo 控制。
例如下属语句用来模拟C/C++语言中的switch 语句:
SWITCH:{
if($gen="male"){print "Mr. ...";last SWITCH}
if($gen="female"){print "Ms. ...";last SWITCH}
}
语句块里同样可以定义变量,前套子程序和语句块。
Python 提供了函数、模块、和类三个层次的抽象。并且使用名字空间在应用
程序中存储对象的信息和数据。关于作用域规则,有所谓的“LGB”规则,即名
字空间查找为先局部(local),再全局(global),再内置(built-in);函数和类不会自
动继承父作用域。局部是指该抽象层次里,全局指当前python 执行文件,内置
是指语言定义,随处可见的一些量和函数。例如:
name=”xiao rong” #global 的name
def hello(): #global 的hello 函数
name=raw_input(“input your name, please:”) #hello 函数内local 的name,built-in
的函数raw_input
print “\nhello ”, name, “!”
如果默认的“LGB”规则不能确定名字(出现通常所说的陷阱),则需要特
别指定。在下面的子程序说明时将说明这一细节。
在python 里通过def fun(arg1, arg2, …)来定义一个函数。采用命名参数制(相
应说法有关键字参数调用),即按名字对应传递;支持默认值;参数也是对象,
且采取引用模型。有默认参数值,且有参数缺省时应注意参数排合适的顺序消除
歧义。下面定义一个函数,它在一定的字符边框里格式输出问候语:
def greeting(name, greetmsg=”hello”, ch=’*’): #name 为要问候人的姓名
str=”\n”+greetmsg+”, “+name+”!” #greetmsg 为问候语
i=0 #ch为边框字符
while(iprntinfo();
输出的结果为:name: xiao rong age: 22。
所以,perl 的类是包,带有构造函数new。对象只不过是同该包绑定的数据
引用(我们可以查看$st 的值即print $st,输出结果为STU=ARRAY(0x200f99c),
与普通引用的值不大一样),在上例中数据引用是一个列表,一般对象数据为哈
希数组。
Perl 类的方法只不过是一个perl 子程序而已,也即通常所说的成员函数。Perl
的方法定义不提供任何特殊语法,但规定方法的第一个参数为对象或其被引用的
包。Perl 有两种方法:静态方法和虚方法(静态方法类似与C++的,但虚方法不
同)。静态方法第一个参数为类名,虚方法第一个参数为对象的引用。方法处理
第一个参数的方式决定了它是静态的还是虚的。静态方法一般忽略掉第一个参
数,因为它们已经知道自己在哪个类了,构造函数即静态方法。虚方法通常首先
把第一个参数shift 到变量self 或this 中,然后将该值作普通的引用使用。Perl
的对象方法调用模型是生成一个方法对象,即从对象和它调用的子程序生成一个
新的“方法”,然后按普通的子程序执行。
Perl 的类封装通过包的作用域限制和子程序的作用域限制以及在@EXPORT
数组来控制,继承主要是通过@ISA 数组来实现,在@ISA 数组中的所有类都是
基类,但基类数据不会自动继承,需要程序员写相应代码。我们用一个例子GSTU
继承前面的STU 类来说明:
package GSTU;
require Exporter;
require STU;
@ISA=qw(Exporter,STU);
@EXPORT=qw(prntinfo);
sub new{
$this=new STU($_[1],$_[2]); #调用基类STU 的构造函数来继承基类的数据
$$this[2]=$_[3]; #列表增加一项研究方向
bless $this;}
sub prntinfo{ #重载方法prntinfo
$self=shift;
$self->STU::prntinfo();
prnit “researching area:$$self[2]\n”
}
1;
上面的代码还涉及到了方法的重载(它们属于不同的名字空间)。继承实际是提供
了一个方法的包搜索空间,如果在本类和基类中找不到,则调AUTOLOAD 子程
序,再不就看UNIVERSAL 子程序是否存在并调用或报错。
Perl 跟踪对象的链接数目,当某对象的最后一个应用释放到内存池时,该对象
就自动销毁。对象的析构发生在代码停止后,脚本将要结束时。对于全局变量而
言,析构发生在最后一行代码运行之后。如果你想在对象被释放之前获取控制权,
可以定义DESTROY()方法。DESTROY()在对象将释放前被调用,使你可以做一
些清理工作。DESTROY()函数不自动调用其它DESTROY()函数,Perl 不做内置
的析构工作。如果构造函数从基类多次bless,DESTROY()可能需要调用其它类
的DESTROY()函数。当一个对象被释放时,其内含的所有对象引用自动释放、
销毁。 一般来说,不需要定义DESTROY()函数,如果需要,其形式如下:
sub DESTROY {
#
# Add code here.
#}
因为多种目的,Perl 使用了简单的、基于引用的垃圾回收系统。任何对象的引
用数目必须大于零,否则该对象的内存就被释放。当程序退出时,Perl 的一个彻
底的查找并销毁函数进行垃圾回收,进程中的一切被简单地删除。在UNIX 类的
系统中,这像是多余的,但在内嵌式系统或多线程环境中这确实很必要。
可以看出,perl 的面向对象概念是比较模糊的,而且一般的建议也是能不用
它的面向对象机制,尽量不用使用。
前面说过,python 作为面向对象的语言比较彻底,很多基本的内置类型就是
面向对象的。如复数数值类型,num=3+0J,可以去它的实部num.real。他的类有
分明的数据和方法说明,如我们定义前面perl 下STU 相同意义的类:
class STU:
def __init__(self, name, age): #self 表示对象本身的引用,实际是一隐藏参数
self.name=name
self.age=age
def prntinfo(self):
print name, age
#定义一个对象st
st=STU(“xiao rong”,22)
st.prntinfo()
由python 的数据抽象和作用域规则确定数据的封装性,这一点和perl 相同。
Python 类继承和C++类似,我们延续上面的例子:
class GSTU(STU):
def __init__(self, name, age, area):
STU.__init__(self, name, age)
self.area=area
def prntinfo(self):
STU.prntinfo(self)
print “research area:”+self.area
因为python 不支持重名函数(函数重载),所以不支持多个构造函数(__init__),
这不同C++和java 等面向对象语言。Python 允许定义所谓的虚构函数__del__(),
尽管和perl 一样语言会进行自动废料收集,但__del__()允许对象在销毁时作一些
更复杂的工作。
九、 异常处理
象python 支持异常处理机制的脚本语言里是少有的。这得益于python 设计
的类似于java 的完全OO,即根本上面向对象。所有的异常是Exception 的字类,
内置了诸如有语法错、算术运算异常、查找异常、环境异常、名字错、键盘中断、
属性错、导入错、运行时错、类型错、断言错、引用错、内存用完、系统异常等
基本异常类,涵盖大部分可能出现的异常并自动检查,可以很方便地进行异常控
制。Python 的异常处理采取终止模型(termination model)。捕捉异常语法
try...except 形式,用raise 产生一个异常如:
import sys
ARGV=sys.argv
a=eval(ARGV[1])
b=eval(ARGV[2]) #按整数读入参数a,b
try: #监察异常
print a/b #打印a/b
except ZeroDivisionError: #处理被0 除的异常
print "divided by zero!"
raise SyntaxError #引发一个语法错误的异常,这样将导致脚本终止
一个语言要支持异常处理,首先应该定义各种异常,并有引发和捕获异常的]
机制。一般异常的引发是由系统自动抛出,并且有默认的捕获和处理;程序员可
以在程序设计时加入代码检查看是否由某种类型的异常抛出,并作相应处理;异
常就近(在最近的一层里)处理,否则逐层传递,没有匹配的捕获,则程序出错
终止。
所以相比而言,perl 的异常处理机制很原始,类似C 语言,靠程序员检查,
然后进行简单的处理,如输出错误信息,退出程序或警告。要想实现上例情形的
perl 脚本可能是:
$a=;
$b=;
die(“divided by zero!”) if b=0; #如果除数为0 退出,别的情形可能用warn 警告
print a/b;
如果有错误的预先检测,如上里的if 语句,则异常情况可以被特别处理;但如
果不进行这种检测,则一旦异常出现,整个脚本被中断或异常将影响其它语句。
针对这种情况,perl 里的eval 函数将有特别的用处。当脚本包含一个eval 函数
调用时,一个perl 解释器新的请求将被建立,即运行了另外的一个解释器,并且
这个解释器的执行阶段解释执行提供给eval 的语句块或表达式,一旦运行结束,
该解释器运行终止。eval 函数的最大特点是运行时间执行perl 源代码,并且和主
脚本代码的运行保持很大的独立性。所以eval 代码出现异常不会严重影响或中
断整个脚本的继续执行。eval 可以接受单个表达式或语句块的输入,要特别注意
的是当eval 一个语句块时,语句块是和其它部分一道被编译的,等到eval 是才
执行,所以可能不具有你所期望的动态效果,如想动态的使用一个包:
$mod=$windows?’DBI:win32’:’DBI’; #根据操作系统的选择不同的模块
eval{use $mod;}; #在eval 块中打开模块$mod,但实际这种情况下$mod 是没有值的
eval 的块和普通的语句块不同,它后面的“;”不可少,它是相对独立的解释单
位。要实现上例的想法,可以用eval 表达式:
$mod=$windows?’DBI:win32’:’DBI’;
eval “use $mod”; #表达式的值是在运行中求得的,故此时$mod 有值
eval 块中的异常信息将存储在系统变量$@中,可以通过访问该变量了解代码的
运行状况。
在perl 标准库里有为方便异常处理的一些模块如Carp 等,但错误的传递和
逐层匹配处理能力较差,依赖于程序员的个人设计,语言不自动检查。
十、 并发控制
并发(concurrence) 是计算一个重要发展方向,也是克服硬件速度发展相对停
滞、待处理问题规模复杂度不断增长的有效机制。很多操作系统通过多进程和多
线程来实现并发处理。进程可以说是一个一个运行中的程序,有自己独立的内存
空间和其他资源。而线程可以看成轻量级的进程,或进程的一个执行体,同一进
程内的线程共享内存和资源以及一些全局环境和变量。进程和线程以及多任务的
其它一些细节在这里不介绍了。Perl 和python 这两种脚本语言设计时都考虑了
对支持进程和线程控制的支持。
Perl 直接支持进程控制,可以在脚本运行时直接fork 出一个子进程,在UNIX
系统这将运行系统调用fork,在win 系列系统创建的是伪进程(模拟子进程,生
存期依赖它的创建进程)。fork 出的子进程直接运行进跟其后面的块中的代码。
进程间的通信组要依赖与信号。Perl 支持POSIX 标准,定义了其中的大部分信
号,信号和信号句柄存在字典中,这和通常的信号句柄系统大不一样。关键字时
信号名字(比POSIX 有所简化,把SIG 省略了,如SIGINT 简化为INT),而相
关值是当收到信号时应执行的动作函数的引用,如$SIG{INT}=sub {print “got
SIGINT”;},表示该进程收到INT 信号时将打印出”got SIGINT”。另外一个进程
间交换信息的方式是通过管道,与shell 中控制类似。
Perl 中通过包Thread 支持多线程。创建线程时将让它执行的函数和参数传给
它,如:
use Thread;
$t=new Thread(\&subprog, args); #创建一个新线程执行subprog 子程序
Thread 包中有join 方法可以控制父线程等待子线程。线程的一些基本控制如获
得名称,用户,父线程号,进程号在Thread 模块里有专门的函数,在此不细说。
当线程间共享变量时可以通过条件变量或信号量(semaphore)来控制。通过
lock 函数给变量加锁,有三个独立的函数可以去锁:cond_wait、cond_signal 和
cond_broadcast 分别对应有线程在该变量上等待,发送信号,广播信号。信号量
能够用来指示一些线程事件,所提供的信息是以数字出现,并且这些数字能相应
的增加或减少,通常的方法是建立一个和可以共享的资源相关联的信号量。在
perl 中如下建立信号量控制:
$sema= new Thread::Semaphore(0); #建立信号量并赋初值0,假定它和$val
变量关联
$val=0;
sub sub1{
while(1){
if ($sema>0)
{
$sema->down;
$val++;
wait(5);
if ($val>100) { last;}
$sema->up;}
}
}
sub sub2{
while(1){
if ($sema>0)
{
$sema->down;
$val--;
wait(10);
if ($val<0) { last;} $sema->up;}
}
}
$t1=new Thread \&sub1; #$t1 线程将执行sub1
$t2=new Thread \&sub2; #$t2 线程将执行sub2
当然,因为一个信号可以终止脚本的运行,所以信号同样可以影响线程的运行,
要建立用于信号操作的线程,必须引入Thread::Signal 模块,接下来的操作大体
和信号句柄类似。
Python 通过os 模块来支持进程操作管理。通过fork 函数创建一个进程,wait
或waitpid 函数等待子进程,通过信号或管道进行进程间通信。例如:
import os
while 1: #wit for a connection
if accepted:
pid=os.fork()
if not pid:
#do the child process
else:
os.waitpid(pid,os.WNOHANG) #非挂起等待pid 子进程结束
在UNIX 或类似系统python 里的os.fork()函数只是操作系统的fork 的别名
(windows 系统暂时不支持fork 函数),fork 出来的子进程执行其后的if 语句的一
个分支(not pid 的分支)。通过os.kill 函数可以给子进程发送信号,而子进程要捕
获信号必须通过signal 中的signal 函数安装信号句柄。下例说明了如何让子进程
捕获SIGALRM 信号执行相应动作:
import os, signal, sys, time
def alarm_hdlr(signum, frame): #定义信号处理函数
print “wake up!”
sys.exit()
pid=os.fork() #创建子进程
if not pid:
signal.signal(signal.SIGALRM, alarm_hdlr) #安装信号句柄
while 1:
print “going to sleep…”
time.sleep(10)
print “now I’m up…”
else:
signal.alarm(5)
python 通过thread 模块来支持多线程,核心函数是start_new_thread(f,(args)),
这个函数启动一个新线程,并让它执行以参数(args)执行f 函数。例如:
import thread, time
def dsptime(interval, prefix=’’): #函数dsptime 每隔一段时间显示一次当前时间
while 1:
print prefix, time.ctime(time.time())
time.sleep(interval)
thread.start_new_thread(dsptime, (10,’now time is:’))
while 1:
pass
线程执行完函数子自动终止,或者可以调用sys.exit()强制其终止。子进程共享并
访问父进程所有的全局对象和环境,python 由多种控制线程访问这些对象的方
法。基本的方法是通过给对象加锁,例如:
import thread, time
cnt=0
cnt_lock=thread.allocate_lock() #定义了一个锁对象(只是一个协议),让它来控制cnt
变量的访问
def inccnt():
while 1:
print "thread 1:excuting ..."
if (cnt_lock.acquire(0)): #看是否能获能对锁变量的控制
global cnt
print "thread 1",thread.get_ident(),"acquired lock..."
cnt+=1
time.sleep(0.01)
cnt_lock.release()
else:
print "thread 1 can’t get lock ..."
time.sleep(1)
def dspcnt():
while 1:
if (cnt_lock.acquire(0)):
global cnt
print "thread 2",thread.get_ident(),"acquired lock..."
print "thread 2 print",cnt
time.sleep(0.01)
cnt_lock.release()
else:
print "thread 2 can’t get lock..."
time.sleep(1)
thread.start_new_thread(inccnt,())
thread.start_new_thread(dspcnt,())
while 1:
pass
输出将显示两个线程的执行情况和对锁的控制。特别注意的是(在perl 和python 中的其它线
程控制机制也有类似的问题),你可以完全不遵守锁对象的协议而直接访问到变量,那样将
无法保证对变量使用的正确性。Python 还可以通过条件变量,信号量,队列和事件等对象
来控制线程共享信息和通信。
以上的几个小节,主要从语言的数据类型、控制结构、作用域、正则表达式
处理、包和模块、面向对象、并发控制、与系统和环境交互等多个方面对perl
和python 进行了较粗略的比较分析,尽量突出两者之间的差别,并给以实例说
明。大部分实例都是自己设计,少部分来自参考书,但都尽量在计算机上试验过。
也对其中的一些特征和细节,如perl 的引用类型,python 的参数传递,进行了的
思考,这些想法可能不是很正确。Perl 和python 各有它们广大的用户群,所以
很难说孰优孰劣,而且因为自己对这两们语言的使用不多,有很多地方理解的不
够深入和准确,请老师指正。
参考文献:
1,Martin C Brown,perl 参考大全,顾凯等译
2,Martin C Brown,python 参考大全,康博译
3,ActivePython Documentation:Tutorial,Reference Manual 部分
4,Perl Tutorial,网友flamephoenix 译
5,A. Tucker, R. Noonan,Programming Languages: Principles and Paradigms
6,Andrew S. Tanenbaum,现代操作系统,陈向群等译
随着系统管理的复杂化和网络运用的扩展,脚本语言在实际编程中的应用越
来越广泛。传统观念是:一般的高级语言如C/C++,Java,Delphi 等用来编写主要
的功能组件,如java 的类和beans,用C/C++写的动态连接库,用VisualBasic 写
的控件等,而脚本语言如javascript,Vbscript,perl,python,sh 等通常认为脚
本语言是介于HTML 应用的发展和脚本语言本身的发展,脚本语言的应用早就超
出仅仅作为常规编程语言的辅助角色而用来直接开发应用系统,著名的网络流控
制系统mrgt 就是用perl 开发的。现在的脚本语言,其功能甚至强大到可以和一
般高级语言相媲美,而且引入较新的程序机制和技术(如OO 和异常处理),加
上固有的简单易用,逐渐发展为程序开发的一支主流。脚本语言的基本特征是语
法简单,跨平台,类型宽松,解释执行。早期的脚本语言?本文选择现今在自由
软件开发里很具代表性和广泛使用的两种脚本语言perl 和python 进行比较研究,
以期使读者能对这两种脚本语言的基本特点和新发展有一定的了解。
一、 两者初识
Perl(可以看作Practical Extraction And Reporting Language 的首字母)语言
最早由Larry Wall 开发,原始动机即作为一个文本提取和报告的实用语言,本来
是基于UNIX 系统,后来发展成能运行于几乎所有的机器体系和操作系统。Larry
Wall 是坚实的免费软件拥护者,所以perl 也成为免费软件之一(但不属GNU),
按自由免费软件的一般模式发展和传播(perl 中的源代码、标准库文件、可选模
块及所有文档都免费提供,并被用户团体所支持)。从1988 年的最初诞生,到现
在的perl 6 系列版本,perl 能够如此稳健蓬勃的发展是和它自由免费、简单高效
(语法上象C 和Unix 的sh,解释执行前会简单编译,有点象java)、强可扩展性、
数据类型灵活、面向对象以及有强大规范的用户团体交流(CPAN, Comprehensive
Perl Archive Network)和幕后支持组织(CPAST, Comprehensive Perl Arcana Society
Tapestry)分不开的。
Python 最初出现在2000 年前后,名字来源一喜剧团体Monty Python,并无
实际代表意义。Python 最初由Guido van Rossum 及其合作者设计开发,后来
python 开发小组多次重组,最终到Digital Creations。Python 和perl 一样是在迅
速稳定发展,目前的一个著名成功业绩是Digital Creations 开发的zope,下一代
开放源码应用服务器以及门户工具。从抽象的功能和特点来说,python 是和perl
最相像的语言,可能和perl 的成功和python 的较晚出现有关。和perl 一样,python
也是自由免费、简单高效、强可扩展性、数据类型灵活、面向对象的。并且python
的功能相对更强大,更易于编写大程序,核心又较小。尽管从抽象的角度,perl
和python 两者有很大的相似,但作为不同的语言,他们却是又有许多差别,下
文从几个主要的方面对两者进行深入的比较研究,尽量能找出它们的异同并对它
们一些进行原理和模型层次的探讨。
下面我们先来看一下如何用这两个语言实现最简单的“hello!”程序,先对
它们有个大概的印象。在perl 情形,先选择一个你比较喜欢的文本编辑器编写
hello.pl 程序如下:
#This is a hello saying demo
print “what is your name?\n”;
$name=;
print “hello $name!”;
在终端或命令行输入perl hello.pl 或直接输入hello.pl 程序将被解释执行,提示输
入你的名字,输入名字xiao rong 后程序在屏幕输出“hello xiao rong!”。
Python 脚本运行有两种方式,一种是交互式的,一种是自动运行脚本。交互
式是在命令行输入python,然后进入交互状态(>>>为主提示符,…为从提示符):
ActivePython 2.4.1 Build 245 (ActiveState Corp.) based on
Python 2.4.1 (#65, Mar 30 2005, 09:33:37) [MSC v.1310 32 bit (Intel)] on
win32
Type "help", "copyright", "credits" or "license" for more information.
>>> name=raw_input("what is your name?\n")
what is your name?
xiao rong
>>> print "hello, ",name,"!"
hello, xiao rong !
>>>
或者编好脚本文件hello.py 如下:
name=raw_input(“what is your name?\n”)
print "hello, ", name, "!"
然后在命令行输入python hello.py 或hello.py 发生的情形和perl 一样。
所以从顶层模型来看,perl 是一个完整的perl 脚本输入自动执行。一个perl
脚本文件和C/C++源程序类似,由一些语句和语句块组成,语句由“;”分隔,
语句块由一对“{}”包括。而python 脚本既可自动执行,又能交互式运行。而
且python 特别的一个地方使它采取缩进来标示分界和层次,不同于一般语言采
用”;”和”{}”。这种缩进方式的程序在运行时依赖一个栈来记录逻辑行的INDENT
(缩进)和DEDENT(抽出)。在读入脚本文件第一行之前,有个数值0 压入栈
底,这个0 直到脚本运行完是不会被弹出的。压进栈中的的数值是从底向顶严格
增加的。每次读到一行新的逻辑行,先比较它的缩进量和栈顶纪录的缩进量大小,
大则表示进入下一层,此时把新的值压入栈并产生一个INDENT 记号,小则表
示此缩进量和前面某处一样(否则错误,这一点很严格),这时把栈中比它大的
量都弹出,产生一个DEDENT 记号。如此可以获知每一行所在的层次,并逐行
解释执行。
二、 数据类型
脚本语言特点之一就是数据类型灵活,变量无需先声明,类型是靠值来动态
确定的。一个变量在一个程序中可以存储不同类型的值。
Perl 支持的内置数据类型有数值(包括整数和浮点)、字符串(包括单个字
符)、引用、列表和数组、关联数组(字典),其中前3 类属标量,在变量名前面
由”$”标示,列表和数组变量名由”@”标示,关联数组名由”%”标示。即变量类型
分三类,标量($varname),列表(@varname),关联数组(%varname)。之所以采
取这样的标示,很可能是和perl 要支持引用相关,因为,要高效支持引用,就得
很方便的知道引用的类型。内置数据类型有内置方法支持,如数值的+-*/,字符
串的连接、比较和匹配(详见下文的正则表达式讨论),列表的排序和翻转。当然
除了内置的类型,还支持新类型,面向对象的自定义对象,在面向对象分析小节
使我们详细讨论。Perl 的大部分数据对象采取值模型,赋值时复制对象。所以它
把引用类型引入(类似C 里的指针,但作为引用类型不能进任何算术运算),以
方便数据的传递,在子程序小节将讨论引用的应用。引用类型属标量,通过在普
通变量前加”\”来获得引用的值,如$rarr=\@array;取值时用类型标示符作用在引
用变量前即可,如@$rar 得到数组@array 的值,而$$rarr[i]取到@array 第i 元素
(数组下表从0 开始)。
Perl 的文件管道句柄用标量来存储,这样很方便对文件和管道的操作。另外,
perl 支持别名(alias),即在变量前加“*”,如:
@array=(1,2,3);
*arr=*array;
$arr[0]=2;
print "\@array:@array\n\@arr:@arr\n";
这里有的一个问题是,如果同时有@array,$array,%array 变量则解释器无法区
分它们的别名。别名实际是指向符号表项的指针,可以用来方便文件句柄和列表
的操作。别名在perl 中又叫类型标识。
Python 支持的内置基本类型有数值、字符串、链表、元组、字典。由于设计
时把python 定位为完全的面向对象(包括数值类型,直接支持复数类型),对对
象操作大多都是靠调用对象的方法来实现。Python 的不同类型变量定义无需特殊
标示。和perl 不一样,python 大部分对象采用引用模型,但是python 不支持引
用型变量。象这类采用引用模型的语言,对对象一般分成两类,可以修改(mutable)
和不可修改(immutable),不可修改的对象如果发生修改操作报错(但python 返
回” NotImplemented ”型值)。
两者的列表数组和链表意义一样,是一个可以同时存多种标量值(整数,字
符串等等)的一个线性表。Python 的链表允许插入、删除、倒置、排序、切片等
操作,长度动态变化;perl 的数组只允许在切片和首尾增加删除元素操作。两者
的关联数组和字典大致一样,存储的是健值对,允许检索,插入,删除。Python
比perl 多的元组是不可修改型(不能在其中增加、删除、重新赋值)可以当作列
表常量( 在这一点上和perl 的列表更象, 它们的输出形式都是
“(item1,item2,…,itemn)”,python 的链表数组形式为[item1,item2,…,itemn]),而
且Python 的元组允许嵌套,相当于广义表(但不允许原地插入删除)。
三、 控制流
perl 支持灵活多样的控制流。有多种条件判断和循环并且支持循环控制和
goto 跳转。首先说一下perl 里的条件和条件表达式。同C 语言一样,perl 没有另
外的boolean 类型,零和空等表示false,其余为ture。条件可能来自比较,函数
返回(如读取),状态和环境的判断(如文件是否存在)。特别注意在perl 中的有
些比较操作,返回3 真值1,0,-1。如数值比较的”<=>”运算符和字符串比较的
cmp 操作。逻辑运算有与(&&或and 连接)、或(||或or 连接)、非(!或not)、
异或(xor)。
Perl 除提供传统的if () { } elsif ( …)
{ } … else{ }条件控制外,还提供
了特别的单行条件语句,语法为statement keyword condexpr。其中keyword 可为
if、unless、while 或until,如: print ("This is zero.\n") if ($var == 0) 和 print ("This
is zero.\n") unless ($var != 0),虽然条件判断写在后面,但却是先执行的,只有满
足条件才执行前面的语句。
Perl 支持while 循环 while ( ) { },until 循
环 until ( ) { },类C 的for 循环,针对列表(数
组) 每个元素的循环foreach , 语法为: foreach localvar (listexpr)
{ statement_block; }(此处的循环变量localvar 是个局部变量,如果在此之前它
已有值,则循环后仍恢复该值。在循环中改变局部变量,相应的数组变量也会改
变),do 循环 do { statement_block } while_or_until (condexpr); do 循环至少执行一
次循环。
退出循环为last,与C 中的break 作用相同;执行下一个循环为next,与C
中的continue 作用相同;Perl 特有的一个命令是redo,其含义是重复此次循环,
即循环变量不变,回到循环起始点,但要注意,redo 命令在do 循环中不起作用。
Goto 语句和C 完全一样。
Python 的控制结构相比方式少一些,但能满足通常的需要(因为支持while
型的循环)。Python 的条件表达式值有Booleans 类型值True 和False 表示,但是
Booleans 是作为Integers 即整数的子类型,其中Ture 就是1,False 就是0。Python
的条件值同样由比较,函数,环境和状态产生。有与(and)、或(or)、非(not)运
算。Pyhton 支持多分支的if…elif…else 条件判断,但书写时必须注意缩排对齐。
Python 支持所谓的对序列中(包括链表和元组)的元素的for 循环“for elements
in Sequence:”,此时要特别注意在循环过程中不应改变列表形状;python 还支持
while 循环, while expression : block [ else : block ]。Python 循环时除支持类似C
中break,continue 等跳转控制语句外,还有一种pass 语句,它什么都不做。
四、 解释和环境
很多脚本语言的运行是需要脚本解释器或专门的虚拟机(类似Java 的
JVM)。而且一般脚本语言不能直接生成可执行文件,所以与环境和操作系统的
交互必须通过一些间接的方式,如更多的通过环境变量和特殊变量。这一节我们
来看一下perl 和python 的解释执行与环境交互。
Perl 和perl 函数的执行可以被某些环境变量影响,许多环境变量在安装perl
时被操作系统或shell 自动设置,有些变量可以在脚本运行时被修改和配置。Perl
通过内置变量哈希表%ENV 提供到当前解释器的环境变量的接口,常用的环境
变量包括HOME,PATH,PWD,HOST,USER 等,环境变量在不同的操作系
统下是有差别的。除了环境变量外,还有一些特殊变量可以用来控制perl 运行或
为perl 运行提供外部信息,如@ARGV 存储传递给程序的参数,@_存储传递给
子程序的参数,$]表保存perl 版本,$@保存最后一次调用eval 产生的错误,$?
保存最后一个子进程返回状态,@INC 包含包搜索目录列表,%INC 保存通过do
和require 得到的程序中每一个文件的列表,%SIG 表示信号句柄(在第十节会详
细介绍),STDERR,STDIN,STDOUT 分别是标准错误、输入、输出句柄。Perl
可以通过system 函数运行程序或外部命令,通过fork 创建子进程。
Perl 在解释执行前实际有一个粗略的编译,解释执行的是操作码(由C 语言
编写的一些基本操作指令,类似与Java 的虚拟机)。尽管perl 是以脚本语言,可
以使用perlcc 命令或其它工具将perl 脚本编译成可执行文件(在windows 下.exe
文件)。但是,编译成可执行文件不会进一步提高perl 代码的执行效率,因为它
只是进一步把操作码翻译成机器指令,而不会进一步的优化机器码。
Python 通过sys 和os 两个模块来和系统与环境进行交互,其中sys 模块处理
一些与具体系统无关的,而os 是针对具体系统的。在sys 模块的argv 数组中可
以获得命令行参数,stdin、stdout、stderr 分别获得标准输入输出错误的句柄,可
以通过sys 里的函数exit 终止脚本的运行。另外sys 模块还提供了一些解释器的
信息,如modules 是当前被加载的模块,path 是个字典,是模块和路径对。os
模块的主要功能是确认用户和进程环境,对诸如文件和文件系统这样的外部系统
的进行控制和通信。os 中的字典environ 提供对当前进程的环境变量的访问,函
数system 和exec 用于启动其它程序或外部命令。关于进程的控制和通信在第十
节并发控制将进一步讨论。
Python 在解释前也是会进行编译的,这一点在使用import 语句导入一个的
模块时看得更清楚,因为它将源代码”.py”编译成”.pyc”再导入,每次导入都会比
较”.py”和”.pyc”文件的时间戳,确定是否需要重新编译。Python 的解释器构架中
有一部分就叫做PVM(python 虚拟机);Python 脚本解释器核心比较小,很容易
用其他高级语言实现,如现在比较流行的Jython,可以看成java 和python 的一
个结合。
五、 正则表达式
本来正则表达式一般不在的程序语言设计时考虑。但考虑到脚本语言,尤其
是早期的脚本语言如awk,很大程度上是为加强在Unix Shell 上的报文处理而设
计。从这个程度上来说,perl 的内置强大正则表达式处理能力更有传统脚本语言
的风格。正则表达式处理一般包括模式串的匹配、提取、替换、分割。而模式串
用一个正则表达式来描述。如一般语言里的标示符用
[a..z]|[A..Z]( [a..z]|[A..Z]|[0..9])*表示以字母开头任意个字母或数字的字符串。
Perl 语言支持对字符串进行模式匹配、替换等丰富的正则表达式相关操作,
使用非常方便。和通常郑泽表达式记号差不多:在一对”/”里表示模式串,即
/pattern/;”.”代表任一字符,”*”表示前一字符的任意重复,”+” 表示前一字符的
至少一次重复;[…]中表示字符可选集,[^…]表示禁止字符;特殊字符用”\”转
义,”|”表示选择项,{n}指定n 次重复等等。“=~”是模式匹配符,表达式$str=~/pat/
意思是判断$str 里面是否存在”pat”串,返回第一个匹配的串(有的话为非零值即
真),可以用它给一个数组赋值,@match=$str=~/pat/g(”g”选项表示匹配所有可
能)。下面举一个复杂点例子来说明正则表达式的使用,在一个文本文件里找出
所有的emial 地址(xxx@xxx.xxx.xxx)输出:
#prntemail.pl 从email.data 文件中找出所有的email 地址
$file="email.data";
if (open($dfile,"$file")){ #打开文件
@data=<$dfile>; #读数据
foreach $line (@data){
print"@email\n" if(@email=$line=~/[^ \\\.]+\@[^ \\\.\@]+\.[^ \\\.\@]+\.[^ \\\.\@]+/g);
#上一行语句为从每行中找出所有的xxx@xxx.xxx.xxx 形式的串并输出
}
close($file);
}
else{print "error in open file $file\n"}
另一常见用法是用split(/pat/, $str) 将字符串用某模式分成多个单词。此外,=~
运算还可以带许多选项来提高使用的灵活性,这里不进一步细说。
Python 语言本身不支持正则表达式操作。进行文本处理时,除内置的字符串
基本操作外,可能还需使用string 模块。由于字符串对象的方法很丰富,没有正
则表达式,也能实现模式的查找、替换,但不如正则表达式方便自然。而要进行
正则表达式的操作,则需要re 模块的支持。
六、 子程序和作用域
每一个语言都会提供不同层次的抽象机制,常见的有语句块、子程序、自定
义数据结构、模块等等。
Perl 提供了子程序、包、模块三种抽象。作用域控制有语句块、子程序、包
三种作用域。Perl 里的基本名字空间是包。包和模块详细在下一节再讨论,在这
里,我们先说一下子程序和语句块。
Perl 子程序涵盖了函数,这两个概念大部分时候不做区分。函数是输入到输
出的映射的一段代码,而子程序是完成一项任务的一个子功能的代码,很自然的,
所有的子程序可以看成函数。Perl 中没有专门的函数声明,在perl 中用sub
[subname] block 来定义子程序。例:
sub hello{
my $name=”xiao rong”; #局部变量$name,$msg 用来传递参数,设定了默认值
my $msg=”hello”;
$name=$_[0]; #待传递的参数自动存储在列表@_中
$msg=$_[1];
print “$msg, $name!”;
}
这定义了一个名字为hello 的子程序。可以通过&hello(name, msg)、hello(name,
msg)、do hello(name, msg)或hello name, msg 来调用(如果调用处在定义前,必
须用&型)。
默认情况下,所有建立的变量都会确认为在当前包范围内的全局变量。Perl
也提供作用域限定符,my 表示局部私有变量,our 表示共享全局变量(即所有
包可见),local 建立全局变量副本(对全局变量访问,访问完之后全局变量值复
原)。
现在再回过来说一下子程序相关的。可以在任何地方定义子程序,这一点容
易从子程序的实际意思理解,这意味着允许子程序任意嵌套。因为函数即是子程
序,所以函数调用时参数不必用扩号限定,而可以象在命令行输入一个命令或运
行一个程序。
子程序可以被引用,即子程序可以看成一种数据类型,用&标示,可以定义
一个引用指向子程序。如上例子程序hello,我们可以如下:
$fun=\&hello;
&$fun(“Mr. Zhang”, “hello”);
定义引用$fun 指向它,并通过解析它调用函数。到此我们可以很好的理解函数的
多种调用方式了。
函数参数传递时,所有的参数看成一个列表,都存在系统变量@_里。因为
系统会自动把参数变成数组,所以在传递参数是要特别小心。比如2 个数组
@array1=(1,2),@array2=(3,4),把这两个参数传给一个子程序,实际子程序得到
列表(1,2,3,4),或者说在子程序内部可见的是一个列表@_=(1,2,3,4)。传哈希表给
子程序,会将哈希表转成键值对顺序列表。如果要传递哈希表或多个数组则可以
通过引用来实现:
sub withref{
my ($rarr, $rhash)=@_; #@_中第一个元素是数组引用,第二个为hash 引用
foreach (@$rarr){…}
foreach $key (%$rhash){…}
}
另外一个用来克服传值缺陷的做法是对列表使用别名,类似与C 语言的指
针,传的是列表的起始地址,而不是复制整个列表的元素。用“*”运算符来标
明,如:
sub alias{
my (*arr)=*_;
foreach (@arr){…}
}
@array=(1,2,4,3);
alias(*array);
因为perl 参数的类型和数目一般不在声明时指定(函数原型的概念是在最近
的perl 版本中才引入),所以默认子程序是处理不定个数和类型的,当然通过值
@_可以得知参数的个数,但一般更常用的是shift 函数。shft 函数去列表的第一
个值并从中删除,默认对@_操作。
在perl 新近版本可以使用函数原型来限定参数的个数和类型。用相应的类型
标示符表示接受的参数的类型,多个参数中间以“;”间隔。另外子程序可以接
受一些诸如locked、lvalue、method 等选项。
Perl 预定义了三个子程序,分别在特定的时间执行,它们是:BEGIN 子程序
在程序启动时被调用;END 子程序在程序结束时被调用;AUTOLOAD 子程序在
找不到某个子程序时被调用。你可以自己定义它们,以在特定时间执行所需要的
动作。若同一个预定义子程序定义了多个,则BEGIN 顺序执行,END 逆序执行。
除了子程序外,perl 还支持语句块结构(有时也成为非限定块),可以给语句
块命名。块类似与一段循环,不过它值运行一次,可以用last,next,redo 控制。
例如下属语句用来模拟C/C++语言中的switch 语句:
SWITCH:{
if($gen="male"){print "Mr. ...";last SWITCH}
if($gen="female"){print "Ms. ...";last SWITCH}
}
语句块里同样可以定义变量,前套子程序和语句块。
Python 提供了函数、模块、和类三个层次的抽象。并且使用名字空间在应用
程序中存储对象的信息和数据。关于作用域规则,有所谓的“LGB”规则,即名
字空间查找为先局部(local),再全局(global),再内置(built-in);函数和类不会自
动继承父作用域。局部是指该抽象层次里,全局指当前python 执行文件,内置
是指语言定义,随处可见的一些量和函数。例如:
name=”xiao rong” #global 的name
def hello(): #global 的hello 函数
name=raw_input(“input your name, please:”) #hello 函数内local 的name,built-in
的函数raw_input
print “\nhello ”, name, “!”
如果默认的“LGB”规则不能确定名字(出现通常所说的陷阱),则需要特
别指定。在下面的子程序说明时将说明这一细节。
在python 里通过def fun(arg1, arg2, …)来定义一个函数。采用命名参数制(相
应说法有关键字参数调用),即按名字对应传递;支持默认值;参数也是对象,
且采取引用模型。有默认参数值,且有参数缺省时应注意参数排合适的顺序消除
歧义。下面定义一个函数,它在一定的字符边框里格式输出问候语:
def greeting(name, greetmsg=”hello”, ch=’*’): #name 为要问候人的姓名
str=”\n”+greetmsg+”, “+name+”!” #greetmsg 为问候语
i=0 #ch为边框字符
while(iprntinfo();
输出的结果为:name: xiao rong age: 22。
所以,perl 的类是包,带有构造函数new。对象只不过是同该包绑定的数据
引用(我们可以查看$st 的值即print $st,输出结果为STU=ARRAY(0x200f99c),
与普通引用的值不大一样),在上例中数据引用是一个列表,一般对象数据为哈
希数组。
Perl 类的方法只不过是一个perl 子程序而已,也即通常所说的成员函数。Perl
的方法定义不提供任何特殊语法,但规定方法的第一个参数为对象或其被引用的
包。Perl 有两种方法:静态方法和虚方法(静态方法类似与C++的,但虚方法不
同)。静态方法第一个参数为类名,虚方法第一个参数为对象的引用。方法处理
第一个参数的方式决定了它是静态的还是虚的。静态方法一般忽略掉第一个参
数,因为它们已经知道自己在哪个类了,构造函数即静态方法。虚方法通常首先
把第一个参数shift 到变量self 或this 中,然后将该值作普通的引用使用。Perl
的对象方法调用模型是生成一个方法对象,即从对象和它调用的子程序生成一个
新的“方法”,然后按普通的子程序执行。
Perl 的类封装通过包的作用域限制和子程序的作用域限制以及在@EXPORT
数组来控制,继承主要是通过@ISA 数组来实现,在@ISA 数组中的所有类都是
基类,但基类数据不会自动继承,需要程序员写相应代码。我们用一个例子GSTU
继承前面的STU 类来说明:
package GSTU;
require Exporter;
require STU;
@ISA=qw(Exporter,STU);
@EXPORT=qw(prntinfo);
sub new{
$this=new STU($_[1],$_[2]); #调用基类STU 的构造函数来继承基类的数据
$$this[2]=$_[3]; #列表增加一项研究方向
bless $this;}
sub prntinfo{ #重载方法prntinfo
$self=shift;
$self->STU::prntinfo();
prnit “researching area:$$self[2]\n”
}
1;
上面的代码还涉及到了方法的重载(它们属于不同的名字空间)。继承实际是提供
了一个方法的包搜索空间,如果在本类和基类中找不到,则调AUTOLOAD 子程
序,再不就看UNIVERSAL 子程序是否存在并调用或报错。
Perl 跟踪对象的链接数目,当某对象的最后一个应用释放到内存池时,该对象
就自动销毁。对象的析构发生在代码停止后,脚本将要结束时。对于全局变量而
言,析构发生在最后一行代码运行之后。如果你想在对象被释放之前获取控制权,
可以定义DESTROY()方法。DESTROY()在对象将释放前被调用,使你可以做一
些清理工作。DESTROY()函数不自动调用其它DESTROY()函数,Perl 不做内置
的析构工作。如果构造函数从基类多次bless,DESTROY()可能需要调用其它类
的DESTROY()函数。当一个对象被释放时,其内含的所有对象引用自动释放、
销毁。 一般来说,不需要定义DESTROY()函数,如果需要,其形式如下:
sub DESTROY {
#
# Add code here.
#}
因为多种目的,Perl 使用了简单的、基于引用的垃圾回收系统。任何对象的引
用数目必须大于零,否则该对象的内存就被释放。当程序退出时,Perl 的一个彻
底的查找并销毁函数进行垃圾回收,进程中的一切被简单地删除。在UNIX 类的
系统中,这像是多余的,但在内嵌式系统或多线程环境中这确实很必要。
可以看出,perl 的面向对象概念是比较模糊的,而且一般的建议也是能不用
它的面向对象机制,尽量不用使用。
前面说过,python 作为面向对象的语言比较彻底,很多基本的内置类型就是
面向对象的。如复数数值类型,num=3+0J,可以去它的实部num.real。他的类有
分明的数据和方法说明,如我们定义前面perl 下STU 相同意义的类:
class STU:
def __init__(self, name, age): #self 表示对象本身的引用,实际是一隐藏参数
self.name=name
self.age=age
def prntinfo(self):
print name, age
#定义一个对象st
st=STU(“xiao rong”,22)
st.prntinfo()
由python 的数据抽象和作用域规则确定数据的封装性,这一点和perl 相同。
Python 类继承和C++类似,我们延续上面的例子:
class GSTU(STU):
def __init__(self, name, age, area):
STU.__init__(self, name, age)
self.area=area
def prntinfo(self):
STU.prntinfo(self)
print “research area:”+self.area
因为python 不支持重名函数(函数重载),所以不支持多个构造函数(__init__),
这不同C++和java 等面向对象语言。Python 允许定义所谓的虚构函数__del__(),
尽管和perl 一样语言会进行自动废料收集,但__del__()允许对象在销毁时作一些
更复杂的工作。
九、 异常处理
象python 支持异常处理机制的脚本语言里是少有的。这得益于python 设计
的类似于java 的完全OO,即根本上面向对象。所有的异常是Exception 的字类,
内置了诸如有语法错、算术运算异常、查找异常、环境异常、名字错、键盘中断、
属性错、导入错、运行时错、类型错、断言错、引用错、内存用完、系统异常等
基本异常类,涵盖大部分可能出现的异常并自动检查,可以很方便地进行异常控
制。Python 的异常处理采取终止模型(termination model)。捕捉异常语法
try...except 形式,用raise 产生一个异常如:
import sys
ARGV=sys.argv
a=eval(ARGV[1])
b=eval(ARGV[2]) #按整数读入参数a,b
try: #监察异常
print a/b #打印a/b
except ZeroDivisionError: #处理被0 除的异常
print "divided by zero!"
raise SyntaxError #引发一个语法错误的异常,这样将导致脚本终止
一个语言要支持异常处理,首先应该定义各种异常,并有引发和捕获异常的]
机制。一般异常的引发是由系统自动抛出,并且有默认的捕获和处理;程序员可
以在程序设计时加入代码检查看是否由某种类型的异常抛出,并作相应处理;异
常就近(在最近的一层里)处理,否则逐层传递,没有匹配的捕获,则程序出错
终止。
所以相比而言,perl 的异常处理机制很原始,类似C 语言,靠程序员检查,
然后进行简单的处理,如输出错误信息,退出程序或警告。要想实现上例情形的
perl 脚本可能是:
$a=;
$b=;
die(“divided by zero!”) if b=0; #如果除数为0 退出,别的情形可能用warn 警告
print a/b;
如果有错误的预先检测,如上里的if 语句,则异常情况可以被特别处理;但如
果不进行这种检测,则一旦异常出现,整个脚本被中断或异常将影响其它语句。
针对这种情况,perl 里的eval 函数将有特别的用处。当脚本包含一个eval 函数
调用时,一个perl 解释器新的请求将被建立,即运行了另外的一个解释器,并且
这个解释器的执行阶段解释执行提供给eval 的语句块或表达式,一旦运行结束,
该解释器运行终止。eval 函数的最大特点是运行时间执行perl 源代码,并且和主
脚本代码的运行保持很大的独立性。所以eval 代码出现异常不会严重影响或中
断整个脚本的继续执行。eval 可以接受单个表达式或语句块的输入,要特别注意
的是当eval 一个语句块时,语句块是和其它部分一道被编译的,等到eval 是才
执行,所以可能不具有你所期望的动态效果,如想动态的使用一个包:
$mod=$windows?’DBI:win32’:’DBI’; #根据操作系统的选择不同的模块
eval{use $mod;}; #在eval 块中打开模块$mod,但实际这种情况下$mod 是没有值的
eval 的块和普通的语句块不同,它后面的“;”不可少,它是相对独立的解释单
位。要实现上例的想法,可以用eval 表达式:
$mod=$windows?’DBI:win32’:’DBI’;
eval “use $mod”; #表达式的值是在运行中求得的,故此时$mod 有值
eval 块中的异常信息将存储在系统变量$@中,可以通过访问该变量了解代码的
运行状况。
在perl 标准库里有为方便异常处理的一些模块如Carp 等,但错误的传递和
逐层匹配处理能力较差,依赖于程序员的个人设计,语言不自动检查。
十、 并发控制
并发(concurrence) 是计算一个重要发展方向,也是克服硬件速度发展相对停
滞、待处理问题规模复杂度不断增长的有效机制。很多操作系统通过多进程和多
线程来实现并发处理。进程可以说是一个一个运行中的程序,有自己独立的内存
空间和其他资源。而线程可以看成轻量级的进程,或进程的一个执行体,同一进
程内的线程共享内存和资源以及一些全局环境和变量。进程和线程以及多任务的
其它一些细节在这里不介绍了。Perl 和python 这两种脚本语言设计时都考虑了
对支持进程和线程控制的支持。
Perl 直接支持进程控制,可以在脚本运行时直接fork 出一个子进程,在UNIX
系统这将运行系统调用fork,在win 系列系统创建的是伪进程(模拟子进程,生
存期依赖它的创建进程)。fork 出的子进程直接运行进跟其后面的块中的代码。
进程间的通信组要依赖与信号。Perl 支持POSIX 标准,定义了其中的大部分信
号,信号和信号句柄存在字典中,这和通常的信号句柄系统大不一样。关键字时
信号名字(比POSIX 有所简化,把SIG 省略了,如SIGINT 简化为INT),而相
关值是当收到信号时应执行的动作函数的引用,如$SIG{INT}=sub {print “got
SIGINT”;},表示该进程收到INT 信号时将打印出”got SIGINT”。另外一个进程
间交换信息的方式是通过管道,与shell 中控制类似。
Perl 中通过包Thread 支持多线程。创建线程时将让它执行的函数和参数传给
它,如:
use Thread;
$t=new Thread(\&subprog, args); #创建一个新线程执行subprog 子程序
Thread 包中有join 方法可以控制父线程等待子线程。线程的一些基本控制如获
得名称,用户,父线程号,进程号在Thread 模块里有专门的函数,在此不细说。
当线程间共享变量时可以通过条件变量或信号量(semaphore)来控制。通过
lock 函数给变量加锁,有三个独立的函数可以去锁:cond_wait、cond_signal 和
cond_broadcast 分别对应有线程在该变量上等待,发送信号,广播信号。信号量
能够用来指示一些线程事件,所提供的信息是以数字出现,并且这些数字能相应
的增加或减少,通常的方法是建立一个和可以共享的资源相关联的信号量。在
perl 中如下建立信号量控制:
$sema= new Thread::Semaphore(0); #建立信号量并赋初值0,假定它和$val
变量关联
$val=0;
sub sub1{
while(1){
if ($sema>0)
{
$sema->down;
$val++;
wait(5);
if ($val>100) { last;}
$sema->up;}
}
}
sub sub2{
while(1){
if ($sema>0)
{
$sema->down;
$val--;
wait(10);
if ($val<0) { last;} $sema->up;}
}
}
$t1=new Thread \&sub1; #$t1 线程将执行sub1
$t2=new Thread \&sub2; #$t2 线程将执行sub2
当然,因为一个信号可以终止脚本的运行,所以信号同样可以影响线程的运行,
要建立用于信号操作的线程,必须引入Thread::Signal 模块,接下来的操作大体
和信号句柄类似。
Python 通过os 模块来支持进程操作管理。通过fork 函数创建一个进程,wait
或waitpid 函数等待子进程,通过信号或管道进行进程间通信。例如:
import os
while 1: #wit for a connection
if accepted:
pid=os.fork()
if not pid:
#do the child process
else:
os.waitpid(pid,os.WNOHANG) #非挂起等待pid 子进程结束
在UNIX 或类似系统python 里的os.fork()函数只是操作系统的fork 的别名
(windows 系统暂时不支持fork 函数),fork 出来的子进程执行其后的if 语句的一
个分支(not pid 的分支)。通过os.kill 函数可以给子进程发送信号,而子进程要捕
获信号必须通过signal 中的signal 函数安装信号句柄。下例说明了如何让子进程
捕获SIGALRM 信号执行相应动作:
import os, signal, sys, time
def alarm_hdlr(signum, frame): #定义信号处理函数
print “wake up!”
sys.exit()
pid=os.fork() #创建子进程
if not pid:
signal.signal(signal.SIGALRM, alarm_hdlr) #安装信号句柄
while 1:
print “going to sleep…”
time.sleep(10)
print “now I’m up…”
else:
signal.alarm(5)
python 通过thread 模块来支持多线程,核心函数是start_new_thread(f,(args)),
这个函数启动一个新线程,并让它执行以参数(args)执行f 函数。例如:
import thread, time
def dsptime(interval, prefix=’’): #函数dsptime 每隔一段时间显示一次当前时间
while 1:
print prefix, time.ctime(time.time())
time.sleep(interval)
thread.start_new_thread(dsptime, (10,’now time is:’))
while 1:
pass
线程执行完函数子自动终止,或者可以调用sys.exit()强制其终止。子进程共享并
访问父进程所有的全局对象和环境,python 由多种控制线程访问这些对象的方
法。基本的方法是通过给对象加锁,例如:
import thread, time
cnt=0
cnt_lock=thread.allocate_lock() #定义了一个锁对象(只是一个协议),让它来控制cnt
变量的访问
def inccnt():
while 1:
print "thread 1:excuting ..."
if (cnt_lock.acquire(0)): #看是否能获能对锁变量的控制
global cnt
print "thread 1",thread.get_ident(),"acquired lock..."
cnt+=1
time.sleep(0.01)
cnt_lock.release()
else:
print "thread 1 can’t get lock ..."
time.sleep(1)
def dspcnt():
while 1:
if (cnt_lock.acquire(0)):
global cnt
print "thread 2",thread.get_ident(),"acquired lock..."
print "thread 2 print",cnt
time.sleep(0.01)
cnt_lock.release()
else:
print "thread 2 can’t get lock..."
time.sleep(1)
thread.start_new_thread(inccnt,())
thread.start_new_thread(dspcnt,())
while 1:
pass
输出将显示两个线程的执行情况和对锁的控制。特别注意的是(在perl 和python 中的其它线
程控制机制也有类似的问题),你可以完全不遵守锁对象的协议而直接访问到变量,那样将
无法保证对变量使用的正确性。Python 还可以通过条件变量,信号量,队列和事件等对象
来控制线程共享信息和通信。
以上的几个小节,主要从语言的数据类型、控制结构、作用域、正则表达式
处理、包和模块、面向对象、并发控制、与系统和环境交互等多个方面对perl
和python 进行了较粗略的比较分析,尽量突出两者之间的差别,并给以实例说
明。大部分实例都是自己设计,少部分来自参考书,但都尽量在计算机上试验过。
也对其中的一些特征和细节,如perl 的引用类型,python 的参数传递,进行了的
思考,这些想法可能不是很正确。Perl 和python 各有它们广大的用户群,所以
很难说孰优孰劣,而且因为自己对这两们语言的使用不多,有很多地方理解的不
够深入和准确,请老师指正。
参考文献:
1,Martin C Brown,perl 参考大全,顾凯等译
2,Martin C Brown,python 参考大全,康博译
3,ActivePython Documentation:Tutorial,Reference Manual 部分
4,Perl Tutorial,网友flamephoenix 译
5,A. Tucker, R. Noonan,Programming Languages: Principles and Paradigms
6,Andrew S. Tanenbaum,现代操作系统,陈向群等译
Comments