视频讲解:

https://space.bilibili.com/70431433?spm_id_from=333.1007.0.0

文章和代码:

https://github.com/zyf-ngu/Qmatter

和对象

类和对象是Python编程的核心构件,类有2个核心的组件,一是变量,类中称为属性,二是函数,类中称为方法,属性就是变量,方法就是函数,只是在类里重新起了个名字。

类可以理解为更高层次的函数,把某一类对象都有的通用行为抽象出来,比如猫科动物类;对象是类的实例化,比如实例化一个猫。一个类可以通过继承的方式在保留原类的不断扩展独有的一些特点

Python中,类在定义时,使用class关键字,类名首字母大写类名后可以加括号,括号可以为空。不为空的话一般是写继承的基类名

属性

基本概念

前面介绍类的属性就是变量,那么属性有哪些种类呢?又该如何赋值呢?

属性种类:

属性包括类属性和实例属性,类属性就是所有实例都有的,在类的一级结构下,可通过类名和实例名访问,实例属性在init初始化方法里,只能通过实例名访问实例属性又包括私有属性,前面加双下划线,只能在类的方法中访问,实例名无法访问。

类的实例也可以作为属性给类添加的细节越来越多:属性和方法清单以及文件都越来越长。在这种情况下,可能需要将类的一部分作为一个独立的类提取出来。然后把这个独立的类作为大类的一个属性。

属性赋值

类属性在类定义时会赋值(默认值),类的所有实例都会共享这个初始值;

实例属性在_init__方法里赋值,self.instance_attirbute=0。 值的来源有2种 ,一是直接给定初始值,可以为0或者空字符串等;二是使用_init__括号里的形参(也就意味着实例属性可以和括号里的形参数量不一致。)但我们知道,形参只是符号,没有具体值,真正的值来自调用函数传过来的实参。

那么怎么调用__init__方法呢?

答案就是在类的实例化时,__init__方法会自动调用,因此实例化时,类名括号里的参数应该和__init__方法形参一致(有默认值的可不用)。如果有继承,那么也需要包括父类的__init__方法形参

类的属性和方法的参数的比较:

特征

属性(Attribute)

方法参数(Parameter)

定义位置

类内部直接定义或通过__init__初始化

方法定义时的括号内声明

生命周期

随对象存在,直到对象销毁

仅在方法调用期间存在

数据流向

存储对象状态数据

传递外部数据到方法内部

访问方式

通过self.属性名访问

在方法内直接使用参数名访问

修改权限

通常通过方法间接修改

作为输入数据不可直接修改(除非是可变对象)

实例属性是在实例化时确定的,能够根据实例化时传递的参数给不同的实例赋予不同的属性值,灵活性较高;而类属性的初始值在类定义时就确定了,可以当做常量和默认值使用。

属性访问和修改

这2种属性都可以通过实例名访问和动态修改类属性还可以直接通过类名访问修改使用句点表示法实例名.属性

类属性修改所有的实例对象访问得到的结果都会随着修改;而实例属性的话,每个实例都有属于自己的属性副本,一个实例对象修改不会影响其他实例对象的访问结果。

可以以三种不同的方式修改属性的值:

直接通过实例进行修改(不推荐,不安全)

通过方法进行设置(推荐)

通过方法进行递增(增加特定的值)。

代码实战

# 类属性和实例属性
class BaseClass:
    # 类属性
    class_attribute='这是类属性'

    def __init__(self,instance_attribute):
        # 实例属性
        self.instance_attribute1=instance_attribute  # 通过形参赋值,类实例化时传入
        self.__instance_attribute2=0  # 可以直接初始化,不用通过形参赋值,且是私有属性,实例名和类名不能直接访问

# 类实例化,类名后的参数需要和__init__方法的形参一致
obj1=BaseClass(instance_attribute='instance_value1')
obj2=BaseClass(instance_attribute='instance_value2')


# 类属性访问,包括类名和实例名
print('类名访问类属性:',BaseClass.class_attribute)
print('实例名访问类属性:',obj1.class_attribute)
print('实例名访问类属性:',obj2.class_attribute)


