Python-类的继承

1、基本概念

1.1、简介

面向对象三要素之一,继承Inheritance
人类和猫类都继承自动物类。
个体继承自父母,继承了父母的一部分特征,但也可以有自己的个性。
在面向对象的世界中,从父类继承,就可以直接拥有父类的属性和方法,这样可以减少代码冗余、多复用。子类也可以定义自己的属性和方法。

1.2、不用继承的示例

复制代码
class Animal:
    def shout(self):
        print('Animal shouts')
a = Animal()
a.shout()

class Cat:
    def shout(self):
        print('Cat shouts')
c = Cat()
c.shout()
复制代码
上面的2个类虽然有关系,但是定义时并没有建立这种关系,而是各自完成定义。
动物类和猫类都会叫,但是它们的叫法有区别,所以分别定义。

1.3、使用继承的示例

复制代码
class Animal:
    def __init__(self, name):
        self._name = name

    def shout(self): # 一个通用的叫方法
        print('{} shouts'.format(self._name))

    @property
    def name(self):
        return self._name

a = Animal('monster')
a.shout()

class Cat(Animal):
    pass

cat = Cat('garfield')
cat.shout()
print(cat.name)

class Dog(Animal):
    pass

dog = Dog('ahuang')
dog.shout()
print(dog.name)
复制代码
上例可以看出,通过继承,猫类、狗类不用写代码,直接继承了父类的属性和方法。

1.4、继承

class Cat(Animal) 这种形式就是从父类继承,括号中写上继承的类的列表。
继承可以让子类从父类获取特征(属性和方法)

1.5、父类

Animal就是Cat的父类,也称为基类、超类。

1.6、子类

Cat就是Animal的子类,也称为派生类。

2、继承的定义

2.1、语法

复制代码
class 子类名(基类1[,基类2,...]):
    语句块

# 如果类定义时,没有基类列表,等同于继承自object。在Python3中,object类是所有对象的根基类。
class A:
    pass
# 等价于
class A(object):
    pass

注意,上例在Python2中,两种写法是不同的。
Python支持多继承,继承也可以多级。
复制代码

2.2、查看继承的特殊属性和方法

特殊属性和方法       含义
__bases__            # 类的基类元组
__base__             # 类的基类元组的第一项
__mro__              # 显示方法查找顺序,基类的元组
mro()                # 方法 同上,返回列表
__subclasses__()     # 类的子类列表
复制代码
class A:
    pass

print(A.__base__)
print(A.__bases__)
print()
print(A.mro())
print(A.__mro__)
print(int.__subclasses__())
print(bool.mro())
复制代码

3、Python不同版本的类

3.1、简介

Python2.2之前类是没有共同的祖先的,之后,引入object类,它是所有类的共同祖先类object。
Python2中为了兼容,分为古典类(旧式类)和新式类。
Python3中全部都是新式类。
新式类都是继承自object的,新式类可以使用super。

3.2、定义方法

 以下代码在Python2.x中运行

3.2.1、古典类(旧式类)

class A: pass

3.2.2、新式类

复制代码
class B(object): pass
print(dir(A))
print(dir(B))
print(A.__bases__)
print(B.__bases__)

b = B()
print(b.__class__)
print(type(b))
复制代码

3.2.3、古典类

a = A()
print(a.__class__)
print(type(a))  # <type 'instance'>

4、继承中的访问控制

4.1、代码示例

复制代码
class Animal:
    __a = 10
    _b = 20
    c = 30
    def __init__(self):
        self.__d = 40
        self._e = 50
        self.f = 60
        self.__a += 1
    def showa(self):
        print(self.__a)
        print(self.__class__.__a)
    def __showb(self):
        print(self._b)
        print(self.__a)
        print(self.__class__.__a)

class Cat(Animal):
    __a = 100
    _b = 200

c = Cat()
c.showa()
c._Animal__showb()

