本笔记参考视频为:https://www.bilibili/video/BV1ex411x7Em?p=101,有兴趣的可以直接移步B站

Python编程-高级

  • 1 面向对象(OPP)基本概念
    • 1.1 过程和函数
    • 1.2 面向过程和面向对象基本概念
  • 2 类和对象
    • 2.1 类和对象的概念
      • 2.1.1 类
      • 2.1.2 对象
    • 2.2 类和对象的关系
    • 2.3 类的设计
      • 2.3.1 类名的确定
      • 2.3.2 属性和方法的确定
  • 3 面向对象基础语法
    • 3.1 dir内置函数
    • 3.2 定义简单的类(只包含方法)
      • 3.2.1 定义只包含方法的类
      • 3.2.2 创建对象
      • 3.2.3 第一个面向对象程序
    • 3.3 方法中的self参数
      • 3.3.1 案例改造——类的外部给对象增加属性
      • 3.3.2 使用self在方法内部输出每一只猫的名字
    • 3.4 初始化方法
      • 3.4.1 之前代码存在的问题——在类的外部给对象增加属性
      • 3.4.2 初始化方法
      • 3.4.3 在初始化方法内部定义属性
      • 3.4.4 改造初始化方法——初始化的同时设置初始值
    • 3.5 内置方法和属性
      • 3.5.1 \_\_del\_\_方法(类似析构函数)
      • 3.5.2 \_\_str\_\_方法
  • 4 面向对象封装案例
    • 4.1 封装
    • 4.2 小明爱跑步
      • 4.2.1 小明爱跑步扩展——小美也爱跑步
    • 4.3 摆放家具
    • 4.4 封装案例2 —— 士兵突击
    • 4.5 身份运算符
  • 5 私有属性和私有方法
    • 5.1 应用场景及定义方式
    • 5.2 伪私有属性和私有方法
  • 6 继承
    • 6.1 单继承
      • 6.1.1 继承的概念、语法和特点
      • 6.1.2 方法的重写
      • 6.1 3 父类的私有属性和私有方法
    • 6.2 多继承
      • 6.2.1 多继承的使用注意事项
      • 6.2.2 新式类与旧式(经典)类
  • 7 多态
    • 7.1 多态案例演练
  • 8 单例
  • 9 异常
  • 10 模块和包
    • 10.1 模块
      • 10.1.1 模块的概念
      • 10.1.2 模块的几种导入方式
      • 10.1.3 模块的搜索顺序
      • 10.1.4 原则——每一个文件都应该是可以被导入的
    • 10.2 包

1 面向对象(OPP)基本概念

面向对象编程——Object Oriented Orogramming简写OOP

  • 之前学习的编程方式是面向过程
  • 面向过程和面向对象,是两种不同的编程方式

1.1 过程和函数

  • 过程是早期的一个编程概念
  • 过程类似于函数,只能执行,但是没有返回值
  • 函数不仅能执行,还可以返回结果

1.2 面向过程和面向对象基本概念

  • 面向过程
    • 把完成某一个需求的所有步骤从头到尾逐步实现
    • 根据开发需求,将某些功能独立的代码封装成一个又一个函数
    • 最后完成的代码,就是顺序地调用不同的函数
    • 特点:
      • 注重步骤与过程,不注重职责分工
      • 如果需求复杂,代码会变得很复杂
      • 开发复杂项目,没有固定的套路,开发难度很大!
  • 面向对象
    • 相比较函数,面向对象是更大的封装,根据职责在一个对象中封装多个方法
    • 在完成某一个需求前,首先确定职责——要做的事情(方法)
    • 根据职责确定不同的对象,在对象内部封装不同的方法(多个)
    • 最后完成的代码,就是顺序地让不同的对象调用不同的方法
    • 特点:
      • 注重对象和职责,不同的对象承担不同的职责
      • 更加适合应对复杂的需求变化,是专门应对复杂项目开发,提供的固定套路
      • 需要在面向过程的基础上,再学习一些面向对象的语法