# 实例属性访问
# print('类名访问实例属性:',BaseClass.instance_attribute1) # 报错,类名不能访问实例属性
print('实例名访问实例属性:',obj1.instance_attribute1)
print('实例名访问实例属性:',obj2.instance_attribute1)
# print('实例名访问实例属性:',obj2.instance_attribute2) # 报错,私有属性不能直接访问


# 类属性修改
BaseClass.class_attribute='类直接修改后的类属性'
print('实例名访问修改后的类属性:',obj1.class_attribute) # 输出:类直接修改后的类属性
obj1.class_attribute='实例直接修改后的类属性'
print('另一个实例名访问实例修改后的类属性:',obj2.class_attribute)  # 输出:类直接修改后的类属性

# 实例属性修改
obj1.instance_attribute1='实例修改后的实例属性'
print('实例名访问修改后的实例属性:',obj1.instance_attribute1)  # 输出:实例修改后的实例属性
print('另一个实例名访问修改后的实例属性:',obj2.instance_attribute1)  # 输出:instance_value2(实例属性没变)

方法

类中的函数称为方法;有关函数的一切都适用于方法,就目前而言,唯一重要的差别是调用方法的方式即需要在调用的方法前面加实例/对象名,以表明是哪个实例对象调用的,因为同一个方法名不同的实例可能实现的不一样

方法种类

方法又包括基本方法初始化方法__init__类方法@classmethod静态方法@staticmethod,基本方法和初始化方法有第一个隐参数self,类方法有第一个隐参数cls,静态方法无隐参数,不需要访问实例属性。

1. __init__() 初始化方法

__init__()是一个特殊的初始化方法,每当根据类创建新实例时,Python都会自动运行它。在这个方法的名称中,开头和末尾各有两个下划线,这是一种约定,表示类私有

方法__init__()定义中,形参self必不可少,还必须位于其他形参的前面。为何必须在方法定义中包含形参self呢?因为Python调用这个__init__()方法来创建实例时,将自动传入实参self。每个与类相关联的方法调用都自动传递实参self,它是一个指向实例本身的引用,让实例能够访问类中的属性和方法。

2.基本方法

类中的基本方法的第一个形参都是self,可以有其它的形参,若有,则调用时需要按照普通函数那样传入对应的实参;类中调用基本方法:self.+名字。类外调用:实例名.方法名。

3.类方法

方法上面使用@classmethod装饰器表示,第一个参数为cls,使用类名.方法名进行调用,也可以通过类的实例来调用。更常见和推荐通过类名调用。

可以访问和修改类属性(cls.class_attribute)。不能直接访问或修改特定实例的属性(因为没有 self 指向实例)。

4.静态方法

方法上面使用@staticmethod装饰器表示,参数只需要正常形参即可,使用类名.方法名进行调用。

定义独立于类和实例状态的辅助功能不能直接访问或修改类属性或实例属性(因为没有 cls 或 self)。如果需要操作数据,必须通过参数显式传递。本质上就是一个放在类里面的普通函数仅仅因为逻辑上属于这个类而放在这里。比如执行与类相关但不依赖于类或实例具体状态的辅助计算、格式化、验证等。

5.抽象方法

方法上面使用@abstractmethod,需要继承抽象类ABC。具体见下面的抽象继承。

继承

如果要编写的类是另一个现成类的特殊版本,可使用继承类名括号内写另一个类的名字,即为继承,括号内的类为父类/基类,新建的为子类。子类自动获得父类的属性和方法,并可通过扩展或修改实现新功能。

class Parent:             # 父类/基类

    pass

class Child(Parent):      # 子类/派生类 (Parent 写在括号中)

    pass

继承的功能意义

继承就是子类默认拥有基类完整的属性和方法,然后又可以改动和新增自己的属性和方法,还可以重写父类的方法,只需要与要重写的父类方法同名。这样子类实例化对象后直接调用子类的方法而不是父类的方法,若没有重写,则仍调用父类的方法这点在实际项目中经常遇到,子类继承父类之后,实例化对象调用的方法在子类的代码里没找到,大概率就是父类有这个方法,子类未重写。

注意一个特殊情况,即子类实例调用父类的其中一个方法A,该方法调用父类的另一个方法B,而子类重写了方法B,那么子类实例调用方法A的时候最终也会调用子类的方法B。这就是类的多态性。

