Python教程(三):类&对象、闭包、装饰器、类型注解、MRO

Python总结(三)

本系列其他教程:

Python教程(一):基本语法、流程控制、数据容器
Python教程(二):函数、异常、模块&包、文件读取、常用模块

文章目录

  • Python总结(三)
    • 一、类&对象
      • [1.1 成员方法](#1.1 成员方法)
      • [1.2 构造方法](#1.2 构造方法)
      • [1.3 类属性&类方法&静态方法](#1.3 类属性&类方法&静态方法)
      • [1.4 魔术方法](#1.4 魔术方法)
      • [1.5 私有成员](#1.5 私有成员)
      • [1.6 继承](#1.6 继承)
        • [1.6.1 单继承&多继承](#1.6.1 单继承&多继承)
        • [1.6.2 复写](#1.6.2 复写)
        • [1.6.3 调用父类同名属性](#1.6.3 调用父类同名属性)
        • [1.6.4 多态和抽象](#1.6.4 多态和抽象)
      • [1.7 自定义属性](#1.7 自定义属性)
    • 二、类型注解
      • [2.1 基本用法](#2.1 基本用法)
      • [2.2 容器类型注解](#2.2 容器类型注解)
      • [2.3 type定义类型注解](#2.3 type定义类型注解)
      • [2.4 Union联合类型](#2.4 Union联合类型)
    • 三、闭包
    • 四、装饰器
    • 五、构造方法重写补充
      • [5.1 子类单继承没有定义构造方法](#5.1 子类单继承没有定义构造方法)
      • [5.2 子类单继承重写构造方法](#5.2 子类单继承重写构造方法)
      • [5.3 MRO](#5.3 MRO)
        • [5.3.1 子类多继承没有定义构造方法](#5.3.1 子类多继承没有定义构造方法)
        • [5.3.2 子类多继承重写构造方法](#5.3.2 子类多继承重写构造方法)

一、类&对象

使用 class 关键字来定义类,通过 对象=类名称() 来创建对象。

  • 对象也像变量一样,不用声明类型。
  • 创建对象的写法就像调用 "类函数" 一样。

1.1 成员方法

  • 类中定义的成员方法必须要有一个形参 self,且必须放在所有参数的第一位。
    • self 表示类对象自身的意思,在成员方法内想要访问成员变量必须使用 self 来访问。
    • 使用类对象调用成员方法的时候,可以当 self 参数不存在。
      • 当使用类对象调用成员方法时,self 会自动被 Python 传入。
python 复制代码
# 定义类
class Student:
    # 成员变量
    name = None

    # 成员方法,必须要有self参数
    def myMethod(self, address):
        # 通过self来访问成员变量
        print(f"大家好,我叫{self.name},我的地址是{address}")
        
# 创建对象
s1 = Student()
# 定义成员变量值
s1.name = 'Jay'
# 调用成员方法(当作self参数不存在)
s1.myMethod('Taiwan') # 大家好,我叫Jay,我的地址是Taiwan

1.2 构造方法

  • 构造方法在创建类对象的时候会自动调用。
    • Python中构造方法的名称固定为 __init__() ,前后各两个下划线。
    • 如果自己不定义构造方法,默认会有一个空参的构造方法;如果自己定义了构造方法,则默认的空参构造方法会失效。
  • 构造方法也属于成员方法,也必须有 self 参数。
  • 经常用构造方法在创建对象的时候给成员变量赋值。
    • 以下代码在类中并没有定义三个属性而构造方法直接使用了,下面会解释。
python 复制代码
class Student:

    # 构造方法
    def __init__(self, name, age, address):
        self.name = name
        self.age = age
        self.address = address

    def myInformation(self):
        print(self.name, self.age, self.address)

# 创建对象自动调用构造方法,self当作不存在
stu1 = Student('Jay', 30, 'Taiwan')
stu1.myInformation() # Jay 30 Taiwan

1.3 类属性&类方法&静态方法

  • 类属性:

    • 对象是从类创造的,它们的属性和方法是各个对象独有的,互相不能共享。
    • 而类也有属性和方法,且它们可以和类创建的所有对象共享,并且类本身不用创建对象也能访问。
    • 以下代码定义了类属性,而不是对象属性:
      • 访问类属性要使用类名,而不是self,self表示对象。
    python 复制代码
    class Student:
        # 以下都是类属性
        name = None
        age = None
        count = 0
    
        def __init__(self, name, age):
            # 访问类属性要使用类名,而不是self,self表示对象
            Student.count += 1
    
    print(Student.count) # 0
    stu1 = Student('Tom', 12)
    print(stu1.count) # 1
    stu2 = Student('Jack', 18)
    print(stu2.count) # 2
    print(Student.count) # 2
    # 可以发现所有对象和类本身都共享类属性count
  • 对象属性:

    • 使用 self 定义在 __init__ 构造方法中,每个对象独有。
    • 以下代码定义了对象属性,而不是类属性:
      • 访问对象属性要使用self,而不是类名。
    python 复制代码
    class Student:
        def __init__(self, name, age, address):
            self.name = name
            self.age = age
            self.address = address
    • 注意:定义了对象属性后一般不会再定义同名的类属性,这也是为什么第1.2节代码中没有定义类属性的原因。
  • 类方法:

    • 类方法也不需要创建对象,通过类名就可以访问,通过对象也可以访问。
    • 使用 @classmethod 装饰器定义,第一个参数是 cls,而不是 selfcls 表示类本身而不是对象本身,类方法中访问类中的其他类方法和类属性必须使用 cls
    python 复制代码
    class MyClass:
        # 类属性
        class_attribute = 2
    
        @classmethod
        def class_method(cls):
            # 类方法通过cls访问类属性
            return cls.class_attribute
    
    # 调用类方法
    print(MyClass.class_method()) # 2
    
    # 也可以通过实例调用类方法,但效果相同
    obj = MyClass()
    print(obj.class_method()) # 2
  • 对象方法:

    • 定义在类中,第一个参数是self,可以访问对象属性。
  • 静态方法:

    • 静态方法通过 @staticmethod 装饰器在类中定义。
    • 静态方法不使用 clsself 作为第一个参数。
    • 静态方法类和对象都可以访问。
    • 静态方法用于实现一些与类相关的功能,但在实现过程中不需要访问或修改类或对象的成员(不使用cls、self参数也没法访问),一般只是用来做与本类相关的一些逻辑运算。
    python 复制代码
    class MyClass:
        @staticmethod
        def static_method(arg1, arg2):
            return arg1 + arg2
    
    # 调用静态方法
    result = MyClass.static_method(5, 10)
    print(result)  # 输出: 15
    
    # 也可以通过实例调用静态方法,但效果相同
    obj = MyClass()
    result = obj.static_method(5, 10)
    print(result)  # 输出: 15

综合案例:

python 复制代码
class Car:
    # 类属性
    wheels = 4
    
    def __init__(self, brand, model, year):
        # 对象属性
        self.brand = brand
        self.model = model
        self.year = year

    # 类方法
    @classmethod
    def is_motor_vehicle(cls):
        return cls.wheels == 4

    # 对象方法
    def car_details(self):
        return f"{self.brand} {self.model} ({self.year})"

    # 静态方法
    # 不访问类或对象中的任何成员
    @staticmethod
    def calculate_mileage(distance, fuel_used):
        return distance / fuel_used

# 创建实例
car1 = Car("Toyota", "Camry", 2020)
car2 = Car("Honda", "Accord", 2019)

# 调用对象方法
print(car1.car_details())  # 输出: Toyota Camry (2020)
print(car2.car_details())  # 输出: Honda Accord (2019)

# 调用类方法
print(Car.is_motor_vehicle())  # 输出: True

# 调用静态方法
print(Car.calculate_mileage(500, 25))  # 输出: 20.0

# 访问类属性
print(Car.wheels)  # 输出: 4

# 访问对象属性
print(car1.brand)  # 输出: Toyota
print(car2.model)  # 输出: Accord

1.4 魔术方法

Python内置的对象方法称为魔术方法(第一个参数是self):

__str__

  • 用于控制输出对象的时候输出的内容,而不是输出没什么用的内存地址。
  • 该方法必须返回一个字符串,这个返回的字符串就是打印对象时输出的内容。
python 复制代码
# 没有定义__str__魔术方法
class Student1:
     def __init__(self, name, age):
         self.name = name
         self.age = age

stu1 = Student1('Jack', 12)
print(stu1) # <__main__.Student object at 0x0000016A8BDE5810>

# 使用__str__魔术方法
class Student2:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f'{self.name} {self.age}'

stu2 = Student2('Jack', 12)
# 输出魔术方法返回的内容
print(stu2) # Jack 12

__lt__

  • 直接进行对象的比较是会报错的,该方法用于支持对象之间的比较。
  • 该方法传入的第二个参数 other 指的是另一个参与比较的对象,而 self 参数指的是对象本身。
  • 该方法的返回值是 True 或 False。
python 复制代码
class Student1:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __lt__(self, other):
        return self.age < other.age

stu1 = Student1('Jack', 12)
stu2 = Student1('Jack', 15)
# stu2对应方法中的other参数
print(stu1 < stu2) # True

其他魔术方法:

  • __le__:用于支持对象之间进行比较的 <=>= 两种运算符。
    • 用法和 __lt__ 一模一样。
  • __eq__:用于支持对象之间进行比较的 == 运算符。
    • 用法和 __lt__ 一模一样。

1.5 私有成员

  • 私有成员不能被类对象访问和使用。
  • 私有成员能被类中的其他成员访问和使用。
  • 私有成员的定义只需要在方法或变量名之前加 __ 两个下划线即可。
python 复制代码
class Student1:
	#成员变量也可以有默认值
    __address = '北京'

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

    def myAddress(self):
        # 访问私有成员
        return self.__address

stu1 = Student1('Jack', 12)
print(stu1.__address) # 对象访问私有成员会报错
print(stu1.myAddress()) # 北京

1.6 继承

1.6.1 单继承&多继承
  • 继承表示子类从父类继承成员变量和方法(不含私有),然后子类拥有了继承下来的成员,可以直接使用(就当成子类中也定义了相同的成员)。
  • 写法:class 类名(父类名)
    • 可以多继承,多个父类名在括号中使用逗号隔开。
    • 如果继承下来的多个父类有同名的成员变量或方法,则以括号中从左到右的顺序来排优先级。

单继承:

python 复制代码
# 定义父类
class Phone:
    IMEI = None
    simCard = None

    def callBy4G(self):
        print('父类Phone的成员方法被调用')

# 子类继承父类
class XIAOMI(Phone):
    color = 'green'

x1 = XIAOMI()
print(x1.color) # green
# 子类对象可以直接调用继承下来的父类方法
x1.callBy4G() # 父类Phone的成员方法被调用

多继承:

python 复制代码
class Phone:
    IMEI = None
    simCard = None

    def callBy4G(self):
        print('父类Phone的成员方法被调用')

class NFCReader:
    producer = 'NVINDA'
    
    def readNFC(self):
        print('读取了NFC')
        
class HUAWEI(Phone, NFCReader):
    # 拥有了继承下来的两个父类的所有成员
    pass
1.6.2 复写
  • 子类可以复写和父类同名的变量或方法,重写后子类再调用就会调用重写后的而不是父类的。
  • 重写的方式是直接定义一个和父类中同名的新方法或新变量。
python 复制代码
class Phone:
    IMEI = None
    simCard = None
    name = "手机大王"

    def callBy4G(self):
        print('父类Phone的成员方法被调用')

# 子类继承父类
class XIAOMI(Phone):
    color = 'green'
    def __init__(self, name):
        # 从父类继承下来的属性被修改
        self.name = name

    # 进行了重写
    def callBy4G(self):
        print('子类XIAOMI的成员方法被调用')

x1 = XIAOMI("小米手机")
x1.callBy4G() # 调用的是重写后的方法而不是父类的
print(x1.name) # 小米手机
1.6.3 调用父类同名属性
  • 子类中调用父类属性有2种方式(仅针对属性,调用父类方法见下述):
    • 父类名.属性名
    • super().属性名
python 复制代码
class Phone:
    IMEI = None
    simCard = '父类的sim卡'

    def callBy4G(self):
        print('父类Phone的成员方法被调用')

class XIAOMI(Phone):
    color = 'green'
    simCard = '子类重写的sim卡'

    # 进行了重写
    def callBy4G(self):
        # 调用父类成员
        print(super().simCard)

x1 = XIAOMI()
x1.callBy4G() # 父类的sim卡
1.6.4 多态和抽象
  • 抽象指的是父类来确定有哪些方法和成员,但什么都不做(pass),具体的实现由子类重写决定。
  • 多态指的是参数接收父类,但其实传递的是子类对象,也就是父类抽象,子类实际工作。
python 复制代码
# 抽象类中定义抽象方法
class Animal:
    def noise(self):
        pass

# 定义子类
class Dog(Animal):
    def noise(self):
        print("I am a dog")

# 参数接受一个父类对象
def makeNoise(animal: Animal):
    animal.noise()

# 实际传递子类对象
makeNoise(Dog()) # I am a dog

1.7 自定义属性

  • 对于一些不需要任何参数,然后返回一个值的成员函数,可以使用 @property 装饰器来将它们变成"属性",以后这些方法就可以当作属性来使用,而不用调用函数。
  • 定义后函数名就是属性名。
python 复制代码
class Student:

    baseAge = 12

    @property
    def age(self):
        return self.baseAge * 2
    
stu1 = Student()
# 把age函数当成一个属性来使用了
print(stu1.age) # 24

二、类型注解

2.1 基本用法

  • 对代码实际执行没有任何影响,只是帮助第三方IDE工具做类型提示,如自动补全和类型检查。
  • 支持的范围:
    • 变量、对象的类型注解。
    • 函数的形参和返回值的类型注解。
  • 写法:
    • 变量、对象的注解直接在原本的代码基础上,变量或者对象之后加 : 类型 即可,其他赋值等代码都不动。
      • 对象的类型注解就是所属的类。
    • 方法的返回值注解:def 函数名() -> 返回值类型

2.2 容器类型注解

  • 元组类型设置类型详细注解,需要将每一个元素都标记出来。
  • 字典类型设置类型详细注解,需要2个类型,第一个是key第二个是value。

2.3 type定义类型注解

  • 除了使用 变量: 类型 这种语法做注解外,也可以在注释中进行类型注解。
    • 语法:# type: 类型

2.4 Union联合类型

  • 对于可以存放各种类型的容器来说,如果一个一个的把所有元素的类型都按顺序写出来会比较麻烦,可以使用联合类型指出所有可能的类型即可。

  • 用法:先从typing导包,然后 Union[所有可能的类型逗号隔开]

python 复制代码
from typing import Union

# data可能是int, str种的一种
def process_data(data: Union[int, str]) -> None:
    if isinstance(data, int):
        print(f"Processing integer: {data}")
    elif isinstance(data, str):
        print(f"Processing string: {data}")

process_data(42)
process_data("Hello")
  • isinstance 用来判断对象和类的关系,返回 True 或 False:
    • 第一个参数写对象,第二个参数写类。

三、闭包

  • 闭包指的是函数进行嵌套,内部函数使用外层函数的参数,并且外层函数返回值是内层函数本身。
  • 如果一个变量只是用来传递给仅一个函数使用,可以使用闭包增加安全性。
python 复制代码
def outer(num1: str):
    def inner(num2: str):
        # 内部函数可直接使用外层函数的参数
        print(f'我是{num2},我引用了外层的参数{num1}')
    # 外层函数直接返回内部函数本身
    return inner

# 此时out1就相当于inner函数,当作函数来使用
out1 = outer('Jay')
out1('Tom') # 我是Tom,我引用了外层的参数Jay
  • 如果不使用闭包,直接在inner之外定义num1变量,会存在被导包后被其他模块篡改num1变量的风险,而使用闭包避免这种风险,增加安全性。

  • 可以在内部函数中使用 nonlocal 关键字修饰外层函数的参数,从而使得内部函数可以修改这个参数的值。

    python 复制代码
    def outer(num1: int):
        def inner(num2: int):
            # 使用nonlocal关键字修饰外层函数的参数,从而可以修改他
            nonlocal num1
            num1 += num2
            return num1
        return inner
    
    fn = outer(10)
    print(fn(20)) # 30
    print(fn(20)) # 50
    • 注意:观察上方代码的最后两行,并不是两次输出都是30,那么得出结论:外层函数的变量在内层函数是共用的,也就是一旦外层函数的参数被修改,那么对内层函数不论调用几次都整体生效,不会重置。
  • 闭包优缺点:

    • 优点:
      • 无需定义全局变量即可通过内层函数持续访问、修改它的参数。
      • 增加代码安全性。
    • 缺点:
      • 内部函数持续引用外部函数的值,会导致这一部分内存空间不释放,占用内存(但是几个变量不释放的内存可以忽略不计)。

四、装饰器

  • 装饰器也是闭包,其作用是在不改变原有函数功能的基础上,为函数增加新功能。
  • 装饰器本质上是一个接受函数作为参数并返回内层函数的函数。也就是上述闭包模块的外层函数的参数不再是一个变量,而是一个函数。

举例:想要在调用sleep函数前后各执行一些代码:

python 复制代码
# 外层函数的参数接收一个函数
def outerFunc(func):
    def inner():
        print('睡觉前做的事情')
        func()
        print('睡觉后做的事情')
    return inner

def sleep():
    print('我要睡觉了啊')

inner = outerFunc(sleep)
inner()
"""
睡觉前做的事情
我要睡觉了啊
睡觉后做的事情
"""

代码可使用装饰器的语法糖来写:

python 复制代码
def outerFunc(func):
    def inner():
        print('睡觉前做的事情')
        func()
        print('睡觉后做的事情')
    return inner

# 使用@+外层函数名的方式
@outerFunc
def sleep():
    print('我要睡觉了啊')

# 直接调用sleep函数即可实现上述功能
sleep()
"""
睡觉前做的事情
我要睡觉了啊
睡觉后做的事情
"""
  • 在这个例子中,自定义的 @outerFunc 就是一个装饰器,sleep() 是被装饰器修饰的函数。调用 sleep() 其实是在调用外层函数 outerFunc() 的内层方法 inner(),且外层方法的参数是被修饰的 sleep() 函数。
  • sleep() 函数被修饰,所以调用 sleep() 函数就是修饰后的内容,而不是函数原本的内容。

总结一下具体写法:

  1. 想要修饰哪个函数就给哪个函数加装饰器。
  2. 定义一个与装饰器同名的外层函数,并接收一个参数(这个参数实际就是被修饰的函数)。
  3. 定义内层函数(这是将要被实际执行的函数),并在内层函数中使用外层函数的参数(被修饰的函数)。
  4. 直接调用被修饰的函数,运行结果就是被修饰后的代码(内层函数)。

注意:外层函数要在被修饰的函数之前定义,因为Python代码从上往下执行,否则Python识别不到当前行之后的内容。

五、构造方法重写补充

注意事项:

  • 在Python中,如果子类没有显式定义构造函数(__init__方法),当实例化子类对象时,Python 会自动调用父类的构造函数。这是 Python 的默认行为,以使用父类的构造方法提供的功能。
  • 如果需要在子类中添加更多的初始化逻辑,可以定义子类自己的 __init__ 方法,并且一定要在其中调用父类的 __init__ 方法,确保父类的初始化逻辑也能够执行。
  • 调用父类的属性(两种方式无差别):
    • 父类名.属性名
    • super().属性名
  • 调用父类的方法(无论构造方法还是普通方法都要遵守以下两个规则):
    • 父类名.方法名:一定需要显式传递 self 作为第一个参数。
    • super().方法名:一定不需要显示传递 self 作为第一个参数。因为 super() 会自动处理这个参数。在多继承情况下更有用,所以推荐以后都使用这种方式。

5.1 子类单继承没有定义构造方法

python 复制代码
class Base1:
    def __init__(self):
        print("父类1构造方法执行了!")

class Child(Base1):
    # 没有显示调用父类的构造方法
    pass

# 创建 Child 类的实例
child = Child()

输出:

tex 复制代码
父类1构造方法执行了!

5.2 子类单继承重写构造方法

使用super()调用:

python 复制代码
class Base1:
    def __init__(self):
        print("父类1构造方法执行了!")

    def normalMethod(self):
        print("调用了父类的普通方法")

class Child(Base1):
    def __init__(self):
        super().__init__()
        super().normalMethod()
        print("子类构造方法执行了!")

# 创建 Child 类的实例
child = Child()

输出:

tex 复制代码
父类1构造方法执行了!
调用了父类的普通方法
子类构造方法执行了!

使用父类名调用:

python 复制代码
class Base1:
    def __init__(self):
        print("父类1构造方法执行了!")

    def normalMethod(self):
        print("调用了父类的普通方法")

class Child(Base1):
    def __init__(self):
        Base1.__init__(self)
        Base1.normalMethod(self)
        print("子类构造方法执行了!")

# 创建 Child 类的实例
child = Child()

5.3 MRO

  • 在Python中,使用 super() 函数来调用父类的构造方法遵循了方法解析顺序(Method Resolution Order,MRO)。MRO是Python确定方法继承顺序的一种方式,它是基于C3线性化算法。
  • super() 的工作方式:在构造方法中使用 super().__init__() 时,Python会查找当前类的MRO来确定下一个类,并调用下一个类的构造方法。
  • 通过 类名.__mro__ 可以得到一个类的MRO顺序。

如何确定一个类的MRO呢?比如这段代码:

python 复制代码
class Base1:
    def __init__(self):
        print("父类1构造方法执行了!")

    def normalMethod(self):
        print("调用了父类1的普通方法")

class Base2:
    def __init__(self):
        print("父类2构造方法执行了!")

    def normalMethod(self):
        print("调用了父类2的普通方法")

class Child(Base1, Base2):
    def __init__(self):
        super().__init__()

print(Child.__mro__)
# 输出:(<class '__main__.Child'>, <class '__main__.Base1'>, <class '__main__.Base2'>, <class 'object'>)

1. 首先确定大方向

  • 当前基类一定是排在第一位的,然后考虑当前类的父类,按照父类引用的顺序,从左到右给父类排序。
  • 如果一个基类有多个父类,那么这个父类的MRO也会按照和子类相同的算法(C3线性化算法)来计算。
  • 所有的Python类最终都继承自 object 类,所以 object 是MRO中的最后一个元素。
  • 最后,Python将当前类、父类的MRO、以及 object 合并起来,形成当前类的MRO。

目前的MRO顺序:Child、Base1的MRO、Base2的MRO、object

2. 根据C3线性化算法,以下是确定两个父类MRO的详细步骤:

  • 对每个父类,列出它的MRO(这俩父类都没继承其他类):

    • Base1的MRO是[Base1, object]
    • Base2的MRO是[Base2, object]
  • 从两个父类列表的第一个元素开始,如果这个元素在其他列表中也是第一个元素,或者不在其他列表中,那么它就被添加到最终的MRO中。然后从所有包含这个元素的列表中移除这个元素。(可以简单的认为是在剔除重复已有元素)

  • 重复上述步骤,直到所有列表都为空。

故最终可以得到以下顺序:

  1. 选择 Base1(因为它在第一个列表中,并且不在其他列表中),最终MRO得到 [Child, Base1]。第一个列表的Object不做处理,因为它不满足条件。
  2. 然后选择第二个列表的 Base2(因为它不在其他列表中),最终MRO得到 [Child, Base1, Base2]
  3. 最后选择 object得到最终的MRO[Child, Base1, Base2, object]

如果父类也有继承的类:

python 复制代码
class BaseA:
    def __init__(self):
        print("BaseA init")

class BaseB:
    def __init__(self):
        print("BaseB init")

class Base1(BaseA):
    def __init__(self):
        super().__init__()
        print("Base1 init")

class Base2(BaseB):
    def __init__(self):
        super().__init__()
        print("Base2 init")

class Child(Base1, Base2):
    def __init__(self):
        super().__init__()
        print("Child init")

print(Child.__mro__)

输出:

tex 复制代码
(<class '__main__.Child'>, <class '__main__.Base1'>, <class '__main__.BaseA'>, <class '__main__.Base2'>, <class '__main__.BaseB'>, <class 'object'>)

注意:只有使用 super().__init__() 方式时才会使用到MRO的逻辑,如果通过父类名调用的话,和MRO无关。

5.3.1 子类多继承没有定义构造方法

如果子类没有定义构造方法,则子类会根据MRO的顺序,只有第一个父类的构造方法会被自动调用,其他父类的构造方法不会被调用:

python 复制代码
class Base1:
    def __init__(self):
        print("父类1构造方法执行了!")

class Base2:
    def __init__(self):
        print("父类2构造方法执行了!")

class Child(Base1, Base2):
    pass

child = Child()
#只会输出:父类1构造方法执行了!
5.3.2 子类多继承重写构造方法

注意:子类中如果通过父类名显示调用了父类的构造方法,则没有调用所有父类的构造方法也不会报错,因为Python支持选择性调用父类的构造方法。

多继承更推荐使用 super() 方式调用父类构造方法,因为只使用一个 super() 就可以自动加载所有的父类构造方法而不用一个个去调用:

python 复制代码
class Base1:
    def __init__(self):
        print("父类1构造方法执行了!")

class Base2:
    def __init__(self):
        print("父类2构造方法执行了!")

class Child(Base2, Base1):
    def __init__(self):
        super().__init__()
        print("Child init")

child = Child()

输出:

tex 复制代码
父类2构造方法执行了!
Child init

为什么子类使用了 super() 方式,但是只调用了一个父类的构造方法解释:

Child类的MRO顺序:(<class '__main__.Child'>, <class '__main__.Base2'>, <class '__main__.Base1'>, <class 'object'>)

  1. super().__init__() Child 的构造方法中被调用,它根据MRO调用 Base2 的构造方法。
  2. 但是 Base2 中没有使用 super() ,所以就不会再继续往上调用了(只有遇到了 super() 方法才会使用MRO逻辑)。

综合示例:

python 复制代码
class BaseA:
    def __init__(self):
        super().__init__()
        print("BaseA init")

class BaseB:
    def __init__(self):
        print("BaseB init")

class Base1(BaseA):
    def __init__(self):
        super().__init__()
        print("Base1 init")

class Base2(BaseB):
    def __init__(self):
        super().__init__()
        print("Base2 init")

class Child(Base1, Base2):
    def __init__(self):
        super().__init__()
        print("Child init")

# 创建 Child 类的实例
child = Child()

Child的MRO:

(<class '__main__.Child'>, <class '__main__.Base1'>, <class '__main__.BaseA'>, <class '__main__.Base2'>, <class '__main__.BaseB'>, <class 'object'>)

运行结果:

tex 复制代码
BaseB init
Base2 init
BaseA init
Base1 init
Child init

注意:调用完最上层的构造方法之后要开始回溯。

相关推荐
nqqcat~3 分钟前
函数的引用/函数的默认参数/函数的占位参数/函数重载
开发语言·c++·算法
getapi8 分钟前
cursor全栈网页开发最合适的技术架构和开发语言
开发语言·架构
daily_233319 分钟前
c++领域展开第十六幕——STL(vector容器的了解以及各种函数的使用)超详细!!!!
开发语言·c++·vector·visual studio code
努力学习的小廉26 分钟前
【C++】 —— 笔试刷题day_5
开发语言·c++
观无37 分钟前
C#的简单工厂模式、工厂方法模式、抽象工厂模式
java·开发语言·c#
前端开发张小七39 分钟前
Python 学习总结-2
python
蜡笔小新..1 小时前
开发、科研、日常办公工具汇总(自用,持续更新)
论文阅读·人工智能·python·插件·zotero
发财哥fdy1 小时前
3.12-3 html
人工智能·python·tensorflow
斯密码赛我是美女1 小时前
日志Python安全之SSTI——Flask/Jinja2
后端·python·flask
图图不是秃秃1 小时前
Java构造方法详解:从入门到实战
java·开发语言