类和对象
这一部分介绍 Python 里与面向对象程序设计有关的概念的结构。

面向对象程序设计的基本概念包括类、对象(类的实例)、方法、继承、动态方法约束等。

类定义

Python 的类机制用于定义程序里需要的类型。一个定义好的类建立了一个类型,其使用就像系统的内部类型,可以产生这个类型的对象,产生的对象具有这个类定义的行为。实际上,系统里的许多类型也是类,例如 Python 里表示异常的各种类型。

Python 里类定义的语法是

class  className :
    <statement-组>
类定义与函数定义类似,也是 Python 的一种语句,它的执行才产生所需的效果。允许放类定义的位置也与函数定义一样,可以把它放在函数里,甚至放在 if 语句的分支里,作为局部定义。当然这些不常见,也不很有用。

人们一般只在类定义里写 def 语句,用这种方式为这个类定义一些局部函数。实际上,完全可以在类定义里写其他语句。写在类定义里的函数定义通常具有特殊的形式,下面会详细讨论类里定义的具有特殊形式的函数和 "方法" 之间的关系。

但程序的执行进入一个类定义时,就会以这个类定义作为作用域,创建一个新的名字空间。在类定义里的所有赋值和定义等都在这个局部的名字空间里生效。由执行类定义创建的名字空间通常也会一直存在,除非明确地用 del 命令删除。当一个类定义的执行正常完成时,解释器将根据这个类定义创建一个类对象,其中包装了这个类里的所有定义;然后转回原来的名字空间,在该名字空间里建立这个类对象与相应类名字的约束。

类对象的操作

类对象支持两种操作:属性访问和实例化(即,创建类的实例对象)。

从类名出发可以访问类里的属性(变量或函数),取得它们的值(某种对象,其中的函数属性的值是函数对象)。类的属性都是可写属性,也可以给它们赋新的值。此外,也可以给没有在类定义里面出现的属性赋值,这样赋值将给这个类增加一个新属性,并在类的名字空间里建立一个新约束。此外,每个类都有一个默认的 __doc__ 属性,其值是类的文档串。

在一个类已经定义好(建立了相应的类对象及其类名约束)之后,可以通过实例化操作创建这个类的实例对象。实例化在语法上采用函数调用的语法形式,最简单情况就像调用一个无参函数,如

	x = className()
这个语句将创建类 className 的一个新实例(实例对象),并将其赋给 x 作为值。如果没有下面介绍的初始化机制,这种简单调用创建的是该类的一个空对象。

实例对象的初始化和使用

可以在创建类的实例对象时自动对其进行初始化。要在创建实例对象时进行自动初始化,就需要在相应的类里定义一个名为 __init__ 的特殊方法:
  • 如果类里有 __init__ 方法,系统在创建的实例化创建对象时就会自动调用这个方法;
  • 这个方法的第一个参数(通常用 self)总表示当前创建的对象,可以通过属性赋值的方式为创建的实例对象增加属性并给定其初始值;
  • __init__ 可以有更多的参数。如果确实有这样的参数,在类的实例化操作中就需要为它们提供实际参数值(写在类名后面的括号里)。这些值将被作为调用 __init__ 的实际参数。__init__ 函数里可以基于这些参数对实例对象做特定的初始化。

类实例的属性

如前所述,每个类实例对象对应着一个名字空间。建立了实例对象后,就可以通过属性引用的方式访问其数据属性。类实例的数据属性不需要在类定义里说明,只要给数据属性赋值,就会自动建立这种属性(就像局部变量一样)。数据属性的全体构成实例对象的状态,人们通常用自动调用的 __init__ 函数建立实例对象的初始状态,用在类里定义的其他函数(见下)查看或者修改实例对象的状态。

方法函数的定义和使用

类实例的另一类属性是方法。在一个类里定义的函数可以成为这个类的实例的方法。

如果希望类里定义的某个函数能作为类实例的方法使用,该函数至少要有一个表示调用对象的形参。这个形参通常取名 self(实际上可以用任何名字,用 self 是 Python 的编程习惯),它作为函数的第一个形参。还可以根据需要引入更多形参。下面将把这种函数简称为"方法函数"。方法函数并不特殊,从其他方面看也就是一般的函数,Python 函数的各方面特征在这里都适用。

如果通过一个类 C 的实例对象 o 以属性引用的形式调用类 C 里定义的方法函数,就会创建一个方法对象(不是简单的函数对象),把类实例 o 和这个方法函数约束在方法对象里。在执行这个方法函数时,o 将作为函数的第一个实参。在函数的定义里通过第一个形参的属性访问,就是对调用对象 o 的属性访问(取值或赋值)。

