Python Tutorial(九):类

与其它编程语言相比,Python的类机制添加了最小的新语法和语义。它是C++和Modula-3中的类机制的混合。Python的类提供了面向对象编程的所有的标准特性,类继承机制允许有多个基类,一个子类可以重写基类中的任何方法,一个方法可以调用基类里面的同名方法。对象可以包含任意数量和种类的数据。就像模块那样,类参与Python的动态天性,在运行时被创建,创建后可以被进一步修改。

在C++术语中,通常类的成员(包括数据成员)是公有的,所有的成员函数都是虚的。在Modula-3中,从对象的方法里面引用对象的成员没有简写形式。方法函数被声明为拥有一个显式的第一个参数来表示这个对象,在调用时隐式提供。就像在Smalltalk中,类它们本身就是对象。这提供引入和重命名的语义。不像C++和Modula-3,内建类型可以被用户用作扩展的基类。像C++,许多内建的操作符都有特殊的语法,可以为类的实例重新定义。

9.1 名称和对象简介

同一个对象有个性的,和多个名称(在多个范围里)。在其它语言中称为别名。通常第一次看Python时可以不用管它。在处理不可变的基本类型时可以被安全的忽略。当牵扯到可变对象时,别名对Python代码的语义可能有一个惊奇的作用。这通常被用于程序的好处,因为别名的行为在某些方面像指针。例如,传递一个对象比较节省开销,因为只有一个指针被传递。如果一个函数修改了作为参数被传递的对象,调用方将会看到这个改变,这样就去除了两种不同参数传递机制的需要,像在Pascal中那样。

9.2 Python作用域和命名空间

在介绍类之前,我首先不得不告诉你一些有关Python作用域的规则。类定义对命名空间玩了一些整洁技巧,你需要知道作用域和命名空间如何工作,并且完全的理解正在发生什么。顺便地,有关本主题的知识对于高级Python程序员是有用的。

让我们以一些定义开始。

一个命名空间是一个从名称到对象的映射。许多命名空间当前被实现为Python的字典,但是那通常以任何方式来说都不是显而易见的,未来将会改变。命名空间的示例是:内建名称的集合;模块里的全局名称;函数调用里的局部名称。在某种意义上一个对象的属性的集合也形成一个命名空间。关于命名空间需要知道的重要事情是不同的命名空间里的名称之间绝对没有关系。例如,两个不同的模块可以都定义一个函数maximize,不会产生混淆,模块的用户必须使用模块名称作为前缀。

顺便提一下,任何跟在点后面的名称我都用属性称呼它,例如,在表达式z.real中,real是对象z的一个属性。严格的说,引用模块里的名称是属性引用,在表达式modname.funcname,modname是一个模块对象,funcname是它的一个属性。在这种情况下,恰好有一个直接的映射在模块的属性和定义在模块里的全局名称,它们共享同样的命名空间。

属性可以是只读的或可写的。在后一种情况,可以对一个属性赋值。模块属性是可写的,你可以写modname.the_answer = 42。可写属性也可以使用del语句进行删除。例如,del modname.the_answer将移除属性the_answer从名叫modname的对象。

命名空间在不同的时刻被创建,有不同的生命周期。包含内建名称的命名空间在Python解释器启动的时候被创建,并且不再删除。一个模块的全局命名空间在模块定义被读入时创建,通常,模块的命名空间也持续到解释器的退出。通过解释器的顶层调用执行的语句,要么从脚本文件读入或交互式的,被认为是一个叫做__main__模块的一部分,所以它们有它们自己的全局命名空间。(内建的名称实际上也在一个模块里,叫做builtins)

一个函数的局部命名空间在函数被调用时创建,在函数返回或引发一个在函数里没有被处理的异常时被删除。(实际上,忘记是一个较好的方式来描述实际发生了什么)当然,递归调用时,每一次调用都有它们自己的局部命名空间。

一个作用域是Python程序的一个正文区域,在那里一个命名空间被直接访问。这里的直接访问意味着一个对名称的未限定引用将尝试在命名空间里查找。

虽然作用域是静态决定的,但是却是动态使用的。在执行期间,至少有三个嵌套的作用域,它们的命名空间是直接被访问:

  • 最里层的作用域,首先被搜索,包含局部名称。
  • 任何的封闭函数作用域,从最近的封闭作用域开始搜索,包含非局部,但是也非全局的名称。
  • 倒数第二个作用域包含当前模块的全局名称。
  • 最外层的作用域,最后搜索,包含内建名称。

