Python 面向对象三大特性

继承

一、继承初识

我们先来看下面几段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Cat:
def __init__(self, name, sex, color):
self.name = name
self.sex = sex
self.color = color

def eat(self):
pass


class Dog:
def __init__(self, name, sex, color):
self.name = name
self.sex = sex
self.color = color

def eat(self):
pass


class Bird:
def __init__(self, name, sex, color):
self.name = name
self.sex = sex
self.color = color

def eat(self):
pass

我们定义了猫的类、狗的类还有鸟的类,他们都有类似的对象属性和方法,如果往后还要定义更多的动物类,且这些类拥有的属性和方法都相同,又需要重新复写这些代码。所以我们就可以使用继承的思想来实现类似的这种需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Animal:
def __init__(self, name, sex, color):
self.name = name
self.sex = sex
self.color = color

def eat(self):
pass

class Cat(Animal):
pass

class Dog(Animal):
pass

我们先定义一个 Animal 类,再将定义的 Cat 类和 Dog 类加一个括号,括号里传一个 Animal 类名,这就代表着我定义的 Cat 类和 Dog 类是继承于 Animal 类的,可以使用 Animal 类中的相关属性和方法。括号里面的称为父类(基类或者超类),括号外面的称为子类(或者派生类)。

使用继承思想有哪些好处:

  • 优化代码,节省代码
  • 提高代码的复用性
  • 提高代码的维护性
  • 让类与类之间发生关系(组合是让对象之间发生关系)

二、对父类的调用

子类以及子类实例化的对象,可以访问父类的任何方法或变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Animal:
breath = '呼吸'

def __init__(self, name, sex, age):
self.name = name
self.sex = sex
self.age = age

def eat(self):
print(self) # <__main__.Person object at 0x0000020663933588>
print('动物都需要进食....')

class Person(Animal): # 括号里面的 父类,基类,超类 括号外面的 子类,派生类.
pass

class Cat:
pass

class Dog:
pass

p1 = Person('cdc', 'boy', 18)
print(p1.__dict__)
  • 子类的类名可以访问父类的所有内容
1
2
print(Person.breath)  # 呼吸
Person.eat(111) # 动物都需要进食....
  • 子类实例化的对象也可以访问父类所有内容
1
2
3
4
5
print(p1.breath)
p1.eat()

# 当子类的实例化对象调用继承的父类中的方法时,会将子类对象的实例化空间传给父类对应方法中的self参数
print(p1) # <__main__.Person object at 0x0000020663933588>

实例化对象查找相关的属性时,会先在实例空间内进行查找,找不到就会去本类中进行查找,还是找不到就再去父类中查找……

类名查找对应属性时,先从自身的名称空间进行查找,查找不到再去父类中查找,永远不可能从实例化的对象中查找。

三、只调用子类的方法

在子类创建这个方法,如果方法名与父类相同,按照执行顺序,会优先调用子类的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Animal:
breath = '呼吸'

def __init__(self, name, sex, age):
self.name = name
self.sex = sex
self.age = age

def eat(self):
print(self)
print('动物都需要进食....')


class Person(Animal): # 括号里面的 父类,基类,超类 括号外面的 子类,派生类.
def eat(self):
print("我爱吃面条...")

def sleep(self):
print("我爱睡懒觉")

p1 = Person('cdc', 'boy', 18)
p1.eat() # 我爱吃面条...
p1.sleep()

四、只调用父类的方法

子类中不要定义与父类同名的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Animal:
breath = '呼吸'

def __init__(self, name, sex, age):
self.name = name
self.sex = sex
self.age = age

def eat(self):
print(self)
print('动物都需要进食....')


class Person(Animal): # 括号里面的 父类,基类,超类 括号外面的 子类,派生类.

def func1(self):
pass

p1 = Person('cdc', 'boy', 18)
p1.eat() # 动物都需要进食...

五、同时调用父类的方法和子类的方法

  • 方式一,通过类名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Animal:
breath = '呼吸'

def __init__(self, name, sex, age):
self.name = name
self.sex = sex
self.age = age

def eat(self):
print(self)
print('动物都需要进食....')