设 C 类里定义了方法函数 m,再设变量 x 的值是 C 类的对象

    x.m(......)	建立方法对象并用参数表里给定的参数执行它
    h = x.m	让 x 约束到这里建立的方法对象,以后可以通过 h 调用该方法对象(写 h(......))
假设在类 C 里定义了方法函数 f,C.f 就是一个函数(其值是个函数对象)。假设变量 x 的值是类 C 的一个实例对象,x.f 的值就是基于 x 和 f 建立的一个方法对象,方法调用 x.f() 等价于函数调用 C.f(x)。方法的其他参数可以方法调用的实参提供。

方法对象的最常见使用方式是直接通过类实例的方法调用(如上面第一个例子)。但由于方法对象也是一种对象,因此也可以作为值使用,例如将其赋给某个变量,或者作为实参传给其他的函数,然后在程序里的其他地方作为方法对象调用(上面第二个例子)。

注意,方法对象和函数对象是不同的,它有两个成分:一个是由类中的函数定义产生的函数对象,另一个是调用时约束的类实例对象。在这个方法对象最终执行时,其中的实例对象将被作为函数的第一个实参。

几点说明

在一个类定义被执行,从而创建了相应的类对象之后,完全可以通过属性赋值的方式为其增加新的属性,无论是数据属性还是函数属性。但做这种事情时要特别当心,在一个类里的数据属性将覆盖其同名的方法属性,不当的属性赋值可能破坏原来的定义。人们通常采用某种命名规则防止发生这种错误。

在执行类里的函数定义的时候,也是生成一个函数对象并建立它与函数名之间的约束(与执行普通函数定义时的情况一样),这也意味着,一个类的某个方法函数的实际定义完全可以不出现在这个类的定义内部。可以把函数的定义放在类定义外面的其他任何地方,在类定义里通过属性赋值建立类属性与这个函数对象的约束。当然,要想作为方法函数,这个函数定义里一定要有引用调用对象的第一个参数。

在一个方法函数里,可以通过其第一个参数 self 调用在同一个类里有定义的其他方法函数,例如在方法函数 f 里调用另一个方法函数 g,就应该写 self.g(…)。此外,在方法函数里也可以访问全局名字空间里的变量和函数,必要时可以加入 global 声明。

Python 语言没提供任何信息隐藏机制,不能把属性定义为只能在一个类里访问。信息保护问题只能靠编程约定和良好的编程习惯来保证。

继承

继承机制用于基于已有的类定义新的类,定义的新类称为已有类的"派生类",被继承的类称为派生类的"基类"。

在通过继承定义新的类时,可以原封不动地使用已有类的功能,只是在其基础上扩充新功能;也可以根据需要任意修改一些已有功能,同时也可以扩充新功能。

继承的一个作用是重复利用已有的代码,简化新功能的开发。另一个作用更重要,就是建立起一些类型之间的关系。由一个类生成的对象称为是这个类的 "实例对象"(也可以简称为这个类的 "实例",或这个类的 "对象")。派生类的对象也看作是其基类的对象,可以在要求基类的实例对象的上下文中使用,面向对象编程的许多重要技术都利用了类之间的继承关系。

一个类可能是另一个类的派生类,它也可能又被作为基类定义其他的派生类。在一个程序里,类的继承关系形成了一种层次结构(显然,不应该出现类之间的循环继承关系,这种情况是编程错误)。

在 Python 里有一个最基本的内置类 object,其中定义了一些在所有的类里都需要的功能。实际上,如果程序里的类定义中没有说明基类,它就自动地以类 object 作为基类。也就是说,任何用户定义类都是 object 的直接或间接派生类。另外,Python 系统定义了一批内置类,各种基本类型形成了一套层次结构,系统中的内置异常也形成了一套层次结构。这些方面的更多细节参见 Python 语言手册第 3 节。

派生类的定义语法:

Calss DerivedCalssName (BaseClass) :
    ... ...
列在派生类定义名后面括号里的 "参数" 基类必须在定义这个派生类的同一个名字空间里有定义。当然,完全可以用更复杂的表达式来表示基类,只要对这种表达式的求值确实能得到一个类,例如,可以用在另一个模块里有定义的类作为基类来定义派生类。

在处理一个派生类时,系统会自动地在构造出的类对象里记录其基类的信息,以支持使用这个类时的属性查找。如果要在派生类里找一个属性,但该类里并不存在这个属性,系统就继续到该类的基类里去查找,这一过程一直递归进行到没有可用基类为止。