class Parent:
    def methodA(self):
        print("Parent: methodA called")
        self.methodB()  # 此处实际调用子类重写的 methodB
    def methodB(self):
        print("Parent: methodB called")
class Child(Parent):
    def methodB(self):  # 重写父类的 methodB
        print("Child: methodB called")
child = Child()
child.methodA()  # 调用父类的 methodA 但输出 Child: methodB called  # 实际调用子类重写的 methodB

继承的核心功能如下

功能

说明

示例代码片段

代码复用

子类自动获得父类所有非私有属性和方法

child = Child()
child.parent_method()

功能扩展

子类添加新属性和方法

class Child(Parent):
    def new_method(self):...

方法重写

子类定义同名方法覆盖父类实现

class Child(Parent):
    def method(self):...

多态支持

不同子类对同一方法有不同实现

obj.method() 根据实际对象类型调用对应实现

接口标准化

通过抽象基类强制子类实现特定方法

from abc import ABC, abstractmethod

继承的种类

单继承子类只继承一个父类(最基础形式)

class Animal:
    def speak(self):
        print('动物会发出声音')
class Dog(Animal):
    def spark(self):
        print('小狗会汪汪叫')

dog=Dog()
dog.speak()  # 调用父类的方法, 输出:动物会发出声音
dog.spark()  # 调用自己的方法,输出:小狗会汪汪叫

多继承子类同时继承多个父类(Python 特色)

class Flyer:

    def fly(self):

        print("在空中飞行")

class Swimmer:

    def swim(self):

        print("在水里游泳")

class Duck(Flyer, Swimmer):  # 多继承

    def quack(self):

        print("嘎嘎嘎!")

duck = Duck()

duck.fly()   # 来自 Flyer -> 在空中飞行

duck.swim()  # 来自 Swimmer -> 在水里游泳

duck.quack() # 子类自有方法 -> 嘎嘎嘎!

多层继承;形成继承链(祖父 → 父 → 子)

class Vehicle:

    def transport(self):

        print("运输工具")

class Car(Vehicle):

    def run(self):

        print("在公路上行驶")

class ElectricCar(Car):  # 多层继承

    def charge(self):

        print("电能驱动")

tesla = ElectricCar()

tesla.transport()  # 继承自 Vehicle -> 运输工具

tesla.run()        # 继承自 Car -> 在公路上行驶

tesla.charge()     # 自有方法 -> 电能驱动

菱形继承(钻石问题):多继承中父类有共同祖先(通过 MRO 算法解决)

class A:
    def show(self):
        print('A')
class B(A):
    def show(self):
        print('B')
class C(A):
    def show(self):
        print('C')
class D(B,C):
    # def show(self):
    #     print('D')  # (1)如果子类重写方法,则直接调用自己的方法
    pass              # (2)如果子类不重写方法,则按照MRO顺序调用父类祖先类方法
d=D()
d.show()  # 重写输出D;不重写:输出B
print('D.mro:',D.mro())   # D.mro: [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

抽象基类继承:强制子类实现特定方法(接口约束)

from abc import ABC, abstractmethod

class Shape(ABC):  # 抽象基类

    @abstractmethod

    def area(self):  # 抽象方法

        pass

class Circle(Shape):

    def __init__(self, radius):

        self.radius = radius

    

    def area(self):  # 必须实现抽象方法

        return 3.14 * self.radius ** 2

# shape = Shape()  # 报错:不能实例化抽象类

circle = Circle(5)

print(circle.area())  # 78.5

方法解析顺序(MRO)

MRO(Method Resolution Order)是Python中用于在多继承中确定方法调用顺序的算法。

1. 什么是MRO? MRO是一个顺序列表,它定义了在多继承中,当子类调用一个方法时,Python解释器搜索该方法的顺序。 在Python中,每个类都有一个__mro__属性,它是一个列表,按照方法解析顺序列出了该类及其所有基类。

其核心功能如下:

解决菱形继承问题,保证子类调用方法时按照合适的顺序,保证方法调用的确定性和一致性避免父类方法被多次调用或遗漏调用(在super()的使用中体现)。