如果一个名称被声明为全局的,然后所有的引用和赋值直接到包含模块全局名称的中间作用域。要重新绑定在最内层作用域外发现的变量,nonlocal语句可以被使用,如果没有声明nonlocal,那些变量是只读的(一个尝试向这样一个变量写将简单的在最里层作用域创建一个新的局部变量,同名的外层范围变量没有改变)。

通常,局部作用域引用当前函数的局部名称。在函数外面,局部作用域引用和全局作用域相同的命名空间:模块的命名空间。类定义位于局部作用域里的另一个命名空间。

重要的是要认识到作用域是本文的决定的。定义在模块里的函数的全局作用域是那个模块的命名空间,无论是从什么地方或通过什么别名来调用它。换句话说,实际对名称的搜索是动态完成的,在运行时,然而,语言定义是朝着静态名称解决进化,在编译时,所以不要依赖动态名称解决(事实上,局部变量已经被静态的决定)

Python的一个特别的巧合是,如果没有global语句的影响,对一个名称的赋值总是进入到最里层的作用域。赋值并不拷贝数据,它们仅仅把名称绑定到对象。对于删除这也是真的:语句del x从局部作用域引用的命名空间里删除x的绑定。事实上,引入新名称的所有操作使用局部作用域:特别的,import语句和函数定义在局部作用域里绑定模块或函数名称。

global语句用来指示特殊的变量驻留在全局作用域,应该在那里被弹回。nonlocal语句指示特殊的变量驻留在一个封闭的作用域,并且应该在那里被弹回。

9.2.1 作用域和命名空间示例

这个示例演示如何引用不同的作用域和命名空间,global和nonlocal如何影响变量的绑定:

示例代码的输出是:

注意,局部赋值是如何没有改变spam的scope_test的绑定。nonlocal赋值改变了spam的scope_test的绑定,global赋值改变的是模块级别的绑定。

你可以看到在global赋值之前没有以前的对spam的绑定。

9.3 第一次看类

类引入了一小点新语法,三种新的对象类型,和一些新的语义。

9.3.1 类定义语法

最简单的类定义形式像这样:

类定义,像函数定义一样,在它们有任何作用前必须先被执行。(你可以把类定义放到一个if语句的分支里,或一个函数里面)

在实践中,一个类定义里面的语句通常是函数定义,其它语句也是允许的,并且有些时候比较有用。类里面的函数定义通常有一个特殊的参数列表形式。通过对方法的调用约定被支配。

当进入一个类定义时,一个新的命名空间被创建,并且用作局部作用域。因此,所有对局部变量的赋值都进入这个新的命名空间。特别的,函数定义把新函数的名称绑定到这里。

当正常离开一个类定义时,一个类对象被创建。这是一个基本的包装围在通过类定义创建的命名空间的内容的外围。下一节我们将学习更多有关类对象的内容。原始的局部作用域(进入类定义前,正在起作用的那个)被恢复,这个类的对象在这里绑定到类定义头部给出的那个名称上。

9.3.2 类对象

类对象支持两种类型的操作,属性引用和实例化。

在Python里,对于所有的属性引用都使用标准的语法:obj.name。合法的属性名称是在类对象被创建时类的命名空间里的所有的名称。所以,如果类的定义看起来像这样:

那么MyClass.i和MyClass.f是合法的属性引用,分别返回一个整数和一个函数对象。类属性也可以被赋值,所以你可以通过赋值来改变MyClass.i的值。__doc__也是一个合法的属性,返回类的文档字符串。

类实例化使用函数写法。假定类对象是一个没有参数的函数,并且返回一个类的新实例。例如:

创建类的一个新实例并把这个对象赋给局部变量x。

实例化操作创建一个空的对象。许多类喜欢创建使实例定制到一个特定的初始状态的对象。因此一个类可以定义一个特别的方法叫做__init__(),像这样:

当类定义了一个__init__()方法时,对于一个新创建的类实例,类实例化会自动的调用__init__()方法。所以在这个示例里,一个新的、初始化的实例可以这样获得:

当然,__init__()方法可以有参数,这样有更大的灵活性。在那种情况下,给类实例化操作符的参数被传递到__init__()方法。例如:

