平时为了解决代码重复的问题,我们有了函数,那么在对象中,我们是怎么处理的呢?
主要有两种方式:
继承, 组合
继承:
在开发程序的过程中,如果我们定义了一个类A,然后又想新建立另外一个类B,但是类B的大部分内容与类A的相同时
我们不可能从头开始写一个类B,这就用到了类的继承的概念。通过继承的方式新建类B,让B继承A,
B会‘遗传’A的所有属性(数据属性和函数属性),实现代码重用。
下面猫狗都继承自Animal
class Animal: def eat(self): print("%s 吃 " % self.name) def drink(self): print("%s 喝 " % self.name) def shit(self): print("%s 拉 " % self.name) def pee(self): print("%s 撒 " % self.name) class Cat(Animal): def __init__(self, name): self.name = name self.breed = '猫' def cry(self): print('喵喵叫') class Dog(Animal): def __init__(self, name): self.name = name self.breed = '狗' def cry(self): print('汪汪叫')
上面cat和dog,通过继承都有了吃喝拉撒的功能
当然子类也可以添加自己新的属性或者在自己这里重新定义这些属性(不会影响到父类),
需要注意的是,一旦重新定义了自己的属性且与父类重名,那么调用新增的属性时,
就以自己为准了。
组合
组合指的是,在一个类中以另外一个类的对象作为数据属性,称为类的组合
class Teacher: def __init__(self, name, age, gender, salary, level): self.name = name self.age = age self.gender = gender self.salary = salary self.level = level def walk(self): print("%s is walking" % self.name) def teach(self): print("%s is teaching" % self.name) class Date: def __init__(self, year, mon, day): self.year = year self.mon = mon self.day = day def tell_birth(self): print("%s %s %s" % self.year, self.mon, self.day) Ali = Teacher('Ali', 84, 'female', 30000, -1) Birth = Date(1900, 13, 43) Ali.birth = Birth # 组合, Ali 有生日, 组合的应用,可以理解为将Teacher类与 Birth类组合到一起了
这里Birth是Date类的对象,我们将它作为Ali这个实例的属性,将它们组合到一起。
接口
接口继承实质上是要求“做出一个良好的抽象,这个抽象规定了一个兼容接口,使得外部调用者无需关心具体细节,
可一视同仁的处理实现了特定接口的所有对象”——这在程序设计上,叫做归一化
class Interface:#定义接口Interface类来模仿接口的概念 def read(self): #定接口函数read pass def write(self): #定义接口函数write pass class Txt(Interface): #文本,具体实现read和write def read(self): print('文本数据的读取方法') def write(self): print('文本数据的读取方法') class Sata(Interface): #磁盘,具体实现read和write def read(self): print('硬盘数据的读取方法') def write(self): print('硬盘数据的读取方法') class Process(Interface): def read(self): print('进程数据的读取方法') def write(self): print('进程数据的读取方法')
事实上在python中根本就没有一个叫做interface的关键字,上面的代码只是看起来像接口,
其实并没有起到接口的作用,子类完全可以不用去实现接口。
那我们为什么要用接口呢?
接口提取了一群类共同的函数,可以把接口当做一个函数的集合。然后让子类去实现接口中的函数。
这么做的意义在于归一化,就是只要是基于同一个接口实现的类,那么所有的这些类产生的对象在使用时,从用法上来说都一样。
归一化,让使用者无需关心对象的类是什么,只需要知道这些对象都具备某些功能就可以了,这极大地降低了使用者的使用难度。
在这点上它与抽象基类有些相似,关于抽象基类可以参考我另一博客:http://www.cnblogs.com/Andy963/p/7111467.html
这里用上面的例子进行修改:
import abc class File(metaclass=abc.ABCMeta): @abc.abstractmethod def read(self): #定义抽象方法,无需实现功能 pass @abc.abstractmethod def write(self): #定义抽象方法,无需实现功能 pass class Txt(File): #文本,具体实现read和write def read(self): print('文本数据的读取方法') def write(self): print('文本数据的读取方法') class Sata(File): #磁盘,具体实现read和write def read(self): print('硬盘数据的读取方法') def write(self): print('硬盘数据的读取方法') class Process(File): def read(self): print('进程数据的读取方法') def write(self): print('进程数据的读取方法')
这里与接口不同的是:抽象基类的子类必须实现基类的方法,否则不能实例化。但是所谓的接口的子类可以不实现接口方法,没有强制性
抽象类是一个介于类和接口之间的一个概念,同时具备类和接口的部分特性,可以用来实现归一化设计。
继承顺序
在python2中存在新式类和经典类的区别,但在python3 中只有新式类
# 定义方式不同 # 经典类 class A: pass # 新式类 class A(object): pass
1.普通情况
看下面的例子:这种情况比较简单,新式类与经典类的情况都一样:
class E: def test(self): print('From E') class A(E): def test(self): print("From A") class B: def test(self): print("From B") class C: def test(self): print("From C") class D(A, B, C): def test(self): print("From D") d = D() print(D.mro()) print(d.test())
由于这里都是新式类,可以通过类的mro()方法来查看继承顺序:
我们运行下看看:(我们需要注意的是,新式类最后都是默认继承自object,也就下面最后一个)
$ python cnblog.py
[<class '__main__.D'>, <class '__main__.A'>, <class '__main__.E'>, <class '__main__.B'>, <class '__main__.C'>, <class 'object'>]
From D
None
如果是经典类,我们可以一个个的注释掉每个类的test方法,由于我们调用d.test(),所以它会先去d这个对象找,
接着是它的类D,如果找不到,它会接着到它继承的那些类里找。这样也能找出继承的顺序
第二种情况,相对复杂,它们都继承自同一个类
可以使用上面相同的mro方法,查看它继承的顺序
第三种情况:当这个个A是经典类的情况
子类调用父类的方法
看下面的例子:
方法一:
class People: def __init__(self, name, age, gender): self.name = name self.age = age self.gender = gender def walk(self): print("%s is walking..." % self.name) def foo(self): print("From father %s ." % self.name) class Student(People): def __init__(self, name, age, gender, grade): # 前三个属性people类中已经定义了,这里可以调用父类 People.__init__(self, name, age, gender) self.grade = grade Jack = Student('Jack', 18, 'male', 98) print(Jack.name, Jack.age, Jack.gender, Jack.grade)
运行:
$ python cnblog.py Jack 18 male 98
方法二:
我们对上面的代码略作修改:
class Student(People): def __init__(self, name, age, gender, grade): # 前三个属性people类中已经定义了,这里可以调用父类 #People.__init__(self, name, age, gender) super().__init__(name, age, gender) self.grade = grade
运行结果与第一种方法一样一样的。
这里只演示了初始化方法的调用,对于一些自定义方法的使用也是相同的道理,如People类有walk方法,那么Student类如果也要定义一个walk方法:
# 每个类中都继承了且重写了父类的方法 # 每个类中都继承了且重写了父类的方法 def walk(self ): print("do something") super().walk() # 或者写成:super(Student, self).walk()
第一种方法,指名道姓调用People类的初始化方法,也就是__init__函数
第二种方法,调用父类的__init__方法,实际是绑定方法
在python2中,第二种方法实际是:
super(Student, self).__init__(name, age, gender)
我们看看不使用super会带来的一些问题:
# 每个类中都继承了且重写了父类的方法 class A: def __init__(self): print('A的构造方法') class B(A): def __init__(self): print('B的构造方法') A.__init__(self) class C(A): def __init__(self): print('C的构造方法') A.__init__(self) class D(B, C): def __init__(self): print('D的构造方法') B.__init__(self) C.__init__(self) f1 = D() print(D.__mro__)
我们运行:
$ python cnblog.py D的构造方法 B的构造方法 A的构造方法 C的构造方法 A的构造方法 ( < class '__main__.D' > , < class '__main__.B' > , < class '__main__.C' > , < class '__main__.A' > , < class 'object' > )
我们可以看到,在实例化D这个类时,先调用了B类的__init__方法(构造方法),由于B继承了A,所以它又调用了A的__init__方法,
接着是C类__init__方法,而C也继承了A类的__init__方法,所以再次调用了A的__init__方法。
那么,我能不能把上面的__init__调用改成super呢?
例如这样:
class D(B, C): def __init__(self): print('D的构造方法') super().__init__()
只要每个重定义的方法统一使用super()并只调用它一次,那么控制流最终会遍历完整个MRO列表,每个方法也只会被调用一次
class A: def __init__(self): print('A的构造方法') class B(A): def __init__(self): print('B的构造方法') super().__init__() class C(A): def __init__(self): print('C的构造方法') super().__init__() class D(B,C): def __init__(self): print('D的构造方法') super().__init__() f1 = D() print(D.mro()) # python2中没有这个属性
运行正常:
$ python cnblog.py D的构造方法 B的构造方法 C的构造方法 A的构造方法 [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
在python2中,super().__init__(), 被写成这样:super(B, C).__init__() ,但实际上在python3中同样支持这种写法(测试python3.5)
class A: def __init__(self): print('A的构造方法') class B(A): def __init__(self): print('B的构造方法') super(B, self).__init__() class C(A): def __init__(self): print('C的构造方法') super(C, self).__init__() class D(B, C): def __init__(self): print('D的构造方法') super(D, self).__init__() f1 = D() print(D.__mro__) # python2中没有这个属性
super在这里的好处是什么呢?
1.通过mro的方式来保证各个父类的函数被逐一调用,而且保证每个父类函数只调用一次(如果每个类都使用super)
2.相较于方法一中的直接调用父类的初始化方法,这样更易于维护。方法一中可以看到:如果父类被修改,那么子类中所有的调用都得修改
封装
说到封装,我们先来看个例子:
class A: def foo(self): print("From A.foo") self.bar() def bar(self): print("From A.bar") class B(A): def bar(self): print("From B.bar") t = B() t.foo()
我们运行来看看结果:
$ python cnblog.py From A.foo From B.bar
为什么是B.bar呢?
我们来分析分析过程:
调用foo方法时,先去t对象找,显然t对象没有,然后去t的类找,B中也没有,然后去B的父类找,找到了然后执行
print("From A.foo") ,接着执行self.bar(), 由于这里的self是B,所以又跑回去执行类B中的bar。
显然,出现上面这样的结果(最后又跑会B类了)并不是我们想要的,那如果我们要执行类A中的bar怎么办呢?
class A: def foo(self): print("From A.foo") self.__bar() def __bar(self): print("From A.bar") class B(A): def __bar(self): print("From B.bar") t = B() t.foo()
这里就用到了封装,也就是让外部类无法调用我的方法,像这里的__bar方法,只能是类自己调用,或者你可以称为私有属性。
那为什么要用封装呢?
封装不是单纯意义的隐藏:
1:封装数据的主要原因是:保护隐私
2:封装方法的主要原因是:隔离复杂度
封装其实分为两个层面,但无论哪种层面的封装,都要对外界提供好访问你内部隐藏内容的接口
(接口可以理解为入口,有了这个入口,使用者无需且不能够直接访问到内部隐藏的细节,只能走接口,
并且我们可以在接口的实现上附加更多的处理逻辑,从而严格控制使用者的访问)
第一个层面的封装(什么都不用做):创建类和对象会分别创建二者的名称空间,
我们只能用类名.或者obj.的方式去访问里面的名字,这本身就是一种封装
class A: def __init__(self, name, age): self.name = name self.age = age a = A('andy', 20) print(a.name)
对于这一层面的封装(隐藏),类名.和实例名.就是访问隐藏属性的接口
第二个层面的封装:类中把某些属性和方法隐藏起来(或者说定义成私有的),
只在类的内部使用、外部无法访问,或者留下少量接口(函数)供外部访问。
在python中用双下划线的方式实现隐藏属性(设置成私有的)
class A: def __init__(self, name, age): self.__name = name # 定义时,就转化成了:self._A__name self.__age = age # 定义时,就转化成了:self._A__name def give_name(self): print(self.__name) a = A('andy', 20) print(a.name)
# self._A__name 这种形式也是访问私有属性的唯一方法
这种自动变形的特点:
1.类中定义的__x只能在内部使用,如self.__x,引用的就是变形的结果。
2.这种变形其实正是针对外部的变形,在外部是无法通过__x这个名字访问到的。
2.在子类定义的__x不会覆盖在父类定义的__x,因为子类中变形成了:_子类名__x,
而父类中变形成了:_父类名__x,即双下滑线开头的属性在继承给子类时,子类是无法覆盖的。
$ python cnblog.py Traceback (most recent call last): File "cnblog.py", line 17, in <module> print(a.name) AttributeError: 'A' object has no attribute 'name'
可以看到,已经无法直接访问name属性的值 了
稍作修改:
class A: def __init__(self, name, age): self.__name = name self.__age = age def give_name(self): print(self.__name) a = A('andy', 20) print(a.give_name())
我们看看结果:
$ python cnblog.py andy None
对于这一层面的封装(隐藏),我们需要在类中定义一个函数(接口函数)在它内部访问被隐藏的属性,然后外部就可以使用了
这样来访问是通过一个函数访问,是不是不太好?
为了a.name这种形式,我们使用python内置的装饰器:
class A: def __init__(self, name, age): self.__name = name self.__age = age @property def name(self): return(self.__name) a = A('andy', 20) print(a.name)
运行:
$ python cnblog.py andy
这种通过前置双下划线的方法并没有真正意义上限制我们从外部直接访问属性,
知道了类名和属性名就可以拼出名字:_类名__属性,然后就可以访问了,如a._A__N。
python并不会真的阻止你访问私有的属性,模块也遵循这种约定,如果模块名以单下划线开头,
那么from module import *时不能被导入,但是你from module import _private_module依然是可以导入的
这里的property就是特性:
使用特性有什么好处呢?
property是一种特殊的属性,访问它时会执行一段功能(函数)然后返回值。像上面的name函数,它会返回name函数的返回值。
将一个类的函数定义成特性以后,对象再去使用的时候obj.name,根本无法察觉自己的name是执行了一个函数然后计算出来的,
这种特性的使用方式遵循了统一访问的原则
面向对象的封装有三种方式:
【public】
这种其实就是不封装,是对外公开的
【protected】
这种封装方式对外不公开
private】
这种封装对谁都不公开
这理还是详细说下property:
在上面的例子的基础上,假如我们还需要设置name的属性、删除name属性呢?
很显然,name函数名已经被战胜了,不能继续使用了
class A: def __init__(self, name, age): self.__name = name self.__age = age @property def name(self): return(self.__name) @name.setter def name(self, name): self.__name = name @name.deleter def name(self, name): raise TypeError("Forbiden") a = A('andy', 20) print(a.name) a.name = 'ANDY' print(a.name)
运行:
$ python cnblog.py andy ANDY
待续.....