Python学习笔记四(面向对象)

传送门

python及pycharm安装配置-CSDN博客

Python学习笔记(一)-CSDN博客

Python学习笔记(二)-CSDN博客

Python学习笔记三(面向对象)-CSDN博客

python学习笔记五(面向对象实战版)-CSDN博客

目录

一、继承

1.1什么是继承

题外话:多继承的问题

1.2重写

1.3 super()函数

1.3.1调用父类的方法

1.3.2多重继承中的调用(硬核)

1.3.3 super的工作原理

二、多态

三、抽象类和接口


一、继承

1.1什么是继承

在上一章我们讲了python面向对象的基本概念以及封装、继承、多态三大特性中的封装,这个部门我们来说继承和多态。

还是举个栗子来引入继承。

猫和狗都属于动物。动物都会吃饭和喝水,也会跑,那么我们定义一个动物类,并且定义吃饭、喝水、跑这三种方法,现在我们需要定义猫类和狗类,我们照例也需要给猫类狗类定义吃饭、喝水、跑这三种方法。大家发现问题没,那如果我现在需要继续定义兔子,老虎,鸟,狮子等等几十个类,那就要复制粘贴几十中方法写到每个类中,费时费力,代码也看着头疼。

这个时候我们就用到继承机制,继承机制经常用于创建和现有类功能类似的新类,又或是新类只需要在现有类基础上添加一些成员(属性和方法),但又不想直接将现有类代码复制给新类。也就是说,通过使用继承这种机制,可以轻松实现类的重复使用。就像父母和孩子一样,你有父母的共同特征,所以,像动物类,我们称之为父类,猫、狗这些继承动物类的,我们称之为子类。

子类继承父类时,只需在定义子类时,将父类(可以是多个)放在子类之后的圆括号里即可。语法格式如下:

class 类名(父类1, 父类2, ...):
    #类定义部分

如果该类没有显式指定继承自哪个类,则默认继承 object 类(object 类是 Python 中所有类的父类,即要么是直接父类,要么是间接父类)。另外,Python 的继承是多继承机制,即一个子类可以同时拥有多个直接父类。 

以下例子中,Person同时继承People类和Animal类,所以同时有say方法和display方法。

class People:
    def say(self):
        print("我是一个人,名字是:",self.name)
class Animal:
    def display(self):
        print("人也是高级动物")
#同时继承 People 和 Animal 类
#其同时拥有 name 属性、say() 和 display() 方法
class Person(People, Animal):
    pass
zhangsan = Person()
zhangsan.name = "张三"
zhangsan.say()
zhangsan.display()

#输出结果
我是一个人,名字是: 张三
人也是高级动物

题外话:多继承的问题

事实上,大部分面向对象的编程语言,都只支持单继承,即子类有且只能有一个父类。而 Python 却支持多继承。我自己的体验是当时学java时觉得java的继承很清晰,代码也很容易看,而python的多继承有时候我会看着不习惯。和单继承相比,多继承容易让代码逻辑复杂、思路混乱,一直备受争议,中小型项目中较少使用。并且,和单继承相比,使用多继承经常需要面临的问题是,多个父类中包含同名的类方法。

对于这种情况,Python 的处置措施是:根据子类继承多个父类时这些父类的前后次序决定,即排在前面父类中的类方法会覆盖排在后面父类中的同名类方法。 

比如刚才那个例子,如果Animal中也有一个say方法,那么实际调用的是People里面的。

建议:虽然python支持多继承,但日常生活中我们还是尽量使用单继承,这样可以保证编程思路更清晰,也可以避免不必要的麻烦。

1.2重写

刚才在介绍继承的时候,可能已经有读者发现问题了,子类继承父类,即继承父类中的方法,那如果子类需要继承父类,但父类中有些方法不适用,难道就不能继承了吗?

其实也可以,比如我们写一个鸟类,鸟有翅膀,会飞,但是世界之大无奇不有,比如鸵鸟,有翅膀,但不会飞,这个时候我们就要重写父类方法。

重写即子类从父类继承得来的类方法中,大部分是适合子类使用的,但有个别的类方法,并不能直接照搬父类的,如果不对这部分类方法进行修改,子类对象无法使用。针对这种情况,我们就需要在子类中重复父类的方法。 

关于鸵鸟这个我们来写个代码:

class Bird:
    #鸟有翅膀
    def isWing(self):
        print("鸟有翅膀")
    #鸟会飞
    def fly(self):
        print("鸟会飞")

class Ostrich(Bird):
    # 重写Bird类的fly()方法
    def fly(self):
        print("鸵鸟不会飞")

# 创建Ostrich对象
ostrich = Ostrich()
#调用 Ostrich 类中重写的 fly() 类方法
ostrich.fly()

#运行结果
鸵鸟不会飞

可以看到,因为 Ostrich 继承自 Bird,因此 Ostrich 类拥有 Bird 类的 isWing() 和 fly() 方法。其中,isWing() 方法同样适合 Ostrich,但 fly() 明显不适合,因此我们在 Ostrich 类中对 fly() 方法进行重写。 在重写后Ostrich调用的是重写之后的 fly() 类方法。

