python面向对象——三大特性

python面向对象——三大特性


1. 引言

Python是面向对象的语言,自然也支持面向对象的三大特性:封装、继承、多态。

因为Python2已经较为古老,所有除非是阐述区别,大部分情况下Python2的经典类在博文中我都会忽略。

2. 继承

继承是一种创新类的方式,在Python中,新建的类可以继承一个或多个父类,父类又可以称为基类或超类,新建的类称为派生类或子类。

Python中的类的继承可分为:单继承和多继承

优点:减少了代码的冗余

实现了代码的重用,子类可以继承父类,并且可以继承多个父类(多继承),子类可以使用父类所拥有的的属性和方法(除私有属性和方法)

2.1 代码实现——单继承和多继承

class ParentClass1:
    pass

class ParentClass2:
    pass

class SubClass1(ParentClass1):
    pass

class SubClass2(ParentClass2):
    pass
# 查看继承
print(SubClass1.__bases__)  # __base__只能查看从左到右第一个子类,__bases__则是查看所有继承的父类
print(SubClass2.__bases__)
# 输出
(<class '__main__.ParentClass1'>,)
(<class '__main__.ParentClass2'>,)

经典类和新式类的区别:

  1. 只有在Python2中才有新式类和经典类,Python3中统一都是新式类
  2. 在Python2中,没有显示的继承object类的类,以及该类的子类都是经典类
  3. 在Python2中,显示的声明继承object的类,以及该类的子类都是新式类
  4. 在Python3中,默认继承object类,object类继承自type类(我们通过继承type类来创建元类),即Python3所有的类都是新式类

2.2 继承与抽象

抽象就是抽取类似的,或者比较像的部分。抽象最主要的作用是划分类别(隔离关注点,降低复杂度)

下图中,奥巴马和梅西被抽象成类;人、猪、狗被抽象成父类;

1392643-20180828162604126-1987263597-7a9c1061d657491f975a4c2446353c7e

继承:是基于抽象的结果,通过编程实现,先经历抽象这个过程,才能过过继承的方式表达抽象结构

下图中,人、猪、狗都继承了动物父类,成为了动物父类的子类,然后将类实例化为具体的人物或形象。

1392643-20180828162957082-1443880562-a3cdc81ced854d8cae43e0a1cd452f87

2.3 继承与重用性

在开发的过程中,我们定义了一个A类,然后又想建立一个B类,但是B类的大部分内容与A类相同,我们就不用重写B类,只需要让B类继承A类,这就是类的继承的理念。

当B类继承A类后,B类就会或者A类中的属性,实现代码重用,这就是重用性

下面我们通过一个动物的类实现来示范类的继承和重用性。

class Animal:
    '''
    创建了一个Animal类,作为父类,后续让Dog类和Person类继承
    '''
    def __init__(self, name, attacking, life_value):
        self.name = name  # 人和狗都具有名字属性
        self.attacking = attacking  # 人和狗都会攻击
        self.life_value = life_value  # 人和狗都具有生命值

    def eat(self):
        print('{}在吃饭'.format(self.name))

class Dog(Animal):
    '''
    Dog类,继承了Animal类,具有了Animal类中的所有属性
    '''
    def bite(self,people):
        '''
        派生:狗具有咬人技能
        '''
        people.life_value -= self.attacking

class Person(Animal):
    '''
    人类,继承Animal类,具有Animal类中的所有属性
    '''
    def attack(self, dog):
        '''
        人具有攻击技能
        '''
        dog.life_value -= self.attacking

egg = Person('奥巴马',10,1000)
ha = Dog('二傻',50,1000)
print(ha.life_value)
print(egg.attack(ha))
print(ha.life_value)

2.4 派生

子类也可以添加自己新的属性和定义旧的属性,这些都不会影响到父类,但是一定重新定义了自己的属性,且属性名和父类相同,那么调用该属性时,就会调用新的属性。