生成派生类的实例的方式与非派生类完全一样,DerivedClassName() 将建立该类的一个新实例对象。同样可以在派生类里定义 __init__ 函数,做新实例对象初始化。在实际中,我们经常需要在派生类的实例对象里也包含基类实例的所有数据属性,在这种情况下,在创建新的派生类对象时需要对基类的数据属性进行初始化。

通过实例对象引用方法的过程是首先在本类中查找,而后到本类的基类中查找,直到找到一个具有同样名字的函数对象,基于它建立一个方法对象。

在派生类里可以覆盖基类的方法定义(也就是说,重新定义同名的方法。一旦有重新定义,在其实例对象的方法解析中就不会找到基类的方法了)。假设在基于某实例对象调用的一个方法里调用到另一个方法,由于后一方法也是基于该实例对象调用的,查找方式只与这个实例对象的类型有关,与前一方法的定义位置无关。假设类 C 继承类 B,o 是 C 类的实例对象,假设 C 中没有 f 的定义,由于继承性,o.f 调用的实际上会是 B 里定义的 f;再假设 f 里调用了 g,而 C 和 B 里都有 g 的定义。那么在 o.f 执行中调用 g 时,用的将是 o.g 建立的方法对象,调用的将是 C 里定义的 g。这种情况称为 "动态约束",这种函数称为虚函数。

如果希望一个派生类里定义的覆盖函数是基类函数的扩充,即希望在这个函数的实现里利用原函数的功能,那么可以用 BaseClass.methodName(…) 的形式调用基类的方法。这种形式可用于指定调用基类的任何一个函数(无论本派生类是不是覆盖了那个函数)。特别是可以在派生类的 __init__ 方法里调用基类的 __init__ 方法,以利用基类的初始化函数为实例对象中那些在基类实例中也有的数据属性设置初始值。定义的形式为:

class DerivedClass (BaseClass) :
    def __init__(self, ...) :
        BaseClass.__init__(self, ...)
        ... ...
这里在调用基类的 __init__ 时也把必要的参数传给它,包括 self。最后的 "... ..." 表示为初始化派生类的实例里新扩充的属性而写的代码段。

方法的动态约束

假定 B 是派生类 C 的基类,它们的定义分别是:
# code showing dynamic binding
class B :
    def f (self) :
        self.g()
    def g (self) :
        print('B.g called.')

class C(B) :
    def g (self) :
        print('C.g called.')
显然,如果 x 引用着一个 B 类的对象,调用 x.f() 将调用 B 类里定义的 g 并打印出 "B.g called."。但如果 y 引用着一个 C 类的对象时调用 y.f() 呢?

由于 C 里没有 f 的定义,y.f() 执行时实际调用的是 B 类里定义的 f。在 f 里要调用 self.g,问题是怎么确定要调用的函数 g。从程序正文看,f 定义在类 B 里,类 B 里 self 的类型是 B,根据这个类型查找,应该找到 B 类里定义的 g。这种根据静态程序正文确定方式对象的方式称为 "静态约束"。但 Python 不这样做,它和其他面向对象语言一样,基于当时 self 所表示的实例对象的类型去确定应该调用的 g,称为 "动态约束"。

y.f() 的执行过程是:从 y 找到了 C 类的实例对象,下面需要基于它确定方法函数 f 的定义。由于 C 类里没有定义 f,按规则应该到 C 类的基类中去查找 f。在 C 的基类 B 里找到了 f 的定义,因此就应该执行它。在 f 的执行中遇到调用 self.g(),由于 self 的值是 C 类的实例对象,确定 g 的工作再次从 C 类开始进行。由于 C 类里有 g 的定义,执行这个方法函数打印出了 "C.g called."。

两个特殊函数

下面两个函数用于判断实例与类的关系和类之间的继承关系:
  • isinstance(obj, cls) 检查对象 obj 是否类 cls 的实例,它实际上可以用于检测任何对象的类型。
  • issubclass(cls1, cls2) 检查 cls1 是否 cls2 的子类。基本类型之间也有子类型关系。祥情见标准库手册有关基本类型的介绍(语言手册 3.2 节)。

多继承

一个派生类也可以有多个基类,这种技术(和结构)被称为多继承(multi inheritance)。具有多个基类的派生类的实例对象将继承该类的所有基类的方法属性,这样可以重用多个基类的里已经定义好的功能。