2 类和对象

2.1 类和对象的概念

对象是面向对象编程的两个核心概念

2.1.1 类

  • 类是对一群具有相同特征或者行为的事物的一统称,是抽象的,不能直接使用
    • 特征被称为属性
    • 行为被称为方法
  • 类就相当于制造飞机时的图纸,是一个模板,是负责创建对象的

2.1.2 对象

  • 对象是由类创建出来的一个具体存在,可以直接使用
  • 由哪一个类创建出来的对象,就拥有在哪一个类中定义的属性和方法
  • 对象就相当于用图纸制造的飞机

在程序开发中,应该现有类,再有对象

2.2 类和对象的关系

  • 类是模板,对象是根据类这个模板创建出来的,应该先有类,再有对象
  • 类只有一个,而对象可以有很多个
    • 不同的对象之间属性可能会各不相同
  • 类中定义了什么属性和方法,对象中就有什么属性和方法,不可能多,也不可能少

2.3 类的设计

在使用面向对象开发前,应该首先分析需求,确定以下程序中需要包含那些类
以植物大战僵尸游戏为例分析类的设计如下图

在程序开发中,要设计一个类,通常需要满足以下三个要素

  1. 类名:这类事物的名字,满足大驼峰命名法
  2. 属性:这类事物具有什么样的特征
  3. 方法:这类事物具有什么样的行为

2.3.1 类名的确定

名词提炼法:分析整个业务流程,出现的名词,通常就是找到的类

2.3.2 属性和方法的确定

  • 对对象的特征描述,通常可以定义成属性
  • 对象具有的行为(动词),通常可以定义成方法

3 面向对象基础语法

3.1 dir内置函数

ipython中快速确认某个东西是对象的方法:
1.在标识符/数据后输一个.,然后按下TAB键,ipython会提示该对象能够调用的方法列表(只能看到一些常用的方法列表)
2.使用内置函数dir传入标识符/数据,可以查看对象内的所有属性及方法


提示:__方法名__格式的方法是Python提供的内置方法/属性,以下是一些常用的内置方法/属性

序号方法名类型作用
01__new__方法创建对象时,会被自动调用
02__init__方法对象被初始化时,会被自动调用
03__del__方法对象从内存被销毁前,会被自动调用
04__str__方法返回对象的描述信息,print函数输出使用

3.2 定义简单的类(只包含方法)

3.2.1 定义只包含方法的类

语法格式

class 类名:
    def 方法1(self,参数列表):
        pass
        
    def 方法2(self,参数列表):
        pass
  • 方法的定义格式和之前学习过的函数几乎一样
  • 区别在于第一个参数必须是self

3.2.2 创建对象

语法格式

对象变量 = 类名()

3.2.3 第一个面向对象程序

需求
小猫鱼,小猫

分析

  1. 定义一个猫类Cat
  2. 定义两个方法eatdrink
  3. 按照需求——不需要定义属性

    示例
# 创建猫类
class Cat:
    def eat(self):
        print("小猫爱吃鱼")

    def drink(self):
        print("小猫要喝水")

# 创建一个对象
Tom = Cat()

Tom.drink()
Tom.eat()

结果

引用概念的强调

在面向对象开发中,引用的概念是同样适用的!

  • 在python中使用类创建对象之后,tom变量中仍然记录的是对象在内存中的地址
  • 也就是tom变量引用了新建的猫对象
  • 使用print输出对象变量,默认情况下,是能够输出这个变量引用的对象是由哪一个类创建的对象,以及在内存中的地址

提示:在计算机中,通常使用十六进制表示内存地址

  • %d可以以10进制输出数字
  • %x可以以16进制输出数字

3.3 方法中的self参数

3.3.1 案例改造——类的外部给对象增加属性

  • 在Python中,要给对象设置属性,非常的容易,但是不推荐使用,因为对象属性的封装应该封装在类的内部
  • 只需要在类的外部代码中直接通过.设置一个属性即可