class Lol:
    def __init__(self, hero_name, attacking, life_value):
        self.hero_name = hero_name
        self.attacking = attacking
        self.life_value = life_value

    def move_top(self):
        print('{} 已到上路,请支援'.format(self.hero_name))

    def move_mid(self):
        print('{} 已到上中,请支援'.format(self.hero_name))

    def move_end(self):
        print('{} 已到下路,请支援'.format(self.hero_name))

    def attack(self,enemy):
        enemy.life_value -= self.attacking

class Garen(Lol):
    pass

class Riven(Lol):
    pass

a1 = Garen('盖伦',100,300)
b1 = Riven('3Q瑞文',60,200)
print('15:01 当前生命值 %s' % a1.life_value)
b1.attack(a1)
print('15:05 当前生命值 %s'% a1.life_value)
# 输出
15:01 当前生命值 300
15:05 当前生命值 240

我们自己定义新的和父类同名的attak函数,在定义一个新的函数。

class Riven(Lol):
    def attack(self,enemy):  # 定义新的父类同名的函数,不再使用父类的attack函数,之后调用以新的为准
        print('瑞文释放技能了')
    def low_blood(self):  # 定义新的函数
        print('血量低')

当我们使用新建的函数属性的时候,并实现功能的时候,可能会用到父类中重名的函数,此时我们使用调用普通函数的方式:类名.func(),此时和普通函数的调用方式类似,因此即便是self参数我们也需要传值。

class Riven(Lol):
    def __init__(self, hero_name, attacking, life_value, skin):
        # 调用父类功能,需要重新调用__init__,连self参数都需要传值
        Lol.__init__(self,self, hero_name, attacking, life_value)
        self.skin = skin  # 新属性

    def attack(self,enemy):  # 定义新的父类同名的函数,不再使用父类的attack函数,之后调用以新的为准
        print('瑞文释放技能了')

    def low_blood(self):  # 定义新的函数
        print('血量低')

没有有感觉比较麻烦,其实在Python3中我们一般用super()方法,效果如下:

class Riven(Lol):
    def __init__(self, hero_name, attacking, life_value, skin):
        # 调用父类功能,需要重新调用__init__,连self参数都需要传值
        # Lol.__init__(self, hero_name, attacking, life_value)
        # super(Riven,self).__init__()
        super().__init__(hero_name, attacking, life_value)
        self.skin = skin  # 新属性

    def attack(self,enemy):  # 定义新的父类同名的函数,不再使用父类的attack函数,之后调用以新的为准
        print('瑞文释放技能了')

    def low_blood(self):  # 定义新的函数
        print('血量低')

例子中的3中方法都是可以实现,上文中的LOL类还是太复杂了,我们通过一个简单的实例来证明继承的结果都是相同的。

class Base(object):
    def __init__(self):
        print('base 继承自object')

class Son(Base):
    def __init__(self):
        # Base.__init__(self)  # 方法一
        # super(Son, self).__init__()  # 方法二
        super().__init__()  # 方法三
        print('子类继承自父类 Base')

result = Son()
# 输出
base 继承自object
子类继承自父类 Base

方法二和方法三其实一样,可以归纳为一种方法(方法三是方法二的简写),我们推荐使用的super()方法,因为它可以解决钻石继承的问题,马上我们会讨论它,如何防止Base被初始化两次。

2.5 抽象类和接口类

2.5.1 接口类

继承有两种用途:

1.继承基类的方法,并且做出自己的改变或扩展(代码重用)

2.声明某个子类兼容于某基类,定义一个接口类interface,接口类中定义了一些接口名(即函数名)并且没有实现接口功能,子类继承接口类,并且实现接口中的功能。

在Python中默认是没有接口类的:

  • 接口类不能被实例化
  • 接口类中的方法不能被实现

PS:S接口类这个坑,下次再填,哈哈哈哈哈。

2.5.2 抽象类