class Person(Animal):

def __init__(self, name, sex, age, skin):
Animal.__init__(self, name, sex, age)
self.skin = skin

p1 = Person('cdc', 'boy', 18, "yellow")
print(p1.name) # cdc
print(p1.sex) # boy
print(p1.age) # 18
print(p1.skin) # yellow
  • 方式二,super()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Animal:
breath = '呼吸'

def __init__(self, name, sex, age):
self.name = name
self.sex = sex
self.age = age

def eat(self, args):
print(self)
print('动物都需要进食....')
print(f"我爱吃{args}")


class Person(Animal):

def __init__(self, name, sex, age, skin):
# super(Person, self).__init__(name, sex, age) # 可以简写成以下方式
super().__init__(name, sex, age)
self.skin = skin

def eat(self, args):
super().eat(args)
print("我爱吃面条")


p1 = Person('cdc', 'boy', 18, "yellow")
print(p1.name) # cdc
print(p1.sex) # boy
print(p1.age) # 18
print(p1.skin) # yellow

p1.eat("馒头")
"""
动物都需要进食....
我爱吃馒头
我爱吃面条
"""

六、新式类和经典类

  • 新式类:凡是继承object类都是新式类。python3 所有的类都是新式类,因为 python3 中的类都默认继承 object。
  • 经典类:不继承object类都是经典类。python2 既有新式类,又有经典类。所有的类默认都不继承 object类,所有的类默认都是经典类;可以让其继承 object 转变为新式类
1
2
3
4
5
6
7
8
# python2环境下
# 经典类
class A:
pass

# 新式类
class A(object):
pass

七、单继承和多继承的顺序

**单继承:**单继承的查询顺序在新式类和经典类中没有区别,都是现在本类中查找,找不到再去父类中查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A:
def func(self):
print('IN A')

class B(A):
pass
# def func(self):
# print('IN B')

class C(B):
pass
# def func(self):
# print('IN C')

c1 = C()
c1.func()

# 查询顺序:C B A

**多继承:**新式类遵循广度优先,经典类遵循深度优先

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class A:
# pass
def func(self):
print('IN A')

class B(A):
pass
# def func(self):
# print('IN B')

class C(A):
pass
# def func(self):
# print('IN C')

class D(B):
pass
# def func(self):
# print('IN D')

class E(C):
pass
# def func(self):
# print('IN E')

class F(D,E):
pass
# def func(self):
# print('IN F')

f1 = F()
f1.func()
1
2
3
4
5
6
7

A
B(A) C(A)

D(B) E(C)
F(D,E)

经典类:深度优先,即一条路走到黑。按照F的继承顺序从左往右查找,即如果F中没有,就去D中查找,D中没有就去B中查找,B中没有就去A中查找,找到就结束,再找不到就报错。

新式类:广度优先,一条路走到倒数第二级,判断,如果其他路能走到终点,则返回走另一条路。如果不能,则走到终点。按照F的继承顺序从左往右查找,即如果F中没有,就去D中查找,D中没有就去B中查找,此时在B处判断,如果没有其他途径能到达A,就去A中查找,找到就结束,再找不到就报错;显然上述列子中是有其他途径的(FECA),因此要返回从E开始查询,E中没有就去C中查询,C没有就去A中查找,找到就结束,再找不到就报错。因此广度优先的查询顺序为:FDBECA

八、多继承C3算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
			 H
G(H) F(H)
E(G) D(F)
C(E) B(D)
A(BCD)

第一步,先把每一条深度遍历和A自己的继承顺序放到一个列表中,所以我们可以得到以下四个列表:
[B,D,F,H] [C,E,G,H] [D,F,H] [B,C,D]
第二步,每个列表中第一值作为头,其余的值作为尾,如在[B,D,F,H]中,B是头,DFH是尾,其余列表同样按照这个规则;
第三步,从第一个列表的头开始,如果这个头不在其余任意一个列表的尾中,则把该头单独拿出来放在一个新的列表,并把所有列表中的该字母去掉;如果该头出现在了其他列表的尾中,则跳过该头,取第二个列表的头进行相同操作;最后新列表中的字母顺序即为多继承的查找顺序。

