本节书摘来自华章出版社《数据结构与算法:Python语言描述》一书中的第2章,第2.3节,作者 裘宗燕,更多章节内容可以访问云栖社区“华章计算机”公众号查看
2.3类的定义和使用
前面给出了两个有理数类的定义,帮助读者得到一些有关Python类机制的直观认识。本节将介绍Python类定义的进一步情况。本书中对类的使用比较规范,涉及的与Python类定义相关的机制不多,只需要有最基本的了解就可以学习后面内容。另一方面,本书的主题是数据结构和算法,并不计划全面完整地介绍Python语言的面向对象机制和各种使用技术。本节主要想给读者提供一些可参考的基本材料,因此,下面有关Python语言的相关介绍将限制在必要的范围内,供读者参考,不深入讨论。有关Python面向对象技术的更多细节,请读者参考其他书籍和材料。
2.3.1类的基本定义和使用
本小节介绍Python类定义和使用的基本情况。
类定义
类定义的基本语法是:
class <类名>:
<语句组>
一个类定义由关键词class开始,随后是用户给定的类名,一个冒号,以及一个语句组。这个语句组称为类(定义)的体部分。
与函数定义类似,类定义也是Python的一种语句。类定义的执行效果(语义)就是建立起这个定义描述的类。在Python里建立的类也是一种对象,表示一个数据类型。类对象的主要作用就是可以创建这个类的实例(称为该类的实例对象)。
一个类定义确定了一个名字空间,位于类体里面的定义都局部于这个类体,这些局部名字在该类之外不能直接看到,不会与外面的名字冲突。在执行一个类定义时,将以该定义为作用域创建一个新的名字空间。类定义里的所有语句(包括方法定义)都在这个局部名字空间里产生效果。这样创建的类名字空间将一直存在,除非明确删除(用del)。当一个类定义的执行完成时,Python解释器创建相应的类对象,其中包装了该类里的所有定义,然后转回到原来的(也就是该类定义所在的)名字空间,在其中建立这个新的类对象与类名字的约束。在此之后通过类名字就能引用相应的类对象了。
在很多类定义的体里只有一组def语句(前面Rational类就是如此),为这个类定义一组局部函数。实际上,完全可以在这里写其他语句,其可能用途后面有简单讨论。类里定义的变量和函数等称为这个类的属性。这里的函数定义常采用一种特殊形式,下面将详细介绍具有这种特殊形式的函数与“方法”之间的关系。
类定义可以写在程序里的任何地方,这一点与函数定义的情况类似。例如,可以把类定义放在某个函数定义里,或者放在另一类定义里,效果是在那里建立一个局部的类定义。但在实践中,人们通常总是把类定义写在模块最外层,这样定义的(类)类型在整个模块里都可以用,而且允许其他模块里通过import语句导入和使用。
类对象及其使用
前面说了,执行一个类定义将创建起一个类对象,这种对象主要支持两种操作:属性访问和实例化(即创建这个类的实例对象)。
在Python语言里,所有属性引用(属性访问)都采用圆点记法。例如,基于模块名引用其中的函数(如math.sin(...)等),类也是如此。在程序里,可以从类名出发,通过属性引用的方式访问有定义的属性,取得它们的值。类里的数据属性(相当于在类里有定义的局部变量)可以保存各种值,类中函数定义生成其函数属性,这种函数属性的值就是一个函数对象,可以通过类名和属性名,采用圆点记法调用。此外,每个类对象都有一个默认存在的__doc__数据属性,其值是该类的文档串。
在定义好一个类后,可以通过实例化操作创建该类的实例对象。实例化采用函数调用的语法形式,最简单情况就像是调用一个无参函数,例如
x = className()
假设className是一个已有定义的类。上面语句将创建className类的一个新实例(实例对象),并把该对象赋给变量x。如果className类里没有定义初始化函数,这一简单调用创建的是该类的一个空对象,其中没有数据属性。
2.3.2实例对象:初始化和使用
虽然Python允许先创建空对象,而后再逐步加入所需的属性,但在实际编程中人们大都不这样做。这样做不太合适的主要原因有两个:一是因为一个个地给对象加入属性,工作很琐碎也很麻烦;更重要的是这样创建出属于同一个类的对象,需要自己维持某种规范性,否则就不能保证在程序运行中的安全使用。
实例对象的初始化
创建一个类的实例对象时,人们通常希望对它做一些属性设置,保证建立起的对象状态完好,具有所需要的性质,也就是说,希望在创建类的实例对象时自动完成适当的初始化。Python类中具有特殊名字__init__的方法自动完成初始化工作:
如果在一个类里定义了__init__方法,在创建这个类的实例时,Python解释器就会自动调用这个方法。
__init__方法的第一个参数(通常用self作为参数名)总表示当前正在创建的对象。方法体中可以通过属性赋值的方式(形式为self.fname的赋值,fname是自己选定的属性名)为该对象定义属性并设定初始值。
__init__可以有更多形式参数。如果存在这种参数,在创建该类的实例对象时,就需要为(除第一个self之外的)形式参数提供实际参数值,用表达式写在类名后面的实参表里。在创建实例对象时,这些实际参数将被送给__init__,使它可以基于这些实参对实例对象做特定的初始化。
在前面定义有理数类时定义了初始化函数。因此,语句
twothirds = Rational(2, 3)
的执行中将完成一系列动作:①创建一个Rational类型的对象;②调用Rational类的__init__函数给这个对象的两个属性赋值;③返回新建的对象。最后,赋值语句把这个新对象赋给变量twothirds,作为其约束值。
类实例(对象)的数据属性
对于已有的类实例对象,可以通过属性引用的方式访问其数据属性。在Python语言里,类实例的数据属性不需要专门声明,只要给对象的属性赋值,就会自动建立这种属性(就像普通变量一样)。每个实例对象是一个局部名字空间,其中包含该对象的所有数据属性及其约束值,对象的全体数据属性的取值情况构成该对象的状态。如果建立的是空对象,它就有一个空的名字空间。如果在类里定义了初始化函数,创建的实例对象就会包含该函数设置的属性。例如,上面语句创建的Rational类的实例有一个局部名字空间,其中包含两个属性名num和den,它们被分别约束于整数值2和3。
由于上述情况,人们在定义类时,通常总是通过自动调用的__init__函数建立实例对象的初始状态,用类里定义的其他函数查看或修改实例对象的状态。实际上,Python允许在任意方法里给原本没有的(也就是说,初始化函数没建立的)属性赋值,这种赋值将扩大该对象的名字空间。但在实际中这种做法并不多见。
一个实例对象是一个独立的数据体,可以像其他对象一样赋给变量作为约束值,或者传进函数处理,或者作为函数的结果返回等。实例对象也可以作为其他实例对象的属性值(无论是同属一个类的实例对象,或不属于同一个类的实例对象),这种情况形成了更复杂的对象结构。在复杂的程序里,这种情况很常见。
方法的定义和使用
除数据属性外,类实例的另一类属性就是方法。
在一个类定义里按默认方式定义的函数,都可以作为这个类的实例对象的方法。但是,如果确实希望类里的一个函数能作为该类实例的方法使用,这个函数至少需要有一个表示其调用对象的形参,放在函数定义的参数表里的第一个位置。这个形参通常取名self(实际上可以用任何名字,用self作为参数名是Python社团的习惯做法)。除了self之外,还可以根据需要为函数引入更多形参。下面将称类里的这种函数为(实例)方法函数。除了是在类里定义而且至少有一个形参外,方法函数并没有别的特殊之处,从其他方面看它们就是一般的函数,Python关于函数的规定在这里都适用。
简单地说,如果类里定义了一个方法函数,这个类的实例对象就可以通过属性引用的方式调用这个函数。在用x.method(…) 的形式调用方法函数method时,对象x将被作为method的第一个实参,约束到方法函数的第一个形参self,其他实参按Python有关函数调用的规定分别约束到method的其他形参,然后执行该函数的体代码。
说得更准确些。如果在程序里通过某个类C的实例对象o,以属性引用的形式调用类C里定义的方法函数m,Python解释器就会创建一个方法对象,把实例对象o和方法函数m约束在这个方法对象里。在(后面)执行这个方法对象时,o就会被作为函数m的第一个实参。在函数m的定义里通过形参self的属性访问,都实现为对调用对象o的属性访问(取值或赋值)。看一个具体例子(和写法):
假设在类C里定义了方法函数m,C.m就是一个函数,其值是普通的函数对象,就像采用其他方式定义的函数一样,例如math.sin的值就是一个函数对象。
假设变量p的值是类C的一个实例,表达式p.m的值就是基于这个实例和函数m建立的一个方法对象。
使用方法对象的最常见方式是直接通过类实例做方法调用。例如,假设类C的方法函数m有三个形参,变量p的值是类C的实例,从p出发调用m就应写成p.m(a, b) 的形式,这里假设a和b是适合作为另两个参数的表达式。
从上面的说明不难看到,方法调用p.m(…) 实际上等价于函数调用C.m(p, …)。方法的其他参数可以通过调用表达式中的其他实参提供。
方法对象也是一种(类似函数的)对象,可以作为对象使用。例如,可以把方法对象赋给变量,或者作为实参传入函数,然后在函数的其他地方作为函数去调用。在上面假设的情况下,程序里完全可以写“q=p.m”,而后可以在其他地方写调用q(a, b),表示用a和b作为实参调用这个方法。
注意,方法对象和函数对象不同,它实际上包含了两个成分:一个是由类中的函数定义生成的函数对象,另一个是调用时约束的(属于相应类的)一个实例对象。在这个方法对象最终执行时,其中的实例对象将被作为函数的第一个实参。
2.3.3几点说明
对于类定义、方法定义等机制,有下面几点说明:
在执行了一个类定义,从而创建了相应的类对象之后,还可以通过属性赋值的方式为这个类(对象)增加新属性。不仅可以为其增加数据属性,也可以增加函数属性。但是这时需要特别当心,如果新属性与已有函数属性同名,就会覆盖同名的属性,这种情况有时可能是编程错误。人们一般采用特殊的命名规则避免这种错误。同样情况也可能出现在__init__方法里。在初始化方法里赋值的属性与类定义中的方法同名是一种常见编程错误,应特别注意。举例说,假设Rational类定义了名字为num的解析操作,如果在__init__函数里给self.num赋值,就会覆盖同名的方法定义。前面类定义里的数据属性名是 _num,也避免了这种名字冲突。
如果需要在一个方法函数里调用同一个类里的其他方法函数,就需要明确地通过函数的第一个参数(self),以属性描述的方式写方法调用。例如,在方法函数f里调用另一个方法函数g,应该写self.g(...)。
从其他方面看,方法函数也就是定义在类里面的函数。其中也可以访问全局名字空间里的变量和函数,必要时也可以写global或nonlocal声明。
Python提供了一个内置函数isinstance,专门用于检查类和对象的关系。表达式isinstance(obj, cls)检查对象obj是否为类cls的实例,当obj的类是cls时得到True,否则得到False。实际上,isinstance可以用于检测任何对象与任何类型的关系。例如检查一个变量或参数的值是否为int类型或float类型等。
静态方法和类方法
除了前面介绍的实例方法之外,类里还可以定义另外两类函数:
第一类是前面介绍过的静态方法,定义形式是在def行前加修饰符 @staticmethod。静态方法实际上就是普通函数,只是由于某种原因需要定义在类里面。静态方法的参数可以根据需要定义,不需要特殊的self参数。可以通过类名或者值为实例对象的变量,以属性引用的方式调用静态方法。例如,在前面Rational类里用Rational._gcd(...)的形式调用静态方法_gcd,也可以写self._gcd(...)。注意,静态方法没有self参数。这也意味着,无论采用上面哪种调用形式,参数表里都必须为每个形参提供实参,这里没有自动使用的self参数。
类里定义的另一类方法称为类方法,定义形式是在def行前加修饰符 @classmethod。这种方法必须有一个表示其调用类的参数,习惯用cls作为参数名,还可以有任意多个其他参数。类方法也是类对象的属性,可以以属性访问的形式调用。在类方法执行时,调用它的类将自动约束到方法的cls参数,可以通过这个参数访问该类的其他属性。人们通常用类方法实现与本类的所有对象有关的操作。
这里举一个例子。假设所定义的类需要维护一个计数器,记录程序运行中创建的该类的实例对象的个数。可以采用下面的定义:
class Countable:
counter = 0
def __init__(self):
Countable.counter += 1
@classmethod
def get_count(cls):
return Countable.counter
x = Countable()
y = Countable()
z = Countable()
print(Countable.get_count())
类定义的其他部分省略。
为了记录本类创建的对象个数,Countable类里定义了一个数据属性counter,其初值设置为0。每次创建这个类的对象时,初始化方法__init__就会把这个对象计数器加一。类方法get_count访问了这个数据属性。上面程序片段在运行时将输出整数3,表示到执行print语句为止已经创建了3个Countable对象。
类定义的作用域规则
类定义作为Python语言里的一种重要定义结构,也是一种作用域单位。在类里定义的名字(标识符)具有局部作用域,只在这个类里可用。如果需要在类定义之外使用,就采用基于类名字的属性引用方式。例如,下面定义是合法的:
class C:
a = 0
b = a + 1
x = C.b
然而,在前面例子里可以看到一个情况:counter是类Countable的数据属性,但是在Countable类的两个方法里,都是通过类名和圆点形式,采用属性引用的形式访问counter。实际上,在Python里必须这样做,在这方面,类作用域里的局部名字与函数作用域里局部名字有不同的规定。
对于函数定义,其中局部名字的作用域自动延伸到内部嵌套的作用域。正因为这样,如果在一个函数f里定义局部函数g,在g的函数体里可以直接使用f里有定义的变量,或使用在f里定义其他局部函数,除非这个名字在g里另有定义。
对于类定义,情况则不是这样。在类C里定义的名字(C的数据属性或函数属性名),其作用域并不自动延伸到C内部嵌套的作用域。因此,如果需要在类中的函数定义里引用这个类的属性,一定要采用基于类名的属性引用方式。
私有变量
在面向对象的程序设计领域,人们通常把类实例对象里的数据属性称作实例变量。因为它们就像是定义在实例对象的名字空间里的变量。
在一些面向对象语言里,允许把一些实例变量定义为私有变量,只允许在类定义的内部访问它们(也就是说,只允许在实例对象的方法函数里访问),不允许在类定义之外使用。实际上,在类之外根本就看不到这种变量,这是一种信息隐藏机制。Python语言里没有为定义私有变量提供专门机制,没有办法说明某个属性只能在类的内部访问,只能通过编程约定和良好的编程习惯来保护实例对象里的数据属性。
在Python编程实践中,习惯约定是把以一个下划线开头的名字作为实例对象内部的东西,永远不从对象的外部去访问它们。无论这样的名字指称的是(类或类实例的)数据成员、方法,还是类里定义的其他函数。也就是说,在编程中永远把具有这种名字的属性看作类的实现细节。在前面的Rational类里,数据属性_num和_den、函数属性_gcd都是这种情况,在这个类定义之外都不应该使用。
另外,如果一个属性以两个下划线开头(但不是以两个下划线结尾),在类之外采用属性访问方式直接写这个名字将无法找到它。Python解释器会对类定义具有这种形式的名字做统一的改名。有关情况见Python语言文档。
此外,具有__add__形式(前后各有两个下划线)的名字有特殊的意义,除了前面介绍过的表示各种算术运算符、比较运算符的特殊名字和__init__、__str__之外,还有一大批特殊名字。有关细节请查看Python语言文档。
实际上,在Python编程中,上述约定不仅仅针对类及其实例对象,也适用于模块等一切具有内部结构的对象。
2.3.4继承
基于类和对象的程序设计被称为面向对象的程序设计,在这里的基本工作包括三个方面:定义程序里需要的类(也是定义新类型);创建这些类的(实例)对象;调用对象的方法完成计算工作,包括完成对象之间的信息交换等。
在Python语言里做面向对象的程序设计,首先要根据程序的需求定义一组必要的类。前面几小节已经介绍了类定义的基本机制,本节将介绍另一种重要机制—继承。继承的作用主要有两个:一个是可以基于已有的类定义新类,通过继承的方式复用已有类的功能,重复利用已有的代码(已有的类定义),减少定义新类的工作量,简化新功能的开发,提高工作效率。另一个作用实际上更重要,就是建立一组类(类型)之间的继承关系,利用这种关系有可能更好地组织和构造复杂的程序。
继承、基类和派生类
在定义一个新的类时,可以列出一个或几个已有的类作为被继承的类,这样就建立了这个新定义类与指定的已有类之间的继承关系。通过继承定义出的新类称为所列已有类的派生类(或称子类),被继承的已有类则称为这个派生类的基类(或父类)。派生类将继承基类的所有功能,可以原封不动地使用基类中已定义的功能,也可以根据需要修改其中的一些功能(也就是说,重新定义其基类已有的某些函数属性)。另一方面,派生类可以根据需要扩充新功能(定义新的数据和/或函数属性)。
在概念上,人们把派生类(子类)看作其基类(父类)的特殊情况,它们的实例对象集合具有一种包含关系。假设类C是类B的派生类,C类的对象也看作C的基类B的对象。人们经常希望在要求一个类B的实例对象的上下文中可以使用其派生类C的实例对象。这是面向对象编程中最重要的一条规则,称为替换原理。许多重要的面向对象编程技术都需要利用类之间的继承关系,也就是利用替换原理。
一个类可能是其他类的派生类,它又可能被用作基类去定义新的派生类。这样,在一个程序里,所有的类根据继承关系形成了一种层次结构(显然不能出现类之间的循环继承,这种情况是编程错误,Python系统很容易检查这种错误)。Python有一个最基本的内置类object,其中定义了一些所有的类都需要的功能。如果一个类定义没说明基类,该类就自动以object作为基类。也就是说,任何用户定义类都是object的直接或间接派生类。另外,Python系统定义了一批内置类,各种基本类型形成了一套层次结构。系统中的内置异常也形成了一套层次结构,有关情况在2.4节简单介绍。
基于已有类BaseClass定义派生类的语法形式是:
class <类名>(BaseClass, ...):
<语句组>
列在类名后面括号里的“参数”就是指定的基类,可以有一个或者多个,它们都必须在这个派生类定义所在的名字空间里有定义。Python允许用更复杂的表达式描述所需要的基类,只要这个表达式的值确实是个类对象。例如,可以在用import语句导入另一个模块之后,利用在该模块里有定义的类作为基类,定义自己的派生类。
Python内置函数issubclass检查两个类是否具有继承关系,包括直接的或间接的继承关系。如果cls2是cls1直接的或间接的基类,表达式issubclass(cls1,cls2)将返回True,否则返回False。实际上,Python的一些基本类型之间也有子类(子类型)关系。详情请查看标准库手册中有关基本类型的介绍。
作为最简单的例子,下面定义了一个自己的字符串类:
class MyStr(str):
pass
这个类将继承内置类型str的所有功能,没做任何修改或扩充。但它是另一个新类,是str的一个派生类。有了这个定义,就可以写:
s = MyStr(1234)
issubclass(MyStr, str)
isinstance(s, MyStr)
isinstance(s, str)
第一个语句创建了一个MyStr类型的对象,后三个表达式的值都是True,其中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异常。Python解释器处理派生类的定义时,将在构造出的类对象里记录其基类的信息,以支持使用这个类(及其对象)时的属性查找。
如前所述,定义派生类时可以覆盖基类里已有的函数定义(也就是说,重新定义一个同名函数)。按照上述查找过程,一旦某函数在派生类里重新定义,在其实例对象的方法调用解析中,就不会再去使用基类里原来定义的方法了。
假设在某个实例对象调用的一个方法f里调用了另一个方法g,而且后一方法也是基于这个实例对象调用的(通过self.g(...))。在这种情况下,查找方法g的过程就只与这个实例对象(的类型)有关,与前一方法f是在哪个类里定义的情况无关。
考虑一个实例。假定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.')
如果在创建B类的实例对象x之后调用x.f(),显然将调用B类里定义的g并打印出 “B.g called.”。但如果创建一个C类的实例对象y并调用y.f()呢?
由于C类里没有f的定义,y.f()实际调用的是B类里定义的f。由于在f的定义里出现了调用self.g,现在出现了一个问题:如何确定应该调用的函数g。从程序的正文看,正在执行的方法f的定义出现在类B里,在类B里,self的类型应该是B。如果根据这个类型去查找g,就应该找到类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.”。
在程序设计领域,这种通过动态约束确定调用关系的函数称为虚函数。
标准函数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(也就是说,从C1开始查找)。由于C1里有m1的定义,最终调用的是C1里的函数m1。显然,这种形式的super函数调用(并进而调用基类的某方法函数)只能出现在方法函数的定义里。在实际调用时,当前实例将被作为被调用函数的self实参。
函数super的第二种使用形式是super(C,obj).m(...),这种写法要求从指定的类C的基类开始查找函数属性m,调用里出现的obj必须是类C的一个实例。Python解释器找到函数m后将用obj作为该函数的self实参。这种写法可以出现在程序的任何地方,并不要求一定出现在类的方法函数里。
函数super其他调用形式的使用情况更特殊,这里就不详细介绍了。