print(c.c)
print(c._Animal__d)
print(c._e, c.f, c._Animal__a)
print(c.__dict__)
print(c.__class__.__dict__.keys())
复制代码

4.2、实例属性查找顺序

从父类继承,自己没有的,就可以到父类中找。
私有的都是不可以访问的,但是本质上依然是改了名称放在这个属性所在类或实例的__dict__中。知道这个新名称就可以直接找到这个隐藏的变量,这是个黑魔法技巧,慎用。

实例属性查找顺序
实例的__dict__ → 类__dict__ →如果有继承→ 父类 __dict__
如果搜索这些地方后没有找到就会抛异常,先找到就立即返回了。

4.3、小结

继承时,公有成员,子类和实例都可以随意访问;私有成员被隐藏,子类和实例不可直接访问,但私有
变量所在的类内的方法中可以访问这个私有变量。
Python通过自己一套实现,实现和其它语言一样的面向对象的继承机制。

5、方法的重写、覆盖override

5.1、覆盖-示例

复制代码
class Animal:
    def shout(self):
        print('Animal shouts')

class Cat(Animal):
    # 覆盖了父类方法
    def shout(self):
        print('miao')

a = Animal()
a.shout()
c = Cat()
c.shout()
print(a.__dict__)
print(c.__dict__)
print(Animal.__dict__)
print(Cat.__dict__)
# Animal shouts
# miao
复制代码

5.2、调用父类-示例

复制代码
class Animal:
    def shout(self):
        print('Animal shout')

class Cat(Animal):
    # 覆盖了父类方法
    def shout(self):
        print('miao')

    # 覆盖了自身的方法,显式调用了父类的方法
    def shout(self):
        print(super())
        print(super(Cat, self))

        super().shout()
        super(Cat, self).shout()  # 等价于super()
        self.__class__.__base__.shout(self)  # 不推荐

a = Animal()
a.shout()
c = Cat()
c.shout()
print(a.__dict__)
print(c.__dict__)
print(Animal.__dict__)
print(Cat.__dict__)
复制代码
super()可以访问到父类的类属性。
静态方法和类方法,是特殊的方法,也是类属性,所以访问方式一样。

6、继承时使用初始化

6.1、继承调用示例

6.1.1、代码

复制代码
class A:
    def __init__(self, a):
        self.a = a


class B(A):
    def __init__(self, b, c):
        self.b = b
        self.c = c

    def printv(self):
        print(self.b)
        # print(self.a)  # 出错吗?


f = B(200, 300)
print(f.__class__.__bases__)
f.printv()
print(f.__dict__)
复制代码

6.1.2、分析

上例代码可知:
如果类B定义时声明继承自类A,则在类B中__bases__中是可以看到类A。
但是这和是否调用类A的构造方法是两回事。
如果B中调用了父类A的构造方法,就可以拥有父类的属性了。如何理解这一句话呢?
观察B的实例 f 的__dict__中的属性。

6.1.3、使用self传值示例

复制代码
class A:
    def __init__(self, a):
        self.a = a


class B(A):
    def __init__(self, b, c):
        A.__init__(self, b + c)  # 注意,self是谁,什么类型?是B的实例对象
        self.b = b
        self.c = c

    def printv(self):
        print(self.b)
        print(self.a)  # a=500


f = B(200, 300)
print(f.__class__.__bases__)
f.printv()
print(f.__dict__)
复制代码

6.2、示例1:调用父类的__init__方法

复制代码
class A:
    def __init__(self):
        self.a1 = 'a1'
        self.__a2 = 'a2'
        print('init in A')

class B(A):
    pass

b = B()
print(b.__dict__)

# init in A
# {'a1': 'a1', '_A__a2': 'a2'}
复制代码

6.3、示例2:调用子类的__init__不会调用父类的__init__

复制代码
class A:
    def __init__(self):
        self.a1 = 'a1'
        self.__a2 = 'a2'
        print('init in A')


