目录
面向对象编程
常用术语/名词解释
抽象/实现
封装/接口
合成
派生/继承/继承结构
泛化/特化
多态
自省/反射
访问限制
私有属性和公有属性
私有方法的设置
类
创建类
声明与定义
类属性
类的数据属性
类的方法属性(函数属性)
查看类的属性
特殊的类属性
实例
初始化:通过调用类对象来创建实例
__init__()“构造器”方法
__new__()“构造器”方法
__del__()“解构器”方法
实例属性
在构造器中首先设置实例属性
默认参数提供默认的实例安装
__init__()应当返回None
实例的属性可在创建实例后自行增减
查看实例属性
特殊的实例属性
内建类型属性
实例属性VS类属性
访问类属性
从实例中访问类属性须谨慎
类属性持久性
绑定和方法调用
调用绑定方法
调用非绑定方法
静态方法和类方法
staticmethod()和classmethod()内建函数
使用函数修饰符
组合
子类和派生
继承
__bases__类属性
通过继承覆盖方法
从标准类型派生
多重继承
多态
类、实例和其他对象的内建函数
issubclass()
isinstance()
hasattr()、getattr()、setattr()、delattr()
dir()
super()
vars()
抽象指对现实世界问题和实体的本质表现、行为和特征建模,建立一个相关的子集,可以用于描绘程序结构,从而实现这种模型。抽象不仅包括这种模型的数据属性,还定义了这些数据的接口。对某种抽象的实现就是对此数据及与之相关接口的现实化( realization)。现实化这个过程对于客户程序应当是透明而且无关的。
封装描述了对数据/信息进行隐藏的观念,它对数据属性提供接口和访问函数。通过任何客户端直接对数据的访问,无视接口与封装性都是背道而驰的,除非程序员允许这些操作。作为实现的一部分,客户端根本就不需要知道在封装之后,数据属性是如何组织的。在 Python中,所有的类属性都是公开的,但名字可能被“混淆”了,以阻止未经授权的访问,但仅此而已,再没有其他预防措施了。这就需要在设计时,对数据提供相应的接口,以免客户程序通过不规范的操作来存取封装的数据属性。
合成扩充了对类的描述,使得多个不同的类合成为一个大的类,来解决现实问题。合成描述了一个异常复杂的系统,比如一个类由其他类组成,更小的组件也可能是其他的类,数据属性及行为,所有这些合在一起,彼此是“有一个”的关系。比如, RepairShop“有一个”技工(应该至少有一个吧),还“有个”顾客(至少一个)。
派生描述了子类的创建,新类保留已存类类型中所有需要的数据和行为,但允许修改或者其他的自定义操作,都不会修改原类的定义。
继承描述了子类属性从祖先类继承这样一种方式。从前面的例子中,技工可能比顾客多个汽车技能属性,但单独的来看,每个都“是一个”人。继承结构表示多“代”派生,可以描述成一个“族谱”,连续的子类,与祖先类都有关系。
泛化表示所有子类与其父类及祖先类有一样的特点,所以子类可以认为同祖先类是“是一个”(is-a)的关系,因为一个派生对象(实例)是祖先类的一个“例子”。比如,技工“是一个”人,车“是一个”交通工具等。
特化描述所有子类的自定义,也就是,什么属性让它与其祖先类不同。
多态的概念指出了对象如何通过他们共同的属性和动作来操作及访问,而不需考虑他们具体的类多态表明了动态(后来又称运行时)绑定的存在,允计重载及运行时类型确定和验证。
自省表示给予程序员某种能力来进行像“手工类型检查”的工作,它也被称为反射。这个性质展示了某对象是如何在运行期取得自身信息的。传一个对象给我们,我们可以查出它有什么能力。比如内建函数dir()和type(),以及类的特殊属性__dict__,__name__及__doc__都能实现自省的效果。
在player类内定义了name和age属性,然后创建类的实例wxj,通过实例访问类的name和age属性并对其做出修改,最后通过实例调用类方法printInfo()打印出球员的姓名和年龄。此时,name和age属性是公有属性,可以从外部进行访问和修改。
假设想限制外部代码对类内部属性的修改,则可以将属性设为私有变量。方法是在属性名称前加双下划线。
以上结果表明,通过外部代码对私有变量的修改并未成功,证明了私有变量无法从外部访问。
想要修改类的私有变量,可以在类的内部实现一个设置私有变量的方法setPlayer(),外部代码通过该方法可实现对私有变量的修改
因为私有方法无法被外部代码访问,故执行时会报错
类是现实世界的抽象的实体以编程形式出现。实例是这些对象的具体化。
Python使用class关键字来创建。简单的类的声明可以是关键字后紧跟类名:
class className: 'class documentation string' #‘类文档关键字’ pass #类体
基类是一个或多个用于继承的父类的集合;类体由所有声明语句,类成员定义,数据属性和函数组成。类通常在一个模块的顶层进行定义,以便类实例能够在类所定义的源代码文件中的任何地方被创建。
对于 Python函数来说,声明与定义类没什么区别,因为他们是同时进行的,定义(类体)紧跟在声明(含class关键字的头行[Header line〕)和可选(但总是推荐使用)的文档字符串后面。同时,所有的方法也必须同时被定义。如果对OOP很熟悉,请注意 Python并不支持纯虚函数(像C++)或者抽象方法(如在Java中),这些都强制程序员在子类中定义方法作为替代方法。
什么是属性呢?属性就是属于另一个对象的数据或者函数元素,可以通过我们熟悉的句点属性标识法来访问。一些 Python类型比如复数有数据属性(实部和虚部),而另外一些,像列表和字典,拥有方法(函数属性)
有关属性的一个有趣的地方是,当你正访问一个属性时,它同时也是一个对象,拥有它自己的属性,可以访问,这导致了一个属性链,比如, my Thing、 subThing、 subSubThing等。常见例子如下:
- sys.stdout.write('foo')
- print myModule myclass.__doc__
- myList.extend (map(upper, open('x')readlines()))
数据属性仅仅是所定义的类的变量。它们可以像任何其他变量一样在类创建后被使用,并且,要么是由类中的方法来更新,要么是在主程序其他什么地方被更新。
这种属性已为OO程序员所熟悉,即静态变量,或者是静态数据。它们表示这些数据是与它们所属的类对象绑定的,不依赖于任何类实例。如果你是一位Java或C++程序员,这种类型的数据相当于在个变量声明前加上 static关键字。
静态成员通常仅用来跟踪与类相关的值。大多数情况下,你会考虑用实例属性,而不是类属性。在后面,我们正式介绍实例时,将会对类属性及实例属性进行比较。
class className: 'class documentation string' #‘类文档关键字’ foo=100 print(className.foo) cn=className() print(cn.foo)
运行结果:
100
100
1.方法
方法,比如下面,类 MyClass中的 myNoActionMethod方法,仅仅是一个作为类定义一部分定义的函数(这使得方法成为类属性)。这表示 myNoActionMethod仅应用在 MyClass类型的对象(实例)上。这里, myNoActionMethod是通过句点属性标识法与它的实例绑定的。
class MyClass(): def myNoActionMethod(self): pass mc=MyClass() mc.myNoActionMethod()
任何像函数一样对 myNoActionMethod自身的调用都将失败:
myNoActionMethod()
运行结果:
Traceback (most recent call last):
File "F:/pythonworkplace/learn_class.py", line 14, in <module>
myNoActionMethod()
NameError: name 'myNoActionMethod' is not defined
引发了 NameError异常,因为在全局名字空间中,没有这样的函数存在。这就告诉你 my NoActionMethod是一个方法,表示它属于一个类,而不是全局空间中的名字。如果 myNonActionMethod是在顶层作为函数被定义的,那么我们的调用则会成功。
下面展示的是,甚至由类对象调用此方法也失败了。
MyClass.myNoActionMethod()
运行结果:
Traceback (most recent call last):
File "F:/pythonworkplace/learn_class.py", line 15, in <module>
MyClass.myNoActionMethod()
TypeError: myNoActionMethod() missing 1 required positional argument: 'self'
2.绑定
为与OOP惯例保持一致, Python严格要求,没有实例,方法是不能被调用的。这种限制即 Python所描述的绑定概念( binding),在此,方法必须绑定(到一个实例)才能直接被调用。非绑定的方法可能可以被调用,但实例对象一定要明确给出,才能确保调用成功。然而,不管是否绑定,方法都是它所在的类的固有属性,即使它们几乎总是通过实例来调用的。
要知道一个类有哪些属性,有两种方法。最简单的是使用dir()内建函数。另外是通过访问类的字典属性__dict__,这是所有类都具备的特殊属性之一。
class MyClass(object): 'MyClass class definition' myVersion='1.1' def showMyVersion(self): print(MyClass.myVersion)
根据上面定义的类,我们使用dir()内建函数和类的字典属性__dict__来查看类的属性:
print(dir(MyClass)) print(MyClass.__dict__)
运行结果:
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'myVersion', 'showMyVersion']
{'__module__': '__main__', '__doc__': 'MyClass class definition', 'myVersion': '1.1', 'showMyVersion': <function MyClass.showMyVersion at 0x0000018B824B97B8>, '__dict__': <attribute '__dict__' of 'MyClass' objects>, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>}
从上面可以看到,dir()返回的仅是对象的属性的一个名字列表,而__dict__返回的是一个字典,它的键(key)是属性名,键值( value)是相应的属性对象的数据值。
根据上面定义的类MyClass,有如下结果:
print(MyClass.__name__) print(MyClass.__doc__) print(MyClass.__bases__) print(MyClass.__dict__) print(MyClass.__module__) print(MyClass.__class__)
运行结果:
MyClass
MyClass class definition
(<class 'object'>,)
{'__module__': '__main__', '__doc__': 'MyClass class definition', 'myVersion': '1.1', 'showMyVersion': <function MyClass.showMyVersion at 0x0000025D441297B8>, '__dict__': <attribute '__dict__' of 'MyClass' objects>, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>}
__main__
<class 'type'>
如果说类是一种数据结构定义类型,那么实例则声明了一个这种类型的变量。
Python创建实例非常简单,不需要new关键字,直接用过用实例名=类名([参数表])的方式即可,如:
mc=MyClass()
(个人总结:类似于c++中构造函数,系统默认给你创建,如有需要,可自行创建)
当类被调用,实例化的第一步是创建实例对象。一旦对象创建了,Python检查是否实现了__init__()方法。默认情况下,如果没有定义(或覆盖)特殊方法__init__(),对实例不会施加任何特别的操作。任何所需的特定操作,都需要程序员实现__init__(),覆盖它的默认行为。如果 __init__()没有实现,则返回它的对象,实例化过程完毕。
然而,如果__init__()已经被实现,那么它将被调用,实例对象作为第一个参数(self)被传递进去,像标准方法调用一样。调用类时,传进的任何参数都交给了__init__().实际中,你可以想像成这样:把创建实例的调用当成是对构造器的调用。
与__init__()相比,__new__()方法更像一个真正的构造器。类型和类在版本2.2就统一了, Python用户可以对内建类型进行派生,因此,需要一种途径来实例化不可变对象,比如派生字符串、数字等。
在这种情况下,解释器则调用类的__new__()方法,一个静态方法,并且传入的参数是在类实例化操作时生成的。__new__()会调用父类的__new__()来创建对象(向上代理)
为何我们认为__new__()比__init__()更像构造器呢?这是因为__new__()必须返回一个合法的实例,这样解释器在调用__init__()时,就可以把这个实例作为self传给它。调用父类的__new__()来创建对象正像其他语言中使用new关键字一样。
同样,有一个相应的特殊解构器(destructor)方法名为__del__()。然而,由于 Python具有垃圾对象回收机制(靠引用计数),这个函数要直到该实例对象所有的引用都被清除掉后才会执行。 Python中的解构器是在实例释放前提供特殊处理功能的方法,它们通常没有被实现,因为实例很少被显式释放。所以这个方式不是很常用,这里不过多记录了。
实例仅拥有数据属性(方法严格来说是类属性),他们只是与某个类的实例相关联的数据值,并且可以通过句点属性标识法来访问。这些值独立于其他实例或类。当一个实例被释放后,它的属性同时也被清除了
“实例化”实例属性
构造器是最早可以设置实例属性的地方,因为__init__()是实例创建后第一个被调用的方法。再没有比这更早的可以设置实例属性的机会了。一旦__init__()执行完毕,返回实例对象,即完成了实例化过程。
在实际应用中,带默认参数的 __init__()提供一个有效的方式来初始化实例。在很多情况下,默认值表示设置实例属性的最常见的情况,如果提供了默认值,我们就没必要显式给构造器传值了。需要明白一点,默认参数应当是不变的对象:像列表(lst)和字典( dictionary)这样的可变对象可以扮演静态数据,然后在每个方法调用中来维护它们的内容。
class BoyFriend(object): def __init__(self,name,age=25,weight=135): self.name=name self.age=age self.weight=weight def printBoyfriend(self): print(self.name) print(self.age) print(self.weight) BF=BoyFriend('YB') #对于实例属性中的name在__init__()中是位置参数,不可省略,其他参数是默认参数 BF.printBoyfriend() abc=BoyFriend('abc',12,36)#对默认参数也做了修改 abc.printBoyfriend()
运行结果:
YB
25
135
abc
12
36
如果定义了构造器,它不应当返回任何对象,因为实例对象是自动在实例化调用后返回的。相应地,__init__()就不应当返回任何对象(应当为None);否则,就可能出现冲突,因为只能返回实例。试着返回非None的任何其他对象都会导致 TypeError异常。
实例的属性和一般的变量一样,可以自己创建
class C(object): spam=100 c=C() c.num=5 c.length=3 print(dir(c)) print(c.__dict__)
运行结果:
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'length', 'num', 'spam']
{'num': 5, 'length': 3}
但是有没有观察到一个现象:dir(实例)是可以看到包含类和实例中所有的属性,但是实例的__dict__属性只显示显式添加的属性。
查看实例属性同样可以使用内建函数dir()或者实例的__dict__属性。
print(dir(BF)) print(BF.__dict__)
运行结果:
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'age', 'name', 'printBoyfriend', 'weight']
{'name': 'YB', 'age': 25, 'weight': 135}
对内建类型来说,他们也属于类,可以使用dir()内建方法。
x=3+0.14j print(dir(x))
运行结果:
['__abs__', '__add__', '__bool__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__int__', '__le__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__pos__', '__pow__', '__radd__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rmod__', '__rmul__', '__rpow__', '__rsub__', '__rtruediv__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', 'conjugate', 'imag', 'real']
我们可以清楚地查看到复数有哪些方法。仔细观察发现,他们本身没有__dict__属性,故不能用__dict__属性查看内建类型的属性。
类属性仅是与类相关的数据值,和实例属性不同,类属性和实例无关。这些值像静态成员那样被引用,即使在多次实例化中调用类,它们的值都保持不变。不管如何,静态成员不会因为实例而改变它们的值,除非实例中显式改变它们的值。
类和实例都是名字空间。类是类属性的名字空间,实例则是实例属性的。
关于类属性和实例属性,还有一些方面需要指出。你可采用类来访问类属性,如果实例没有同名的属性的话,你也可以用实例来访问。
类属性可通过类或实例来访问。下面的示例中,类C在创建时,带一个 version属性,这样通过类对象来访问它是很自然的了,比如 C.version。当实例c被创建后,对实例c而言,访问 c.version会失败,不过Python首先会在实例中搜索名字 version,然后是类,再就是继承树中的基类。本例中, version在类中被找到了:
class C(object): version=1.2 c=C() print(C.version) print(c.version) C.version+=0.1 print(C.version) print(c.version)
运行结果:
1.2
1.2
1.3
1.3
然而,我们只有当使用类引用 version时,才能更新它的值,像上面的 C.version递增语句。如果尝试在实例中设定或更新类属性会创建一个实例属性 c.version,后者会阻止对类属性 C.version的访问。因为第一个访问的就是 c.version,这样可以对实例有效地“遮蔽”类属性 C.version,直到 C.version被清除掉。
与通常 Python变量一样,任何对实例属性的赋值都会创建一个实例属性(如果不存在的话)并且对其赋值。如果类属性中存在同名的属性,有趣的副作用会产生:。
class C(object): version=1.2 c=C() print(C.version) print(c.version) c.version+=0.1 print(c.version) print(C.version)
运行结果:
1.2
1.2
1.3
1.2
所以,给一个与类属性同名的实例属性赋值,我们会有效地“隐藏”类属性,但一旦我们删除了这个实例属性,类属性又重见天日。
但是!在类属性可变的情况下,情况又不一样:
class Foo(object): x={2003:'abc'} foo=Foo() print(foo.x) foo.x[2004]='asd' print(foo.x) print(Foo.x)
运行结果:
{2003: 'abc'}
{2003: 'abc', 2004: 'asd'}
{2003: 'abc', 2004: 'asd'}
但是执行下面的操作:
del foo.x #没有遮蔽所以不能删除掉
运行结果:
Traceback (most recent call last):
File "F:/pythonworkplace/learn_class.py", line 65, in <module>
del foo.x #没有遮蔽所以不能删除掉
AttributeError: x
静态成员,顾名思义,任凭整个实例(及其属性)的如何进展,它都不理不睬(因此独立于实例)。同时,当一个实例在类属性被修改后才创建,那么更新的值就将生效。类属性的修改会影响到所有的实例:
class C(object): spam=100 c1=C() print(c1.spam) C.spam+=100 print(C.spam) print(c1.spam) c2=C() print(c2.spam) del c1 C.spam+=200 print(c2.spam)
运行结果:
100
200
200
200
400
回顾一下方法的相关知识:
- 方法仅仅是类内部定义的函数(这意味着方法是类属性而不是实例属性)
- 方法只有在其所属的类拥有实例时,才能被调用。当存在一个实例时,方法才被认为是绑定到那个实例了。没有实例时方法就是未绑定的。
- 任何一个方法定义中的第一个参数都是变量self,他表示调用此方法的实例对象。
方法,不管绑定与否,都是由相同的代码组成的。唯一的不同在于是否存在一个实例可以调用此方法。在很多情况下,程序员调用的都是一个绑定的方法。假定现在有一个 MyClass类和此类的一个实例mc,而你想调用 MyClass.foc()方法。因为已经有一个实例,你只需要调用 mc.foo()就可以。记得self在每一个方法声明中都是作为第一个参数传递的。当你在实例中调用一个绑定的方法时,self不需要明确地传入了。当你还没有一个实例并且需要调用一个非绑定方法的时候你必须传递self参数。
调用非绑定方法并不经常用到。需要调用一个还没有任何实例的类中的方法的一个主要的场景是:你在派生一个子类,而且你要覆盖父类的方法,这时你需要调用那个父类中想要覆盖掉的构造方法。如:
class human(): def __init__(self,id): self.id=id class BoyFriend(human): def __init__(self,id,name): human.__init__(self,id) #调用非绑定方法 self.name=name
静态方法仅是类中的函数(不需要实例)。
先看一下使用静态方法和类方法的例子:
class TestStaticMethod(): def foo(): print('TestStaticMethod') foo=staticmethod(foo) class TestClassMethod(): def foo(cls): print("TestClassMethod") print('foo() is part of class:',cls.__name__) foo=classmethod(foo)
对应的内建函数被转换成它们相应的类型,并且重新赋值给了相同的变量名。如果没有调用这两个函数,二者都会在 Python编译器中产生错误,显示需要带self的常规方法声明。现在,我们可以通过类或者实例调用这些函数,这没什么不同:
tsm=TestStaticMethod() TestStaticMethod.foo() tsm.foo() tcm=TestClassMethod() TestClassMethod.foo() tcm.foo()
运行结果:
TestStaticMethod
TestStaticMethod
TestClassMethod
foo() is part of class: TestClassMethod
TestClassMethod
foo() is part of class: TestClassMethod
通过使用解构器,我们可以避免像上面那样的重新赋值
class TestStaticMethod(): @staticmethod def foo(): print('TestStaticMethod') class TestClassMethod(): @classmethod def foo(cls): print("TestClassMethod") print('foo() is part of class:',cls.__name__)
一个类被定义后,目标就是要把它当成一个模块来使用,并把这些对象嵌入到你的代码中去,同其他数据类型及逻辑执行流混合使用。有两种方法可以在你的代码中利用类。第一种是组合(composition),就是让不同的类混合并加入到其他类中,来增加功能和代码重用性。你可以在一个大点的类中创建你自己的类的实例,实现一些其他属性和方法来增强对原来的类对象。另一种方法是通过派生,我们将在下一节中讨论它。
举例来说,让我们想象一个对地址本类的加强性设计。如果在设计的过程中,为names、 addresses等创建了单独的类,那么最后我们可能想把这些工作集成到 AddrBookEntry类中去,而不是重新设计每一个需要的类。这样就节省了时间和精力,而且最后的结果是容易维护的代码——一块代码中的bug被修正,将反映到整个应用中。
这样的类可能包含一个Name实例,以及其他如 StreetAddress、 Phone(home、work、 telefacsimile、pager、 mobile等)、 Email(home、work等),还可能需要一些Date实例( birthday、 wedding、 anniversary等)。下面是一个简单的例子:
class NewAddrBookEntry(object): def __init__(self,nm,ph): self.name=Name(nm) #创建Name实例 self.phone=Phone(ph)#创建Phone实例 print('Created instance for:',self.name)
NewAddrBookEntry类由它自身和其他类组合而成。这就在一个类和其他组成类之间定义了一种“有一个”(has-a)的关系。比如,我们的 NewAddrBookEntry类“有一个”Name类实例和一个 Phone实例。
创建复合对象就可以实现这些附加的功能,并且很有意义,因为这些类都不相同。每一个类管理它们自己的名字空间和行为。不过当对象之间有更接近的关系时,派生的概念可能对你的应用程序来说更有意义,特别是当你需要一些相似的对象,但却有少许不同功能的时候。
当类之间有显著的不同,并且(较小的类)是较大的类所需要的组件时,组合表现得很好,但当你设计“相同的类但有一些不同的功能”时,派生就是一个更加合理的选择了。
OOP(面向对象编程)的更强大功能之一是能够使用一个已经定义好的类,扩展它或者对其进行修改,而不会影响系统中使用现存类的其他代码片段。OOD(面向对象设计)允许类特征在子孙类或子类中进行继承。这些子类从基类(或称祖先类、超类)继承它们的核心属性。而且,这些派生可能会扩展到多代。在一个层次的派生关系中的相关类(或者是在类树图中垂直相邻)是父类和子类关系。从同一个父类派生出来的这些类(或者是在类树图中水平相邻)是同胞关系。父类和所有高层类都被认为是祖先。
class Parent(object):#定义父类 def parentMethod(self): print('parentMethod') class Child(Parent):#定义子类 def childMethod(self): print('childMethod') p=Parent()#父类的实例 p.parentMethod() c=Child()#子类的实例 c.childMethod()#子类调用它的方法 c.parentMethod()#子类调用父类的方法
运行结果:
parentMethod
childMethod
parentMethod
子类可以调用父类的方法。
继承描述了基类的属性如何“遗传”给派生类。一个子类可以继承它的基类的任何属性,不管是数据属性还是方法。
注:
- 在Python中是支持多重继承的,也就是我们可以为一个类同时指定多个父类
- 可以在类名的()后边添加多个类,来实现多重继承
- 多重继承,会使子类同时拥有多个父类,并且会获取到所有父类中的方法
- 在开发中没有特殊的情况,应该尽量避免使用多重继承,因为多重继承会让我们的代码过于复杂
- 如果多个父类中有同名的方法,则会现在第一个父类中寻找,然后找第二个,然后找第三个。。。
- 前边父类的方法会覆盖后边父类的方法
举个例子如下。P是一个没有属性的简单类。C从P继承而来(因此是它的子类),也没有属性:
class P(object): pass class C(P): pass c=C() print(c.__class__) #子类“是一个”父类 print(C.__bases__)#子类的父类
运行结果:
<class '__main__.C'>
(<class '__main__.P'>,)
因为P没有属性,C没有继承到什么。下面我们给P添加一些属性:
class P(object): 'P class' def __init__(self): print('create a instance of',self.__class__.__name__) class C(P): pass
现在所创建的P有文档字符串(__doc__)和构造器,当我们实例化P时它被执行,如下面的交互会话所示:
p=P() print(p.__class__) #显示p所属的类名 print(P.__bases__)#父类的父类 print(P.__doc__)#父类的文档字符串
运行结果:
create a instance of P
<class '__main__.P'>
(<class 'object'>,)
P class
“created an instance”是由__init__()直接输出的。我们也可显示更多关于父类的信息。我们现在来实例化C,展示__init__()(构造)方法在执行过程中是如何继承的:
c=C() print(c.__class__) print(C.__bases__) print(C.__doc__)
运行结果:
create a instance of C
<class '__main__.C'>
(<class '__main__.P'>,)
None
C没有声明 __init__()方法,然而在类C的实例c被创建时,还是会有输出信息。原因在于C继承了P的__init__()。 __base__元组列出了其父类P。需要注意的是文档字符串对类,函数/方法,还有模块来说都是唯一的,所以特殊属性doc不会从基类中继承过来
前面的内容中,我们已经概要地介绍了__bases__类属性,对任何(子)类,它是一个包含其父类( parent)的集合的元组。注意,我们明确指出“父类”是相对所有基类(它包括了所有祖先类)而言的。那些没有父类的类,它们的 bases属性为空。下面我们看一下如何使用 bases的:
class A(object):pass class B(A):pass class C(B):pass class D(C,B): pass print(A.__bases__) print(C.__bases__) print(D.__bases__)
运行结果:
(<class 'object'>,)
(<class '__main__.B'>,)
(<class '__main__.C'>, <class '__main__.B'>)
有个有趣的现象,如D先后继承BC,会报错:
class A(object):pass class B(A):pass class C(B):pass class D(B,C): pass print(A.__bases__) print(C.__bases__) print(D.__bases__)
运行结果:
Traceback (most recent call last):
File "F:/pythonworkplace/learn_class.py", line 148, in <module>
class D(B,C):
TypeError: Cannot create a consistent method resolution
order (MRO) for bases B, C
个人总结的话,就是有多个父类,最近的这个父类应该是后面父类的派生类(欢迎批评指正)。
但是多继承多个父类之间没有相互继承关系,就不存在顺序问题。
若子类和父类有相同的方法,但是实现的功能有些不同,这个同名(参数也相同)函数就是对父类对应函数的覆盖。如下例:
class P(object): def foo(self): print("P") p=P() p.foo() class C(P): def foo(self): print('C') c=C() c.foo()
运行结果:
P
C
如果你想在子类中调用父类的同名函数,需要在子类的重写方法里显式地调用基类方法。如下:
class P(object): def foo(self): print("P") class C(P): def foo(self): P.foo(self) print('C') c=C() c.foo()
运行结果
P
C
注意,在这个(未绑定)方法调用中我们显式传递了self。还有一个更好地方式是使用super()内建方法,super()不仅能找到基类方法,而且还为我们传进self。
class P(object): def foo(self): print("P") class C(P): def foo(self): super(C,self).foo() print('C') c=C() c.foo()
运行结果:
P
C
注意:重写__init()不会自动调用基类的__init__
1.不可变类型的例子
如对浮点型保留2位小数的操作:
class RoundFloat(float): def __new__(cls,val): return float.__new__(cls,round(val,2)) print(RoundFloat(1.5656))
或者
class RoundFloat(float): def __new__(cls,val): return super(RoundFloat,cls).__new__(cls,round(val,2)) print(RoundFloat(1.5689))
2.可变类型的例子
如对字典进行排序输出的例子:
class SortedKeyDict(dict): def keys(self): return sorted(super(SortedKeyDict,self).keys()) d=SortedKeyDict((('zheng-cai',67),('hui-jun',68),('xin-yi',2))) print('By iterator:',[key for key in d]) print('By keys():',d.keys())
运行结果:
By iterator: ['zheng-cai', 'hui-jun', 'xin-yi']
By keys(): ['hui-jun', 'xin-yi', 'zheng-cai']
在上例中,通过keys迭代过程是以散列顺序的形式,而使用我们(重写的)keys()方法则将keys变为字母排序方式了。
多重继承,即允许子类继承多个基类。
下面介绍一下方法解释顺序(MRO)(我也没太弄懂 欢迎批评指正)
如下例,P1,P2均为C1,C2的父类,C1,C2又是GC的父类。P1中定义了foo(),P2中定义了foo()和bar(),C2中定义了bar()。
class P1(object): def foo(self): print('P1-foo') class P2(object): def foo(self): print('P2-foo') def bar(self): print('P2-bar') class C1(P1,P2): pass class C2(P1,P2): def bar(self): print('C2-bar') class GC(C1,C2): pass gc=GC() gc.foo() gc.bar() print(GC.__mro__)
运行结果:
P1-foo
C2-bar
(<class '__main__.GC'>, <class '__main__.C1'>, <class '__main__.C2'>, <class '__main__.P1'>, <class '__main__.P2'>, <class 'object'>)
代码解读:
在执行gc.foo()的时候,查找顺序是:GC =>C1 =>C2 =>P1
z在执行gc.bar()的时候,查找顺序是: GC=> C1 => C2
最新版的Python采用广度优先算法,先查找的是同胞兄弟,当查找foo(),它检查GC,然后是C1,C2, 然后在P1中找到,如果P1中没有,查找会到达P2。
有一个__mro__属性,告诉查找的顺序
语法:
issubclass(sub,sup)
issubclass()布尔函数判定一个类是否是另一个给定类的实例。
issubclass()返回True的情况:给出的子类sub确实是父类sup的一个子类(反之,则为 False)。这个函数也允许“不严格”的子类,即一个类可视为其自身的子类,所以,这个函数如果当sub就是sup(是其本身),或者从sup派生而来(是其后代),则返回True。
issubclass()的第二个参数可以是可能的父类组成的元组(tuple),只要第一个参数是给定元组中任何一个候选类的子类时,就会返回True。
isinstance()判定一个对象是否是一个给定类的实例。
语法:
isinstance(obj1,obj2)
class male(): pass class female(): pass m=male() f=female() print(isinstance(m,male)) print(isinstance(f,female)) print(isinstance(m,female))
运行结果:
True
True
False
print(isinstance(m,f))
运行结果:
Traceback (most recent call last):
File "F:/pythonworkplace/learn_class.py", line 214, in <module>
print(isinstance(m,f))
TypeError: isinstance() arg 2 must be a type or tuple of types
注意:第二个参数应当是类,不然,你会得到一个 TypeError。但如果第二个参数是一个类型对象,则不会出现异常。这是允许的,因为你也可以使用 isinstance()来检查一个对象obj1是否是ob2的类型,比如:
print(isinstance(2,int)) print(isinstance('123',str))
运行结果:
True
True
同 issubclass()-样, isinstance()也可以使用一个元组作为第二个参数。
判断某个实例中是否包含/得到/设置/删除某种属性
*attr系列函数可以在各种对象下工作,不限于类(class)和实例(instances)。然而,因为在类和实例中使用极其频繁,就在这里列出来了。需要说明的是,当使用这些函数时,你传入你正在处理的对象作为第一个参数,但属性名,也就是这些函数的第二个参数,是这些属性的字符串名字。换句话说在操作obj.attr时,就相当于调用*attr(obj,’attr’…)系列函数——下面的例子讲得很清楚。
hasattr()函数是布尔型的,它的目的就是为了决定一个对象是否有一个特定的属性,一般用于访问某属性前先作一下检查。
getattr()和setattr()函数相应地取得和赋值给对象的属性, getattr()会在你试图读取一个不存在的属性时,引发 AttributeError异常,除非给出那个可选的默认参数。 setattr()将要么加入一个新的属性,要么取代一个已存在的属性。而delattr()函数会从一个对象中删除属性。
下面一些例子使用到了*attr()系列内建函数:
class myClass(object): def __init__(self): self.foo=100 myInst=myClass() print(hasattr(myInst,'foo'))#检查myInst对象是否有foo属性 print(getattr(myInst,'foo')) print(hasattr(myInst,'bar'))
运行结果:
True
100
False
print(getattr(myInst,'bar'))
运行结果:
Traceback (most recent call last):
File "F:/pythonworkplace/learn_class.py", line 226, in <module>
print(getattr(myInst,'bar'))
AttributeError: 'myClass' object has no attribute 'bar'
print(getattr(myInst,'bar','oops!'))#给出可选的默认参数
oops!
setattr(myInst,'bar','my attr') print(dir(myInst)) print(getattr(myInst,'bar')) delattr(myInst,'foo') print(dir(myInst)) print(hasattr(myInst,'foo'))
运行结果:
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'bar', 'foo']
my attr
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'bar']
False
dir()作用在实例上时,显示实例变量,还有在实例所在的类及所有它的基类中定义的方法和类属性。
dir()作用在类上时,则显示类以及它的所有基类的__dict__中的内容。但它不会显示定义在元类中的类属性。
dir()作用在模块上时,则显示模块的__dict__的内容。
dir()不带参数时,则显示调用者的局部变量。
找到相应的父类,然后方便调用相关属性。
vars()内建函数与dir()相似,只是给定的对象参数都必须有一个__dict__属性。
var()返回一个字典,它包含了对象存储于其__dict__中的属性(键)和值。如果提供的对象没有这样一个属性,则会引发一个Type Error异常。如果没有提供对象作为vars()的一个参数,它将显示一个包含本地名字空间的属性(键)及其值的字典,也就是, locals()。
class C(object): pass c=C() c.foo=100 c.bar='Python' print(c.__dict__) print(vars(c))
运行结果:
{'foo': 100, 'bar': 'Python'}
{'foo': 100, 'bar': 'Python'}
更多Python框架梳理,请参考: 【Python学习】Python最全总结
有问题请下方评论,转载请注明出处,并附有原文链接,谢谢!如有侵权,请及时联系。