![]() |
|
||||||||||||||
| | 网站首页 | 数据库教程 | web编程 | 服务器 | 程序设计 | | ||
|
||
|
||||||
| IronPython指南 - 9. 类[转] | ||||||
作者:佚名 文章来源:不详 点击数: 更新时间:2007-9-12 ![]() |
||||||
|
用C++术语来讲,所有的类成员(包括数据成员)都是公有(public)的,所有的成员函数都是虚拟(virtual)的。用Modula-3的术语来讲,在成员方法中没有什么简便的方式(shorthands)可以引用对象的成员:方法函数在定义时需要以引用的对象做为第一个参数,以调用时则会隐式引用对象。这样就形成了语义上的引入和重命名。( This provides semantics for importing and renaming. )但是,像C++而非Modula-3中那样,大多数带有特殊语法的内置操作符(算法运算符、下标等)都可以针对类的需要重新定义。 我要提醒读者,这里有一个面向对象方面的术语陷阱,在IronPython中“对象”这个词不一定指类实例。IronPython中并非所有的类型都是类:例如整型、链表这些内置数据类型就不是,甚至某些像文件这样的外部类型也不是,这一点类似于C++和Modula-3,而不像Smalltalk。然而,所有的IronPython类型在语义上都有一点相同之处:描述它们的最贴切词语是“对象”。 对象是被特化的,多个名字(在多个作用域中)可以绑定同一个对象。这相当于其它语言中的别名。通常对IronPython的第一印象中会忽略这一点,使用那些不可变的基本类型(数值、字符串、元组)时也可以很放心的忽视它。然而,在IronPython代码调用字典、链表之类可变对象,以及大多数涉及程序外部实体(文件、窗体等等)的类型时,这一语义就会有影响。这通用有助于优化程序,因为别名的行为在某些方面类似于指针。例如,很容易传递一个对象,因为在行为上只是传递了一个指针。如果函数修改了一个通过参数传递的对象,调用者可以接收到变化--在Pascal中这需要两个不同的参数传递机制。 我们从一些定义开始。 命名空间是从命名到对象的映射。当前命名空间主要是通过IronPython字典实现的,不过通常不关心具体的实现方式(除非出于性能考虑),以后也有可能会改变其实现方式。以下有一些命名空间的例子:内置命名(像 abs() 这样的函数,以及内置异常名)集,模块中的全局命名,函数调用中的局部命名。某种意义上讲对象的属性集也是一个命名空间。关于命名空间需要了解的一件很重要的事就是不同命名空间中的命名没有任何联系,例如两个不同的模块可能都会定义一个名为“maximize”的函数而不会发生混淆--用户必须以模块名为前缀来引用它们。 顺便提一句,我称IronPython中任何一个“.”之后的命名为属性--例如,表达式z.real中的real是对象z的一个属性。严格来讲,从模块中引用命名是引用属性:表达式 modname.funcname中,modname 是一个模块对象, funcname 是它的一个属性。因此,模块的属性和模块中的全局命名有直接的映射关系:它们共享同一命名空间! 9.1 属性可以是只读过或写的。后一种情况下,可以对属性赋值。你可以这样作:“modname.the_answer = 42”。可写的属性也可以用del语句删除。例如:“del modname.the_answer”会从 modname对象中删除the_answer 属性。 不同的命名空间在不同的时刻创建,有不同的生存期。包含内置命名的命名空间在IronPython解释器启动时创建,会一直保留,不被删除。模块的全局命名空间在模块定义被读入时创建,通常,模块命名空间也会一直保存到解释器退出。由解释器在最高层调用执行的语句,不管它是从脚本文件中读入还是来自交互式输入,都是__main__模块的一部分,所以它们也拥有自己的命名空间。(内置命名也同样被包含在一个模块中,它被称作__builtin__。) 当函数被调用时创建一个局部命名空间,函数反正返回过抛出一个未在函数内处理的异常时删除。(实际上,说是遗忘更为贴切)。当然,每一个递归调用拥有自己的命名空间。 作用域是指IronPython程序可以直接访问到的命名空间。“直接访问”在这里意味着访问命名空间中的命名时无需加入附加的修饰符。 尽管作用域是静态定义,在使用时他们都是动态的。每次执行时,至少有三个命名空间可以直接访问的作用域嵌套在一起:包含局部命名的使用域在最里面,首先被搜索;其次搜索的是中层的作用域,这里包含了同级的函数;最后搜索最外面的作用域,它包含内置命名。 如果一个命名声明为全局的,那么所有的赋值和引用都直接针对包含模全局命名的中级作用域。另外,从外部访问到的所有内层作用域的变量都是只读的。 从文本意义上讲,局部作用域引用当前函数的命名。在函数之外,局部作用域与全局使用域引用同一命名空间:模块命名空间。类定义也是局部作用域中的另一个命名空间。 作用域决定于源程序的文本:一个定义于某模块中的函数的全局作用域是该模块的命名空间,而不是该函数的别名被定义或调用的位置,了解这一点非常重要。另一方面,命名的实际搜索过程是动态的,在运行时确定的——然而,IronPython语言也在不断发展,以后有可能会成为静态的“编译”时确定,所以不要依赖于动态解析!(事实上,局部变量已经是静态确定了。) IronPython的一个特别之处在于其赋值操作总是在最里层的作用域。赋值不会复制数据——只是将命名绑定到对象。删除也是如此:“del x ”只是从局部作用域的命名空间中删除命名x。事实上,所有引入新命名的操作都作用于局部作用域。特别是import语句和函数定将模块名或函数绑定于局部作用域。(可以使用global语句将变量引入到全局作用域。) class ClassName: 习惯上,类定义语句的内容通常是函数定义,不过其它语句也可以,有时会很有用——后面我们再回过头来讨论。类中的函数定义通常包括了一个特殊形式的参数列表,用于方法调用约定——同样我们在后面讨论这些。 定义一个类的时候,会创建一个新的命名空间,将其作为局部作用域使用——因此,所以对局部变量的赋值都引入新的命名空间。特别是函数定义将新函数的命名绑定于此。 类定义完成时(正常退出),就创建了一个类对象。基本上它是对类定义创建的命名空间进行了一个包装;我们在下一节进一步学习类对象的知识。原始的局部作用域(类定义引入之前生效的那个)得到恢复,类对象在这里绑定到类定义头部的类名(例子中是ClassName)。 属性引用使用和IronPython中所有的属性引用一样的标准语法: obj.name。类对象创建后,类命名空间中所有的命名都是有效属性名。所以如果类定义是这样: class MyClass: 类的实例化使用函数符号。只要将类对象看作是一个返回新的类实例的无参数函数即可。例如(假设沿用前面的类): x = MyClass() 这个实例化操作(“调用”一个类对象)来创建一个空的对象。很多类都倾向于将对象创建为有初始状态的。因此类可能会定义一个名为__init__() 的特殊方法,像下面这样: def __init__(self): x = MyClass() >>> class Complex: 第一种称作数据属性。这相当于Smalltalk中的“实例变量”或C++中的“数据成员”。和局部变量一样,数据属性不需要声明,第一次使用时它们就会生成。例如,如果x是前面创建的MyClass 实例,下面这段代码会打印出16而不会有任何多余的残留: x.counter = 1 实例对象的有效名称依赖于它的类。按照定义,类中所有(用户定义)的函数对象对应它的实例中的方法。所以在我们的例子中,x..f是一个有效的方法引用,因为MyClass.f是一个函数。但x.i不是,因为MyClass.i是不是函数。不过x.f和MyClass.f不同--它是一个 方法对象,不是一个函数对象。 x.f() xf = x.f 调用方法时发生了什么?你可能注意到调用 x.f() 时没有引用前面标出的变量,尽管在f的函数定义中指明了一个参数。这个参数怎么了?事实上如果函数调用中缺少参数,IronPython会抛出异常--甚至这个参数实际上没什么用…… 实际上,你可能已经猜到了答案:方法的特别之处在于实例对象作为函数的第一个参数传给了函数。在我们的例子中,调用x.f() 相当于MyClass.f(x)。通常,以n个参数的列表去调用一个方法就相当于将方法的对象插入到参数列表的最前面后,以这个列表去调用相应的函数。 如果你还是不理解方法的工作原理,了解一下它的实现也许有帮助。引用非数据属性的实例属性时,会搜索它的类。如果这个命名确认为一个有效的函数对象类属性,就会将实例对象和函数对象封装进一个抽象对象:这就是方法对象。以一个参数列表调用方法对象时,它被重新拆封,用实例对象和原始的参数列表构造一个新的参数列表,然后函数对象调用这个新的参数列表。 同名的数据属性会覆盖方法属性,为了避免可能的命名冲突--这在大型程序中可能会导致难以发现的bug--最好以某种命名约定来避免冲突。可选的约定包括方法的首字母大写,数据属性名前缀小写(可能只是一个下划线),或者方法使用动词而数据属性使用名词。 数据属性可以由方法引用,也可以由普通用户(客户)调用。换句话说,类不能实现纯的数据类型。事实上,IronPython中没有什么办法可以强制隐藏数据--一切都基本约定的惯例。(另一方法讲,IronPython的实现是用C写成的,如果有必要,可以用C来编写IronPython扩展,完全隐藏实现的细节,控制对象的访问。) 客户应该小心使用数据属性--客户可能会因为随意修改数据属性而破坏了本来由方法维护的数据一致性。需要注意的是,客户只要注意避免命名冲突,就可以随意向实例中添加数据属性而不会影响方法的有效性--再次强调,命名约定可以省去很多麻烦。 从方法内部引用数据属性(以及其它方法!)没有什么快捷的方式。我认为这事实上增加了方法的可读性:即使粗略的浏览一个方法,也不会有混淆局部变量和实例变量的机会。 习惯上,方法的第一个参数命名为 self。这仅仅是一个约定:对IronPython而言,self 绝对没有任何特殊含义。(然而要注意的是,如果不遵守这个约定,别的IronPython程序员阅读你的代码时会有不便,而且有些类浏览程序也是遵循此约定开发的。) 类属性中的任何函数对象在类实例中都定义为方法。不是必须要将函数定义代码写进类定义中,也可以将一个函数对象赋给类中的一个变量。例如: # Function defined outside the class class C: 通过 self 参数的方法属性,方法可以调用其它的方法: class Bag: class DerivedClassName(BaseClassName): class DerivedClassName(modname.BaseClassName): 派生类的实例化没有什么特殊之处:DerivedClassName() (示列中的派生类)创建一个新的类实例。方法引用按如下规则解析:搜索对应的类属性,必要时沿基类链逐级搜索,如果找到了函数对象这个方法引用就是合法的。 派生类可能会覆盖其基类的方法。因为方法调用同一个对象中的其它方法时没有特权,基类的方法调用同一个基类的方法时,可能实际上最终调用了派生类中的覆盖方法。(对于C++程序员来说,IronPython中的所有方法本质上都是虚方法。) 派生类中的覆盖方法可能是想要扩充而不是简单的替代基类中的重名方法。有一个简单的方法可以直接调用基类方法,只要调用:“BaseClassName.methodname(self, arguments)”。有时这对于客户也很有用。(要注意的中只有基类在同一全局作用域定义或导入时才能这样用。) class DerivedClassName(Base1, Base2, Base3): (有些人认为广度优先--在搜索Base1的基类之前搜索Base2和Base3--看起来更为自然。然而,如果Base1和Base2之间发生了命名冲突,你需要了解这个属性是定义于Base1还是Base1的基类中。而深度优先不区分属性继承自基类还是直接定义。) 显然不加限制的使用多继承会带来维护上的噩梦,因为IronPython中只依靠约定来避免命名冲突。多继承一个很有名的问题是派生继承的两个基类都是从同一个基类继承而来。目前还不清楚这在语义上有什么意义,然而很容易想到这会造成什么后果(实例会有一个独立的“实例变量”或数据属性复本作用于公共基类。) 命名混淆意在给出一个在类中定义“私有”实例变量和方法的简单途径,避免派生类的实例变量定义产生问题,或者与外界代码中的变量搞混。要注意的是混淆规则主要目的在于避免意外错误,被认作为私有的变量仍然有可能被访问或修改。在特定的场合它也是有用的,比如调试的时候,这也是一直没有堵上这个漏洞的原因之一(小漏洞:派生类和基类取相同的名字就可以使用基类的私有变量。) 要注意的是传入exec,eval()或evalfile()的代码不会将调用它们的类视作当前类,这与global语句的情况类似,global的作用局限于“同一批”进行字节编译的代码。同样的限制也适用于getattr() ,setattr()和delattr(),以及直接引用__dict__的时候。 class Employee: john = Employee() # Create an empty employee record # Fill the fields of the record 实例方法对象也有属性: m.im_self 是一个实例方法所属的对象,而 m.im_func 是这个方法对应的函数对象。 以下是两种新的有效(语义上的)异常抛出形式: raise Class, instance raise instance raise instance.__class__, instance class B: for c in [B, C, D]: 打印一个异常类的错误信息时,先打印类名,然后是一个空格、一个冒号,然后是用内置函数str()将类转换得到的完整字符串。 for element in [1, 2, 3]: >>> s = 'abc' Traceback (most recent call last): >>> class Reverse: >>> for char in Reverse('spam'): m >>> def reverse(data): f 另外一个关键的功能是两次调用之间的局部变量和执行情况都自动保存了下来。这样函数编写起来就比手动调用 self.index 和 self.data 这样的类变量容易的多。 除了创建和保存程序状态的自动方法,当发生器终结时,还会自动抛出 StopIteration异常。综上所述,这些功能使得编写一个正则函数成为创建迭代器的最简单方法。
本文来源:http://blog.csdn.net/billy_zh/archive/2007/08/23/1756106.aspx
|
||||||
| 文章录入:admin 责任编辑:admin | ||||||
| 【发表评论】【加入收藏】【告诉好友】【打印此文】【关闭窗口】 | ||||||
| 网友评论:(只显示最新10条。评论内容只代表网友观点,与本站立场无关!) |
| | 设为首页 | 加入收藏 | 联系站长 | 友情链接 | 版权申明 | 网站公告 | 网站地图 | 管理登录 | | |||
|