面向对象编程Python的类(超详细,绝佳实例)

面向对象编程Python中的类(超详细,绝佳实例)

  • 1.类
    • 1.1类定义
    • 1.2类定义进阶
    • 1.3ADT描述形式的类
  • 2.类的定义与使用
    • 2.1类的基本定义与使用
    • 2.2实例对象:初始化和使用
    • 2.3 几点说明
    • 2.4 继承
  • 3.异常
    • 3.1异常类和自定义异常
    • 3.2 异常的传播和捕捉
    • 3.3 内置的标准异常类
  • 4.类定义实例:学校认识管理系统中的类
    • 4.1问题分析和设计
    • 4.2人事记录类的实现

1.类

利用class定义(类定义)实现抽象数据类型。Python中基于class的编程技术,称为面向对象技术。

1.1类定义

类(class)定义机制用于定义程序里需要的类型,定义好的一个类就像一个系统内部类型,可以产生该类型的对象(也称为该类的实例),实例对象具有这个类的描述行为。实际上,Python语言把内置类型都看作类。
示例:

class Rationl:
	def __init__(self,num,den=1):
		self.num = num
		self.den = den
	
	def plus(self,another):
		den = self.den*another.den
		num = (self.num*another.den+self.den*another.num)
		return Rational(num,den)
	
	def print(self):
		print(str(self.num)+"/"+str(self.den))
  1. class是关键字,表示由这里开始一个类定义。class之后是给定的类名和一个表示类头部结束的冒号。这部分称为类定义的头部,随后是类定义的体部分,形式上就是一个语句组。定义一个类,通常是为了创建该类的实例,称为该类的实例对象,简称这个类的对象。
  2. 类的体部分通常主要是一批函数定义,所定义的函数称为这个类的方法。最常见的方法是操作本类的实例对象的方法,称为实例方法。这种方法总是从本类的对象出发去调用,其参数表里的第一个参数就表示实际使用时的调用对象,通常以self作为参数名。
  3. 在一个类里,通常会定义一个名为__init__的方法(其名字时在init的前后加两个下划线符号),称为初始化方法,其工作是构造本类的新对象。创建实例对象采用函数调用的描述形式,以类名作为函数名,这时系统将建立一个该类的新对象,并自动对这个对象执行__init__方法。例如:r1 = Rational(3,5)就是要求创建一个值为3/5的有理数对象,并把这个新对象赋给变量r1。调用时应给除self之外的其他实际参数。Rational类的__init__方法要求两个实参,上面的语句中3和5。求值表达时Python系统先建立一个新对象,然后把这个对象作为__init__方法的self参数去执行方法体。在Rational类的__init__方法里有两个语句,要求用实参的值给self.num和self.den赋值。在实例方法的体中,self.frame形式的写法表示本类实例对象的属性,其中frame称为属性名。与Python变量的情况类似,程序里不需要说明对象有哪些属性,赋值时就会创建。上面初始化方法要求给Rational对象的两个属性赋值,创建本类对象时就会为它建立相应的属性并赋值以相应的值。
  4. 类里的其它实例方法也应该以self作为第一个参数。
  5. 上面类定义里的print方法只有一个参数self,其调用形式应该是r1.print(),不要求其他实参。

1.2类定义进阶

如前所述,类定义的一类重要作用是支持创建抽象的数据类型。在创建这种抽象时,人们不希望暴露其实现的内部细节。例如,对于有理数类,不希望暴露这种对象内部是用两个整数分别表示分子和分母。对更复杂的抽象,信息隐藏的意义可能更重要。由于隐藏抽象的内部信息在软件邻域意义重大,有些编程语言提供专门的机制。Python语言没有专门服务于这种需求的机制,只能依靠一些编程约定。

首先,人们约定,在一个类的定义里,由下划线_开头的属性名(和函数名)都当作内部使用的名字,不应该在类之外使用。另外,Python对类定义里以两个下划线开头(但不可以以两个下划线结尾)的名字做了特殊处理,使得在类定义之外不能直接用这个名字访问。这是另一种保护方式。下面定义更好的有理数类时将遵循这些约定。

在创建有理数时,应该考虑约去其分子和分母的最大公约数,避免无意义的资源浪费。为了完成简化,需要定义一个求最大公约数的函数cgd。这里出现一个问题:应该在那里定义这个函数。稍加分析就会发现,现在出现两种情况:首先,gcd的参数应该是两个整数,他们不属于被定义的有理数类型。此外gcd的计算并不依赖任何有理数类的对象,因此其参数表中似乎不应该以表示有理数的self作为第一个参数。但另一方面,这个gcd是为有理数类的实现而需要使用的一种辅助功能,根据信息局部化的原则,局部使用的功能不应该定义为全局函数。综合这两点情况,gcd应该是在有理数类里定义的一个非实例方法。

Python把在类里定义的这种方法称为静态方法(与实例方法不同),描述时需要在函数定义的头部之前加修饰符@staticmethod。静态方法的参数表中不应该有self参数,在其他地方没有限制。对于静态方法,可以从其定义所在类的名字出发通过圆点形式调用,也可以从该类的对象出发用过圆点形式调用。本质上说,静态方法就是在类里面定义的普通函数,但也是该类的局部函数。