Python中默认具有抽象类,但是需要借助模块实现

抽象类是一个特殊的类,它的特殊在于:

  • 1. 抽象类只能被继承,不能被实例化
  • 2. 父类的方法,子类必须实现
  • 3. 抽象类(父类)的方法可以被实现
from abc import ABCMeta, abstractmethod

class Animal(metaclass=ABCMeta):
    @abstractmethod
    def eat(self):
        print('打开粮食的袋子')
        print('放一个吃饭的碗')
        print('把饭盛到碗里')

    @abstractmethod
    def sleep(self):
        pass

class Dog(Animal):
    # 实现吃喝睡的方法
    def eat(self):
        super().eat()  # 继承父类
        # super(Dog, self).eat()
        print('dog is eating')

    def sleep(self):
        print('dog is sleeping')

d = Dog()
d.eat() # 特点3.抽象类(父类)的方法可以被实现
a = Animal()
# 输出
打开粮食的袋子
放一个吃饭的碗
把饭盛到碗里
dog is eating  # 特点2.父类的方法子类必须实现
Traceback (most recent call last):
  File "/Users/gray/Desktop/test.py", line 26, in <module>
    a = Animal()  # 特点1.抽象类的方法不能被实例化
TypeError: Can't instantiate abstract class Animal with abstract methods eat, sleep

抽象类和接口类的的区别:接口类不能实现方法,抽象类可以实现方法

抽象类和接口类的相同点:都是用来做约束的,均不能被实例化

抽象类和接口类的使用场景:

  • 当几个子类的父类有相同的功能需要被实现的时候用抽象类(特点2,特点3)
  • 当几个子类具有相同的功能,但是实现个不同的时候选用接口类(特点2不符合)

2.5.3 抽象类和接口类的多继承

  • 在继承抽象类的时候,我们应该避免多继承;
  • 在继承接口类的时候,我们反而鼓励多继承;

2.6 钻石继承(菱形继承)

钻石继承,也叫菱形继承。在多继承的时候,根据类的类型(是不是感觉有点拗口[笑])会有不同的区别,那么继承的顺序就会有区别,别急,我们往下看。

2.6.1 继承顺序

