提到面向对象,我们会毫不犹豫地想到其三大特征:
- 封装
- 继承
- 多态
下面,我们将由简至难,依次讨论封装、继承和多态。
一. 封装 enclosure
封装:
指隐藏类的实现细节,让使用者不用关心这些细节,而是通过尽可能少的方法或属性来操作对象。
如何封装?:
通过私有属性和方法。
私有属性和方法:
以双下划线 __
开头,不以双下划线结尾的标识符为私有成员。
私有成员只能在此类的方法中进行访问和修改。
扩展:
了解 java 的读者可能知道,java 中使用了 private
default
protected
public
关键字,实现了更丰富的封装。
示例:用私有属性和私有方法封装属性和方法
class A:
__dd = 300
def __init__(self):
# 创建私有属性,此属性在类外无法访问
self.__e = 100
@staticmethod
# 私有方法
def __en():
print('私有方法 __en 被调用!')
@classmethod
def get__dd(cls):
print("私有的类变量 __dd: {}".format(cls.__dd))
def info(self):
print('A的实例方法 info 访问私有属性 __e :', self.__e)
# 调用私有方法
self.__en()
通过实例方法 info
可以间接访问私有属性及私有方法:
>> a = A()
>> a.info()
A 的实例方法 info 访问私有属性 __e : 100
私有方法 __en 被调用!
在外部直接访问私有属性及私有方法:
>> a.__en()
...
AttributeError: 'A' object has no attribute '__en'
>> a.__e
...
AttributeError: 'A' object has no attribute '__e'
>> A.__dd
...
AttributeError: type object 'A' has no attribute '__dd'
下面我们分别查看对象 a
的属性及 A
的属性:
>> a.__dict__
{'_A__e': 100}
>> mappingproxy({'__module__': '__main__',
'_A__dd': 300,
'__init__': <function __main__.A.__init__(self)>,
'_A__en': <staticmethod at 0x21553ea79a0>,
'get__dd': <classmethod at 0x21553ea7340>,
'info': <function __main__.A.info(self)>,
'__dict__': <attribute '__dict__' of 'A' objects>,
'__weakref__': <attribute '__weakref__' of 'A' objects>,
'__doc__': None})
可见私有属性/方法实际上被命名为了 _A私有属性/方法
,因此使用 a.__e
A.__dd
a.__en()
抛出了 AttributeError
异常。
二. 继承 inheritance / 派生 derived
什么是继承 / 派生?
继承是指从已有的类中派生出新的类,新类具有原类的行为,并能扩展新的行为;
派生类就是从一个已有的类衍生出新的类,在新的类上可以添加新的属性和行为;
作用:
- 用继承派生机制,可以将一些共有功能加在基类中,实现代码共享。
- 将共有的属性和方法向上提,形成抽象。
- 在不改变超类的代码基础上改变原有功能。
名词:
基类 base class
/ 超类 super class
/ 父类 father class
三者是等价的。
派生类 derived class
/ 子类 child class
二者是等价的。
2.1 单继承
语法:
class 类名(基类名):
语句块
单继承即派生类由一个基类衍生而来:
class MyList(list):
def insert_head(self, n):
self.insert(0, n)
类的 __base__
属性用来记录此类的父类:
>> MyList.__base__
list
子类对象可以当成父类对象来使用:
>> L = MyList(range(3,6))
>> L
[3, 4, 5]
>> L.append(6)
>> L
[3, 4, 5, 6]
子类可以添加新的行为 / 属性:
>> L.insert_head(2)
>> L
[2, 3, 4, 5, 6]
示例 2:
Human
类用来描述人类的共同行为:
class Human:
@staticmethod
def say(what):
print('说:', what)
@staticmethod
def walk(distance):
print('走了', distance, '公里')
运行结果:
>> h1 = Human()
>> h1.say('Today is a good day.')
>> h1.walk(5)
说: Today is a good day.
走了 5 公里
Student
类描述学生的共同行为:
class Student(Human):
@staticmethod
def study(subject):
print('学习', subject)
运行结果:
>> s1 = Student()
>> s1.say('How are you?')
>> s1.walk(5)
>> s1.study('Python')
说: How are you?
走了 5 公里
学习 Python
Teacher
类描述老师的共同行为:
class Teacher(Student):
@staticmethod
def teach(content):
print('正在教', content)
运行结果:
>> t1 = Teacher()
>> t1.say('I am a teacher.')
>> t1.walk(3)
>> t1.teach('讲解继承派生')
>> t1.study('滑冰')
2.2 覆盖 override
覆盖是指在有继承关系的类中,子类中实现了与父类同名的方法,子类实例调用该方法时,实际调用的是子类中覆盖版本的方法,这种现象被称为覆盖。
super
函数用来返回绑定超类的实例,用超类的实例来调被子类覆盖的父类方法:
-
super(type, obj)
返回绑定超类的实例,要求obj
必须为type
类型的实例; -
super()
返回绑定超类的实例,等同于super(__class__, self)
; -
super()
必须放在方法内调用。
显示调用父类的构造方法:
当子类中实现了 __init__
方法,父类的构造方法并不会被调用,需要显示调用父类的构造方法:super().__init__(参数)
。
示例 1:在方法内使用 super()
class Human:
def __init__(self, n, a):
self.name = n
self.age = a
def info(self):
print('name:', self.name)
print('age:', self.age)
class Student(Human):
def __init__(self, n, a, s):
# 显示调用父类的初始化方法
super().__init__(n, a)
self.score = s
def info(self):
super().info()
print('score:', self.score)
运行结果:
>> h1 = Human('Alex', 22)
>> h1.info()
name: Alex
age: 22
>> s1 = Student('Thomas', 25, 99)
>> s1.info()
name: Thomas
age: 25
score: 99
示例 2:用 super
构造函数来间接调用父类中被覆盖的方法
class A:
def work(self):
print('A.work 被调用!')
class B(A):
def work(self):
print('B.work 被调用!')
def super_work(self):
"""此方法先调用子类的方法,再调用父类的方法"""
self.work()
# super().work()
# super(B,self).work()
super(__class__, self).work()
调用父类 A
中的 work
:
>> a = A()
>> a.work()
A.work 被调用!
调用子类 B
中的 work
:
>> b = B()
>> b.work()
B.work 被调用!
在子类中调用被父类覆盖的方法:
>> super(B, b).work()
A.work 被调用!
>> b.super_work()
B.work 被调用!
A.work 被调用!
2.3 多继承 multiple inheritance
多继承是指一个子类继承自两个或两个以上的基类。
语法:
class 类名(基类名1, 基类名2...);
说明:
- 一个子类同时继承自多个父类,父类中的方法可以同时被继承下来;
- 如果两个父类中有同名的方法,则在子类中又没有覆盖此方法时,调用结果难以确定;
问题:
多继承可能会有标识符(名字空间)冲突的问题;
多继承的 MRO(Method Resolution Order)
问题:
类的 __mro__
属性用来记录属性或方法的查找顺序。
示例:
class A:
def m(self):
print('A.m()')
class B:
def m(self):
print('B.m()')
class C:
def m(self):
print('C.m()')
class D(A, B, C):
def m(self):
super(A, self).m()
super(B, self).m()
super().m()
print('D.m()')
初始化子类,并调用子类的方法 m
:
>> d = D()
>> d.m()
B.m()
C.m()
A.m()
D.m()
上面的打印顺序怎么解释呢?我们不妨先来看 D.__mro__
记录的查找顺序:
>> D.__mro__
(__main__.D, __main__.A, __main__.B, __main__.C, object)
super()
函数根据 MRO 顺序来查找方法:类 D
同时继承自类 A
,B
, C
,且这些类中都有方法 m
,在调用 super
时,方法的查找顺序是按照 D.__mro__
属性指定的顺序:
- 在
D.m()
中首先调用的是super(A, self).m()
,A
的上一个是B
,因此首先打印B.m()
; - 接下来调用
super(B, self).m()
,B
的上一个是C
,因此打印C.m()
; -
supper().m()
,即D
的上一个,因此打印A.m()
。
2.4 用于类的函数
issubclass(cls, class_or_tuple)
:
判断一个类是否继承自其它的类,如果此类是 class
或 tuple
中的一个派生子类,则返回 True
,否则返回 False
。
一切类型都是 object
的子类。
三. 多态 polymorphic
什么是多态:
多态是指在有继承 / 派生关系的类中,调用基类对象的方法,实际能调用子类的覆盖方法,这种现象被称为多态。
说明:
- 多态调用的方法与对象相关,不与类型相关;
- Python 全部对象都只有运行时状态(动态),没有 C++ 语言里的编译时状态(静态)。
示例:示意多态的使用
class Shape:
def draw(self):
print('Shape 的 draw() 被调用')
class Point(Shape):
def draw(self):
print('正在画一个点')
class Circle(Point):
def draw(self):
print('正在画一个圆')
def my_draw(s):
s.draw()
my_draw
中的 s.draw
调用谁是在运行时由 s
的类动态决定的:
>> my_draw(Shape())
Shape 的 draw() 被调用
>> my_draw(Point())
正在画一个点
>> my_draw(Circle())
正在画一个圆