class B(A):
    def __init__(self):
        self.b1 = 'b1'
        print('init in B')


b = B()
print(b.__dict__)

# B实例一旦定义了初始化__init__方法,就不会自动调用父类的初始化__init__方法,需要手动调用。
复制代码

6.4、示例3:super调父类__init__

复制代码
class A:
    def __init__(self):
        self.a1 = 'a1'
        self.__a2 = 'a2'
        print('init in A')


class B(A):
    def __init__(self):
        # super().__init__()
        # super(B, self).__init__()
        self.b1 = 'b1'
        print('init in B')
        A.__init__(self)


b = B()
print(b.__dict__)  # 注意__a2
复制代码

6.5、小结

如果在子类中覆盖了父类的__init__方法,那么在子类的__init__方法中,应该显式调用父类的__init__方法
Python中并不限制在子类的__init__方法中调用父类的__init__方法的位置,但一般都应该尽早的调用
推荐使用 super().__init__() 或 super(B, self).__init__()

7、继承

7.1、单继承

上面的例子中,类的继承列表中只有一个类,这种继承称为单一继承。
OCP原则:多用“继承”、少修改。对扩展开放,对修改封闭。
继承的用途:在子类上实现对基类的增强,实现多态

7.2、多继承

7.2.1、简介

一个类继承自多个类就是多继承,它将具有多个类的特征。

7.2.2、多继承弊端

复制代码
多继承很好的模拟了世界,因为事物很少是单一继承,但是舍弃简单,必然引入复杂性,带来了冲突。
如同一个孩子继承了来自父母双方的特征。那么到底眼睛像爸爸还是妈妈呢?孩子究竟该像谁多一点
呢?
多继承的实现会导致编译器设计的复杂度增加,所以有些高级编程语言舍弃了类的多继承。
C++支持多继承;Java舍弃了多继承。
Java中,一个类可以实现多个接口,一个接口也可以继承多个接口。Java的接口很纯粹,只是方法的声
明,继承者必须实现这些方法,就具有了这些能力,就能干什么。
多继承可能会带来二义性,例如,猫和狗都继承自动物类,现在如果一个类多继承了猫和狗类,猫和狗
都有shout方法,子类究竟继承谁的shout呢?
解决方案
实现多继承的语言,要解决二义性,深度优先或者广度优先。
复制代码

7.2.3、多继承的缺点

当类很多且继承复杂的情况下,继承路径太多,很难说清什么样的继承路径。
Python语法是允许多继承,但Python代码是解释执行,只有执行到的时候,才发现错误。
团队协作开发,如果引入多继承,那代码很有可能不可控。
不管编程语言是否支持多继承,都应当避免多继承。
Python的面向对象,我们看到的太灵活了,太开放了,所以要团队守规矩。

7.2.4、Python多继承实现

class ClassName(基类1, 基类2[, ...]):
    类体

左图是多继承(菱形继承),右图是单一继承
复制代码
多继承带来路径选择问题,究竟继承哪个父类的特征呢
Python使用MRO(method resolution order方法解析顺序)解决基类搜索顺序问题。
历史原因,MRO有三个搜索算法:
经典算法,按照定义从左到右,深度优先策略。2.2版本之前
左图的MRO是MyClass,D,B,A,C,A
新式类算法,是经典算法的升级,深度优先,重复的只保留最后一个。2.2版本
左图的MRO是MyClass,D,B,C,A,object
C3算法,在类被创建出来的时候,就计算出一个MRO有序列表。2.3之后支持,Python3唯一支持的算法
左图中的MRO是MyClass,D,B,C,A,object的列表
C3算法解决多继承的二义性
经典算法有很大的问题,如果C中有方法覆盖了A的方法,也不会访问到C的方法,因为先访问A的(深度优先)。
新式类算法,依然采用了深度优先,解决了重复问题,但是同经典算法一样,没有解决继承的单调性。
C3算法,解决了继承的单调性,它阻止创建之前版本产生二义性的代码。求得的MRO本质是为了线性化,且确定了顺序。
单调性:假设有A、B、C三个类,C的mro是[C, A, B],那么C的子类的mro中,A、B的顺序一致就是单调的。
复制代码