9.3.3 实例对象

那么现在我们可以对实例对象做些什么呢?惟一被实例对象理解的操作是属性引用。有两种合法的属性名称,数据属性和方法。

数据属性对应于Smalltalk中的实例变量,C++中的数据成员。数据属性不需要被声明,像局部变量一样,当第一次被赋值时它们突然就存在了。例如,如果x是MyClass的实例,下面的代码片段将打印出值16,没有留下一个踪迹:

另一种实例属性引用是方法。方法是属于对象的一个函数。(在Python里,术语方法对于类实例并不是惟一的,其它对象类型也有方法。)

一个实例对象合法的方法名取决于它的类。通过定义,一个类的所有是函数对象的属性定义了它的实例的相应方法。所以在我们的示例里,x.f是一个合法的方法引用,因为MyClass.f是一个函数,但是x.i不是,因为MyClass.i不是。但是x.f和MyClass.f并不是一回事,它是一个方法对象,不是一个函数对象。

9.3.4 方法对象

通常一个方法会被立即调用在它被界定以后:

在MyClass示例里,这将会返回字符串'hello world'。然而,立即调用一个方法是没有必要的。x.f是一个方法对象,可以被存储到其它地方,并且在以后的时间调用。例如:

将一直打印hello world,直到最后。

当一个方法被调用时究竟发生了什么?你应该注意到x.f()被调用而没有参数,即使f()的函数定义指定了一个参数。那么参数发生了什么呢?可以确定的是Python会引发一个异常当一个函数要求一个参数但在调用时没有,即使这个参数实际上并不使用。

事实上,你或许已经猜到了答案,关于方法的特别的事情是对象作为函数的第一个参数被传入。在我们的示例中,调用x.f()和MyClass.f(x)是相等的。一般来说,调用一个带有n个参数的方法等同于调用相应的函数,并把方法所属的对象插入到参数列表的第一个参数前面。

如果你仍然不理解方法如何工作,看一下实现也许会使事情变得清晰。当一个不是数据属性的实例属性被引用时,它的类被寻找。如果这个名称指示一个合法的类属性是一个函数对象,一个方法对象通过把这个实例对象和那个一起发现的函数对象打包进一个抽象对象的方式被创建,这就是这个方法对象。当这个方法对象使用一个参数列表被调用时,一个新的参数列表将从这个实例对象和这个参数列表被构造,这个函数对象将使用这个新的参数列表被调用。

9.4 随机备注

数据属性重写了相同名称的方法属性;为了避免意外的名称冲突,这将会引起比较难发现的问题在大程序里,一个聪明的做法是使用一些约定来把冲突的机会降到最小。可能的约定包括方法名称大写,数据属性名称加一个小的惟一字符串前缀(或许就是一个下划线),或方法使用动词,数据属性使用名词。

数据属性可以被方法和一个对象的普通用户引用。换句话说,类不是可用的对于实现纯抽象数据类型。事实上,在Python里面没什么东西能够强迫数据隐藏,它都是基于约定的。(从另一方面说,Python是用C实现的,可以完全的隐藏实现细节和控制对一个对象的访问,如果有必要的话;这一点可以通过C写的Python扩展来使用。)

客户端应该细心的使用数据属性,客户端或许搅乱不变的维护通过方法冲压它们的数据属性。注意,客户端可以添加它们自己的数据属性到一个实例对象上而不影响方法的合法性,和名称冲突被避免,一个命名约定在这里可以省去许多头疼的事情。

在一个方法里面没有简单的写法来引用数据属性(或其它方法)。我发现实际上这增加了方法的可读性,在翻阅方法时,不存在局部变量和实例变量冲突的机会。

通常,方法的第一个参数叫做self。这就是一个约定,self这个名字对于Python绝对没有特别的意义。注意,如果你的代码不跟随约定的话对于其它Python程序员来说可读性会降低。也可以想象,一个类浏览程序也需要依赖于这个约定来写。

任何函数对象是一个类属性,定义了这个类的实例的一个方法。函数定义被本文的封闭在类定义里面不是必须的。把一个函数对象赋给类的局部变量也是可以的。例如:

现在f,g和h都是类C的属性并且指向函数对象,所以它们都是C的实例的方法,h和g是相等的,这样的实践通常会是程序的读者困惑。