以上一节中的代码为例,在创建了小猫对象Tom = Cat()之后,想要从类外部给Tom增加一个姓名属性,可以直接在执行代码出输入Tom.name = "Tom"即可

3.3.2 使用self在方法内部输出每一只猫的名字

想要在类内部调用某个属性,可以直接self.属性名

哪一个对象调用的方法,self就是哪一个对象的引用。self类似C++里面的this指针

  • 在类封装的方法内部,self就表示当前调用方法的对象自己
  • 调用方法时,程序员不需要传递self函数
  • 在方法内部
    • 可以通过self.访问对象的属性
    • 也可以通过 self.调用其他的对象方法

示例

# 创建猫类
class Cat:
    def eat(self):
        print("%s 爱吃鱼" % self.name)

    def drink(self):
        print("%s 要喝水" % self.name)

# 创建一个对象
Tom = Cat()

Tom.name = "Tom"

Tom.drink()
Tom.eat()

结果

3.4 初始化方法

3.4.1 之前代码存在的问题——在类的外部给对象增加属性

执行上述程序解释器会报错,因为在执行eat()drink()方法的时候还没有name属性

3.4.2 初始化方法

  • 当使用类名()创建对象时,会自动执行以下操作:
    • 为对象在内存中分配空间——创建对象
    • 为对象的属性设置初始值——初始化方法(init)
  • 这个初始化方法就是__init__方法,__init__是对象的内置方法

__init__方法是专门用来定义一个类具有那些属性的方法

3.4.3 在初始化方法内部定义属性

  • __init__方法内部使用self.属性名=属性的初始值就可以定义属性
  • 定义属性之后,再使用Cat类创建的对象,都会拥有该属性

示例

# 创建猫类
class Cat:
    def __init__(self):
        self.name = "Jerry"

    def eat(self):
        print("%s 爱吃鱼" % self.name)

    def drink(self):
        print("%s 要喝水" % self.name)

# 创建一个对象
Tom = Cat()


Tom.drink()
Tom.eat()

结果

3.4.4 改造初始化方法——初始化的同时设置初始值

  • 在开发中,如果希望在创建对象的同时,就设置对象的属性,可以对__init__方法进行改造
    • 把希望设置的属性值,定义成__init__方法的参数
    • 在方法内部使用self.属性=形参接受外部传递的参数
    • 在创建对象时,使用类名(属性1, 属性2...)调用

示例

# 创建猫类
class Cat:
    def __init__(self, name_):
        self.name = name_

    def eat(self):
        print("%s 爱吃鱼" % self.name)

    def drink(self):
        print("%s 要喝水" % self.name)

# 创建一个对象
Tom = Cat("汤姆")
Tom.drink()
Tom.eat()

lazy_cat = Cat("大懒猫")
lazy_cat.drink()
lazy_cat.eat()

结果

3.5 内置方法和属性

序号方法名类型作用
01__del__方法对象被从内存中销毁前,会被自动调用
02__str__返回对象的描述信息,print函数输出使用

3.5.1 __del__方法(类似析构函数)

  • 在Python中
    • 当使用类名()创建对象时,为对象分配空间后,自动调用__init__方法
    • 当一个对象被从内存中销毁前,会自动调用__del__方法
  • 应用场景
    • __init__改造初始化方法,可以让创建对象更加灵活
    • __del__如果希望在对象销毁前,再做一些事情,可以考虑以下该方法
  • 生命周期
    • 一个对象从调用类名()创建,生命周期开始
    • 一个对象的__del__方法一旦被调用,生命周期结束
    • 再对象的生命周期内,可以访问对象属性,或者让对象调用方法

示例

# 创建猫类
class Cat:
    def __init__(self, name):
        self.name_ = name

        print("%s 来了" % self.name_)

    # 定义析构函数
    def __del__(self):

        print("%s 走了" % self.name_)

# 创建一个对象
tom = Cat("Tom")

print(tom.name_)

print("-" * 20)

结果

分析
tom是一个全局变量,只有在整个程序结束之后才会被销毁,因此会先打印----------,在执行析构函数中的语句