例:
原始的四个列表:[B,D,F,H] [C,E,G,H] [D,F,H] [B,C,D]
划分头尾:[[B], [D,F,H]] [[C], [E,G,H]] [[D], [F,H]] [[B], [C,D]]
先找B,发现B在其他列表的尾中没有出现过,把B提取出来,并删掉所有列表中的B[D,F,H] [C,E,G,H] [D,F,H] [C,D] 新列表[B]
重新划分头尾:[[D], [F,H]] [[C], [E,G,H]] [[D], [F,H]] [[C], [D]]
D,发现D在列表[C,D]的尾中,所以跳过D,用下一个列表的头C开始匹配,发现C满足条件,把C提取出来,删掉所有列表中的C[D,F,H] [E,G,H] [D,F,H] [D] 新列表[B,C]
重新划分头尾:[[D], [F,H]] [[E], [G,H]] [[D] ,[F,H]] [D]
D,发现D满足条件,把D提取出来,删掉所有列表中的D[F,H] [E,G,H] [F,H] 新列表[B,C,D]
......
最后得到新列表 [B,C,D,F,E,G,H]

使用 类名.mro() 方式可以得到广度优先的查询顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class A:
# pass
def func(self):
print('IN A')

class B(A):
pass
# def func(self):
# print('IN B')

class C(A):
pass
# def func(self):
# print('IN C')

class D(B):
pass
# def func(self):
# print('IN D')

class E(C):
pass
# def func(self):
# print('IN E')

class F(D,E):
pass
# def func(self):
# print('IN F')

print(F.mro()) # [<class '__main__.F'>, <class '__main__.D'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

九、抽象类(接口类)

在我们开发项目时,必须要有归一化设计的思想。比如,要实现一个支付的功能,我们可以这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Alipay:
def __init__(self, money):
self.money = money

def pay(self):
print('使用支付宝支付了%s' % self.money)


class Jdpay:
def __init__(self, money):
self.money = money

def pay(self):
print('使用京东支付了%s' % self.money)


A1 = Alipay(100)
A1.pay()
J1 = Jdpay(100)
J1.pay()

虽然在代码和逻辑上都没有什么问题,但是从使用方式上来说,这两者都是支付的功能,但是需要通过不同的方式取调度,这一点是很不合理的,我们可以进行改良

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Alipay:
def __init__(self, money):
self.money = money

def pay(self):
print('使用支付宝支付了%s' % self.money)


class Jdpay:
def __init__(self, money):
self.money = money

def pay(self):
print('使用京东支付了%s' % self.money)


def pay(obj):
obj.pay()


a1 = Alipay(200)
j1 = Jdpay(100)

# 归一化设计
pay(a1)
pay(j1)

无论通过何种方式进行支付,都只需要调用同一个pay方法即可,这就是最典型的归一化设计的思想。

然而,当我们的代码交给其他人接着进行开发的时候,其他人由于不熟悉原来的代码架构,可能不按照原来的代码方式进行续写,此时想要归一化功能就会有问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Alipay:
def __init__(self, money):
self.money = money

def pay(self):
print('使用支付宝支付了%s' % self.money)


class Jdpay:
def __init__(self, money):
self.money = money

def pay(self):
print('使用京东支付了%s' % self.money)

# 新增代码
class Wechatpay:

def __init__(self, money):
self.money = money

def wechat_pay(self):
print('使用微信支付了%s' % self.money)


def pay(obj):
obj.pay()


w1 = Wechatpay(200)
pay(w1) # AttributeError: 'Wechatpay' object has no attribute 'pay'

新增代码部分并未参考上面两个类的写法,自己定义了一个方法,倒是想要实现归一化的时候报错。针对上述情况,我们就可以定义一个抽象类(又称接口类),让后续的类都继承该类,并让后续的类都按照该类的格式进行定义,制定了一个规范。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from abc import abstractmethod, ABCMeta

class Payment(metaclass=ABCMeta): # 抽象类,接口类

@abstractmethod
def pay(self):
pass

def func(self):
pass