要定义具有多个基类的派生类,只需要在定义类时列出多个基类的名字(写在描述基类的括号里)。如果需要继承和初始化多个基类的实例对象的数据属性,可以在派生类的 __init__ 函数里以适当方式和顺序逐个调用各个基类的 __init__ 函数。

多继承的一个新问题是方法调用的确定。由于不同的基类是分别独立定义的,它们有可能包含名字相同的方法函数,因此方法属性的检索过程就很重要。

在最常见的情况里,从派生类的实例对象出发查找方法属性的过程,是一种深度优先过程,在每个层次上从左到右逐个检索各个基类。如果继承关系呈现为一种树形结构,检索过程如下:

  1. 在遇到方法调用 o.f 时,首先在 o 的类 C 里查找方法属性 f,如果存在就用找到的方法;
  2. 如果没找到,就到类 C 的第一个基类 B1 里查找;如果在类 B1 里没找到而且 B1 有基类,就到 B1 的第一个基类里去查找,并如此做下去。
  3. 如果沿着 B1 确定的继承关系检查了 B1 和它的所有可能的直接和间接基类,还是没有找到f的定义,那么就从 C 的第二个基类 B2 开始重复对 B1 相同的查找过程。如果仍未找道,再考虑类 C 的顺序上的下一个基类。
  4. 如果由 C 的所有基类出发的查找都完成了但没有找到结果,就引发一个无定义错误。
由于继承关系未必是树形关系,这个查找还有些细节。这里不详细讨论了。对这个问题感兴趣的人可以参考链接 http://www.python.org/download/releases/2.3/mro/ 下的文档。

私有变量

在面向对象程序设计领域,人们也把实例对象的数据属性称作 "实例变量"。

"私有变量" 是一类实例变量,允许在实例对象的内部访问这些属性的值(也就是说,只允许实例对象的方法函数访问),在实例对象外面不能用,实际上是根本看不到。Python 语言里没提供定义私有变量的机制,只能通过编程约定来保护实例变量的某些属性。

在 Python 语言的编程实践中,人们的习惯约定是把以下划线开头的名字作为类内部的东西,永远不从对象的外部去访问这种属性,无论这样的名字指称的是(类或类实例的)数据成员、函数或者方法。也就是说,在编程中永远把具有这个形式的名字的属性看作是实现细节。这一约定不仅针对类的实例对象,也适用于模块等一切具有内部结构的对象。

标准函数super

super 是一个内置的标准函数,用于在派生类的定义里要求由其直接基类出发开始属性检索。用 super 而不直接用基类名字的方式更加灵活。

super 有多种使用方式,最简单的是不带参数的调用形式。例如,把

	super().m(…)
直接写在方法函数的定义里,从实例对象调用时,解释器就会从当前类的基类开始,按照属性检索规则查找函数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 将首先调用其基类 C1 里的 m1。

需要注意的是,这种形式的 super 函数调用(并调用基类的方法函数 m)只能出现在方法函数的内部,调用时当前实例将作为送给 m 的 self 参数。

第二种使用形式是 super(C, obj).m(...),是要求从指定的类的基类开始找属性,这种写法前面已经见过。在这样调用时,要求 obj 必须是类 C 的实例。系统将从 C 类的基类开始查找函数 m。这种写法可以出现在程序里的任何地方,不要求一定出现在类的方法函数里。 还有一种调用形式是 super(C),这种方式实际使用不多,它返回的是一个未约束的 super 代理对象。

可调用对象,标准函数 callable 和类里的特殊函数 __call__

所谓 "可调用对象",就是可以对它写调用表达式的对象(在它的后面可以跟着一个实际参数表)。例如,函数对象是可调用对象,类对象也是可调用对象(调用类对象的结果是得到一个类实例)。 Python 提供了一个标准函数 callable,对于是可调用对象的实参它将返回 True,如果它返回 False,其实参对象一定是不可调用的。 如果在类 C 里定义了名字为 __call__ 的特殊方法函数,这个类的所有实例对象都是可调用对象。假设 x 是类 C 的一个对象,调用 x(...) 相当于调用 x.__call__(...)。

一些说明

可以用类机制模拟其他语言里的结构或记录。为此只需要定义一个空类。例如
class Employee:
    pass

john = Employee() # Create an empty employee record
# Fill the fields of the record
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000
每个实例方法对象都有两个属性。设m的值是一个方法对象,m.__self__ 得到创建 m 的那个类实例(可称为 m 的实例对象),m.__func__ 得到方法对象里的函数对象。
本页及相关页面(除另声明者外)由裘宗燕创建维护,可自由用于各种学习活动。其他使用需得到作者许可。