8、Mixin

8.1、简介

在Python的很多类的实现中,都可以看到一个Mixin的名字,这种类是什么呢?
类有下面的继承关系

 

文档Document类是其他所有文档类的抽象基类;
Word、Pdf类是Document的子类。

8.2、需求

为Document子类提供打印能力

8.3、在Document中提供print方法

复制代码
假设已经有了下面3个类

class Document:
    def __init__(self, content):
        self.content = content
    def print(self): # 抽象方法
        raise NotImplementedError()
class Word(Document): pass # 其他功能略去
class Pdf(Document): pass # 其他功能略去

基类提供的方法可以不具体实现,因为它未必适合子类的打印,子类中需要覆盖重写。
基类中只定义,不实现的方法,称为“抽象方法”。在Python中,如果采用这种方式定义的抽象方法,子类可以不实现,直到子类使用该方法的时候才报错。
print算是一种能力 —— 打印功能,不是所有的Document的子类都需要的,所有,从这个角度出发,上面的基类Document设计有点问题。
复制代码

8.4、需要打印的子类上增加

如果在现有子类Word或Pdf上直接增加,虽然可以,却违反了OCP的原则,所以可以继承后增加打印功能。因此有下面

复制代码
class Document: # 第三方库,不允许修改
    def __init__(self, content):
        self.content = content

class Word(Document): pass # 第三方库,不允许修改
class Pdf(Document): pass # 第三方库,不允许修改
# 单继承
class PrintableWord(Word):
    def print(self):
        print(self.content)
print(PrintableWord.__dict__)
print(PrintableWord.mro())
pw = PrintableWord('test string')
pw.print()
复制代码
看似不错,如果需要还要提供其他能力,如何继承?
例如,如果该类用于网络,还应具备序列化的能力,在类上就应该实现序列化。
可序列化还可能分为使用pickle、json、messagepack等。
这个时候发现,为了增加一种能力,就要增加一次继承,类可能太多了,继承的方式不是很好了。
功能太多,A类需要某几样功能,B类需要另几样功能,它们需要的是多个功能的自由组合,继承实现很繁琐。

8.5、Mixin代码

复制代码
class Document: # 第三方库,不允许修改
    def __init__(self, content):
        self.content = content

class Word(Document): pass # 第三方库,不允许修改
class Pdf(Document): pass # 第三方库,不允许修改

class PrintableMixin:
    def print(self):
        print(self.content, 'Mixin')

class PrintableWord(PrintableMixin, Word): pass
print(PrintableWord.__dict__)
print(PrintableWord.mro())
复制代码
Mixin就是其它类混合进来,同时带来了类的属性和方法。

9、Mixin类

复制代码
Mixin本质上就是多继承实现的。
Mixin体现的是一种组合的设计模式。
在面向对象的设计中,一个复杂的类,往往需要很多功能,而这些功能由来自不同的类提供,这就需要
很多的类组合在一起。
从设计模式的角度来说,多组合,少继承。
Mixin类的使用原则
Mixin类中不应该显式的出现__init__初始化方法
Mixin类仅实现单一功能,通常不能独立工作,因为它是准备混入别的类中的部分功能实现
Mixin类是类,也可以继承,其祖先类也应是Mixin类
使用时,Mixin类通常在继承列表的第一个位置,例如 class PrintableWord(PrintableMixin, Word): pass
Mixin类和装饰器,都可以实现对类的增强,这两种方式都可以使用,看个人喜好。
如果还需要继承就得使用Mixin类的方式。
复制代码

 

posted @   小粉优化大师  阅读(733)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示