Python中有一个关键字del,可以直接销毁变量,若在打印-------之前销毁tom变量,则会先执行析构函数中的语句,代码如下:

# 创建猫类
class Cat:
    def __init__(self, name):
        self.name_ = name

        print("%s 来了" % self.name_)

    # 定义析构函数
    def __del__(self):

        print("%s 走了" % self.name_)

# 创建一个对象
tom = Cat("Tom")

print(tom.name_)

del(tom)

print("-" * 20)

结果:

3.5.2 __str__方法

  • 在Python中,使用print输出对象变量,默认情况下,会输出这个变量引用的对象是由哪一个类创建的对象,以及在内存中的地址(十六进制表示)[见3.2.3]
  • 如果在开发中,希望使用print输出对象变量时能够打印自定义的内容,就可以利用__str__这个内置方法了

注意:__str__方法必须返回一个字符串

# 创建猫类
class Cat:
    def __init__(self, name):
        self.name_ = name

        print("%s 来了" % self.name_)

    # 定义析构函数
    def __del__(self):

        print("%s 走了" % self.name_)

    def __str__(self):
        # 必须返回一个字符串
        return "我是小猫 %s" % self.name_

# 创建一个对象
tom = Cat("Tom")

print(tom)

如上例,在使用print函数直接打印对象tom时,输出了__str__中返回的内容

4 面向对象封装案例

4.1 封装

  1. 封装时面向对象编程的一大特点
  2. 面向对象编程的第一步 —— 将属性方法封装到一个抽象的
  3. 外界使用类创建对象,然后让对象调用方法
  4. 对象方法的细节都被封装在类的内部

4.2 小明爱跑步

需求

  1. 小明 体重75.0公斤
  2. 小明每次跑步会减肥0.5公斤
  3. 小明每次吃东西体重会增加1公斤


示例

class Person:
    def __init__(self, name, weight) -> None:
        self.name_ = name
        self.weight_ = weight

    def __str__(self) -> str:
        return "我的名字是 %s,我的体重是 %.2f 公斤" % (self.name_, self.weight_)

    def run(self):
        print("跑步好,跑步减肥")
        self.weight_ -= 0.5

    def eat(self):
        print("没吃饱哪有力气减肥")
        self.weight_ += 1

xiaoming = Person("小明", 75.0)

xiaoming.eat()
xiaoming.run()

print(xiaoming)

结果

4.2.1 小明爱跑步扩展——小美也爱跑步

需求

  1. 小明和小美都爱跑步
  2. 小明体重75.0公斤
  3. 小美体重45.0公斤
  4. 每次跑步都会减少0.5公斤
  5. 每次吃东西都会增加1公斤

示例

class Person:
    def __init__(self, name, weight) -> None:
        self.name_ = name
        self.weight_ = weight

    def __str__(self) -> str:
        return "我的名字是 %s,我的体重是 %.2f 公斤" % (self.name_, self.weight_)

    def run(self):
        print("跑步好,跑步减肥")
        self.weight_ -= 0.5

    def eat(self):
        print("没吃饱哪有力气减肥")
        self.weight_ += 1

xiaoming = Person("小明", 75.0)

xiaoming.eat()
xiaoming.run()

print(xiaoming)

xiaomei = Person("小美", 45.0)

xiaomei.eat()
xiaomei.run()

print(xiaomei)

结果

4.3 摆放家具

需求

  1. 房子(House)有户型总面积家具名称列表
    • 新房子中没有任何的家具
  2. 家具(HouseItem)有名字占地面积,其中
    • 席梦思(bed)占地4平米
    • 衣柜(chest)占地2平米
    • 餐桌(table)占地1.5平米
  3. 将以上三件家具添加到房子中
  4. 打印房子时,要求输出户型、总面积、剩余面积、家具名称列表


示例

class HouseItem:
    def __init__(self, name, area) -> None:
        self.name = name
        self.area = area

    def __str__(self) -> str:
        return "[%s] 占地 %.2f " % (self.name, self.area)