class Alipay(Payment):
def __init__(self, money):
self.money = money

def pay(self):
print('使用支付宝支付了%s' % self.money)


class Jdpay(Payment):
def __init__(self, money):
self.money = money

A1 = Alipay(100)
J1 = Jdpay(200) # TypeError: Can't instantiate abstract class Jdpay with abstract methods pay

抽象类中不需要定义方法具体的实现,它的功能就是为子类制定一个必须强制执行的规则。对于抽象类中添加了装饰器的方法来说,子类在定义时,必须要定义该方法,否则报错。这样一来,当别人拿到你的代码后,也可以保证必须按照原来的规则统一编写接口。

多态

多态是指同一类事物可以有多种形态。

多态性是指在不考虑实例类型的情况下使用实例。

1
2
3
在面向对象方法中一般是这样表述多态性:向不同的对象发送同一条消息(!!!obj.func():是调用了obj的方法func,又称为向obj发送了一条消息func),不同的对象在接收时会产生不同的行为(即方法)。也就是说,每个对象可以用自己的方式去响应共同的消息。所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。

比如:老师.下课铃响了(),学生.下课铃响了(),老师执行的是下班操作,学生执行的是放学操作,虽然二者消息一样,但是执行的效果不同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
peo = People()
dog = Dog()
pig = Pig()

# peo、dog、pig都是动物,只要是动物肯定有talk方法
# 于是我们可以不用考虑它们三者的具体是什么类型,而直接使用
peo.talk()
dog.talk()
pig.talk()


# 更进一步,我们可以定义一个统一的接口来使用
def func(obj):
obj.talk()

多态性的好处:

​ 1.增加了程序的灵活性

   以不变应万变,不论对象千变万化,使用者都是同一种形式去调用,如 func(animal)

​ 2.增加了程序额可扩展性

   通过继承animal类创建了一个新的类,使用者无需更改自己的代码,还是用 func(animal) 去调用

在python中,其实并没有多态的概念,或者说在python中处处是多态。这是由于python是弱语言决定的。在强语言类型,如 java 中,在声明一个变量时,必须规定其数据类型,即使后面对变量值进行修改,也必须时该数据类型下的。然而在python中,可以随意改变变量的类型和值,且不管什么数据类型,传入函数或者封装到对象中都可以的。

一、鸭子模型

Python崇尚鸭子类型,即‘如果看起来像、叫声像而且走起路来像鸭子,那么它就是鸭子’。python程序员通常根据这种行为来编写程序。例如,如果想编写现有对象的自定义版本,可以继承该对象,也可以创建一个外观和行为像,但与它无任何关系的全新对象,后者通常用于保存程序组件的松耦合度。

1
2
3
4
5
6
7
8
9
10
11
class Str:
def index(self): # 字符串类型操作索引相关
pass

class List:
def abc(self): # 列表类型操作索引相关
pass

class Tuple:
def rrr(self): # 元组类型操作索引相关
pass

上述例子中,三种类的三种方法均用于操作索引,但是方法名不一样。其实他们的功能类似,我们完全可以把方法名都定义成 index

1
2
3
4
5
6
7
8
9
10
11
class Str:
def index(self): # 字符串类型操作索引相关
pass

class List:
def index(self): # 列表类型操作索引相关
pass

class Tuple:
def index(self): # 元组类型操作索引相关
pass

这样这些类就互称为鸭子,而我们在操作不同数据类型索引的时候,只需要通过调用 index 方法名就行了,函数会根据自身调用的类的不同去执行不同的方法,即虽然接受的函数名一样,但是执行的效果不同。开发人员就不需要考虑是何种数据类型去执行不同的方法来实现同一种功能,简化了调用方式。(原来要分别执行 Str.index,List.abc,Tuple.rrr,现在统一都是 类名.index)

封装

广义的封装:实例化一个对象,给对象空间封装一些属性。

狭义的封装:私有制。

一、私有静态字段

  • 对于私有静态字段,类的外部不能访问
1
2
3
4
5
6
7
8
9
10
11
class A:
name = "cdc"
__age = 18 # 私有静态字段前加__

a1 = A()