还有一个问题:前面简单有理数类的初始化方法没有检查参数,既没有检查参数的类型是否合适,也没有检查分母是否为0。此外,人们传送给初始化方法的实参可能有正负。这些检查和变换都应该在有理数类的初始化方法里完成,保证构造出的有理数都是合法合规的对象。

考虑了上面这些问题后,可以给出有理数类定义(部分):

class Rational:
    @staticmethod
    def _gcd(m,n):
        if n == 0:
            m,n = n,m
        while m != 0:
            m,n = n%m,m
        return n

    def __init__(self,num,den=1):
        if not isinstance(num,int) or not isinstance(den,int):
            raise TypeError
        if den == 0:
            raise ZeroDivisionError
        sign = 1
        if num < 0:
            num,sign = -num,-sign
        if den <0:
            den,sign = -den,-sign
        g = Rational._gcd(num,den)
        self._num = sign*(num//g)
        self._den = den//g

下面考虑Rational类的其他方法。首先,在上面定义中把有理数对象的两个属性当作内部属性,不应该在类外去引用它们。但实际计算中有时候需要提取有理数的分子和分母。为了满足这种需要,应该定义一对解析操作(也就是实例方法):

    def num(self):
        return self._num
    def den(self):
        return self._den

现在考虑有理数的运算。在前面的简单有理数类里定义了名字plus的方法。对于有理数这种数学类型,人们可能更希望使用运算符(+、-、*、/等)描述计算过程,写出形式更自然的计算表达式。Python语言支持这种想法,它为所有算术运算符规定了特殊方法名。Python中所有特殊的名字都以两个下划线开始,并以两个下划线结束。例如:+运算符对应名字__add__,*运算符对应__mul__。下面是实现有理数运算的几个方法定义,其他运算不难类似的实现:

    def __add__(self, other):   # mimic + operator
        den = self._den*other.den()
        num = (self._num*other.den()+self._den*other.num())
        return Rational(num,den)

    def __mul__(self, other):  # mimic * operator
        return Rational(self._num*other.num(),self._den*other.den())

    def __floordiv__(self, other):   # mimic // operator
        if other.num() == 0:
            raise ZeroDivisionError
        return Rational(self._num*other.den(),self._den*other.num())
    
    # ...
    # 其他运算符可以类似定义:
    # -:__sub__,/:__truediv__,%:__mod__,etc.

这里有几个问题值得提出。首先,通过在每个方法最后用Rational(…,…)构造新对象,所有构造出的对象都保证能化为最简单形式,不需要再每个建立新有理数的地方化简问题。这种做法值得提倡。

另外,上面定义除法时用的是整除运算符"//"。在除法方法的开始检查除数并可能抛出异常,也是常规的做法。按照Python的惯例,普通除法"/"的结果应是浮点数,对应的方法是__truediv__,如果需要可以另行定义,实现从有理数到浮点数的转换。

还有请注意:算术运算都要求另一个参数也是有理数对象。如果希望检查这个条件,可以在方法定义的开始加一个条件语句,用内置谓词isinstance(other,Rational)检查。另外other是另一个有理数对象,上面方法定义中没有直接去访问其成分,儿是通过解析函数。

有理数对象经常需要比较相等和不等,有些类的对象需要比较大小。Python为各种关系运算提供了特殊方法名。下面是有理数相等、小于运算的方法定义:

    def __eq__(self, other):
        return self._num*other.den() == self._den*other.num()
    
    def __lt__(self, other):
        return self._num*other.den() < self._den*other.num()
    
    # 其他比较运算符可以类似定义:
    # !=:__ne__,<=:__le__,>:__gt__,>=:__ge__

不等、小于、大于等运算可以类似地实现。

为了方便输出等目的,人们经常在类里定义一个把该类的对象转换到字符串的方法。为了保证系统的str类型转换函数能正确使用,这个方法应该采用特殊名字__str__,内置函数str将调用它。下面是有理数类的字符串转换方法:

    def __str__(self):
        return str(self._num) + "/" +str(self._den)
    
    def print(self):
        print (self._num + '/' + self._den)

至此一个简单的有理数类就基本完成了。总而言之,从使用的各方面看,用类机制定义的类型与Python系统内部类型没什么擦别,地位和用法相同。Python标准库的一些类型就是这样定义的。

1.3ADT描述形式的类

之后将主要采用Python的面向对象技术和类结构定义各种数据类型,为了更好地与之对应,这里对ADT的描述形式做一点改动。后面使用的ADT描述将模仿Python类定义的形式,也认为ADT描述的是一个类型,因此:

  • ADT的基本创建函数将以self为第一个参数,表示被创建的对象,其他参数表示为创建对象时需要提供的其他信息。
  • 在ADT描述中每个操作也都以self作为第一个参数,表示被操作对象。
  • 定义二元运算时也采用同样的形式,其参数表将包括self和另一个同类型对象,操作返回的时运算生成的结果对象。
  • 虽然Python函数定义的参数表里没有描述参数类型的机制,但为了提供更多信息,在下面写ADT定义时,有时还是采用写参数类型的形式,用于说明操作对具体参数的类按这种方式描述的有理数对象ADT如下:
    面向对象编程Python的类(超详细,绝佳实例)_第1张图片

2.类的定义与使用

2.1类的基本定义与使用

类定义:
类定义的基本语法是:

class <类名>:
	<语句组>

一个类定义由关键词class开始,随后是用户给定的类名,一个冒号,以及一个语句组。这个语句组称为类(定义)的体部分。

与函数定义类似,类定义也是Python的一种语句。类定义的执行效果(语义)就是建立起这个定义描述的类。在Python里建立的类也是一种对象,表示一个数据类型。类对象的主要作用就是可以创建这个类的实例(称为该类的实例对象)。

一个类定义确定了一个命名空间,位于类体里面的定义都局部于这个类体,这些局部名字在该类之外不能直接看到,不会与外面的名字冲突。在执行一个类定义时,将该定义为作用域创建一个新的名字空间。类定义里的所有语句(包括方法定义)都在这个局部名字空间里产生效果。这样创建的类名字空间将一直存在,除非明确删除(用del)。当一个类定义的执行完成时,Python解释器创建相应的类对象,其中包装了该类里的所有定义,然后转会到原来的(也就是该类定义所在)名字空间,在其中建立这个新的类对象与类名字的约束。在此之后通过类名字就能引用相应的类对象了。

在很多类定义的体里只有一组def语句,为这个类定义一组局部函数。实际上,完全可以在这里写其他语句。类里定义的变量和函数等称为这个类的属性。这里的函数定义常采用一种特殊形式,下面将详细介绍具有这种特殊形式的函数与“方法”之间的关系。

类对象及其使用:
执行一个类定义将创建起一个类对象,这种对象主要支持两种操作:属性访问和实例化(即创建这个类的实例化对象)。

在Python语言中,所有属性引用(属性访问)都采用圆点记法。如math.sin()等。此外,每个类对象都有一个默认存在的__doc__数据属性,其值时该类的文档串。

定义好一个类后,可以通过实例化操作创建该类的实例对象。如:

x = className()

上面语句将创建className类的一个新实例(实例对象),并把该对象赋给变量x。

2.2实例对象:初始化和使用

实例对象的初始化:
创建一个类的实例对象时,人们通常希望对它做一些属性设置,保证建立的对象状态完好,具有所需要的性质,也就是说,希望在创建类的实例对象时自动完成适当的初始化。Python类中具有特殊名字__init__的方法自动完成初始化工作:

  • 如果在一个类里定义了__init__方法,在创建这个类的实例时,Python解释器就会自动调用这个方法。
  • __init__方法的第一个参数(通常用self作为参数名)总表示当前郑州创建的对象。方法体中可以通过属性赋值的方式(形式为self.fname的赋值,fname是自己选定的属性名)为该对象定义属性并设定初始值。
  • init__可以有更多形式参数。如果存在这种参数,在创建该类的实例对象时,就需要为(除第一个self之外的)形式参数提供实际参数值,用表达式写在类名后面的实参表里。在实例化对象时,这些实际参数将传给__init,使它可以基于这些参数对实例化对象做特定的初始化。

类实例(对象)的数据属性:
对于已有的类实例对象,可以通过属性引用的方式访问其数据属性。

一个实例对象是一个独立的数据体,可以像其他对象一样赋值给变量作为约束值,或者传进函数处理,或者作为函数的结果返回等。实例对象也可以作为其他实例对象的属性值(无论是同属一个类的实例对象,或不属于同一个类的实例对象),这种情况形成更复杂的对象结构。在复杂程序里,这种情况很常见。

方法的定义和使用:
除数据属性外,类实例的另一类属性就是方法。

在一个类定义里按默认方式定义的函数,都可以作为这个类的实例对象的方法。但是,如果确实希望类里的一个函数作为该类实例的方法使用,这个函数至少需要一个表示其调用对象的形参,放在函数定义的参数表里的第一个位置。这个形参通常起名self(实际上可以使用任何名字,用self作为参数是Python社区的习惯做法)。除了self之外,还可以根据需要为函数引入更多形参。下面将称类里的这种函数为(实例)方法函数。除了是在类里定义而且至少一个形参外,方法函数并没有别的特殊之处。

如何使用?如果类里定义了一个方法函数,这个类的实例对象就可以通过属性引用的方式调用这个函数。如x.method(…)。

2.3 几点说明

对于类定义、方法定义等机制,有下面几点说明:

  • 在执行了一个类定义,从而创建了相应的类对象之后,还可以通过属性赋值的方式为这个类(对象)增加新属性,还可以增加函数属性。但是要注意,如果新属性与已有属性同名,就会覆盖同名属性。人们一般采用特殊的命名规则避免这种错误。
  • 如果需要在一个方法函数里调用同一个类里的其他方法函数,就需要明确地通过函数的第一个参数(self),以属性描述的方式写方法调用。
  • 从其他方面看,方法函数也就是定义在类里面的函数。其中也可以访问全局名字空间里的变量和函数,必要时也可以写global或nonlocal声明。
  • Python提供了内置函数isinstance,专门用于检查类和对象的关系。表达式isinstance(obj,cls)检查对象obj是否为类cls的实例,是返回True,反之返回False。

静态方法和静态类:

除了实例方法外,类里还可以定义另外两类函数:

  • 第一类是静态方法,定义形式是在def行前加修饰符@staticmethod。静态方法实际上就是普通函数,只是由于某种原因需要定义在类里面。静态方法的参数可以根据需要定义,不需要特殊的self参数。可以通过类名或者是实例对象的变量,以属性应用的方式调用静态方法。注意静态方法中没有self参数。
  • 类里定义的另一种方法称为类方法,定义形式是在def行前加修饰符@classmethod。这种方法必须有一个表示其调用类的参数,习惯用cls作为参数名,还可以有任意多个其他参数。类方法也是类对象的属性,可以以属性访问的形式调用。在类方法执行时,调用它的类将自动约束到方法的cls参数,可以通过这个参数访问该类的的其他属性。认同通常用类方法实现与本类的所有对象有关的操作。

这里举个例子。假设所定义的累需要维护一个计数器,记录程序运行中创建的该类的实例对象的个数。可以采用下面的定义:

class Countable:
    counter = 0

    def __init__(self):
        Countable.counter += 1

    @classmethod
    def get_count(cls):
        return Countable.counter

a = Countable()
b = Countable()
c = Countable()
print (Countable.counter)

为了记录本类创建的对象个数,Countable类里定义了一个数据属性counter,起初设置为0。每次创建这个类的对象时,初始化方法__init__就会把这个对象计数器加一。类方法get_count访问了这个数据的属性。

类定义的作用域规则:
类定义作为Python语言里的一种重要定义结构,也是一种作用域单位。在类里定义的名字(标识符)具有局部作用域,只在这个类里可用。如果需要在类定义之外使用,就采用基于类名字的属性引用方式。例如:

class C:
	a = 0
	b = a+1
x = C.b

对于函数定义,其中局部名字的作用域自动延伸到内部嵌套的作用域。正因为这样,如果在一个函数f里定义局部函数g,在g的函数体里可以直接使用f里有定义的变量,或使用在f里定义其他局部函数,除非这个名字在g里另有定义。

对于类定义,情况则不是这样。在类C里定义的名字(C的数据数据属性或函数属性名),其作用域并不自动延伸到C内部嵌套的作用域。因此,如果需要在类中的函数定义里引用这个类的属性,一定要采用基于类名的属性引用方式。

私有变量:
在面向对象的程序设计领域,人们通常把类实例对象里的数据属性称作实例变量。因为它们就像是定义在实例对象的名字空间的变量。

在一些面向对象语言里,允许把一些实例变量定义为私有变量,只允许在类定义的内部访问他们(也就是说,只允许在实例对象的方法函数里访问),不允许在类定义之外使用。实际上,在类之外根本就看不到这种变量,这是一种信息隐藏机制。Python语言里没有为定义私有变量提供专门机制,没有办法说明某个属性只能在类的内部访问,只能通过编程约定和良好的编程习惯来保护实例对象里的的数据属性。

在Python实践编程中,习惯约定是把以一个下划线开头的名字作为实例对象内部的东西,永远不从对象的外部去访问它们。无论这样的名字指称的是(类或类实例的)数据成员、方法,还是类里定义的其他函数。也就是说,在编程中永远把具有这种名字的属性看作类的实现细节。

另外,具有__add__形式(前后各有两个下划线)的名字有特殊的意义,除了前没介绍的表示各种算术运算符、比较运算符的特殊名字还有__init__、__str__之外,还有一大批特殊名字。

实际上,Python编程中,上述约定不仅仅针对类及其实例对象,也适用于模块等一切具有内部结构的对象。

2.4 继承

基于类和对象的程序设计被称为面向对象的程序设计,在这里的基本工作包括三个方面:定义程序里需要的类(也就是定义新类);创建这些类的(实例)对象;调用对象的方法完成计算工作,包括完成对象之间的信息交换等。

在Python语言里做面向对象的程序设计,首先要根据程序的需求定义一组必要的类。前面已经介绍了类定义的基本机制,下面介绍另一种重要机制——继承。继承的主要作用有两个:是可以基于已有的类定义新类,通过继承的方式服用已有类的功能,重复利用已有的代码(已有的类定义),减少定义新类的工作量,简化新功能的开发,提高工作效率。是建立一组类(类型)之间的继承关系,利用这种关系有可能更好地组织和构造复杂的程序。

继承、基类和派生类:
在定义一个新的类时,可以列出一个或几个已有的类作为被继承的类,这样就建立了这样新定义类域指定的已有类之间的继承关系。通过继承定义除的新类称为所列已有类的派生类(或称子类),被继承的已有类则称为这个派生类的基类(或父类)。派生类将继承基类的所有功能,可以有原封不动地使用基类中已定义的动能,也可以根据需要修改其中的一些功能(也就是说,重新定义其基类已有的某些函数属性)。另一方面,派生类可以根据需要扩充新功能(定义新的数据、函数属性)。

在概念上,人们把派生类(子类)看作基类(父类)的特殊情况,它们的实例对象集合具有一种包含关系。假设类C是类B的派生类,C类的对象也看作C的基类B的对象。人们经常希望在要求一个类B的实例对象的上下文中可以使用派生类C的实例对象。这是面向对象编程中最重要的一条规则,称为替换原理。许多重要的面向对象编程技术都需要利用类之间的继承关系,也就是利用替换原理。

一个类可能是其他类的派生类,他又可以被看作基类去定义新的派生类。Python有一个最基本的内置类object,其中定义了一些所有的类都需要的功能。如果一个类定义没说明基类,该类就自动继承object作为基类。

基于已有类BaseClass定义派生类的语法形式是:

class <类名>(BaseClass,...):
	<语句组>

列在类名后面括号里的“参数”就是指定的基类,可以有多个,它们都必须在这个派生类定义所在的名字空间里有定义。Python允许用更复杂的表达式描述所需要的基类,只要这个表达式的值确实是个类对象。例如,可以用import语句导入一个模块之后,利用该模块里有定义的类作为基类,定义自己的派生类。
Python内置函数issubclass检查两个类事是否具有继承关系,包括直接的或间接的继承关系。如果cls2是cls1的直接或者间接的基类,表达式issubclass(cls1,cls2)将返回True,反之返回False。实际上,Python的一些基本类之间也有子类(子类型)关系。

下面举一个简单的例子,定义一个自己的字符串类:

class MyStr(str):
	pass

这个类继承了str类的所有功能,没有任何修改或扩充。但它是另一个新类,是str的一个派生类。有了这个定义,我们就可以检查MyStr类和str的关系,issubclass(MyStr,str)。

派生类需要重新定义__init__函数,完成该类实例的初始化。常见情况是要求派生类的对象可以作为基类的对象,用在要求基类对象的环境中。在使用这种对象时,可以调用派生类自己定义的方法,也可以调用基类继承的方法。因此,这种派生类的实例对象里就应该包含基类实例的所有数据属性,在创建派生类的对象时,就需要对基类对象的所有数据数进行进行初始化。完成这一工作的常见方式是直接调用基类的__init__方法,利用它为正创建的实例里那些在基类实例中也有的数据属性设置初值。也就是说,派生类__init__方法定义的常见形式:

class DerivedClass(BaseClass):
    def __init__(self,...):
        BaseClass.__init__(self,...)
        ...... # 初始化函数的其他操作

这里继承BaseClass类定义派生的DerivedClass类。在调用基类的初始化方法时,必须明确写出基类的名字,不能从self出发调用。在调用基类的__init__时,必须把本对象的self作为调用的第一个实参,可能还需要传入另一些实参。这个调用完成派生类实例中属于基类的那部分属性的初始化工作。

在派生类里覆盖基类中已定义的函数时,也经常希望新函数是基类同名函数的某种扩充,也就是希望新函数包含覆盖函数的已有功能。这种情况与__init__的情况类似,处理方法也类似:在新函数定义里,可以用BaseClass.methodName(…)的形式调用基类方法。实际上,可以用这种形式调用基类的任何函数(无论该函数是不是被派生类覆盖,是不是正在定义的这个函数)。同样需要注意,在这种调用中,通常需要把表示本对象的self作为函数调用的第一个实参。

方法查找:
如果从一个派生类的实例对象出发调用方法,Python解释器需要确定应该调用哪个函数。查找过程是从实例对象所属的类开始,如果在这里找到了,即采用,否则去基类里查找。这个过程沿着继承关系进行,在某个类里找到就使用它。如果最终没有找到,那就是属性无定义,Python解释器将报告AttributeError异常。

如前所述,定义派生类时可以覆盖基类里已有的函数定义(也就是重新定义一个同名函数)。按照上述查找过程,以但某函数在派生类里重新定义,在其实例对象的方法调用中,就不再使用基类的原定义方法。

假设在某个实例对象调用的一个方法f里调用了另一个方法g,而且后一方法也是基于这个实例对象调用的(通过self.g(…))。在这种情况下,查找方法g的过程就只与这个实例对象(的类型)有关,与前一方法f时在那个类里定义的情况无关。

考虑一个实例。假定B是C的基类,两个类的定义分别是:

# code showing dynamic binding
class B:
    def f(self):
        self.g()
    def g(self):
        pirnt ('B.g called')

class C(B):
    def g(self):
        print ('C.g called')

如果在创建B类的实例对象x之后调用x.f(),显然调用B类里定义的g并打印“B.g called”。但如果创建一个C类的实例对象y并调用y.f()呢?

由于C类里没有f的定义,y.f()实际调用的B类的f。那么f调用那个g函数呢?从程序正文看,正在执行的方法f的定义出现在B类中,在B类中self的类型应该是B。如果根据这个类型去查找g,就应该找B类中定义的g。采用这种根据静态程序正文去确定被调用方法的规则称为静态约束(另一种说法叫静态绑定)。但Python不这样做,它和多数常见的面向对象语言一样,基于方法调用时的self所表示的那个实例对象的类型去确定应该调用哪个g,这种方式称为动态约束

这样,y.f()的执行过程将将是:由于y值时C类的实例对象,首先基于它确定实际应该调用的方法函数f。由于C类里没有f,去基类B中找到f,再调用self.g(),由于当时self是一个C类的实例对象,确定g的工作再次从调用对象所属C类开始。因此执行结果是打印“C.g called”。

在程序设计领域,这种通过动态约束确定调用关系的函数称为虚函数。

便准函数super()
Python提供了一个内置函数super,把它用在派生类的方法定义里,就是要求从这个类的直接基类开始做属性检索(而不是从这个类本身开始查找)。采用super函数而不直接写具体基类的名字,产生的查找过程更加灵活。如果直接写基类的名字,无论在什么情况下执行,总是调用该基类的方法,而写了super,Python解释器将根据当前类的情况找到相应的基类,自动确定究竟应该使用哪个基类的属性。

函数super有几种使用方式,最简单的是不带参数的调用形式,如:

super().m(...)

如果在一个方法函数的定义里出现这个调用语句,执行时,Python解释器就会从这个对象所属类的基类开始,按照上面介绍的属性检索规则去查找函数m。举例说明:

class C1:
    def __init__(self,x,y):
        self.x = x
        self.y = y
    def m1(self):
        print (self.x,self.y)
    .......
        
class C2(C1):
    def m1(self):
        super().m1()
        print('some special service')
   ........

如果执行类C2里的m1,Python解释器将从C2的基类开始找m1.显然这种形式的super函数调用只能出现在方法函数定义里。在实际调用时,当前实例将被作为被调用函数的self实参。

函数super的第二种使用形式是super(C,obj).m(…),这种写法要求从指定的类C的基类开始查找函数属性m,调用里出现的obj必须是类C的一个实例。Python解释器找到函数m后将obj作为该函数的self实参。这种写法可以出现在程序的任何地方。

3.异常

现在简单介绍Python异常与类的关系,语言内构建的异常类层次结构,以及Python语言如何利用面向对象的观点组织异常处理过程。编程中有时需要自己定义异常(类型),如果需要这样做,就应该选一个异常类,从它派生。

3.1异常类和自定义异常

异常是Python语言中的一套特殊的控制机制,主要用于支持错误的检查和处理,也可以用于实现特殊的控制转移。如果程序执行过程中异常产生,无论是解释器发现的异常情况,还是通过raise语句引发的异常,正常执行控制流立刻终止,解释器转入异常处理模式,查找能处理所发生异常的处理器。如果找不到相应的异常处理器,在交互解释环境下,系统将在环境中输出错误信息,结束当前执行并回到系统的交互状态,等待下一输入。在直接执行方式下,当前程序立即终止。

程序运行中发生的每个异常都有特定的名字,如ValueError\TypeError\ZeroDivisionError等,解释器根据发生的异常去找处理器。Python里处理异常的结构是try语句。每个try语句,可以带任意多个except子句,这种子句就是异常处理器,子句头部用一个表达式描述它捕捉和处理的异常。

实际上,Python的异常都是类(class),运行中产生异常就是生成相应类的实例对象,异常处理机制完全基于面向对象的概念和性质。全部内部异常类构成了一个树形结构,所有异常类的基类是BaseException,其最主要的子类是Exception,内置异常类都是这个类的直接或间接派生类。如果用户需要定义异常,就应该从系统一场中选择一个合适的异常,从它派生出自己的异常类。例如:

class RationalError(ValueError):
	pass

最简单(很常见)的情况只是希望定义一种特殊异常,并不需要这种异常有什么特殊功能(如上即是)。在这种情况下,选一个系统异常类派生出自己的异常类,类体不需要定义任何属性。但为了语法完整,可以在这里写一个pass语句。

运行中发生的异常与处理器的匹配按面向对象的方式处理。假设运行中发生的异常是e,如果一个异常处理器的头部列有异常名E,且isinstance(e,E)为真,那么这个异常处理器就能捕获并处理这个异常e。

3.2 异常的传播和捕捉

运行中的异常可能模块层面的语句的执行中,更多情况是发正在函数语句的执行中。假设函数f的执行中发生异常e,当前执行立即终止,解释器转入异常处理模式,设法找到处理e的处理器。查找过程:略。

3.3 内置的标准异常类

Python语言定义了一套标准的异常类,他们都是BaseEception的派生类,其最重要的子类是Exception,标准异常类都是Exception的直接或间接派生类。

下面是常见异常,我们可以从这些异常类中派生出自己的异常类:
面向对象编程Python的类(超详细,绝佳实例)_第2张图片

4.类定义实例:学校认识管理系统中的类

现在考虑一个综合性的实例:为学校人员管理系统定义所需表示人员信息的类,他们都是数据抽象。

4.1问题分析和设计

学校里有两大类人员,即学生和教职工,他们都是需要在系统中表示的对象。分析这两类人所需要表示的信息,可以看到这里有一些值得注意的情况:两类人员的信息有一些公共部分,又有各自的特殊情况:

  • 首先,作为人员信息,无论学生还是教职工都有姓名、性别、年龄等公共信息。另外为了方便管理,学生应该有一个学号,教职工应该有一个职工号。
  • 作为学生应该有学习记录,包括院系、注册时间,特别是学习期间的学习课程和成绩等。
  • 教职工应该有院系、入职时间、职位和工资等信息。

由于这两类人员信息有公共部分,又有各自的特殊信息,特别适合用面向对象的类继承机制处理。这里首先考虑一个公共人员类,提供记录和查询人员的基本信息。然后从这个公共类分别派生出学生类和教职工类。

显然,表示人员或者是特殊情况(学生、教职工)都是数据表示问题,应该采用抽象数据类型的思想去分析和设计。在开始具体的类定义(编程)之前,下面考虑如何设计出几个合适的抽象数据类型(ADT)。

基本人员ADT的设计
首先考虑一般人员ADT的定义。为了建立这个ADT的具体对象,需要提供一些基本信息,包括人员姓名、性别、出生年月日和一个人员编号(学号或职工号,这里要求提供,具体人员需要具体考虑ADT生成规则)。ADT的解析操作包括提取人员的编号、姓名、性别、出生年月日和年龄。还应允许人员改名,为此定义一个变动操作。由于人员记录可能需要排序,为此要有一个人员对象之间的’小于‘运算符。还需要为输出定义一些辅助操作。根据这些考虑,可以给出下面的抽象数据类型定义:
面向对象编程Python的类(超详细,绝佳实例)_第3张图片
为了管理一个学校的人员,还需要考虑人员统计。这件事可以考虑使用类里的数据属性和类方法完成。

学生ADT的设计
每个学生属于一个院系,入学时间也是确定的。另外学生的学号也应该是按照一套规则生成的,不需要认为选择。新的解析操作需要包括学生的所属院系、入学时间(年)、查看学生成绩单。变动操作应该包括设置选课记录和课程成绩(是变动操作)等。

这里还需要借用面向对象的继承机制,Student(Person)表示继承Person类中除构造函数之外的所有其他操作。
面向对象编程Python的类(超详细,绝佳实例)_第4张图片
实现这个ADT的时候还需要考虑一个生成学生好的内部函数,具体技术后面再说。

教职工ADT的设计
与学生的情况相对应,教职工ADT应该有取得院系、工资、入职时间等的解析函数,以及设置这些数据变动的操作:
面向对象编程Python的类(超详细,绝佳实例)_第5张图片

4.2人事记录类的实现

在实现ADT前先定义两个异常类,人们在定义自己的异常类时候,多数采用最简单的方式:只是选择一个Python标准的异常类作为基类,派生时不定义任何方法或数据属性。这里定义了两个,之后我们会用到的异常类:

class PersonTpyeError(TyperError):
	pass
class PersonValueError(ValueError):
	pass

公共人员类的实现
将这个类命名为Person,为了统计在程序中建立人员个数,需要为Person类引入一个数据属性_num,每当创建的时候对象就将其值加一。Person类的__init__方法完成这一工作。
下面是Person类的开始部分:

class Person:
	_num = 0
	def __init__(self,name,sex,birthday,ident):
		if not (isinstance(sex,str)):
			raise PersonValueError(name,sex)
		try:
			birth = datatime.date(*birthday)#生成一个日期对象
		except:
			raise PersonValueError('wrong date:',birthday)
		self._name = name
		self._sex = sex
		self._birthday = birth
		self._id = ident
		Person._num += 1  # 实例计数

__init__方法的主要工作是检查参数合法性,设置对象的数据属性。这些检查非常重要。最麻烦的是出生日期的检查,需要考虑很多情况,这里我们用标准库包datetime里的date类,其构造含函数要求三个参数,如果实参不是合法日期值就会引发异常。之后用try…except…捕获异常的发生。

Person类的其他方法都非常简单:

		def id(self):return self._id
		def name(self):renturn self._name
		def sex(self):return self._sex
		def birthday:renturn self._birthday
		def age(self):return (datetim.date.today().year-self._birthday.year)
		def set_name(self,name):
			if not isinstance(name,str):
				raise PersonValueError("set_name",name)
			self._name = name
		def __lt__(self,other):
			if not isinstance(other,Person):
				raise PersonTypeError(other)
			return self._id < other.id

在这个类里还需要定义一个类方法,以便取得类中的人员计数值。另外定义两个与输出有关的方法,他们都很简单:

	@class
	def num(cls):return Person._num
	
	def __str__(self):
		return "".join((self._id,self._name,self._sex,str(self._birthday)))
	
	def details(self):
		return "".join(("编号:"+self._id,"姓名:"+self._name,"性别:"+self._sex,"出生日期:"+self._birthday))

这里的想法是让__str__提供对象的基本信息,details方法提供完整细节。注意join要求参数是可迭代对象,这里选用元组。

下面是完整示例:

import datetime
class PersonValueError:
    pass

class PersonTypeError:
    pass


class Person:
    _num = 0
    def __init__(self,name,sex,birthday,ident):
        if not (isinstance(name,str)) and sex in ('女','男'):
            raise PersonValueError(name,sex)
        try:
            birth = datetime.date(*birthday)
        except:
            raise PersonValueError('Worng data:',birthday)
        self._name = name
        self._sex = sex
        self._birthday = birth
        self._id = ident
        Person._num += 1

    def id(self):
        return self._id

    def name(self):
        return self._name

    def sex(self):
        return self._sex

    def birthday(self):
        return self._birthday

    def id(self):
        return self._id

    def set_name(self,name):
        if not isinstance(name,str):
            raise PersonValueError('set_name',name)
        self._name = name

    def __lt__(self, another):
        if not isinstance(another,Person):
            raise PersonTypeError(another)
        return self._id < another._id

    @classmethod
    def num(cls):
        return Person._num

    def __str__(self):
        return " ".join((self._id,self._name,self._sex,str(self._birthday)))

    def details(self):
        return ",".join(("编号:"+self._id,"姓名:"+self._name,"性别:"+self._sex,"出生日期:"+str(self._birthday)))


# p1 = Person('gai','男',(1999,7,2),'19191211')
# p2 = Person('gbi','男',(1992,7,2),'19191311')
# p3 = Person('gci','女',(1993,7,2),'19191011')
# p4 = Person('gdi','男',(1998,7,2),'19191111')
#
# plist2 = [p1,p2,p3,p4]
# for p in plist2:
#     print(p)
#
# print ('\nAfter sorting:')
# plist2.sort()
# for p in plist2:
#     print (p.details())
#
# print ('People created:',Person.num(),'\n')
#
#
# if p2
#     print ('yes')
# else:
#     print ('no')

由于定义了__str__方法,因此可以使用print输出Person对象。后几个语句还展示了可以对人员对象的表排序(表的sort方法里使用“小于”运算符),以及通过Person类名调用类方法num的情况。

至此我们实现了前面ADT要求的功能。

学生类的实现

完整示例:

class Student(Person):
    _id_num = 0

    @classmethod
    def _id_gen(cls):  # 实现学生号生成规则
        cls._id_num += 1
        year = datetime.date.today().year
        return "1{:04}{:05}".format(year,cls._id_num)

    def __init__(self,name,sex,birthday,department):
        Person.__init__(self,name,sex,birthday,Student._id_gen())
        self._department = department
        self._enroll_date = datetime.date.today()
        self._courses = {
     }

    def set_course(self,course_name):
        self._courses[course_name] = None

    def set_score(self,course_name,score):
        if course_name not in self._courses:
            raise PersonValueError('No this course selected:',course_name)
        self._courses[course_name] = score

    def scores(self):
        return [(cname,self._courses[cname]) for cname in self._courses]

    def details(self):
        return ','.join(((Person.details(self)),"入学日期:"+str(self._enroll_date),
                        "院系:"+self._department,'课程记录:'+str(self.scores())))

教职工类的实现
完整实例:

class Staff(Person):
    _id_num = 0
    @classmethod
    def _id_gen(cls,birthday):
        cls._id_num += 1
        birth_year = datetime.date(*birthday).year
        return '0{:04}{:05}'.format(birth_year,cls._id_num)

    def __init__(self,name,sex,birthday,entry_date=None):
        super().__init__(name,sex,birthday,Staff._id_gen(birthday))

        if entry_date:
            try:
                self._entry_date = datetime.date(*entry_date)
            except:
                raise PersonValueError('Wrong date:',entry_date)

        else:
            self._entry_date = datetime.date.today()
        self._salary = 1720 # 默认工资,可修改
        self._department = '未定' # 需要另行设置
        self._position = '未定' # 需要另行蛇者

    def set_salary(self,amount):
        if not type(amount) is int:
            raise TypeError
        self._salary = amount

    def set_position(self,position):
        self._position = position

    def set_department(self,department):
        self._department = department

    def details(self):
        return ','.join((super().details(),'入职日期:'+str(self._entry_date),'院系:'+self._department
                        ,'职位:'+self._position,'工资:'+str(self._salary)))


p1 = Staff('张字之','女',(1974,10,16))
p2 = Staff('李国强','男',(1962,5,26))
p3 = Student('王小小','女',(2001,2,23),'信工')

print (p1)
print (p2)
print (p3)



p1.set_department('数学')
p1.set_position('副教授')
p1.set_salary(9000)

print (p1.details())
print (p2.details())
print (p3.details())

你可能感兴趣的:(面向对象,Python类,继承,Python中异常类)