好了,这个看似棘手的问题解决了,现在又出现一个新的问题,如果我们在子类中重写了从父类继承来的类方法,那么当在类的外部通过子类对象调用该方法时,Python 总是会执行子类中重写的方法。 那我们想调用父类中这个被重写的方法,怎么调用呢?

回答这个问题前,我们先介绍一个前面一直没有说但一直在用的概念。全局变量和局部变量,全局变量就是在函数外部定义的变量。所有函数内部都可以使用这个变量。局部变量是在函数内部定义的变量。这个变量只能在定义这个变量的函数内部使用。

一个python程序是一个全局空间,其中的类就相当于一个类空间,类空间里面的方法相当于全局空间的函数。那么刚才那个问题就是在全局空间中,调用类空间中的函数,此时我们只需要在调用该函数时备注类名即可。

class Bird:
    #鸟有翅膀
    def isWing(self):
        print("鸟有翅膀")
    #鸟会飞
    def fly(self):
        print("鸟会飞")
class Ostrich(Bird):
    # 重写Bird类的fly()方法
    def fly(self):
        print("鸵鸟不会飞")

# 创建Ostrich对象
ostrich = Ostrich()
#调用 Bird 类中的 fly() 方法
Bird.fly(ostrich)

#运行结果
鸟会飞

通过类名调用实例方法的这种方式,又被称为未绑定方法。即使用类名调用其类方法,Python 不会为该方法的第一个 self 参数自定绑定值,因此采用这种调用方法,需要手动为 self 参数赋值。 

注意和上面对比学习。 

同样的,如果有构造函数,也会调用从 People 继承来的构造函数。

class People:
    def __init__(self,name):
        self.name = name
    def say(self):
        print("我是人,名字为:",self.name)

class Animal:
    def __init__(self,food):
        self.food = food
    def display(self):
        print("我是动物,我吃",self.food)
#People中的 name 属性和 say() 会遮蔽 Animal 类中的
class Person(People, Animal):
    pass

per = Person("zhangsan")
per.say()
#per.display()

#运行结果
我是人,名字为: zhangsan

如果此时把per.display()的注释去掉,运行这行代码,则会进行报错 ,如图:

原因是从 Animal 类中继承的 display() 方法中,需要用到 food 属性的值,但由于 People 类的构造方法“遮蔽”了Animal 类的构造方法,使得在创建 per 对象时,Animal 类的构造方法未得到执行,所以程序出错。

面对这种情况,正确的做法是定义 Person 类自己的构造方法(等同于重写第一个直接父类的构造方法)。但如果在子类中定义构造方法,则必须在该方法中调用父类的构造方法。

在子类的构造方法中调用父类的构造方法有两种办法,一种就是在类的外部调用其中的实例方法,可以像调用普通函数那样,只不过需要额外备注类名,(上文提到的),另一种就是使用 super() 函数。接下来我们简单介绍一下super()函数。

1.3 super()函数

super函数格式:

super().__init__(...)
  1. 调用父类的方法: 当子类继承自父类,并且想要在子类中调用父类的方法时,可以使用 super()。这样可以确保在类继承结构中正确地调用相应的方法。

  2. 多重继承中的调用: 在多重继承的情况下,super() 可以确保按照继承结构的顺序正确地调用相应的方法,以避免冲突和混淆。

 咱们一个一个来说:

1.3.1调用父类的方法

当子类继承自父类,并且想要在子类中调用父类的方法时,可以使用 super()。这样可以确保在类继承结构中正确地调用相应的方法。

class Animal(object):
    def __init__(self, name):
        self.name = name

    def greet(self):
        print('Hello, I am %s.' % self.name)


class Dog(Animal):
    def greet(self):
        super(Dog, self).greet()  #调用父类Animal的greet方法
        print('WangWang...')


d=Dog("xiaohuang")

d.greet()


#输出结果
Hello, I am xiaohuang.
WangWang...

在上面,Animal 是父类,Dog 是子类,我们在 Dog 类重定义了 greet 方法,为了能同时实现父类的功能,我们又调用了父类的方法 。

1.3.2多重继承中的调用(硬核)

ps:对于初学者而言,可以先往后学,这一块涉及到一些底层原理,可以扫一眼,听不懂其实对于日常初级编程,没有很大影响,本来多重继承就让人晕晕乎乎,也不常用。感兴趣的可以看一看。

class Base(object):
    def __init__(self):
        print("enter Base")
        print("leave Base")


class A(Base):
    def __init__(self):
        print("enter A")
        super(A,self).__init__()
        print("leave A")


class B(Base):
    def __init__(self):
        print("enter B")
        super(B,self).__init__()
        print("leave B")

class C(A,B):
    def __init__(self):
        print("enter C")
        super(C,self).__init__()
        print("leave C")


c=C()

#输出结果
enter C
enter A
enter B
enter Base
leave Base
leave B
leave A
leave C