class House:
    def __init__(self,house_type, area) -> None:
        self.house_type = house_type
        self.area = area

        # 剩余面积
        self.free_area = area

        # 家具列表
        self.item_list = []

    def __str__(self) -> str:
        return "户型:%s\n总面积:%.2f[剩余:%.2f]\n家具:%s" % (self.house_type, self.area, self.free_area,self.item_list)

    def add_item(self, item):
        print("要添加的家具:%s" % item)

        # 判断能否装下
        if item.area > self.free_area:
            print("家具太大,房子装不下")

            return
        # 更新家具列表
        self.item_list.append(item.name)

        # 更新剩余面积
        self.free_area -= item.area

bed = HouseItem("席梦思", 4)
chest = HouseItem("衣柜", 2)
table = HouseItem("餐桌",1.5)

my_home = House("两室一厅",60)

my_home.add_item(bed)
my_home.add_item(chest)
my_home.add_item(table) 

print(my_home)

结果:

4.4 封装案例2 —— 士兵突击

需求

  1. 士兵 许三多有一把AK47
  2. 士兵可以开火
  3. 能够发射子弹
  4. 枪装填子弹——增加子弹数量

定义没有初始值的属性
在定义属性时,如果不知道设置什么初始值,可以设置为None

  • None关键字表示什么都没有
  • 表示一个空对象,没有方法和属性,是一个特殊的变量
  • 可以将None赋值给任何一个变量

fire方法需求

  • 判断是否有枪,没有枪没法冲锋
  • 喊一声口号
  • 装填子弹
  • 射击

示例

class Gun:
    def __init__(self, type) -> None:
        self.type = type

        self.bullet_count = 0

    def add_bullet(self, count):
        self.bullet_count += count

    def shoot(self):
        # 判断有无子弹
        if self.bullet_count <= 0:
            print("没子弹啦!!!")
            return
        
        # 发射子弹
        self.bullet_count -= 1

        # 提示发射信息
        print("[%s] biubiubiu~! [%d]" % (self.type, self.bullet_count))

class Soldier:
    def __init__(self, name) -> None:
        self.name = name

        # 新兵一开始没有枪
        self.gun = None

    def fire(self):
        # 判断是否有枪
        if self.gun == None:
            print("[%s] 还没有枪" % self.name)
            return

        # 喊一声口号
        print("同志们,冲啊!!")

        # 装填子弹
        self.gun.add_bullet(40)

        # 射击
        self.gun.shoot()


ak47 = Gun("AK47")

xusanduo = Soldier("许三多")
xusanduo.gun = ak47
xusanduo.fire()

结果

4.5 身份运算符

身份运算符用于比较两个对象的内存地址是否一致——是否是同一个对象的引用

  • Python中针对None比较时,建议使用is判断
运算符描述实例
isis是判断两个标识符是不是引用同一个对象x is y,类似id(x) == id(y)
is notis not是判断两个标识符是不是引用不同对象x is not y,类似id(a)!=id(b)

is 与 == 区别
is用于判断两个变量引用对象是否为同一个
==用于判断引用变量的值是否相等

5 私有属性和私有方法

5.1 应用场景及定义方式

应用场景

  • 在实际开发中,对象的某些属性或方法可能只希望在对象的内部被使用,而不希望再外部被访问到
  • 私有属性就是对象不希望公开的属性
  • 私有方法就是对象不希望公开的方法

定义方式
在定义属性或方法时,在属性名或者方法名前增加两个下划线,定义的就是私有属性或方法



5.2 伪私有属性和私有方法

提示:在日常开发中,不要使用这种方式,访问对象的私有属性或私有方法

在Python中,并没有真正意义的私有

  • 在给属性、方命名时,实际是对名称做了一些特殊处理,使得外界无法访问
  • 解释器的处理方式:在名称前面加上_类名=>_类名_名称

6 继承