方法可以调用其它方法通过使用self参数的方法属性:

方法可以引用全局名称以像普通函数那样的方式。和一个方法关联的全局作用域是包含它定义的模块。(一个类从来不被用作一个全局作用域)一个人比较罕见的遇到了在一个方法里面使用全局数据的好理由,有许多全局作用域的合法使用。首先,导入到全局作用域里面的函数和模块可以被方法和定义在它里面的函数和类使用。通常,包含方法的类它自己定义在这个全局作用域里面,在下一节我们将发现一些好的理由,为什么一个方法想要引用它自己的类。

每一个值都是一个对象,因此有一个类(也叫做它的类型)它被存储为objct.__class__。

9.5 继承

当然,一个语言特性将不值得拥有名称类,如果它不支持继承。一个子类定义的语法看起来像这样:

名称BaseClassName必须定义在包含子类定义的作用域里。在基类名称的那个地方,其它的任意表达式也是允许的。这会比较有用,例如,当基类定义在其它模块里面:

子类定义的执行的进行和基类是一样的。当类对象被构建时,基类被记住。这用来解析属性引用,如果一个要求的属性在类里面没有发现,搜索将进行到基类里面。这个规则将递归的往下应用如果这个基类也继承了其它的类。

关于子类的实例化并没有什么特别之处,DerivedClassName()创建一个新的类实例。方法引用按下面方式被解析,相应的类属性被搜索,如果必要的话沿着基类的链往下进行,如果这能返回一个函数对象,方法引用就是合法的。

子类可以重写基类的方法。因为方法没有特权当调用同一个对象的其它方法时,一个基类的方法调用同一个基类的另一个方法将结束调用一个重写它的子类的一个方法。(对于C++程序员,所有Python里面的方法实际上都是虚的)

一个子类里面的重写方法事实上是想扩展而不是简单的替换基类里面的同名方法。有一个简单的方式可以直接调用基类里面的方法,就调用BaseClassName.methodname(self, arguments)。这偶尔对客户也非常有用。(注意,在这个全局作用域里面,如果基类作为BaseClassName是可访问的,上面的方法才可以工作)

Python有两个内建函数和继承一起使用:

  • 使用isinstance()来检测一个实例的类型,isinstance(obj, int)将返回True当且仅当obj.__class__是int或某个类继承自int。
  • 使用issubclass()来检测类继承,issubclass(bool, int)是True,因为bool是int的一个子类。然而,issubclass(float, int)是False,因为float不是int的一个子类。

9.5.1 多重继承

Python也支持多重继承的形式。一个带有多个基类的类定义看起来像这样:

基于多种目的,在最简单的情况下,你可以认为搜索一个从父类继承过来的属性是深度优先,从左到右,不会在一个类里面搜索两次当它处于继承层次的重叠处时。因此,如果一个属性在DerivedClassName里面没有发现,然后搜索Base1,然后递归的搜索Base1的所有基类,如果在那里没有发现,然后搜索Base2,等等。

事实上,比上面稍微复杂些,方法的解析顺序动态的改变来支持协作的调用super()。这种方式也出现在其它一些多继承语言中,比单继承里面的super调用更强大。

动态排序是必要的,因为多重继承的所有情况都展示出一个或多个菱形关系(那里至少有一个父类可以在最底层的类里面通过多个路径访问到。)例如,所有的类都继承自object,所以多继承的任何情况都提供多于一条的路径到达object。为了保持基类被访问不多于一次,动态算法以一个方式线性化搜索顺序,保持在每一个类里面指定的从左到右的顺序,那就调用每一个父类仅一次,那就是单调的(意味着一个类可以被子类化而不影响它父类的优先权顺序)。综合起来,这些属性使采用多重继承来设计可靠的和可扩展的类变成可能。

9.6 私有变量

除了在一个对象里面否则不能被访问的私有实例变量在Python里是不存在的。然而,有一个被多数Python代码遵循的约定,一个以下划线为前缀的名称应该被认为是API的非公共部分(它是否是一个函数,一个方法或一个数据成员)。它应该被认为是一个详细实现,并且服从改变而不用通知。