2. MRO的原理(C3线性化算法 Python使用C3线性化算法来计算MRO。该算法遵循以下三个原则:

- 子类优先于父类:例如,在class C(A, B)中,A和B是父类,则C的MRO中C本身在最前面。

- 继承图中保持基类的顺序:例如,class C(A, B)中,A在B前面,那么在MRO中A的基类也会在B的基类前面(前提是不违反子类优先)。

- 单调性:若类 X 在类 Y 前,则 X 的所有子类也在 Y 前。

算法步骤(简述):

- 对于每个类,它的MRO是它自身加上其父类的MRO的合并

- 合并规则: 给定类定义 class D(B, C):计算 L(D) = D + merge(L(B), L(C), [B, C])

merge 操作

取第一个列表的头部元素

如果该元素不在其他列表的尾部(非第一个位置)

则添加到结果中并从所有列表中移除

否则,检查下一个列表的头

重复直到所有列表为空

3. 计算示例

class A:

Pass

class B(A):

Pass

class C(A):

Pass

class D(B, C):

pass

# 计算过程:

L(A) = [A]

L(B) = [B] + merge(L(A)) = [B, A]

L(C) = [C] + merge(L(A)) = [C, A]

L(D) = [D] + merge(L(B), L(C), [B, C])

      = [D] + merge([B,A], [C,A], [B,C])

# 第一步:取 B(在第一个列表头,且不在其他列表尾

      = [D, B] + merge([A], [C,A], [C])

# 第二步:取 C(在第二个列表头,且不在其他列表尾)

      = [D, B, C] + merge([A], [A])

# 第三步:取 A

      = [D, B, C, A]

任何类都可以通过 类名.mro() 或 类名.__mro__ 查看 MRO 顺序:

print(D.mro())# 输出: [<class '__main__.D'>, <class '__main__.B'>, #        <class '__main__.C'>, <class '__main__.A'>, #        <class 'object'>]

当 MRO 无法计算时,Python 会抛出类型错误:TypeError: Cannot create a consistent method resolution order (MRO) for bases A, B

super() 函数

前面介绍MRO算法的时候讲到,它可以解决多继承中的方法调用顺序问题,事实上,MRO算法只是提供了继承关系的解析顺序,而真正按照这个顺序完成方法调用的是super()函数。

super()函数用于在子类中调用父类(超类)的方法,包括初始化方法。它会根据方法解析顺序(MRO)动态地确定要调用的父类方法,从而解决了多继承中的方法调用顺序问题

super() 的核心原理

super() 基于 MRO(Method Resolution Order,方法解析顺序) 工作:具体来说:super() 在当前类的 MRO 中查找下一个类,返回一个代理对象,通过该对象调用父类方法。

1. 基本用法:调用父类方法

先看一个最基本的用法,在子类init方法中调用父类的init方法,

class Animal:

    def __init__(self, name):

        self.name = name

        print(f"Animal initialized: {self.name}")

class Dog(Animal):

    def __init__(self, name, breed):

        super().__init__(name)  # 使用super()调用父类构造方法

        # Animal.__init__(self,name) # 硬编码即直接使用父类名称调用,需要加个self

        self.breed = breed

        print(f"Dog initialized: {self.breed}")

dog = Dog("Buddy", "Golden Retriever")"""

输出:

Animal initialized: Buddy

Dog initialized: Golden Retriever

"""

同理可进行扩展,在子类的普通方法调用父类的普通方法,在子类的类方法里调用父类的类方法。但是静态方法中不能使用super()函数,因为无法绑定实例

# 多继承
class Camera:
    def take_photo(self):
        print("Taking photo")
class Phone:
    def make_call(self):
        print("Making call")
class SmartPhone(Camera, Phone):
    def use_features(self):
        # super().take_photo()  # 使用super 调用 Camera 的方法
        # super().make_call()   # 使用super调用 Phone 的方法
        Camera.take_photo(self)  # 直接使用父类名称调用 Camera 的方法
        Phone.make_call(self)  # 直接使用父类名称调用 Phone 的方法

phone = SmartPhone()
phone.use_features()

那么super()函数的优势或者说到底解决了什么问题呢?难度仅仅是不用写每个父类的名称而统一使用super().调用吗?

前面讲到super()函数是解决了菱形继承的调用顺序问题,我们先看一下不使用super()函数会出现什么问题:

4. 菱形继承问题解决方案

class A:
    def process(self):
        print("Processing in A")
class B(A):
    def process(self):
        print("Processing in B")
        super().process()
        # A.process(self)
class C(A):
    def process(self):
        print("Processing in C")
        super().process()
        # A.process(self)
class D(B, C):
    def process(self):
        print("Processing in D")
        # super().process()
        B.process(self)
        C.process(self)
    def process2(self):
        print("Processing2 in D")
        super().process()
        # B.process(self)
        # C.process(self)
d = D()
d.process2()

结合上面的代码我们来分析一下使用super()函数的作用:

(1)第一种情况,所有的类都不使用super()函数,那么第一个问题就是当类D多继承B和C的时候,需要分别写出B.process(self)和C.process(self),顺序需要我们手动写;然后运行的时候会输出:D-B-A-C-A,也就是说是按照B.process(self)和C.process(self)依次运行的,也就导致B和C分别调用了一次A;

(2)第二种情况,最后的子类D使用super()函数,其它类使用父类名称,则输出:D-B-A,可以看到,遗漏了类C的方法,这是因为D找到B之后直接使用父类名称调用了A就结束了。但这种情况适用于需要根据条件明确使用不同的父类,分条件调用父类方法。

(3)第三种情况,全部使用super()函数:则输出D-B-C-A,这说明是按照MRO提供的解析顺序调用父类方法的;

(4)第四种情况:最后的子类在不同名称的方法内部调用父类的方法,输出的顺序和上面一致。这说明什么?说明super()只是一个调用方法,可以在任意一个子类方法中使用,只需要标注要调用的父类方法。前面三种情况是为了完整介绍super()函数调用父类方法的顺序,但在实际项目中,子类继承的多个父类不可能都拥有同样名称的方法,因此并不是说super()函数只能在同名方法中使用。

(5)第五种情况,B或C不使用super()函数,也不使用父类名称调用,则会输出D-B,停止到不继续调用的类,这意味着在子类调用某个方法时,super()函数只是按照MRO算法的顺序去寻找这个方法,找到之后会执行;但并不是所有同名方法都会被执行(那子类方法重写就没有意义了),而是只有这个方法内部又调用了super()或父类名称,那么就会继续执行MRO链中下一个类的同名方法,以此类推,直到整个MRO链被遍历完或者某个方法没有调用super()而终止。

深度学习中使用 super() 的原因分析

在深度学习框架(如 PyTorch)中,super() 被广泛用于网络构建尤其实是在初始化方法里,主要原因如下:

1. 确保父类初始化正确执行

深度学习模型通常继承自框架的基类(如 nn.Module),这些基类包含关键初始化逻辑

import torch.nn as nn

class MyModel(nn.Module):

    def __init__(self):

        super().__init__()  # 必须调用父类初始化

        self.layer = nn.Linear(10, 5)

    

    def forward(self, x):

        return self.layer(x)

不调用 super().__init__() 的后果

模型无法注册子模块(参数不被识别)

无法正确转移到 GPU

无法保存/加载模型状态

2. 支持模块化设计

深度学习网络常采用分层结构super() 实现各层的协作

class BaseBlock(nn.Module):

    def __init__(self, in_channels):

        super().__init__()

        self.conv = nn.Conv2d(in_channels, in_channels, 3, padding=1)

    def forward(self, x):

        return self.conv(x)

class ResidualBlock(BaseBlock):

    def __init__(self, in_channels):

        super().__init__(in_channels)  # 初始化基础卷积   

    def forward(self, x):

        return x + super().forward(x)  # 调用父类forward并添加残差连接

继承 vs 组合

继承 (is-a 关系)

组合 (has-a 关系)

关系

"是一个" (狗是动物)

"有一个" (汽车有发动机)

代码

class Dog(Animal):

class Car:
  def __init__(self):
    self.engine = Engine()

优点

代码复用性强

降低耦合度

缺点

可能产生深度耦合

需显式调用组件方法

原则

优先使用组合,继承用于真正"是"的关系

灵活构建复杂系统

总结:Python 继承要点

最佳实践:避免深度继承链(建议 ≤3 层)

多继承时使用 Mixin 类(单一功能的小类)

优先组合而非继承降低耦合度

更多推荐