面向对象三大特性

  1. 封装根据职责将属性和方法封装到一个抽象类中
  2. 继承实现代码的重用,相同的代码不需要重复的编写
  3. 多态不同的对象调用相同的方法,产生不同的执行结果,增加代码的灵活度

6.1 单继承

6.1.1 继承的概念、语法和特点

继承的概念:子类拥有父类的所有方法属性


1)继承的语法
class 类名(父类名):

  • 子类继承自父类,可以直接享受父类中已经封装好的方法,不需要再次开发
  • 子类中应该根据职责,封装子类特有的属性和方法

2)专业术语

  • Dog类是Animal类的子类Animal类是Dog类的父类Dog类从Animal继承
  • Dog类是Animal类的派生类Animal类是Dog类的基类Dog类从Animal派生

3)继承的传递性

  • C类从B类继承,B类又从A类继承
  • 那么C类就有B类和A类的所有属性和方法

子类拥有父类以及父类的父类中封装的所有属性方法

6.1.2 方法的重写

应用场景
父类的方法实现不能满足子类需求时,可以对方法进行重写(overwrite)

重写父类方法有两种情况

  1. 覆盖父类的方法
  2. 对父类的方法进行扩展

1)覆盖父类的方法

  • 如果在开发中,父类的方法实现子类的方法实现,完全不同
  • 就可以使用覆盖的方式,在子类中重新编写父类的方法实现

具体的实现方式,就相当于在子类中定义了一个和父类同名的方法并且实现

重写之后,在运行时,只会调用子类中重写的方法,而不再会调用父类封装的方法

2)对父类方法进行扩展

  • 如果在开发中,子类的方法实现中包含父类的方法实现
    • 父类原本封装的方法实现是子类方法的一部分
  • 就可以使用扩展的方式
    1. 在子类中重写父类的方法
    2. 在需要的位置使用super().父类方法来调用父类方法的执行
    3. 代码其他的位置针对子类的需求,编写子类特有的代码实现

关于super

  • 在Python中,super是一个特殊的类
  • super()就是使用super类创建出来的对象
  • 最常使用的场景就是在重写父类方法时,调用在父类中封装的方法实现

示例

class Animal:
    def eat(self):
        print("吃!")

    def drink(self):
        print("喝!")

    def run(self):
        print("跑!")

    def sleep(self):
        print("睡!")

class Dog(Animal):
    def bark(self):
        print("汪汪汪")

class XiaoTianQuan(Dog):

    def bark(self):
        
        # 针对子类特有的需求,编写代码
        print("哮天犬叫!")

        # 使用super(). 调用原本在父类中封装的方法
        super().bark()

        # 增加其他子类的代码
        print("吃月亮!")


xtq = XiaoTianQuan()

xtq.bark()

结果

调用父类方法的另外一种方式

在Python2.x中,如果需要调用父类的方法,还可以使用以下方式:
父类名.方法(self)

  • 这种方式,目前在Pyrhon3.x还支持这种方式
  • 这种方式不推荐使用,因为一旦父类发生变化,方法调用位置的类名同样需要修改

提示

  • 在开发时,父类名super()两种方式不要混用
  • 如果使用当前子类名调用方法,会形成递归调用,出现死循环

6.1 3 父类的私有属性和私有方法

  1. 子类对象不能在自己方法内部,直接访问父类的私有属性或私有方法
  2. 子类对象可以通过父类的公有方法间接访问到私有属性私有方法
    • 私有属性、方法时对象的隐私,不对外公开,外界以及子类都不能直接访问
    • 私有属性、方法通常用于做一些内部的事情

  • B的对象不能直接访问__num2属性
  • B的对象不能再demo方法内访问__num2属性
  • B的对象不可以在demo方法内,调用父类的test方法
  • 父类的test方法内部,能够访问__num2属性和__test方法

6.2 多继承

概念

  • 子类可以拥有多个父亲,并且具有所有父类的属性和方法

    语法
    class 子类名(父类名1, 父类名2...):

6.2.1 多继承的使用注意事项

问题提出
如果不同的父类中存在同名的方法,子类对象在调用方法时,会调用哪一个父类中的方法呢?