因为有一个合法的类私有成员用例(即避免子类定义的名称造成的命名冲突),对于这个机制有一个有限的支持,叫做名称矫正。任何__spam这个形式的标识符(至少两个前导下划线,最多一个尾部下划线)被本文的替换为_classname__spam,这里的classname是当前的类的名称,并去掉前导的下划线。这个矫正被完成和标识符的语法位置无关,只要它发生在一个类的定义内部。

名称矫正是有用的,它让子类重写方法而不打破类内部方法调用。例如:

注意,矫正规则被设计大多数是为了避免事故,它也可以用来访问或修改一个被认为是私有的变量。在特别的情况下这个甚至有用,就像调试。

注意,传递给exec()或eval()的代码不认为正在调用的类的名称是当前的类,这和global语句的作用是相似的,它的作用对于字节编译在一起的代码是同样的限制的。同样的限制应用于getattr(),setattr()和delattr(),和当直接引用__dict__时。

9.7 零碎的

有时,有一个像Pascal的记录或C的结构是非常有用的,把少数的命名数据项打包在一起。一个空的类定义就可以很好的完成:

一段Python代码希望一个特别的抽象数据类型,经常被传入一个类来模拟那个数据类型的方法所取代。例如,如果你有一个函数格式化一些来自文件对象的数据,你可以定义一个类,有read()和readline()方法从一个字符串缓冲区获得数据,并且作为参数传递个它。

实例方法对象也有属性,m.__self__是拥有方法m()的实例对象,m.__func__是和方法对应的函数对象。

9.8 异常也是类

用户定义的异常也是通过类来标识的。使用这个机制可以创建异常的可扩展层次。

有两个新的合法的语义形式对于raise语句:

第一种形式,Class必须是type或它的子类的实例。它是下面的一个简写:

一个except从句中的类是和一个异常可匹配的,如果它和异常是同一个类或是异常的一个基类(其它方式不行,一个except从句列出一个子类,和一个基类是不匹配的)。例如,下面的代码将按照那样的顺序打印B,C,D:

如果except从句反转,把except B放到第一,将会打印出BBB,第一个匹配except从句被触发。

当一个未处理的异常的错误信息被打印出来时,异常的类名称被打印,然后一个冒号和一个空格,最后是实例的字符串表示形式,使用内建的str()函数进行转化。

9.9 迭代器

到现在你可能已经注意到许多容器对象可以使用for语句在它上面进行迭代:

访问的样式清晰,简洁,方便。迭代器的使用遍及和统一Python。在这个场景后面,for语句在容器对象上调用iter()。函数返回一个迭代器对象,它定义了方法__next__(),它每次访问一个容器中的元素。当没有更多元素时,它就引发一个StopIteration异常告诉for循环来终止。你可以使用内建的next()函数调用__next__()方法。下面的示例演示它如何工作:

已经看到了迭代器协议背后的结构,可以很容易的给你的类加上迭代器行为。定义一个__iter__()方法,并返回一个包含__next__()方法对象。如果类定义__next__(),然后__iter__()可以仅仅返回它自己:

9.10 生成器

生成器是一个简单和强大的工具来创建迭代器。它们写起来就像正常的函数,但是任何时候它们想返回数据的时候使用yield语句。每一次在它上面调用next()时,生成器在它离开的地方重新开始(它能记住所有的数据值和最后一次执行的语句)。一个示例演示生成器可以很简单的被创建:

任何能够用生成器完成的事情也能够用基于迭代器的类来完成。使生成器如此兼容的是__iter__()和__next__()方法被自动创建。

另一个关键的特征是局部变量和执行状态在每次调用之间被自动的保存。这使得函数更容易书写和比使用像self.index和self.data这样的实例变量的方式更加清晰。

除了自动方法创建和保存程序状态,当生成器终止时,它们自动的引发StopIteration异常。总之,这些特性使创建一个迭代器很容易,并不比写一个正常的函数多付出努力。

9.11 生成器表达式

一些简单的生成器可以被简洁的写为表达式,使用一个和列表综合相似的语法,但是要用小括号代替大括号。这些表达式被设计为在那里生成器被一个封闭的函数立马使用的情况。生成器表达式比完整的生成器定义更加紧凑但功能较少和比相等的列表综合趋向于更友好的内存使用。

示例:

本文是对官方网站内容的翻译,原文地址:http://docs.python.org/3/tutorial/classes.html

posted on 2013-02-25 21:21  编程新说(李新杰)  阅读(7269)  评论(2编辑  收藏  举报