在2.1的部分,我们讨论了多继承,在Python中是具有多继承的(Java和C#只有单继承),当Python继承多个类的时候,寻址的方式就可以分为两种:

  1. 当类是经典类的时候,多继承会按照深度优先的方式查找
  2. 当类是新式类的时候,多继承会按照广度优先的方式查找

在Python2中,默认的是经典类,只有显式继承object的才是新式类,而Python3中默认的都是新式类,经典类被移除,且不必显式继承object

class C1:  # C1是经典类
  pass
class C2(C1):  # C2是经典类
  pass

class D1(object):  # D1是新式类,且object可省略,默认父类是object
  pass
class D2(D1):  # D2是新式类
  pass

既然Python3中已经移除了经典类,只剩下新式类,那么多继承就会按照广度优先的方式查找,请看下例:

class A:  #class A()和class(object)都可以,但是更推荐 class(object)
    def test(self):
        print('class A')

class B(A):
    def test(self):
        print('class B')

class C(A):
    def test(self):
        print('class C')

class D(B):
    def test(self):
        print('class D')

class E(C):
    def test(self):
        print('class E')

class F(D,E):
    pass

class G(F,E):
    pass

g1 = G()
g1.test()
print(G.mro())  #MRO(Method Resolution Order)采用C3算法
# 输出
[<class '__main__.G'>, <class '__main__.F'>, <class '__main__.D'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
新式类的继承顺序: G->F->D->B->E->C->A->object

2.6.2 继承原理

Python3中的方法mro():MRO(Method Resolution Order)方法解析顺序会根据C3算法构造出一个MRO列表,从左到右开始查找基类,直到找到第一个匹配这个属性的类为止,当然啦,最后一个一定是object类。

MRO列表遵循三条准则:

  1. 子类会优于父类被检查
  2. 多个父类会根据它们在列表中的顺序被检查
  3. 如果下一个类存在两个合法的选择,选择第一个父类

2.7 继承部分总结

继承的作用:

  • 减少代码的重用
  • 提高代码的可读性
  • 规范编程范式
  • 抽象:抽象即抽取相似属性,从具体到抽象的过程
  • 继承:子类继承了父类的方法和属性
  • 派生:子类在父类的方法和属性的基础上产生了新的方法和属性

抽象类接口:

  1. 多继承:
    • 在继承抽象类的过程中,我们应该尽量避免多继承
    • 在集成接口类的过程中,我们鼓励使用多继承
  2. 方法的实现:
    • 在抽象类中,我们可以对一些抽象方法做出基础实现
    • 在接口类中,任何方法都是一种规范,具体的功能需要子类实现

钻石继承(菱形继承):

新式类(Python3):广度优先(ORM(C3算法))

经典类(Python2):深度优先

3. 多态

多态性是指在不考虑实例的情况下使用实例,比如:老师.下课()学生.下课(),老师执行的是下班操作,学生执行的是放学操作,两者消息一样,但是执行的效果不同。

举个例子来说,猫、狗、人都会发出叫声,但是人和动物的语言肯定不同,我们不考虑他们具体的类型是什么而是直接使用:

man = People()
dog = Dog()
cat = Cat()

man.talk()
dog.talk()
cat.talk()

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

3.1 鸭子类型

Python崇尚鸭子类型,“如果看起来像鸭子,那么它就是个鸭子”(笑出声)

如果想要编写现有对象的自定义版本,那么就会继承该对象,再派生。

# 既然是Json处理文件的类,看起来像处理文件的类,那么他就是处理文件的。
class JsonFile:
    def read(self):
        pass
    def write(self):
        pass

class DiskFile:
    def read(self):
        pass
    def write(self):
        pass

4. 封装

**定义:**隐藏对象的属性和实现细节,仅提供公共访问方式

**好处:**1.将变化隔离;2.便于使用;3.提高复用性;4.提高安全性

**封装原则:**1.将不需要对外提供的内容都隐藏起来;2.把属性都隐藏,提供公共方法对其访问

4.1 私有变量

Python中使用双下划线开头的方式将属性隐藏起来,此时它的属性就是私有的。

class A:
    __N= 0  # 使用了双下划线会导致双下滑开头的变量名都变成了:_类名__x的形式
    def __init__(self):
        self.__X = 0  # 形式为self._A__X
    def __fool(self):  # 形式为_A__fool
        print('class A')
    def bar(self):
        self.__fool()  # 只有在类内才可以通过__foo访问

print(A._A__N)  #A_A__nums是可以访问的
# 这种操作并不是严格意义上的限制外部访问,仅仅是一种语法上的变形
print(A._A__fool)  #_A__fool类也是可以访问的

私有变量的特点:

  1. 类内定义的__N只能在内部使用(广义上的)
  2. 这种变形是针对外部的,在外部__N是无法通过这个名字访问到的
  3. 继承后派生的属性不会覆盖父类中的同名属性,因为子类名中属性为:_子类名__x而父类中属性为:_父类名__x,很明显是无法覆盖的

4.2 私有方法

刚刚我们在私有变量里提到了,继承后派生的属性无法覆盖父类中的同名属性。

所以,在继承中,父类如果不想让子类覆盖自己的方法,那么就可以将方法定义为私有的。

class A:
    def fa(self):
        print('class A')
    def test(self):
        self.fa()

class B(A):
    def fa(self):
        print('class B')

b= B()
b.test()# 被子类覆盖
b.fa()
# 输出
class B
class B

不想被覆盖,那么就需要使用私有方法即将方法定义为私有的。

class A:
    def __fa(self):  # 变形为_A__fa
        print('class A')
    def test(self):  # 调用的是_A__fa
        self.__fa()

class B(A):
    def __fa(self):
        print('class B')

b = B()
b.test()  # 私有方法没有被覆盖
# 输出
class A

4.3 property属性

@property是Python内置的一种装饰器,用来修饰方法。

我们可以使用@property来创建只读属性,@property装饰器会将方法转换为相同名称的只读属性,可以和它所定义的属性配合使用,可以防止属性被修改。

class People:
    def __init__(self, name, weight, height):
        self.name = name
        self.weight = weight
        self.height = height
    @property
    def bmi(self):
        return self.weight / (self.height ** 2)

man = People('张三',70,1.75)
# print(man.bmi())  # 我们都会采用这样的调用方法
print(man.bmi)  # 此时的bmi变成了一种属性

从上例可以看书@property装饰器把方法变成了属性,但是是否可以修改呢?我们继续看下一个例子。

import math
class Circle:
    def __init__(self,radius):  # 圆的半径radius
        self.radius = radius

    @property
    def area(self): # 圆的面积为 π×R²
        return math.pi * self.radius ** 2

    @property
    def zhouchang(self): # 圆的周长为 2πR
        return math.pi * self.radius * 2

c = Circle(10)
print('面积为:{}'.format(c.area))
print('周长为:{}'.format(c.zhouchang))
# 输出
面积为:314.1592653589793
周长为:62.83185307179586
c.area = 5  # 不可修改
# 输出
AttributeError: can't set attribute

4.3.1 @property的原理

class property(object):
    """
    Property attribute.
    
      fget
        function to be used for getting an attribute value
      fset
        function to be used for setting an attribute value
      fdel
        function to be used for del'ing an attribute
      doc
        docstring
    
    Typical use is to define a managed attribute x:
    
    class C(object):
        def getx(self): return self._x
        def setx(self, value): self._x = value
        def delx(self): del self._x
        x = property(getx, setx, delx, "I'm the 'x' property.")
    
    Decorators make defining new properties or modifying existing ones easy:
    
    class C(object):
        @property
        def x(self):
            "I am the 'x' property."
            return self._x
        @x.setter
        def x(self, value):
            self._x = value
        @x.deleter
        def x(self):
            del self._x
    """
    def deleter(self, *args, **kwargs): # real signature unknown
        """ Descriptor to change the deleter on a property. """
        pass

    def getter(self, *args, **kwargs): # real signature unknown
        """ Descriptor to change the getter on a property. """
        pass

    def setter(self, *args, **kwargs): # real signature unknown
        """ Descriptor to change the setter on a property. """
        pass

我们从class property(object)的源码就可以知道,@property是实现了三种方法,即:getter()setter()deleter()

4.4 封装部分总结

  1. 为什么要定义私有变量?

    • 不希望这个值被修改
    • 不希望这个值被访问到
    • 有些方法或者属性不希望被子类继承
  2. 私有变量不能被外部定义

  3. 私有变量不能被继承

  4. 广义的封装:把属性函数都放到类中

    狭义的封装:定义私有成员

  5. 类中的私有成员:

    • 私有静态方法
    • 私有对象属性
    • 私有方法

5. 总结

我是真的没想到,Python面向对象的三大特性居然有那么多内容,继承中有单继承、多继承,还有派生,涉及到重写父类方法和覆盖父类方法,如果不想被继承和派生还可以使用封装中的私有方法。

在继承中,涉及到钻石继承(菱形继承),Python2和Python3中的继承顺序思路并不相同。Python3使用的是广度优先策略,根据C3算法构造一个ORM列表,从左到右查找基类。

后续,我会结合博文中的关于Python的基础还有底层实现,画一张思维导图出来。一门语言中的东西实在是太杂太乱,一不小心就容易忘记。






博文的后续更新,请关注我的个人博客:星尘博客

猜你喜欢

转载自blog.csdn.net/u011130655/article/details/113019119