提示:在开发时,应该尽量避免这种容易产生混淆的情况!如果父类之间存在同名的属性或者方法,应该尽量避免使用多继承

Python中的MRO——方法搜索顺序

  • Python中针对提供了一个内置属性__mro__可以查看方法的搜索顺序
  • MRO指method resolution order, 主要用于在多继承判断方法、属性的调用路径

print(C.__mro__)

  • 在搜索方法时,是按照__mro__的输出结果从左至右的顺序查找的
  • 如果在当前类中找到方法,就直接执行,不在搜索
  • 如果没有找到,就查找下一个类中是否有对应的方法,如果找到,就直接执行,不再搜索
  • 如果找到最后一个类,还是没有找到方法没程序报错

示例

class A:
    def test(self):
        print("A -- test")

    def demo(self):
        print("A -- demo")

class B:
    def test(self):
        print("B -- test")

    def demo(self):
        print("B -- demo")

class C(B, A):
    pass

c = C()

c.test()
c.demo()

print(C.__mro__)

结果

分析:
首先看print(C.__mro__)的结果,其顺序是自己的类,继承的父类B,继承的父类A,object类(所有类的基类),所以在调用test()demo()时,现在自己类里面找,没有,再去父类B找,找到了,执行

6.2.2 新式类与旧式(经典)类

object是Python为所有对象提供的基类,提供有一些内置的属性和方法们可以使用dir函数查看

  • 新式类:以object为基类的类,推荐使用
  • 经典类:不以object为基类的类,不推荐使用
  • 在Python 3.x 中定义类时,如果没有指定父类,会默认使用object作为该类的基类 —— Python 3.x 中定义的类都是新式类
  • 在Python 2.x 中定义类时,如果没有指定父类,则不会以object作为基类

新式类和经典类在多继承时 —— 会影响到方法的搜索顺序

为了保证编写的代码能够同时在Python 2.x 和Python 3.x 运行,今后在定义类时,如果没有父类,建议统一继承自object

7 多态

面向对象三大特性

  1. 封装根据职责将属性和方法封装到一个抽象的类中
  2. 继承实现代码的重用,相同的代码不需要重复的编写
  3. 多态不同的子类对象调用相同的父类方法,产生不同的执行结果
    • 多态可以增加代码的灵活度
    • 继承重写父类方法为前提
    • 是调用方法的技巧,不会影响到类的内部设计

7.1 多态案例演练

需求

  1. Dog类中封装方法game
    • 普通狗只是简单的玩耍
  2. 定义XiaoTianDog继承自Dog,并且重写game方法
    • 哮天犬需要在天上玩耍
  3. 定义Person类,并且封装一个和狗玩的方法
    • 在方法内部,直接让狗对象调用game方法

示例

from pyexpat.model import XML_CTYPE_SEQ
from unicodedata import name


class Dog:
    def __init__(self, name) -> None:
        self.name = name

    def game(self):
        print("%s 蹦蹦跳跳地玩耍.." % self.name)

class XiaoTianDog(Dog):
    def game(self):
        print("%s 飞到天上去玩耍.." % self.name)

class Person:
    def __init__(self, name) -> None:
        self.name = name

    def game_with_dog(self, dog):
        print("%s 和 %s 快乐地玩耍.." % (self.name, dog.name))

        dog.game()


wangcai = Dog("旺财")

xtq = XiaoTianDog("哮天犬")

xiaoming = Person("小明")

xiaoming.game_with_dog(wangcai)

xiaoming.game_with_dog(xtq)
    

结果

分析
Person的方法里,写的其实是和父类的Dog玩耍,而解释器会根据实际传入的对象来决定调用父类还是某一个子类,这就是多态

8 单例

9 异常

10 模块和包

10.1 模块