可能看到这个大家已经开始头疼了。实际就是C类继承自A,B,而A和B又分别继承Base类,每一个类的构造函数分别被调用了一次。 

这个栗子其实很好说明了一个问题,那就是super 和父类其实没有实质性的关联,不然enterA后的输出结果应该是enter Base,而不是enter B,(Base是A的父类)

想要搞清楚这个问题,我们应该了解一下super的原理。

1.3.3 MRO列表

首先介绍一下这个东西,在python中,对于你定义的每一个类,Python 会计算出一个方法解析顺序(Method Resolution Order, MRO)列表,它代表了类继承的顺序,我们可以使用下面的方式获得C类的 MRO 列表:

print(C.mro())

[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, 
<class '__main__.Base'>, <class 'object'>]

这个列表真实的列出了类C的继承顺序。C->A->B->Base->object(前面我们提到过,object是所有类的父类)。在方法调用时,是按照这个顺序查找的。
那这个 MRO 列表的顺序是怎么定的呢,它其实是通过一个 C3 线性化算法来实现的,这就越说越深了,为了不跑题,也不打扰大家学python的信心,这个就不说了,感兴趣的读者可以自己去了解一下,总的来说,一个类的 MRO 列表就是合并所有父类的 MRO 列表,并遵循以下三条原则:

  1. 子类永远在父类前面
  2. 如果有多个父类,会根据它们在列表中的顺序被检查
  3. 如果对下一个类存在两个合法的选择,选择第一个父类
1.3.3 super的工作原理

现在我们说super的工作原理:

def super(cls, inst):
    mro = inst.__class__.mro()
    return mro[mro.index(cls) + 1]

代码中,cls 代表类,inst 代表实例 ,那么该代码作用是什么呢?首先获取 inst 的 MRO 列表,然后查找 cls 在当前 MRO 列表中的 index, 并返回它的下一个类,即 mro[index + 1]当你使用 super(cls, inst) 时,Python 会在 inst 的 MRO 列表上搜索 cls 的下一个类。

那么,在刚才的栗子中,类 C 的 init 方法是super(C,self).__init__()。这里的self是C的实例。

MRO结果是:[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.Base'>, <class 'object'>]

所以当C执行完后,会执行A的init,并打印enter A,并执行super(A,self).__init__()。此时这个self也是C的实例,MRO 列表跟上面是一样的,搜索 A 在 MRO 中的下一个类,即B,于是就开始打印enter B。这也就解释了为什么不是打印enter Base。

二、多态

多态指的是一类事物有多种形态,一个抽象类有多个子类(因而多态的概念依赖于继承),不同的子类对象调用相同的方法,产生不同的执行结果,多态可以增加代码的灵活度

多态要满足这两个特性:

  1. 继承:多态一定是发生在子类和父类之间;
  2. 重写:子类重写了父类的方法。
    class Csdn:
        def say(self):
            print("Csdn类的say方法")
    class CPython(Csdn):
        def say(self):
            print("CPython类的say方法")
    class Cjava(Csdn):
        def say(self):
            print("Cjava类的say方法")
    
    a = Csdn()
    a.say()
    
    a = CPython()
    a.say()
    
    a = Cjava()
    a.say()
    
    
    Csdn类的say方法
    CPython类的say方法
    Cjava类的say方法

多态的意义:

对于一个变量,我们只需要知道他是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法(调用方只管调用,不管细节)
当需要新增功能,只需要新增一个Animal的子类实现run()方法,就可以在原来的基础上进行功能扩展,这就是著名的“开放封闭”原则。
“开放封闭”原则:

对扩展开放:允许新增Animal子类
对修改封闭:不需要修改依赖Animal类型的run()等函数。

三、抽象类和接口

抽象类是一种不能被实例化的类,它只能被用作其他类的基类。抽象类中可以定义抽象方法,但是不能实现它们。接口是一种只包含抽象方法的类,它用于定义类之间的通信协议。

抽象类不能被实例化,只能被继承,且子类必须实现抽象方法。

 默认情况下,Python 不提供抽象类。 Python 附带一个模块,该模块为定义抽象基类(ABC)提供了基础,该模块名称为 ABC。ABC 的工作方式是将基类的方法装饰为抽象,然后将具体类注册为抽象基的实现。当使用关键字@abstractmethod 修饰时,方法变得抽象。

举个例子:

from abc import ABC, abstractmethod
 
class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass
 
class Dog(Animal):
    def speak(self):
        return "Woof!"
 
class Cat(Animal):
    def speak(self):
        return "Meow!"
 
d = Dog()
c = Cat()
 
print(d.speak()) 
print(c.speak())  

#输出结果
Woof!
Meow!

为什么要用抽象类:

一个抽象类可以被认为是其他类的蓝图。它允许您创建一组必须在从抽象类构建的任何子类中创建的方法。包含一个或多个抽象方法的类称为抽象类。抽象方法是具有声明但没有实现的方法。在设计大型功能单元时,我们使用抽象类。当我们想为组件的不同实现提供通用接口时,我们使用抽象类。 

  • 38
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值