视频讲解:
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,静态方法无隐参数,不需要访问实例属性。
- 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中用于在多继承中确定方法调用顺序的算法。
- 什么是MRO? MRO是一个顺序列表,它定义了在多继承中,当子类调用一个方法时,Python解释器搜索该方法的顺序。 在Python中,每个类都有一个__mro__属性,它是一个列表,按照方法解析顺序列出了该类及其所有基类。
其核心功能如下:
解决菱形继承问题,保证子类调用方法时按照合适的顺序,保证方法调用的确定性和一致性 **,**避免父类方法被多次调用或遗漏调用(在super()的使用中体现)。
- 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 操作:
取第一个列表的头部元素
如果该元素不在其他列表的尾部(非第一个位置)
则添加到结果中并从所有列表中移除
否则,检查下一个列表的头
重复直到所有列表为空
- 计算示例
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 中查找下一个类,返回一个代理对象,通过该对象调用父类方法。
- 基本用法:调用父类方法
先看一个最基本的用法,在子类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()函数会出现什么问题:
- 菱形继承问题解决方案
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() 被广泛用于网络构建尤其实是在初始化方法里,主要原因如下:
- 确保父类初始化正确执行
深度学习模型通常继承自框架的基类(如 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
无法保存/加载模型状态
- 支持模块化设计
深度学习网络常采用分层结构 ,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 类(单一功能的小类)
优先组合而非继承降低耦合度