10.1.1 模块的概念

  • 模块是Python程序架构的一个核心概念
    • 每一个以扩展名py结尾的Python源码文件都是以恶搞模块
    • 模块名同样也是一个标识符,需要负和标识符的命名规则
    • 在模块中定义的全局变量函数都是提供给外界直接使用的工具
    • 模块就好比是工具包,要想使用这个工具包中的工具,就需要先导入这个模块

10.1.2 模块的几种导入方式

  1. import导入

    • import 模块名1, 模块名2(不推荐)
    • 根据Python最新的书写规范,每个导入的模块应该单独占一行
      • import 模块1
      • import 模块2
    • 导入之后
      • 使用模块名.的方式就可以使用模块提供的工具——全局变量、函数、类
    • import导入时指定别名
      • 如果模块的名字太长,可以使用as指定模块的名称,以方便在代码中使用
      • import 模块名1 as 模块别名
      • 注意:模块别名应该符合大驼峰命名法

  2. from..import导入

    • 如果希望从某一个模块中,导入部分工具,就可以使用from...import的方式
    • import 模块名一次性把模块中所有的工具全部导入,并且通过模块名/别名 访问
    • from 模块名1 import 工具名
    • 导入之后
      • 不需要通过模块名.
      • 可以直接使用模块提供的工具——全局变量、函数、类
    • 注意:如果两个模块存在同名的函数,那么后导入模块的函数,会覆盖掉先导入的函数

    • 开发时import代码应该统一写在代码的顶部,更容易及时发现冲突
    • 一旦发现冲突,可以使用as关键字给其中一个工具起一个别名
  3. from...import * 一次性导入所有工具

    • from 模块名1 import *

    注意:这种方式不推荐使用,因为函数重名并没有任何的提示,出现问题不好排查、

    • 方法3与方法1的区别:(同样是一次性导入所有工具)
      • 方法1在调用时需要使用模块名.
      • 方法3可以直接使用所导入的全局变量、函数、类

10.1.3 模块的搜索顺序

Python解释器在导入模块时,会:

  1. 搜索当前目录指定模块名的文件,如果有就直接导入
  2. 如果没有,再搜索系统目录

注意:在开发时,给文件起名,不要和系统的模块文件重名

Python中每一个模块都有一个内置属性_file_可以查看模块的完整路径

10.1.4 原则——每一个文件都应该是可以被导入的

  • 每一个独立的python文件就是一个模块
  • 在导入文件时,文件中所有没有任何缩进的代码都会被执行一遍

实际开发场景

  • 在实际开发中,每一个模块都是独立开发的,大多都有专人负责
  • 开发人员通常会在模块下方增加一些测试代码
    • 仅在模块内部使用,而被导入到其他文件中不需要执行

通过上述描述,发现一个矛盾点:正常自己写代码时候,会有执行代码,但是当这一部分代码被当作模块使用时,只需要全局变量、函数和类,执行代码不需要。单上在import这个模块时,会把不需要的执行代码全部运行一遍
目标:找到一种方式,可以在单独测试代码的时候正常运行,而当作模块使用的时候不执行测试代码

  • __name__属性

    • __name__属性可以做到,测试模块的代码只在测试情况下被运行,而在被导入时不会被执行
    • __name__是Python的一个内置属性,记录着一个字符串
    • 如果是被其他备件导入的, __name__就是模块名
    • 如果是当前执行的程序, __name__就是 __main__
  • 通过这个属性,可以判断文件的代码是用于测试还是被当作模块,因此可以把测试代码放在如下的一个if语句下

    def main():
    	# 测试代码
    	pass
    	
    if __name__ = '__main__':
    	main()
    

10.2 包

  • 概念
    • 包是一个包含多个模块的特殊目录
    • 目录下有一个特殊的文件__init__. py
    • 包名的命名方式和变量名一致,小写字母+_
  • 好处
    • 使用import 包名可以一次性导入包中所有的模块
  • __init__.py
    • 要在外界中使用中的模块,需要在__init__.py中指定对外界提供的模块列表
      # 从 当前目录 导入 模块列表
      from . import 模块名1
      from . import 模块名2
      

更多推荐

Python自学笔记——高级篇(面向对象)