print(A.name) # cdc
print(a1.name) # cdc

print(A.__age) # AttributeError: type object 'A' has no attribute '__age' 类名不能访问私有静态字段
print(a1.__age) # AttributeError: 'A' object has no attribute '__age' 实例化对象不能访问私有静态字段
  • 对于私有静态字段,类的内部可以访问
1
2
3
4
5
6
7
8
9
10
11
12
class A:
name = "cdc"
__age = 18 # 私有静态字段前加__

def func(self):
print(self.__age)
print(A.__age)

a1 = A()

a1.func() # 18
A.func(a1) # 18
  • 对于私有静态字段来说,只能在本类中内部访问,类的外部、派生类均不可访问
1
2
3
4
5
6
7
8
9
10
11
12
13
class B:
__money = 100000

class A(B):
name = 'cdc'
__age = 18

def func(self):
print(self.__money)
print(A.__money)

a1 = A()
a1.func()

其实私有静态字段在类的外部是可以访问的,这也是python的一个小bug,但是不建议这么去访问。

1
2
3
4
5
6
7
8
9
10
11
12
class A:
name = "cdc"
__age = 18

def func(self):
print(self.__age)
print(A.__age)

print(A.__dict__)
"""
{'__module__': '__main__', 'name': 'cdc', '_A__age': 18, 'func': <function A.func at 0x000001E5ED6ED950>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
"""

我们通过打印A类的名称空间的内容可以发现,其实当python解释器在读取类的定义代码时,读取到__age,就知道这是要定义一个私有静态字段。为了不让类的外部能够访问到,就会把原来存放到类的名称空间的__age前面多添加一个_类名,即_A__age,所以我们从外部想要调用__age时,就会找不到对应静态字段。这也是为什么从类的内部可以访问私有静态变量的原因了,因为在类的内部,当执行到 print(A.__age) 时,解释器会自动把__age变为_A__age,这样就能匹配上了。

所以当我们从外部想要调用私有静态变量时,只需要这样调用

1
2
3
4
5
6
7
8
9
10
11
12
class A:
name = "cdc"
__age = 18

def func(self):
print(self.__age)
print(A.__age)

a1 = A()

print(a1._A__age) # 18
print(A._A__age) # 18

但是记住千万不要这么干,千万不要,不要!

二、私有方法

  • 类外部不能访问
1
2
3
4
5
6
7
8
9
10
11
class A:
name = 'alex'

def __func(self):
print('func....')

def func1(self):
self.__func()

a1 = A()
a1.__func() # 类外部不能访问
  • 类内部可以访问
1
2
3
4
5
6
7
8
9
class A:
name = 'alex'
def __func(self):
print('func....')

def func1(self):
self.__func() # 类的内部可以
a1 = A()
a1.func1() # 类的内部可以访问
  • 派生类不能访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class B:
__money = 100000
def __f1(self):
print('B')

class A(B):
name = 'alex'

def __func(self):
print('func....')

def func1(self):
self.__f1()

a1 = A()
a1.func1() # 类的派生类也不能访问.

三、私有对象属性

  • 类外部不能访问
1
2
3
4
5
6
7
8
9
10
11
12
class A:

def __init__(self, name, age, weight):
self.name = name
self.__age = age
self.__weight = weight

def func(self):
print(self.__age)

a1 = A('cdc', 18, 45)
print(a1.__age)
  • 类内部可以访问
1
2
3
4
5
6
7
8
9
10
11
12
class A:

def __init__(self, name, age, weight):
self.name = name
self.__age = age
self.__weight = weight

def func(self):
print(self.__age)

a1 = A('cdc', 18, 45)
a1.func()
  • 派生类不能访问
1
2
3
4
5
6
7
8
9
10
11
12
13
class B:
def __init__(self, name, age, weight):
self.name = name
self.__age = age
self.__weight = weight


class A(B):
def func(self):
print(self.__age)

a1 = A('cdc', 18, 45)
a1.func()

Python 面向对象三大特性
https://clark-cdc.github.io/2019/05/03/0012-面向对象的三大特性/
作者
clark
发布于
2019年5月3日
许可协议