当前位置: 云端笔记 » 编程 » Python » 【Python】类与对象

【Python】类与对象

Python 的类(Class)与对象(Object)是面向对象编程(OOP)的核心。如果用大白话来形容:类是“设计图纸”,对象是根据图纸生产出来的“真车”。

1.实例属性与实例方法

这是最基础的成员,每个对象(实例)独立拥有一份。

  • 实例属性(Instance Attribute):
  • 定义在 init 初始化方法中,前面必须加 self.。每个对象的属性值可以各不相同。
  • 每个实例独立拥有,每创建一个新对象,就会开辟一块新内存来存储该对象的独有数据。
  • 实例方法(Instance Method):
  • 类的普通函数,第一个参数必须是 self。用来操作具体某个对象的数据。
  • 保存在类中,全员共享,共享方法的代码只在类加载时编译一次,并存放在类对象中,实例只是去调用它。

python一切皆对象,全部存在于堆空间

代码演示:

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

    def speak(self):
        print(f"{self.color}的{self.name}在狂叫...")

    def print_info(self):
        print("color:", id(self.color), "name:", id(self.name))

a1 = Animal("小黄", "黄色")
a1.speak()
a1.print_info()
a2 = Animal("小黑", "黑色")
a2.speak()
a2.print_info()

# 获取它们底层的函数本体
func1 = a1.print_info.__func__
func2 = a2.print_info.__func__

# 验证它们是否是同一个东西
print(func1 is func2)  # 输出: True

# 它们也和类直接调用时的函数是同一个
print(func1 is Animal.print_info)  # 输出: True
运行结果
黄色的小黄在狂叫...
color: 4312898880 name: 4312899360
黑色的小黑在狂叫...
color: 4312899680 name: 4312899600
True
True

结果表明:

  • 所有实例属性拥有独立内存空间
  • 实例方法对所有实例对象与类共用,但是类不能直接调用,需要传递一个实例对象,因为实例方法包含一个self,这个self就是实例对象。

2.属性/方法与命名规则

在 Python 中,属性和方法的命名严格遵循 PEP 8(Python 官方代码风格指南)[1]。当遇到多个单词(多字母)拼接时,Python 统一使用小写字母 + 下下划线(snake_case,蛇形命名法)[1]。这与 Java 或 JavaScript 常见的驼峰命名法(camelCase)截然不同。

  • 普通
    如self.name
  • 约定私有
    使用单下划线_,如self._price。约定的私有。外面能读到,但相当于警告程序员:“这是内部私有变量,别在外面瞎改!”
  • 强行私有
    使用双下划线__,如self.__engine。强行私有(名称 mangling)。外面直接调用 obj.__engine 会报错!
  • 魔术方法
    前后使用双下划线,如__init。由 Python 内核在特定时机自动调用,程序员千万别用这种格式自定义变量!

双下划线私有的底层真相:名字重整
当你写 self._engine = “V8” 时,Python 解释器在编译时,会自动把这个名字偷偷改掉,改成了 类名__engine

3.类属性与类方法

有些东西不应该每辆车都存一份,而应该由“厂家(类)”统一管理,这就是类级别成员。

  • 类属性(Class Attribute):直接写在类里面、函数外面的变量。所有对象共享同一份内存。
  • 类方法(Class Method):使用装饰器 @classmethod 修饰,第一个参数必须是 cls(代表类本身)。通常用于编写“工厂模式”或操作类属性。
  • 静态方法(Static Method):使用装饰器 @staticmethod 修饰,不需要 self 也不需要 cls。它只是一个恰好放在类里面的普通函数,通常用来做工具函数。

代码演示:

class Robot:
    count = 0  # 【类属性】:记录一共生产了多少个机器人

    def __init__(self, name):
        self.name = name  # 实例属性
        Robot.count += 1  # 每次实例化,类属性加 1

    @classmethod
    def get_total_count(cls):  # 【类方法】
        return f"当前工厂一共生产了 {cls.count} 个机器人。"

    @staticmethod
    def is_valid_name(name):  # 【静态方法】
        return len(name) > 0  # 纯工具判断

r1 = Robot("狗蛋")
print(r1.get_total_count())
print(r1.is_valid_name("狗蛋"))
r2 = Robot("阿法狗")
print(r2.get_total_count())
print(r2.is_valid_name("狗蛋"))

print(id(Robot.count), id(r1.count), id(r2.count))

func1 = Robot.get_total_count.__func__
func11 = r1.get_total_count.__func__
func12 = r2.get_total_count.__func__
print("func11==func1:", func11 is func1, "func12==func1:", func12 is func1)

func2 = Robot.is_valid_name
func21 = r1.is_valid_name
func22 = r2.is_valid_name
print("func21==func2:", func21 is func2, "func22==2:", func22 is func2)

Python 3 进行了底层性能优化。当 CPU 顺着类字典(Robot.dict)翻到静态方法时,发现它是 @staticmethod,会在内存中直接当场释放,把最里面的普通函数丢给你。
所以is_valid_name没有.func后缀

执行结果
当前工厂一共生产了 1 个机器人。
True
当前工厂一共生产了 2 个机器人。
True
4308610008 4308610008 4308610008
func11==func1: True func12==func1: True
func21==func2: True func22==2: True

根据结果得出如下结论:

  • 类属性与类方法在内存中是共用的,意味着类属性在任意对象中修改会影响改属性全局。
  • 类方法与静态方法也是共用的
  • 类属性与类方法及静态方法对类与对象都可以调用。

这里我们引入一个新的场景,如果在实例方法内定义了与类属性同名变量呢?

class Robot:
    count = 0  # 【类属性】:记录一共生产了多少个机器人
    count2 = 1

    def __init__(self, name):
        self.name = name  # 实例属性
        self.count = 200

r1 = Robot("狗蛋")
print(r1.count)
print(r1.count2)

r2 = Robot("阿法狗")
print(r2.count)
print(r2.count2)

print(Robot.count)

r2.count = 300
print(r1.count)
print(r2.count)
print(Robot.count)

print(id(r1.count), id(r2.count), id(Robot.count))

r1.count2 = 1000
print(r1.count2)
print(Robot.count2)
运行结果
200
1
200
1
0
200
300
0
4383507608 4385205296 4383501208
1000
1
  • 如果对象初始化时重新声明了一个与类属性同名的变量:程序会在内中开辟新的内存空间存放该变量;外部通过对象调用该变量会优先调用对象已经存在的变量,不会再调用类变量,同名变量的修改与也紧发生在新建的变量
  • 如果对象初始化时没有对已经存在的类属性声明同名变量:对象会先从本对象查找是否已经存在,有就返回,没有就从类属性中查找。
  • 外部创建的对象示例无法修改类属性的值,参考r1.count2=1000,这个操作其实是在r1对象中声明了同名的示例属性,与类属性无关。类属性的修改只能通过”类.属性名”的形式

总结:

  • 外部调用时:优先本实例对象里查找,没有再找类属性是否有
  • 外部实例对属性修改永远仅限实例对象本身属性
  • 类属性的的修改只能通过”类.属性名”的形式

4.各属性与各方法调用范围问题

在开始看范围之前,请在脑海中定格这张内存字典分布图:实例的字典 (self.dict):只存放 实例属性。类的字典 (cls.dict / 类名.dict):存放 实例方法、类属性、类方法、静态方法。

成员类型 定义时的特征 实例对象 调用 类名 直接调用
实例属性 init 里写 self.name 完全允许 绝对不允许
类属性 函数外直接写 允许读取,禁止修改 完全允许
实例方法 第一个参数是 self 完全允许 不允许直接调用
类方法 带 @classmethod,有 cls 完全允许 完全允许
静态方法 带 @staticmethod,无自变量 完全允许 完全允许

5.绝对不允许的四大硬核禁区

以下四种行为在物理层面上是绝对行不通或者会导致灾难性 Bug 的:

禁区 1:通过“类名”直接访问“实例属性”
  • 代码死状:Robot.name ➔ 报 AttributeError。
  • 底层真相:实例属性只在 r1 = Robot(“小强”) 执行后,存在于 r1.dict 堆内存中。类对象自己的字典里只有方法和类属性,根本没有 name 这个键,翻箱倒柜也找不到。
禁区 2:在“类方法/静态方法”内部直接调用“实例属性/实例方法”
  • 代码死状:
@classmethod
def bad_class_method(cls):
    print(self.name) # ❌ 必死!报错:NameError: name 'self' is not defined
  • 底层真相:
    类方法被 CPU 调用时,传入的是类对象本身的内存首地址(给 cls),静态方法更是什么都不传。它们在运行的时空里,压根不知道当前是哪辆“车(实例)”在调用它,它的手里没有实例对象的内存指针,所以绝对无法向下访问实例空间。
禁区 3:通过“实例”修改“类属性”
  • 代码死状:
    r1.count = 99 (语法不报错,但逻辑会死得极惨)。
  • 底层真相:
    修改类属性,在外面必须写 Robot.count = 5,在类方法内部必须写 cls.count = 5。
禁区 4:通过“类名”不传参直接调用“实例方法”
  • 代码死状:
  • Robot.drive() ➔ 报错 TypeError: drive() missing 1 required positional argument: ‘self’。
  • 底层真相:
    通过实例调用 r1.drive() 时,Python 会在幕后把 r1 捆绑作为第一个参数塞给 self。但如果你用类名直接调用,Python 底层的自动捆绑魔术失效了,没人填 self 这个坑,CPU 无法执行机器码,直接罢工报错。

6.魔术方法

魔术方法(Magic Methods) 是 Python 的特殊方法。它们有两个特征:

  • 名字前后各有两个下划线(例如 init)。
  • 无需手动调用。它们会在特定的生命周期或遇到特定运算符时,由 Python 自动触发(也叫双下方法 Dunder Methods)。
    在 Python 中,所有类的基类都是 object。你可以通过以下代码直接打印出系统自带的所有魔术方法:
# 1. 查看基类 object 的所有属性和方法
print(dir(object))

# 2. 过滤出所有的魔术方法
magic_methods = [m for m in dir(object) if m.startswith('__') and m.endswith('__')]
print(magic_methods)

核心魔术方法详解

1.初始化与销毁(生命周期)

(1)init(self, …):
初始化方法。最常用。当 new 创建好对象后,紧接着调用它来给对象绑定属性。
示例:

class Animal:

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

a1 = Animal("小黄")

用来初始化对象操作,真正创建对象是下面的new方法
(2)new(cls, *args, **kwargs):
真正创建实例的方法。在内存中开辟空间,返回实例对象。常用于单例模式。
示例:

class Animal:

    def __init__(self, name):
        print("__init__方法被调用了...")
        self.name = name

    def __new__(cls, *args, **kwargs):
        instance = super().__new__(cls)
        print("__new__方法被调用了...")
        return instance

a1 = Animal("小黄")
运行结果
__new__方法被调用了...
__init__方法被调用了...

结果显示:先调用new方法创建对象,再调用init方法初始化对象。

基于new实现单例模式:

class Animal:
    _instance = None

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

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

a1 = Animal("小黄")
print(f"a1 的内存地址: {id(a1)}\n")

a2 = Animal("小黑")
print(f"a2 的内存地址: {id(a2)}\n")

print("a1 和 a2 是完全同一个内存对象吗?", a1 is a2)
print("现在 a1 的名称是什么?", a1.name)
运行结果
a1 的内存地址: 4375882608

a2 的内存地址: 4375882608

a1 和 a2 是完全同一个内存对象吗? True
现在 a1 的名称是什么? 小黑

运行结果显示a1,a2 是同一个内存对象,最终name是小黑,不是小黄
单例模式常用于数据库连接池、日志记录器或配置读取器,能极大节省内存

(3)del(self):
析构方法。当对象被垃圾回收器销毁前(引用计数归零)自动调用。当一个对象在内存中被彻底销毁时(即引用计数归零,或者程序退出时),Python 的垃圾回收机制会自动触发并执行这个方法 [1]。它通常用来做一些收尾工作,比如关闭文件、断开数据库连接等。下面用一个直观的场景(模拟游戏角色下线)来为你演示 del 的触发机制,以及如何通过 del 关键字手动触发它。

import time

class GameRole:
    def __init__(self, name):
        self.name = name
        print(f"🎮 角色 [{self.name}] 成功登录游戏!")

    def __del__(self):
        # 对象被销毁时自动执行
        print(f"❌ 🔴 警告:角色 [{self.name}] 已被销毁,内存已释放!")

print("--- 实验 1:程序结束时自动销毁 ---")
player1 = GameRole("亚瑟")
print("游戏运行中...")
# 注意:这里我们没有手动删掉 player1,但当程序运行到最后一行结束时,
# Python 会清空内存,此时会自动触发 player1 的 __del__

print("\n--- 实验 2:手动使用 del 关键字断开引用 ---")
player2 = GameRole("妲己")
player3 = player2  # player3 也指向同一个“妲己”对象(引用计数 = 2)

print("尝试第一次删除 player2...")
del player2  # 只是断开了 player2 这个名字的引用,对象在内存里还没死!
print("此时 __del__ 并没有触发,因为 player3 还指着她呢。")

print("尝试第二次删除 player3...")
del player3  # 最后一个引用也被断开了(引用计数归零),对象彻底死亡!
time.sleep(0.5) # 稍微等待,让控制台打印顺序更清晰

print("\n--- 程序彻底结束 ---")
运行结果
--- 实验 1:程序结束时自动销毁 ---
🎮 角色 [亚瑟] 成功登录游戏!
游戏运行中...

--- 实验 2:手动使用 del 关键字断开引用 ---
🎮 角色 [妲己] 成功登录游戏!
尝试第一次删除 player2...
此时 __del__ 并没有触发,因为 player3 还指着她呢。
尝试第二次删除 player3...
❌ 🔴 警告:角色 [妲己] 已被销毁,内存已释放!

--- 程序彻底结束 ---
❌ 🔴 警告:角色 [亚瑟] 已被销毁,内存已释放!
2.比较运算符

当你对两个对象使用 >、<、== 时,Python 底层会调用对应的魔术方法: (1)eq(self, other):对应 ==(判断是否相等)。 (2)ne(self, other):对应 !=(判断是否不等)。 (3)lt(self, other):对应 <(小于)。 (4)le(self, other):对应 <=(小于等于)。 (5)gt(self, other):对应 >(大于)。
(6)ge(self, other):对应 >=(大于等于)。

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

    # 1. 对应 ==
    def __eq__(self, other):
        if isinstance(other, Product):
            return self.price == other.price
        return False

    # 2. 对应 !=
    def __ne__(self, other):
        if isinstance(other, Product):
            return self.price != other.price
        return True

    # 3. 对应 <
    def __lt__(self, other):
        if isinstance(other, Product):
            return self.price < other.price
        return NotImplemented

    # 4. 对应 <=
    def __le__(self, other):
        if isinstance(other, Product):
            return self.price <= other.price
        return NotImplemented

    # 5. 对应 >
    def __gt__(self, other):
        if isinstance(other, Product):
            return self.price > other.price
        return NotImplemented

    # 6. 对应 >=
    def __ge__(self, other):
        if isinstance(other, Product):
            return self.price >= other.price
        return NotImplemented

# --- 运行测试 ---
p1 = Product("鼠标", 50)
p2 = Product("键盘", 150)
p3 = Product("耳机", 50)

print("--- (1) 验证 __eq__ (==) ---")
print(p1 == p3)  # True (价格都是 50)
print(p1 == p2)  # False

print("\n--- (2) 验证 __ne__ (!=) ---")
print(p1 != p2)  # True (价格不等)
print(p1 != p3)  # False

print("\n--- (3) 验证 __lt__ (<) ---")
print(p1 < p2)   # True (50 < 150)

print("\n--- (4) 验证 __le__ (<=) ---")
print(p1 <= p3)  # True (50 <= 50)

print("\n--- (5) 验证 __gt__ (>) ---")
print(p2 > p1)   # True (150 > 50)

print("\n--- (6) 验证 __ge__ (>=) ---")
print(p3 >= p1)  # True (50 >= 50)
运行结果
--- (1) 验证 __eq__ (==) ---
True
False

--- (2) 验证 __ne__ (!=) ---
True
False

--- (3) 验证 __lt__ (<) ---
True

--- (4) 验证 __le__ (<=) ---
True

--- (5) 验证 __gt__ (>) ---
True

--- (6) 验证 __ge__ (>=) ---
True
3. 属性控制(动态拦截属性访问)

(1)getattr(self, name):
当外部访问一个不存在的属性时触发。这个有返回值,如果不写return默认返回None
示例:

class Animal:

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

    def __getattr__(self, name):
        print("属性", name, "不存在")

a1 = Animal("小黄")
print(a1.name)
print(a1.color)
运行结果
小黄
属性 color 不存在
None

当打印不存在属性时,调用了getattr方法并返回None

(2)getattribute(self, name):
无论属性存不存在,只要有人点(obj.name)就一定会触发。这个有返回值,如果不写return默认返回None
示例:

class Animal:

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

    def __getattribute__(self, name):
        print("__getattribute__被调用,name=", name)

a1 = Animal("小黄")
print(a1.name)
print(a1.color)
运行结果
__getattribute__被调用,name= name
None
__getattribute__被调用,name= color
None

(3)setattr(self, name, value):
当给属性赋值(obj.name = value)时触发。

class Animal:

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

    def __setattr__(self, name, value):
        print("__setattr__被调用,name =", name, "value =", value)
        super().__setattr__(name, value)

a1 = Animal("小黄")
a1.color = "红色"
a1.name = "小黑"
运行结果
__setattr__被调用,name = name value = 小黄
__setattr__被调用,name = color value = 红色
__setattr__被调用,name = name value = 小黑
  • 只要有任何地方执行了 对象.属性 = 值 的赋值操作,Python 就会立刻自动拦截并转去调用你写的 setattr 魔术方法。无论是在init还是在类外部都会出发
  • 触发 setattr

init方法里 self.name = name底层逻辑

1.第一步:理解 self 和 name 分别是什么

  • self 是一个内存指针:它是之前由 new 方法在堆内存中开辟好的一块空白对象的地址。
  • 右边的 name 是局部变量:它是你传入的字符串对象(比如 “小黄”)的内存地址。

2.第二步:底层核心机制 —— 属性字典 dict
在 Python 中,几乎每个实例对象在底层都有一个隐藏的字典,叫做 dict,专门用来存储该实例的所有属性。当你写下 self.name = name 时,Python 底层实际上被翻译成了对这个字典的赋值操作:

# 语法糖:self.name = name
# 底层等价于:
self.__dict__['name'] = name

(1)Python 根据 self 的指针找到该实例在堆中的内存块。
(2)找到该实例的 dict 属性空间。
(3)在字典中创建一个键(Key)为 “name”,并将其值(Value)指向字符串 “小黄” 的内存地址。

3.第三步:触发 setattr 拦截管道
在第二步发生之前,Python 还设置了一道过滤网,也就是你刚刚接触过的 setattr 魔术方法。整个赋值的完整调用链路如下:

代码执行:self.name = "小黄"
   │
   ▼
Python 检查当前类是否重写了 __setattr__
   │
   ├──► [有重写] ──────────────────────────► 执行你写的 __setattr__ 逻辑
   │                                           (必须在内部调用 super().__setattr__ 才能存盘)
   │
   └──► [没有重写 (默认情况)] ──────────────► 调用基类 object 的默认 __setattr__
                                               │
                                               ▼
                                         底层的 C 语言代码直接操作:
                                         self.__dict__["name"] = "小黄"

外部执行 对象.属性 = 值 的底层逻辑,与在 init 内部执行 self.name = name 的核心链路完全一模一样。
当你写下 a1.age = 3 时,Python 解释器在底层的运作流程如下:

代码执行:a1.age = 3
   │
   ▼
1. 寻找对象:Python 通过变量名 a1 找到它在堆内存中的实例对象。
   │
   ▼
2. 触发拦截:Python 自动调用该对象所属类(Animal)中的 __setattr__ 方法。
   │
   ├──► 隐式转换:将 a1.age = 3 转换为 Animal.__setattr__(a1, 'age', 3) 
   │
   ▼
3. 存入内存:如果没有被你自定义的逻辑拦截,最终会通过基类 object 写入:
   │   
   └──► a1.__dict__['age'] = 3

(4)delattr(self, name):
当删除属性时触发。
示例:

class Robot:
    def __init__(self, name, battery):
        self.name = name          # 普通属性
        self.battery = battery    # 核心属性(不允许被删除)

    def __delattr__(self, name):
        print(f"⚙️ 拦截到属性删除请求:尝试删除属性 [{name}]")

        # 1. 安全限制:如果是核心属性 battery,直接拒绝并报错
        if name == "battery":
            raise AttributeError(f"❌ 拒绝操作:[{name}] 是机器人的核心组件,不能被删除!")

        # 2. 正常删除:如果是其他普通属性,允许删除
        # 【注意】这里必须调用父类 object 的 __delattr__ 来真正从内存中移除属性
        print(f" 允许删除 [{name}],正在将其从内存中移除...")
        super().__delattr__(name)

# --- 运行测试 ---
rb = Robot("阿童木", 100)

print("--- 1. 尝试删除普通属性 name ---")
print("删除前属性字典:", rb.__dict__)
del rb.name  # 触发拦截
print("删除后属性字典:", rb.__dict__)

print("\n--- 2. 尝试删除核心属性 battery ---")
try:
    del rb.battery  # 触发拦截,将会抛出异常
except AttributeError as e:
    print(e)
运行结果
--- 1. 尝试删除普通属性 name ---
删除前属性字典: {'name': '阿童木', 'battery': 100}
⚙️ 拦截到属性删除请求:尝试删除属性 [name]
 允许删除 [name],正在将其从内存中移除...
删除后属性字典: {'battery': 100}

--- 2. 尝试删除核心属性 battery ---
⚙️ 拦截到属性删除请求:尝试删除属性 [battery]
❌ 拒绝操作:[battery] 是机器人的核心组件,不能被删除!
4. 容器与索引操作

(1)len(self):
当调用 len(obj) 时触发,返回对象的长度。
(2)getitem(self, key):
当通过 obj[key] 获取元素时触发。
(3)setitem(self, key, value):
当通过 obj[key] = value 赋值时触发。
(4)delitem(self, key):
当执行 del obj[key] 时触发。
(5)contains(self, item):
当执行 item in obj 时触发,返回布尔值。

示例:

class Cart:
    def __init__(self):
        # 在内部用一个字典来实际存储商品和数量
        # 格式如:{"苹果": 3, "香蕉": 5}
        self.items = {}

    # 1. 对应 len(obj)
    def __len__(self):
        # 返回购物车中商品的总种类数
        return len(self.items)

    # 2. 对应 obj[key]
    def __getitem__(self, key):
        # 如果商品存在,返回数量;如果不存在,默认返回 0
        return self.items.get(key, 0)

    # 3. 对应 obj[key] = value
    def __setitem__(self, key, value):
        # 设置或更新某种商品的数量
        if value <= 0:
            # 如果数量设为 0 或负数,直接移除该商品
            self.__delitem__(key)
        else:
            self.items[key] = value
            print(f"🛒 购物车更新:[{key}] 的数量调整为 {value} 个")

    # 4. 对应 del obj[key]
    def __delitem__(self, key):
        # 从购物车中彻底删除某种商品
        if key in self.items:
            del self.items[key]
            print(f"🗑️ 移出购物车:商品 [{key}] 已被完全清空")
        else:
            print(f"⚠️ 提示:购物车里本来就没有 [{key}]")

    # 5. 对应 item in obj
    def __contains__(self, item):
        # 判断某种商品是否在购物车中
        return item in self.items

# --- 运行测试 ---
my_cart = Cart()

print("--- (1) 验证 __setitem__ ([key] = value) ---")
my_cart["苹果"] = 3
my_cart["香蕉"] = 5
my_cart["西瓜"] = 1

print("\n--- (2) 验证 __len__ (len()) ---")
print(f"购物车里现在有 {len(my_cart)} 种商品。")  # 触发 __len__

print("\n--- (3) 验证 __getitem__ ([key]) ---")
print(f"苹果的数量: {my_cart['苹果']} 个")  # 触发 __getitem__
print(f"草莓的数量: {my_cart['草莓']} 个")  # 触发 __getitem__ (不存在的商品)

print("\n--- (4) 验证 __contains__ (in) ---")
print("苹果 在购物车里吗?", "苹果" in my_cart)  # True
print("草莓 在购物车里吗?", "草莓" in my_cart)  # False

print("\n--- (5) 验证 __delitem__ (del []) ---")
del my_cart["西瓜"]  # 触发 __delitem__
print(f"删除西瓜后,商品种类剩余: {len(my_cart)} 种")

print("\n--- 补充测试:通过赋值 0 间接触发删除 ---")
my_cart["苹果"] = 0  # 触发 __setitem__ 内部的 __delitem__
运行结果
--- (1) 验证 __setitem__ ([key] = value) ---
🛒 购物车更新:[苹果] 的数量调整为 3 个
🛒 购物车更新:[香蕉] 的数量调整为 5 个
🛒 购物车更新:[西瓜] 的数量调整为 1 个

--- (2) 验证 __len__ (len()) ---
购物车里现在有 3 种商品。

--- (3) 验证 __getitem__ ([key]) ---
苹果的数量: 3 个
草莓的数量: 0 个

--- (4) 验证 __contains__ (in) ---
苹果 在购物车里吗? True
草莓 在购物车里吗? False

--- (5) 验证 __delitem__ (del []) ---
🗑️ 移出购物车:商品 [西瓜] 已被完全清空
删除西瓜后,商品种类剩余: 2 种

--- 补充测试:通过赋值 0 间接触发删除 ---
🗑️ 移出购物车:商品 [苹果] 已被完全清空
5. 可调用对象

call(self, *args, **kwargs):
让一个实例对象可以像函数一样被括号调用。例如 obj()。
示例:

class ConnectionCounter:
    def __init__(self):
        # 对象的私有储物柜,用来维持状态,外部无法轻易篡改
        self.count = 0

    def __call__(self, client_ip):
        # 核心魔法:每次对实例加括号调用,计数加 1 并记录日志
        self.count += 1
        print(f"[计数服务] 新连接自: {client_ip} | 历史总连接数: {self.count}")
        return self.count

# ─── 实战测试 ───
# 1. 实例化一个计数对象
track_connection = ConnectionCounter()

# 2. 奇迹发生:像调用普通函数一样去连续调用这个“实例对象”
track_connection("192.168.1.100") # 输出: 新连接自... | 历史总连接数: 1
track_connection("10.0.0.5")      # 输出: 新连接自... | 历史总连接数: 2
track_connection("127.0.0.1")     # 输出: 新连接自... | 历史总连接数: 3

# 3. 它依然是个对象,你可以随时把它的内部资产捞出来查看
print(f"最终大仓库里存的值是: {track_connection.count}") # 输出: 3
运行结果
[计数服务] 新连接自: 192.168.1.100 | 历史总连接数: 1
[计数服务] 新连接自: 10.0.0.5 | 历史总连接数: 2
[计数服务] 新连接自: 127.0.0.1 | 历史总连接数: 3
最终大仓库里存的值是: 3

每次将对象像函数一样调用时都会调用call方法

延伸:那么有个问题来了,所有类都是object的子类,类()创建对象时应该会调用call方法,因为类也是object的实例嘛,而实际上object类是没有call方法的,那类()实例化对象时到底调谁的call方法呢?
引例:

class Animal:

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

a1 = Animal("小黄")
  • 验证object类是否有call方法
print("__call__ 在 object 的字典里吗?", "__call__" in object.__dict__)
运行结果
__call__ 在 object 的字典里吗? False

object确实没有call方法

  • 查看 Animal类的创建类,a1实例的创建类
class Animal:

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

a1 = Animal("小黄")

print(type(Animal))

print(type(a1))
运行结果
<class 'type'>
<class '__main__.Animal'>

结果表明虽然Animal继承了object类,但是它是由type元类创建的,Animal类是type类的实例,而a1对象是Animal类的实例。所以a1 = Animal(“小黄”)实例化类时应该调用的是type元类的call方法。
验证type类是否有call方法

print("__call__ 在 type 的字典里吗?", "__call__" in type.__dict__)
运行结果
__call__ 在 type 的字典里吗? True

type元类果然是有call方法,所以:当一个”实例对象()”被加圆括号调用时,底层实际执行的,必然是创建它的那个”类(也就是它的亲生妈妈)”里面定义的 call 方法。
所以:

  • a1 = Animal(“小黄”)调用了type元类的call方法实例化了类
  • a1() 时调用的是 Animal类的call方法,如果这个call方法没有重写,会报错。并不会往上调用type的call方法。

到这里又有一个问题了:我们知道创建对象会先调用new方法创建对象,然后再init方法初始化,在上述代码中a1 = Animal(“小黄”)也是在创建对象,由于Animal类其实也是type元类的实例对象,所以会调用type元类的call方法,那么call方法与new方法及init方法存在怎样的联系呢?

当你写下 a1 = Animal(“小黄”) 这一行代码时,Python 解释器在底层实际上执行了一场“总指挥调度、造骨架、填肉身”的闭环交响乐。我们已经知道,这行代码的本质是触发了元类妈妈的 type.call(Animal, “小黄”) 。而 newinit 并不是孤立运行的,它们是 type.call 这个总指挥手里最核心的两张王牌。下面为您用最详细的三大步骤、纯文本内存流向图,彻底还原一个 Python 对象诞生的真实物理全流程,并厘清它与 new 的生死关系。

 程序员写下:a1 = Animal("小黄")
      │
      ▼ (瞬间触发元类总指挥)
 🚀 【步骤 1】:元类总指挥启动 ── 执行 `type.__call__(Animal, "小黄")`
      │
      ├─► 1-1. 总指挥在内核中下达第一道死命令:“去调用 Animal.__new__(Animal, "小黄") !”
      │
      ├─► 1-2. 【核心主角 __new__ 登场】──【物理筑造骨架阶段】
      │        ├── `__new__` 是一个真正的【构造器(Constructor)】。
      │        ├── 它顺着网线直接去找操作系统的 C 语言内核(调用 malloc)。
      │        ├── 申请一块干净的、大小为 `tp_basicsize` 的处女堆内存空间。
      │        ├── 【盖上系统印章】:在这块内存的头部,悄悄写入:
      │        │    ① 引用计数 `ob_refcnt = 1`  .
      │        │    ② 类型指针 `ob_type = 0x5000`(死死指向 Animal 类对象)
      │        └── 最终,`__new__` 带着这块刚出厂、里面空无一物的【内存骨架(实例)】返回给总指挥。
      │
      ▼
 🚀 【步骤 2】:总指挥做安全检查 ── 执行 `if isinstance(instance, cls):`
      │
      ├─► 如果 `__new__` 刚才由于某种原因(比如你故意拦截)没有返回 `Animal` 的骨架,
      │   总指挥就会直接收工,后续的 `__init__` 【绝对不会】执行!
      │
      ├─► 如果检查通过(拿到了合法的骨架),总指挥下达第二道死命令:
      │   “拿着这块骨架,去调用 Animal.__init__(instance, "小黄") !”
      │
      ▼
 🚀 【步骤 3】:肉身填充阶段 ── 执行 `__init__`
      │
      ├── 【核心主角 __init__ 登场】── 它并不是构造器,它只是个【初始化器(Initializer)】!
      ├── 它接过了刚才生产出来的空骨架,把它当成第一个参数 **`self`**。
      └── 开始执行你在类里写的赋值代码:`self.name = "小黄"`,往这个骨架的 `__dict__` 局部储物柜里填肉。

      │
      ▼ (总指挥收工)
 🚀 【完美交工】:`type.__call__` 把这个长满肉、注入了灵魂的完美实例返回给程序员。
     最终,栈区里的变量名便利贴 `a1`,死死贴在了这块内存首地址上。
6. 对象字符串表示

(1)str(self):
定义用户看的内容。当执行 print(obj) 或 str(obj) 时触发,返回对人类友好的字符串。
是给人类(普通用户)看的。追求舒适、好看,把对象伪装得干净漂亮(注重可读性 Readability)。
(2)repr(self):
定义开发者看的内容。当在终端直接回车或执行 repr(obj) 时触发,返回该对象的官方准确表达式。
是给机器(开发者/调试器)看的。追求硬核、真实,把对象的物理真身毫无保留地吐出来(注重无歧义性 Unambiguity)

str_repr核心区别与物理触发时机
触发场景 表层代码示例 底层物理翻找引擎的路径
打印给人看 print(obj)str(obj) 先找类字典里的 str,如果没写,会直接去调 repr 顶包(保底机制)
调试给开发者看 看在终端直接敲 obj 回车repr(obj)logger.debug(obj) 直接且强制去翻找类字典里的 repr
容器(列表/字典)中 print([obj1, obj2]) 无论你是用 print 还是别的,容器内部对元素的序列化百分之百强制调用 repr

示例:

class ClientConnection:
    def __init__(self, ip, port, username):
        self.ip = ip
        self.port = port
        self.username = username

    # 1. 重写 __str__:给普通用户或前端日志看的“漂亮外衣”
    def __str__(self):
        # 追求干净、好读
        return f"用户[{self.username}] 已经成功连接"

    # 2. 重写 __repr__:给开发者调试、崩溃排查(Bug追踪)看的“物理真身”
    def __repr__(self):
        # 行业标准规范:__repr__ 返回的字符串,最好能让人一眼看出它的类名和物理内部参数 [^9^]
        return f"ClientConnection(ip='{self.ip}', port={self.port}, username='{self.username}')"

# ─── 物理测试验证 ───
conn1 = ClientConnection("192.168.1.105", 8888, "Peter半仙")
conn2 = ClientConnection("10.0.0.12", 9999, "黑客小强")

# === 【测试 1:普通打印 ➔ 触发 __str__】 ===
print("--- 测试 1:人类视角 (print) ---")
print(conn1)  # 输出: 用户[Peter半仙] 已经成功连接
print(str(conn1)) # 显式调用,输出相同

# === 【测试 2:开发者调试视角 ➔ 强制触发 __repr__】 ===
print("\n--- 测试 2:机器/开发者视角 (repr) ---")
print(repr(conn1)) 
# 输出: ClientConnection(ip='192.168.1.105', port=8888, username='Peter半仙')

# === 【测试 3:列表容器 ➔ 震撼反转!强制触发 __repr__】 ===
print("\n--- 测试 3:把对象塞进列表里批量打印 ---")
connection_pool = [conn1, conn2]
# 很多人以为列表 print 出来也会是漂亮的中文,其实不然!
# 为了防止歧义,Python 容器层在底层遍历时,会强制去调用每个元素的 __repr__ [^9^]!
print(connection_pool)
运行结果
--- 测试 1:人类视角 (print) ---
用户[Peter半仙] 已经成功连接
用户[Peter半仙] 已经成功连接

--- 测试 2:机器/开发者视角 (repr) ---
ClientConnection(ip='192.168.1.105', port=8888, username='Peter半仙')

--- 测试 3:把对象塞进列表里批量打印 ---
[ClientConnection(ip='192.168.1.105', port=8888, username='Peter半仙'), ClientConnection(ip='10.0.0.12', port=9999, username='黑客小强')]

6.继承

单继承

类属性的继承

引例1:

class A:
    num1 = 1

class B(A):
    pass

print(A.num1)
print(B.num1)
print(A.num1 is B.num1)

B.num1 = 100
print(A.num1)
print(B.num1)

print(A.num1 is B.num1)
运行结果
1
1
True
1
100
False

这段代码虽然简单,但是蕴含大乾坤,首先定义了一个类A,类A定义了类属性num1;类B继承类A,没有定义其他属性。

(1)当打印print(A.num1)时
python根据方法解析顺序(Method Resolution Order, MRO)
Python 的解释器会按照以下步骤进行查找:

  • 查找类 A 的命名空间:
    Python 首先检查 A.dict(即类 A 自身的属性字典)。发现 ‘num1’ 存在于A.dict 中,值为 1。
  • 返回结果:
    Python 直接返回 A.dict[‘num1’] 的值。

而这个A.dict在python解析器读完类定义代码后,会将类属性及方法指针写入这个字典。

(2)当打印print(B.num1)时
python根据方法解析顺序(Method Resolution Order, MRO)
Python 的解释器会按照以下步骤进行查找:

  • 查找类 B的命名空间:
    Python 首先检查 B.dict(即类 B自身的属性字典),没有找得到’num1’这个key。因为在类B里没有num1定义,直接pass
  • Python 继续往类B的父类A继续查找。发现 ‘num1’ 存在于A.dict 中,值为 1。
  • Python 直接返回 A.dict[‘num1’] 的值。

所以此时A.num1与B.num1 就是同一个对象,print(A.num1 is B.num1)自然是True

a is b:比对的是指针(变量里写着的内存门牌号是否完全一致)。它连数据的内容看都不看一眼 [^9^]。a == b:比对的是内容(顺着指针翻开两个对象的储物柜,看里面的数据是否相等)

(3)print(A.num1 is B.num1)
根据(1)(2),此时A.num1 与 B.num1就是同一个对象,所以打印True

(4)B.num1 = 100

  • 写入子类字典
    Python 在子类 B 的命名空间(B.dict)中新建了一个键值对 ‘num1’: 100。
  • 切断继承链接
    此后访问 B.num1 时,Python 会直接在 B类中找到该属性,不再向上查找父类 A。
  • 父类不受影响
    A.dict 中的 ‘num1’: 1 保持原样。A.num1 依然指向整数 1,而 B.num1 指向整数 100。

B.dict与A.dict显然在不同地址空间,num1自然不是同一个对象

(5)print(A.num1)
自然打印A.dict的num1:1
(6)print(B.num1)
自然打印B.dict的num1:100
(7)print(A.num1 is B.num1)
自然打印False

引例2:

class A:
    num1 = 1

class B(A):
    num1 = 2

print('num1' in B.__dict__.keys())

print(A.num1)
print(B.num1)
print(A.num1 is B.num1)

B.num1 = 100
print(A.num1)
print(B.num1)

print(A.num1 is B.num1)
打印结果
True
1
2
False
1
100
False

由于类B虽然继承A,但是代码里重新定义了num1类属性,python解析器在读取完类B代码后会在B.dict内添加B.dict[‘num1’]=2。这与A解耦了,num1完全属于类B,基于引例1的解读,引例2的打印结果就好理解了。

类方法的继承

那么类方法,静态方法也是类似呢?我们来看引例3:

class A:

    @classmethod
    def func1(cls):
        print("classmethod方法")

class B(A):
    pass

print(A.__dict__)
print(B.__dict__)

print(A.func1)
print(B.func1)

print(A.func1 is B.func1)

print(A.func1.__func__ is B.func1.__func__)
print(A.func1.__self__ is B.func1.__self__)
运行结果
{'__module__': '__main__', 'func1': <classmethod(<function A.func1 at 0x100ba0360>)>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
{'__module__': '__main__', '__doc__': None}
<bound method A.func1 of <class '__main__.A'>>
<bound method A.func1 of <class '__main__.B'>>
False
True
False

运行结果表明

  • B.dict没有func1,A.dict有func1
  • A.func1 是>
  • B.func1 是> A.func1与B.func1其实是绑定方法对象,各自包含func,self,其中func才是相同的。

绑定方法对象的内部结构
当你访问 A.func1(类方法)时,生成的临时对象包含两个核心属性:

  • func:指向原始的函数对象(即存储在 A.dict 中的那个 function)。
  • self:指向被绑定的类对象 A(如果是实例方法,则指向实例)。

我在A中看到了只有一个func1的key,其value为)>,这个value是描述符对象
当执行A.func1或者B.func1时:

  • 读取静态 Value:Python 从 A.dict 取出那个不变的 classmethod 对象。
  • 调用 get:Python 调用该对象的 get(instance, owner) 方法。
    如果是 A.func1,传入的 owner 是 A。
    如果是 B.func1,传入的 owner 是 B。
  • 生成动态对象:get 方法根据传入的 owner,现场创建一个新的绑定方法对象(Bound Method)。
    这个新对象里记录了:“我是属于类 B 的,调用我时要把 B 传进去”。

为何print(A.func1.func is B.func1.func) 返回的是True?
A.func1.func 和 B.func1.func 获取的都是同一个内存地址的函数对象

总结:
类方法在继承中共享同一份底层函数代码,但每次访问时会根据调用者动态生成不同的绑定方法对象。

引例4:

class A:

    @classmethod
    def func1(cls):
        print("A的classmethod方法")

class B(A):
    @classmethod
    def func1(cls):
        print("B的classmethod方法")

print(A.__dict__)
print(B.__dict__)

print(A.func1)
print(B.func1)

print(A.func1 is B.func1)

print(A.func1.__func__ is B.func1.__func__)
print(A.func1.__self__ is B.func1.__self__)
运行结果
{'__module__': '__main__', 'func1': <classmethod(<function A.func1 at 0x103218360>)>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
{'__module__': '__main__', 'func1': <classmethod(<function B.func1 at 0x1034013a0>)>, '__doc__': None}
<bound method A.func1 of <class '__main__.A'>>
<bound method B.func1 of <class '__main__.B'>>
False
False
False

根据结果,当类方法重写后,类B的dict中有自己的func1且与类A的func1内存地址不一样,自然不是同一个方法。所以重写就是拥有了自己的类方法。

静态方法的继承

引例5:

class A:

    @staticmethod
    def func2():
        print("A的staticmethod方法")

class B(A):
    pass

print(A.__dict__)
print(B.__dict__)

print(A.func2)
print(B.func2)

print(A.func2 is B.func2)
运行结果
{'__module__': '__main__', 'func2': <staticmethod(<function A.func2 at 0x101680360>)>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
{'__module__': '__main__', '__doc__': None}
<function A.func2 at 0x101680360>
<function A.func2 at 0x101680360>
True

根据运行结果,这个跟引例3很相似,但是确实同一个函数对象,静态方法没有funcself,所以有两个问题:
为何静态方法在没有重写情况下继承的是同一个方法?
因为 Python 中的静态方法(@staticmethod)本质上就是一个普通的函数,在底层不会与特定的类或实例进行绑定(Binding),在 Python 中,不同类型的静态/动态方法在被调用时,底层的处理方式截然不同:

  • 普通实例方法:会动态绑定实例,自动传入 self。
  • 类方法(@classmethod):会动态绑定类,自动传入 cls。因为绑定对象不同,A.func1 is B.func1 会返回 False(它们是动态生成的不同绑定方法对象)。
  • 静态方法(@staticmethod):不绑定任何对象。无论通过类 A 还是类 B 访问,Python 底层描述符(Descriptor)都只会原封不动地返回那个原始的、唯一的函数对象。

描述符机制(Descriptor Protocol)是 classmethod 和 staticmethod 核心差异的幕后推手即)>与)>看着很像,底层是不一样的。

简单来说,描述符是一个定义了 get() 方法的对象。当通过类或实例访问属性时,Python 会自动调用这个 get() 方法。以下是 classmethod 和 staticmethod 在描述符机制上的本质区别:

  • 静态方法的 get 不关心是谁调用了它。无论谁来,它都原封不动地返回被包裹的原始函数。
  • 类方法的 get 非常关心是谁调用了它。它会将调用它的类(owner)和函数打包,动态创建一个新的“绑定方法”对象。

为何静态方法没有funcself两个属性?
态方法(@staticmethod)实际上就是挂载在类的命名空间下的普通函数。它在逻辑上和这个类完全解耦,独立存在。所以没有self比较好理解,但是为何也没有func:

  • 当执行B.func2后其实拿到的就是一个普通函数,父类A中的staticmethod描述符中的get()方法返回的就是一个普通函数,普通函数是没有func属性的。

引例6:

class A:

    @staticmethod
    def func2():
        print("A的staticmethod方法")

class B(A):
    @staticmethod
    def func2():
        print("B的staticmethod方法")

print(A.__dict__)
print(B.__dict__)

print(A.func2)
print(B.func2)

print(A.func2 is B.func2)
运行结果
{'__module__': '__main__', 'func2': <staticmethod(<function A.func2 at 0x104ddc360>)>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
{'__module__': '__main__', 'func2': <staticmethod(<function B.func2 at 0x104ed13a0>)>, '__doc__': None}
<function A.func2 at 0x104ddc360>
<function B.func2 at 0x104ed13a0>
False

根据上面的分析,重写静态方法就好理解了,重写静态方法继承类的dict拥有自己的staticmethod描述符,自然不是同一个函数,其余逻辑与上面一样。

总结:

  • 静态方法继承: 是同一个函数
  • 静态方法如果重写了: 不是同一个函数
实例方法的继承

引例7:

class A:
    def func3(self):
        print("A的实例方法")

class B(A):
    pass

print(A.__dict__)
print(B.__dict__)

print(A.func3)
print(B.func3)

print(A.func3 is B.func3)

a1 = A()
b1 = B()

print(a1.func3)
print(b1.func3)
print(a1.func3 is b1.func3)

print(a1.func3.__func__ is b1.func3.__func__)
运行结果
{'__module__': '__main__', 'func3': <function A.func3 at 0x1053a4360>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
{'__module__': '__main__', '__doc__': None}
<function A.func3 at 0x1053a4360>
<function A.func3 at 0x1053a4360>
True
<bound method A.func3 of <__main__.A object at 0x10547e870>>
<bound method A.func3 of <__main__.B object at 0x10547e750>>
False
True

根据结果我们先总结现象:

  • func3在类A中的dict存在,且是,这就是一个普通函数
  • 当实例方法通过类调用时是同一个函数
  • 当实例方法通过实例调用时不是同一个函数

为何会出现这种现象?
我们知道几乎所有函数或者方法其实底层都有一个get方法,代码验证:

def my_func():
    pass

# 验证普通函数是否含有 __get__ 属性
print(hasattr(my_func, "__get__"))  # 输出: True ✅

# 或者直接在它的属性列表中查找
print("__get__" in dir(my_func))    # 输出: True ✅

当我们通过类.方法 或者实例.方法(通过属性方式)时会触发这个get方法,这个get方法有个instance参数:

  • 如果是类.方法: instance都是None,返回函数本身,所有都一样
  • 如果是实例.方法: instance都是对应调用的实例,get会进行实例绑定,返回方法绑定对象,所以不一样了,但是底层方法是一样的,所以print(a1.func3 is b1.func3)是False(比的是方法绑定对象),print(a1.func3.func is b1.func3.func)(比的是方法绑定对象绑定的方法)是True

总结:
实例方法的继承是同一个方法

  • 类.方法名: 返回就是一个普通函数
  • 实例.方法名: 返回的是一个方法绑定对象,但是实例.方法名.func是同一个函数

我们延伸一下:类方法,静态方法在”类.方法名”与”实例.方法名”是否跟实例方法情况一致?
引例8:

class A:

    @classmethod
    def func1(cls):
        print("A的类方法")

    @staticmethod
    def func2():
        print("A的静态方法")

class B(A):
    pass

print("----------1----------")
print(A.__dict__)
print(B.__dict__)

print("----------2----------")
print(A.func1)
print(A.func2)

print("----------3----------")
print(B.func1)
print(B.func2)

print("----------4----------")
print(A.func1 is B.func1)
print(A.func2 is B.func2)

a1 = A()
b1 = B()

print("----------5----------")
print(a1.func1)
print(a1.func2)

print("----------6----------")
print(b1.func1)
print(b1.func2)

print("----------7----------")
print(a1.func1 is b1.func1)
print(a1.func2 is b1.func2)

print("----------8----------")
print(a1.func1.__func__ is b1.func1.__func__)
运行结果
----------1----------
{'__module__': '__main__', 'func1': <classmethod(<function A.func1 at 0x103694360>)>, 'func2': <staticmethod(<function A.func2 at 0x1037893a0>)>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
{'__module__': '__main__', '__doc__': None}
----------2----------
<bound method A.func1 of <class '__main__.A'>>
<function A.func2 at 0x1037893a0>
----------3----------
<bound method A.func1 of <class '__main__.B'>>
<function A.func2 at 0x1037893a0>
----------4----------
False
True
----------5----------
<bound method A.func1 of <class '__main__.A'>>
<function A.func2 at 0x1037893a0>
----------6----------
<bound method A.func1 of <class '__main__.B'>>
<function A.func2 at 0x1037893a0>
----------7----------
False
True
----------8----------
True

结果分析:

  • 类方法:
  • print(A.func1):
    >
  • print(B.func1):
    >
  • print(a1.func1):
    >
  • print(b1.func1):
    >
  • print(A.func1 is B.func1):
    False
  • print(a1.func1 is b1.func1):
    False
  • print(a1.func1.func is b1.func1.func):
    True
    类方法出现跟实例方法不一样的情况,类方法在继承中,不管是”类.方法名”还是”实例.方法名”,返回的都是方法绑定对象,该对象是新对象,内存地址不同,但是方法绑定对象绑定的方法是同一个方法
  • 静态方法:
  • print(A.func2):
  • print(B.func2):
  • print(a1.func2):
  • print(b1.func2):
    <
  • print(A.func2 is B.func2):
    True
  • print(a1.func2 is b1.func2):
    True
    静态方法不管是”类.方法名”还是”实例.方法名”返回的都是同一个函数(不是函数对象),都是在0x1037893a0的内存地址,也是个普通函数,在A.dict我们找到了它的存放: ‘func2’: )>,staticmethod描述符的get方法始终返回,所以这个get方法是重写了。

如何理解实例方法与静态方法及类方法在继承中的差异?
重点在装饰启器的差异,实例方法没有装饰器,类方法有@classmethod,静态方法有@staticmethod。其实装饰器在底层重写了所有函数都有的get方法以实现不同功能。

##### 最终总结:

  • 重写
    重写其实是开辟了新的内存空间,然后存储到子类的dict字典中,所以重写属于本类,拥有独立空间,跟父不是同一个方法或者属性。
  • 继承
  • 类属性
    先本类dict查找,没有就往父类找,直到找到为止,所以类属性继承是同一个
  • 类方法
    类方法有@classmethod装饰,重写了get方法,类方法在继承中,不管是”类.方法名”还是”实例.方法名”,返回的都是方法绑定对象,该对象是新对象,内存地址不同,但是方法绑定对象绑定的方法是同一个方法
  • 静态方法
    静态方法有@staticmethod装饰,重写了get方法,不管是类还是实例调用,返回的都是同一个普通函数
  • 实例方法
    实例方法没有装饰器装饰,其实就是普通函数,没有重写get方法,当类调用时,由于instance是None,所以没有进行类绑定,直接函数本身,所以是同一个函数;通过实例调用时instance是对应实例,会进行绑定返回方法绑定对象,但是绑定的方法是同一个,指向同一个内存地址。所以实例方法继承是同一个方法
    所以继承都是同一个

单继承中的多级继承

这个指的是什么呢?上面单继承分析都是只继承了一个父类,但是往往父类还是有父类,形成单继承的多级继承,甚至更多级继承。
我们完全可以借助递归思路来理解分析,上面单继承分析两级继承,我们可以把这两级打包为一个整体来分析,然后作为一个整体与爷爷类形成了新的两级继承,过程完全一样,以此类推到多级继承,所以这里不再进行分析。

多继承

上面分析中,我们都是类B只继承类A,但是我们也可以继承其他的类C,类D,如class B(A,C,D)。这就是多继承
示例:

class A:
    def greet(self):
        print("A 的方法")

class B(A):
    def greet(self):
        print("B 的方法")
        super().greet()  # 移交给 MRO 链条中的下一个类

class C(A):
    def greet(self):
        print("C 的方法")
        super().greet()  # 移交给 MRO 链条中的下一个类

# D 同时继承 B 和 C,左侧的 B 优先级更高
class D(B, C):
    def greet(self):
        print("D 的方法")
        super().greet()

# --- 测试运行 ---
d = D()
d.greet()

print("\n--- 类 D 的 MRO 查找顺序 ---")
for cls in D.__mro__:
    print(cls)
多继承的MRO

我们回过来看上面多继承中的示例,如果不运行,我们会有一个疑问,类D继承了类B及类C,它们都有greet方法,实例d.greet()到底是调用哪一个类的greet()方法?这就是本节要讲的多继承的MRO(方法解析顺序)
主要经历过一下搜索算法。以下是这些算法的原理、优缺点以及最新现状。

  • 深度优先、从左至右算法(DLR)
    这是最早期、最直观的搜索算法。早期的 Python 2.2 之前、C++、Perl 等语言都使用过它
  • 算法原理:顺着左边的父类一路往最深处查找,直到没有父类了,再返回来找右边的父类。
  • 致命缺陷:无法解决”菱形继承(钻石问题)”。
    (1)假设 D 继承自 B 和 C,而 B 和 C 都继承自基础类 A。
    (2)DLR 的查找顺序是:D → B → A → C。
    (3)问题:即使右边的父类 C 重写了 A 的方法,由于优先深入到了顶层的 A,导致 A 的旧方法会意外遮蔽 C 的新方法。这样C类的重写没有意义了,因为会优先使用A中方法,A中先找到
  • 引入广度优先的过渡算法(Python 2.2 经典新式类)
    为了解决 DLR 深度优先导致的顶层类提前遮蔽问题,Python 2.2 引入了新式类,并对算法进行了修正。
  • 算法原理:如果遇到菱形继承,公共的祖先类(如 object)会被留到最后才查找。
  • 致命缺陷:违反了单调性(Monotonicity)。单调性是指:如果在类 C 中,父类 X 排在 Y 的前面,那么在 C 的任何子类中,X 也必须排在 Y 的前面。第二代算法在某些复杂的交叉多继承场景下,会导致子类的查找顺序突然发生“父子倒置”或“左右反转”,从而引发极其隐蔽的 Bug。
  • C3 线性化算法(C3 Linearization)
    为了彻底解决单调性问题,Python 2.3 至今(包括最新的 Python 3)、回响/Solidity(智能合约语言)、Perl 6 等语言,全面采用了由科学家提出的 C3 线性化算法。
  • 算法原理:它通过数学上的拓扑排序,将复杂的继承图扁平化地拉成一条绝对唯一的直线列表。它严格遵守 3 个核心原则:
  • 子类优先:子类一定排在所有父类前面。
  • 局部优先级:严格遵守写在括号 (B, C) 里的从左到右的顺序。
  • 单调性:保证所有子类和父类之间的相对顺序在整个继承树中是一致的。
  • Merge 融合机制:C3 算法的核心是 merge() 运算。每次提取一个列表中不在其他列表“尾部”出现的头元素。如果发现拓扑结构冲突,它会直接报错(TypeError)拒绝运行,从而在编译期就把歧义扼杀在摇篮里。

下面我们只讲C3 线性化算法情况

经典菱形继承(Diamond Inheritance)

这是最标准的菱形拓扑结构。子类通过两条不同的路径最终汇聚到同一个老祖先类。

  • 拓扑图
       [  object  ]
            ▲
            │
         [  A  ]
         ▲     ▲
        /       \
     [ B ]     [ C ]
      ▲           ▲
       \         /
        ───[ D ]───
  • C3 算法核心推导
    C3 算法的公式为:MRO(D) = [D] + merge(MRO(B), MRO(C), [B, C])
  • 先计算各父类:
    (1) MRO(A) = [A, object]
    (2) MRO(B) = [B, A, object]
    (3) MRO(C) = [C, A, object]
  • 代入 D 展开:
    (1) MRO(D) = [D] + merge([B, A, object], [C, A, object], [B, C])
    (2) 提 B:B 是第一个列表的头,且不在其他列表的尾部(B 满足条件)。→ 得到 D, B。
    (3) 剩下:merge([A, object], [C, A, object], [C])
    (4) 跳 A 提 C:A 在第二个列表的尾部([C, A…]),不能提!换下一个列表的头元素 C,C 满足条件。→ 得到 D, B, C。
    (5) 剩下:merge([A, object], [A, object])提 A 再提 object。
  • 最终 MRO 链条:D ➔ B ➔ C ➔ A ➔ object
  • 代码实例
class A:
    def execute(self):
        print("➔ 触发 A 的方法")

class B(A):
    def execute(self):
        print("➔ 触发 B 的方法")
        super().execute()

class C(A):
    def execute(self):
        print("➔ 触发 C 的方法")
        super().execute()

class D(B, C):
    def execute(self):
        print("➔ 触发 D 的方法")
        super().execute()

# --- 验证 D 的 MRO ---
print("【菱形继承 MRO 链条】:")
for idx, cls in enumerate(D.__mro__):
    print(f"位置 {idx}: {cls.__name__}")

print("\n【执行方法调用链】:")
d = D()
d.execute()

运行结果

【菱形继承 MRO 链条】:
位置 0: D
位置 1: B
位置 2: C
位置 3: A
位置 4: object

【执行方法调用链】:
➔ 触发 D 的方法
➔ 触发 B 的方法
➔ 触发 C 的方法
➔ 触发 A 的方法
Mixin 组合结构

这是工业界(如 Django 框架源码)最推崇的结构。主线是一个干净的单继承,旁边挂载多个只提供功能、不带状态的 Mixin 工具类。

  • 拓扑图
   [ object ]        [ object ]        [ object ]
       ▲                 ▲                 ▲
       │                 │                 │
   [  Base  ]    [ LogMixin ]   [ AuthMixin ]
       ▲                 ▲                 ▲
       │                 │                 │
       └─────────┬───────┴─────────────────┘
                 │
           [ MyService ]
  • C3 算法核心推导
    公式:MRO(MyService) = [MyService] + merge(MRO(Base), MRO(LogMixin), MRO(AuthMixin), [Base, LogMixin, AuthMixin])
  • 按照括号内从左到右的优先级,且由于各个 Mixin 与 Base 之间相互独立,没有交叉。
  • 拓扑排序会非常干净地从左至右切片提取。
  • 最终 MRO 链条:MyService ➔ Base ➔ LogMixin ➔ AuthMixin ➔ object
  • 示例代码
class Base:
    def run(self):
        print("➔ Base: 核心业务逻辑执行中")

class LogMixin:
    def run(self):
        print("➔ LogMixin: 记录日志...")
        super().run()

class AuthMixin:
    def run(self):
        print("➔ AuthMixin: 验证用户权限...")
        super().run()

# 按照 MRO 优先级:Base ➔ LogMixin ➔ AuthMixin 
class MyService(Base, LogMixin, AuthMixin):
    def run(self):
        print("➔ MyService: 准备启动服务")
        super().run()

# --- 验证 Mixin 组合 MRO ---
print("【Mixin 拓扑 MRO 链条】:")
for idx, cls in enumerate(MyService.__mro__):
    print(f"位置 {idx}: {cls.__name__}")

print("\n【执行 Mixin 调用链】:")
service = MyService()
service.run()

运行结果

【Mixin 拓扑 MRO 链条】:
位置 0: MyService
位置 1: Base
位置 2: LogMixin
位置 3: AuthMixin
位置 4: object

【执行 Mixin 调用链】:
➔ MyService: 准备启动服务
➔ Base: 核心业务逻辑执行中
交叉菱形/多重复杂结构(Crossed Diamond)

在复杂框架设计中可能会遇到这种交叉结构。两个不同的子类,分别继承了两个不同的交叉父类。

  • 拓扑图
           [ object ]
            ▲      ▲
           /        \
       [  X  ]    [  Y  ]
        ▲   \      /   ▲
       /     \    /     \
     /        \  /       \
  [ A ]       [ B ]     [ C ]
    ▲           ▲         ▲
     \          │        /
       ────────[ D ]────
  • C3 算法核心推导
    这里以 A(X), B(X, Y), C(Y) 为设定,D(A, B, C) 为子类。根据 C3 算法的局部优先级与单调性原则进行 Merge 融合:
  • MRO(A) = [A, X, object], MRO(B) = [B, X, Y, object], MRO(C) = [C, Y, object]
  • MRO(D) = [D] + merge(A_list, B_list, C_list, [A, B, C])
  • 提取 D → 提取 A → 提取 B。
  • 此时面临 X 和 C 的取舍。由于 X 是 B 的父类,且 B 内部 X 排在 Y 前面,而 C 绑定 Y。C3 会完美根据拓扑图的入度先提取独立分支。
  • 最终 MRO 链条:D ➔ A ➔ B ➔ X ➔ C ➔ Y ➔ object (注意:由于 B 强行把 X 压在了 Y 前面,所以 X 比 C 更早被提取)。
  • 示例代码
class X:
    def process(self):
        print("➔ 触发 X 的方法")

class Y:
    def process(self):
        print("➔ 触发 Y 的方法")

class A(X):
    def process(self):
        print("➔ 触发 A 的方法")
        super().process()

class B(X, Y):
    def process(self):
        print("➔ 触发 B 的方法")
        super().process()

class C(Y):
    def process(self):
        print("➔ 触发 C 的方法")
        super().process()

class D(A, B, C):
    def process(self):
        print("➔ 触发 D 的方法")
        super().process()

# --- 验证交叉结构 MRO ---
print("【复杂交叉拓扑 MRO 链条】:")
for idx, cls in enumerate(D.__mro__):
    print(f"位置 {idx}: {cls.__name__}")

print("\n【执行交叉拓扑调用链】:")
d_complex = D()
d_complex.process()

运行结果

【复杂交叉拓扑 MRO 链条】:
位置 0: D
位置 1: A
位置 2: B
位置 3: X
位置 4: C
位置 5: Y
位置 6: object

【执行交叉拓扑调用链】:
➔ 触发 D 的方法
➔ 触发 A 的方法
➔ 触发 B 的方法
➔ 触发 X 的方法

总结为:遇到有公共的父类停止搜索,往后查找,然后加上这个父类。

7.多态

多态(Polymorphic) 的字面意思是“多种状态”。在面向对象编程中,它的核心定义是:同一条调用语句,在传入不同的对象时,能够自动展现出不同的行为。

Python 多态的灵魂:鸭子类型(Duck Typing)

在 C++ 或 Java 等静态语言中,要实现多态,两个类必须继承自同一个父类(或者实现同一个接口)。但在 Python 中,多态完全不需要依赖继承!Python 遵循的是著名的 “鸭子类型” 哲学:

如果一只鸟走起来像鸭子,游泳起来像鸭子,叫起来也像鸭子,那么它就是鸭子。

底层本质:Python 在执行 obj.quack() 时,底层的描述符和属性查找机制根本不在乎 obj 的亲爹是谁(不在乎继承链)。它唯一在乎的是:此时此刻,obj.dict 或者它所属类的仓库里(遵循MRO搜索),有没有一个叫 quack 的键? 只要有,它就能成功触发并运行!
示例:

class Duck:
    def quack(self):
        print("🦆 鸭子在嘎嘎叫...")

class Robot:
    def quack(self):
        print("🤖 机器人模仿鸭子叫:哔哔嘎嘎!")

# 统一的调用接口函数
def make_it_quack(animal_obj):
    # 这一条相同的语句,传入不同对象,展现不同行为
    animal_obj.quack() 

# --- 测试多态 ---
duck = Duck()
bot = Robot()

make_it_quack(duck)  # 输出: 🦆 鸭子在嘎嘎叫...
make_it_quack(bot)   # 输出: 🤖 机器人模仿鸭子叫:哔哔嘎嘎!

运行结果

🦆 鸭子在嘎嘎叫...
🤖 机器人模仿鸭子叫:哔哔嘎嘎!

本质:make_it_quack 函数对输入的 animal_obj 没有任何类型限制。点号 . 触发时,如果是 duck,就去 Duck.dict 找方法;如果是 bot,就去 Robot.dict 找方法。两者的个体独立开辟内存空间,互不干扰,从而完美展现出多态。

经典的继承式多态

虽然 Python 不需要继承就能玩转多态,但在大型项目设计中,我们经常结合继承和方法重写(Override)来实现更规范的多态。
继承式多态代码示例:

class FileParser:
    """抽象父类(定义规范)"""
    def parse(self):
        raise NotImplementedError("子类必须重写此方法!")

class JsonParser(FileParser):
    def parse(self):
        print("📋 正在解析 JSON 文件...")

class XmlParser(FileParser):
    def parse(self):
        print("📄 正在解析 XML 文件...")

# 统一调用函数
def start_parsing(parser_obj):
    parser_obj.parse()

# --- 测试运行 ---
parsers = [JsonParser(), XmlParser()]
for p in parsers:
    start_parsing(p)

运行结果

📋 正在解析 JSON 文件...
📄 正在解析 XML 文件...

8.元类

什么是元类:通俗讲,能狗否创建类的类

普通类是创建实例对象的模板,而元类是创建类对象的模板。
如果一个类没有指定自定义元类metaclass,那么这个类基本都是由type类创建,所以type类是元类。

type元类

type类是元类,也就是能够创建类

  • 创建类的语法格式
    type(“类名字符串”, (父类1, 父类2, …), {类属性与方法的字典})
  • 第一个参数(name):”类名字符串”。定义类的名字(即类对象的 name 属性)。
  • 第二个参数(bases):(父类1, 父类2, …)。必须是一个元组(Tuple)。定义类的继承链,直接决定了该类未来的 mro 顺序。如果只继承 object,必须写成单元素元组 (object,),后面的逗号不能省略。
  • 第三个参数(dict):{类属性与方法的字典}。定义类的成员。这里面写的所有键值对,在类诞生后会直接注入到该类的 dict 仓库中。

传统创建类方式示例:

class BaseRobot:
    # 1. 类属性
    species = "AI"

    # 2. 实例方法
    def __init__(self, name):
        self.name = name

    def walk(self):
        print(f"{self.name} 正在行走...")

    # 3. 静态方法
    @staticmethod
    def info():
        print("这是一个机器人基础类")

type元类创建示例:

def robot_init(self, name):
    self.name = name

def robot_walk(self):
    print(f"{self.name} 正在行走...")

def robot_info():
    print("这是一个机器人基础类")

# ---- 核心:使用 type 创建类 ----
DynamicRobot = type(
    "BaseRobot",       # 1. 类名
    (object,),         # 2. 父类元组
    {                  # 3. 类仓库字典(__dict__ 的原材料)
        "species": "AI",
        "__init__": robot_init,
        "walk": robot_walk,
        "info": staticmethod(robot_info)  # 注意:静态方法需要用 staticmethod 包装器包裹
    }
)

无论用哪种形式创建,动态生成的 DynamicRobot 在底层的属性访问和内存表现上没有任何区别;传统写法的 class BaseRobot: 语法,其实就是 Python 解释器在后台帮你自动执行了一句 type(“BaseRobot”, (object,), {…})。只不过传统方式更直观点,其实都是type创建。

type元类可以创建类,那么我们是否可以基于这个类自己定义一个元类呢?
完全是可以的,这就是自定义类,创建类时通过metaclass指定自定义的类。
示例

# 1. 必须继承 type,才能成为一个合法的元类(超级图纸)
class BaseCustomMeta(type):

    # 重写 __new__ 方法,拦截类对象的诞生过程
    # mcs: 元类自己, name: 即将诞生的类名字符串, bases: 父类元组, attrs: 类仓库的原材料字典
    def __new__(mcs, name, bases, attrs):
        print(f"🚀 [元类触发] 发现类 '{name}' 正在准备创建...")

        # 【核心黑魔法发生地】
        # 此时类还没在内存中诞生,attrs 只是一个纯粹的 Python 字典(dict)
        # 既然是普通字典,我们就可以用最基础的语法直接往里写数据!
        attrs["PROJECT_VERSION"] = "2026.08.03"

        print(f"🛠️ [元类动态篡改] 成功往类 '{name}' 的 attrs 字典里注入了 PROJECT_VERSION 属性!")

        # 2. 最终必须调用父类 type.__new__,把修改后的 attrs 传过去,真正开辟内存创造出类对象
        return super().__new__(mcs, name, bases, attrs)

# 编写普通类,通过 metaclass 转向我们的基础自定义元类
class BaseRobot(metaclass=BaseCustomMeta):
    def __init__(self, name):
        self.name = name

# --- 验证与测试 ---
print("\n--- 🏁 开始测试底层内存闭环 ---")

# 1. 直接通过类名访问被元类偷偷注入的类属性
print(f"1. BaseRobot 类属性中是否存在注入值? -> {BaseRobot.PROJECT_VERSION}")

# 2. 实例化对象,看看实例能不能顺着继承查找链拿到它
bot = BaseRobot("Wall-E")
print(f"2. 实例对象 bot 顺着查找链访问到值了吗? -> {bot.PROJECT_VERSION}")

# 3. 彻底验明正身,看看类的妈妈到底是谁
print(f"3. BaseRobot 类的类型(也就是它的元类)是? -> {type(BaseRobot)}")

运行结果

🚀 [元类触发] 发现类 'BaseRobot' 正在准备创建...
🛠️ [元类动态篡改] 成功往类 'BaseRobot' 的 attrs 字典里注入了 PROJECT_VERSION 属性!

--- 🏁 开始测试底层内存闭环 ---
1. BaseRobot 类属性中是否存在注入值? -> 2026.08.03
2. 实例对象 bot 顺着查找链访问到值了吗? -> 2026.08.03
3. BaseRobot 类的类型(也就是它的元类)是? -> <class '__main__.BaseCustomMeta'>

到这里我们又发现了一个问题:
我们在自定义的BaseCustomMeta元类中我们重写了new方法,没有重写call方法,为何没报错?而我门定义的普通类实例对象添加()时如果没重写call方法的,但是会报错,原因在哪里?

  • 因为普通类加(),调用的是创建它的type元类的call方法,而实例对象添加()时是从创建它的普通类中找call方法,普通类只继承object类,object类没有call方法,如果普通类没有写call方法,普通类实例对象添加()是找不到call方法的。
  • 这里自定义元类BaseCustomMeta里继承了type元类,当bot = BaseRobot(“Wall-E”)时它会调用元类BaseCustomMeta的call方法,根据继承原则,元类BaseCustomMeta会继承type的call方法。
  • 在继承的call_方法里会调用object的__new方法,而BaseCustomMeta元类中重写的new方法是用来创建BaseRobot类的new方法,是用来创建BaseRobot类,重写作用是向BaseRobot类中注册类变量PROJECT_VERSION,而不是用来创建BaseRobot类实例的new方法。

这里就可以总结了:

  • type元类的call方法底层调用的new方法就是object类的new方法。
  • type元类的new方法用来创建类
  • 普通类的实例由object类的new方法创建,中间还需要通过call方法

我们尝试在IDE点击type查找call源码

    def __call__(self, *args, **kwargs): # real signature unknown
        """ Call self as a function. """
        pass

结果是real signature unknown(真实签名未知)
在实际运行时,这行 Python 层的 pass 代码连一微秒都没有被执行过,它纯粹是个摆设。解释器底层的语法解析器在零点几微秒内就会直接产生物理闪现,绕过所有的 Python 代码,直接强行冲进 C 语言的 type_call 车间中去执行!

我们从网上找到type元类call方法的C实现
https://github.com/python/cpython/blob/main/Objects/typeobject.c

static PyObject *
type_call(PyObject *self, PyObject *args, PyObject *kwds)
{
    PyTypeObject *type = PyTypeObject_CAST(self);
    PyObject *obj;
    PyThreadState *tstate = _PyThreadState_GET();

#ifdef Py_DEBUG
    /* type_call() must not be called with an exception set,
       because it can clear it (directly or indirectly) and so the
       caller loses its exception */
    assert(!_PyErr_Occurred(tstate));
#endif

    /* Special case: type(x) should return Py_TYPE(x) */
    /* We only want type itself to accept the one-argument form (#27157) */
    if (type == &PyType_Type) {
        assert(args != NULL && PyTuple_Check(args));
        assert(kwds == NULL || PyDict_Check(kwds));
        Py_ssize_t nargs = PyTuple_GET_SIZE(args);

        if (nargs == 1 && (kwds == NULL || !PyDict_GET_SIZE(kwds))) {
            obj = (PyObject *) Py_TYPE(PyTuple_GET_ITEM(args, 0));
            return Py_NewRef(obj);
        }

        /* SF bug 475327 -- if that didn't trigger, we need 3
           arguments. But PyArg_ParseTuple in type_new may give
           a msg saying type() needs exactly 3. */
        if (nargs != 3) {
            PyErr_SetString(PyExc_TypeError,
                            "type() takes 1 or 3 arguments");
            return NULL;
        }
    }

    if (type->tp_new == NULL) {
        _PyErr_Format(tstate, PyExc_TypeError,
                      "cannot create '%s' instances", type->tp_name);
        return NULL;
    }

    obj = type->tp_new(type, args, kwds);
    obj = _Py_CheckFunctionResult(tstate, (PyObject*)type, obj, NULL);
    if (obj == NULL)
        return NULL;

    /* If the returned object is not an instance of type,
       it won't be initialized. */
    if (!PyObject_TypeCheck(obj, type))
        return obj;

    type = Py_TYPE(obj);
    if (type->tp_init != NULL) {
        int res = type->tp_init(obj, args, kwds);
        if (res < 0) {
            assert(_PyErr_Occurred(tstate));
            Py_SETREF(obj, NULL);
        }
        else {
            assert(!_PyErr_Occurred(tstate));
        }
    }
    return obj;
}
static PyObject *
object_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    if (excess_args(args, kwds)) {
        if (type->tp_new != object_new) {
            PyErr_SetString(PyExc_TypeError,
                            "object.__new__() takes exactly one argument (the type to instantiate)");
            return NULL;
        }
        if (type->tp_init == object_init) {
            PyErr_Format(PyExc_TypeError, "%.200s() takes no arguments",
                         type->tp_name);
            return NULL;
        }
    }

    if (type->tp_flags & Py_TPFLAGS_IS_ABSTRACT) {
        PyObject *abstract_methods;
        PyObject *sorted_methods;
        PyObject *joined;
        PyObject* comma_w_quotes_sep;
        Py_ssize_t method_count;

        /* Compute "', '".join(sorted(type.__abstractmethods__))
           into joined. */
        abstract_methods = type_abstractmethods((PyObject *)type, NULL);
        if (abstract_methods == NULL)
            return NULL;
        sorted_methods = PySequence_List(abstract_methods);
        Py_DECREF(abstract_methods);
        if (sorted_methods == NULL)
            return NULL;
        if (PyList_Sort(sorted_methods)) {
            Py_DECREF(sorted_methods);
            return NULL;
        }
        comma_w_quotes_sep = PyUnicode_FromString("', '");
        if (!comma_w_quotes_sep) {
            Py_DECREF(sorted_methods);
            return NULL;
        }
        joined = PyUnicode_Join(comma_w_quotes_sep, sorted_methods);
        Py_DECREF(comma_w_quotes_sep);
        if (joined == NULL)  {
            Py_DECREF(sorted_methods);
            return NULL;
        }
        method_count = PyObject_Length(sorted_methods);
        Py_DECREF(sorted_methods);
        if (method_count == -1) {
            Py_DECREF(joined);
            return NULL;
        }

        PyErr_Format(PyExc_TypeError,
                     "Can't instantiate abstract class %s "
                     "without an implementation for abstract method%s '%U'",
                     type->tp_name,
                     method_count > 1 ? "s" : "",
                     joined);
        Py_DECREF(joined);
        return NULL;
    }
    PyObject *obj = type->tp_alloc(type, 0);
    if (obj == NULL) {
        return NULL;
    }
    return obj;
}

c代码验证了:
type元类的call方法底层调用的new方法就是object类的new方法。

代码验证:

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

# --- 🧪 终极对比实验开始 ---
print("--- 🏁 两种不同写法下,底层 __new__ 的物理内存地址大对撞 ---")

# 1. 用普通语法糖(方法 A)捏出第一个机器人
bot_from_syntax = BaseRobot("Wall-E")

# 2. 用大总管贴脸调用(方法 B)捏出第二个机器人
bot_from_god = type.__call__(BaseRobot, "Eve")

print("\n====== 📊 物理内存数据像素级现场对齐 ======")

# 提取两种写法背后,大总管在内部通过点号查找揪出来的 __new__ 实体
new_from_syntax = BaseRobot.__new__
new_from_god = BaseRobot.__new__

print(f"1. 普通 BaseRobot() 写法背后工作的 __new__ 实体:\n   {new_from_syntax}")
print(f"2. 直接调 type.__call__() 写法背后工作的 __new__ 实体:\n   {new_from_god}")

print("\n------------------------------------------------")

# 终极判定一:两种写法抓出来的 __new__ 函数在内存中是同一个绝对物理实体吗?
is_same_worker = (new_from_syntax is new_from_god)
print(f"   ➔ 结论一【绝对铁证】:两种写法背后的 __new__ is 同一个吗? -> {is_same_worker} ✅")

# 终极判定二:它是不是就是老祖宗 object.__new__?
print(f"   ➔ 结论二【彻底锁死】:它们和 object.__new__ is 同一个吗?     -> {new_from_syntax is object.__new__} ✅")

# 验证三:两个独立的实例,空间依然是各自新开辟的
print(f"   ➔ 结论三:两个实例依然是独立的物理实体吗?             -> {bot_from_syntax is not bot_from_god} ✅")

运行结果

--- 🏁 两种不同写法下,底层 __new__ 的物理内存地址大对撞 ---

====== 📊 物理内存数据像素级现场对齐 ======
1. 普通 BaseRobot() 写法背后工作的 __new__ 实体:
   <built-in method __new__ of type object at 0x101323d68>
2. 直接调 type.__call__() 写法背后工作的 __new__ 实体:
   <built-in method __new__ of type object at 0x101323d68>

------------------------------------------------
   ➔ 结论一【绝对铁证】:两种写法背后的 __new__ is 同一个吗? -> True ✅
   ➔ 结论二【彻底锁死】:它们和 object.__new__ is 同一个吗?     -> True ✅
   ➔ 结论三:两个实例依然是独立的物理实体吗?             -> True ✅

总结:

  • 所有普通类都是有type元类创建
  • 所有普通类的实例化都需要经过type元类的call方法调用object类的new方法创建

这里又有一个python奥秘,当我们点击type源码时,发现type继承object类,而print(type(object))打印的是type类,这就形成类似”鸡生蛋,蛋生鸡的循环”,到底谁生谁呢?这个得看cpython源码了,我们就不在这里纠缠了,反正这个设定造就了python语言的动态性,便捷性。

元类的应用举例
  • 场景一:ORM 框架黑魔法(动态将类属性映射为数据库表)

应用背景:我们在用 Django 或 SQLAlchemy 时,只需要声明 name = CharField(),框架就能自动把这个类变成数据库里的一张表。这背后的始作俑者就是元类。

示例代码:

# 1. 模拟 ORM 里的字段基类
class Field:
    pass

class CharField(Field):
    pass

class IntegerField(Field):
    pass

# 2. 核心黑魔法:ORM 元类
class ORMMetaclass(type):
    def __new__(mcs, name, bases, attrs):
        # 排除对 Model 基类本身的拦截
        if name == "Model":
            return super().__new__(mcs, name, bases, attrs)

        print(f"🕵️‍♂️ [ORM 检阅] 发现数据库模型类 '{name}' 正在创建,开始扫描字段...")

        # 准备一个独立的永久表字段仓库
        fields_map = {}

        # 关键一瞬间:在 attrs 还是纯生字典的时候,把属于 Field 基因的属性全部揪出来
        # 这样可以防止用户在业务代码里不小心改掉这些核心配置
        for key, value in list(attrs.items()):
            if isinstance(value, Field):
                fields_map[key] = value
                del attrs[key] # 从原本的类字典中抽离,彻底保护起来

        # 核心永久注册:把扫描出来的字段字典,以及模拟的数据库表名,强行焊死在类的基因里!
        attrs["_fields"] = fields_map
        attrs["_table_name"] = name.lower() + "_table"

        print(f"🚀 [ORM 永久注册] 成功为类 '{name}' 绑定了表名 '{attrs['_table_name']}' 并固化了字段仓库!")
        return super().__new__(mcs, name, bases, attrs)

# 3. 提供给用户使用的 Model 基类
class Model(metaclass=ORMMetaclass):
    pass

# ---- 业务代码(用户用起来极度丝滑) ----
class User(Model):
    username = CharField()
    age = IntegerField()

# ---- 验证闭环 ----
print("\n--- 🏁 现场像素级验证 ORM 黑魔法 ---")
print(f"1. User 类绑定的真实数据库表名是 -> {User._table_name}")
print(f"2. User 类的原始属性字典里还有 username 吗? -> {'username' in User.__dict__}")
print(f"3. User 永久固化的数据库字段仓库内容 -> {list(User._fields.keys())}")

运行结果

🕵️‍♂️ [ORM 检阅] 发现数据库模型类 'User' 正在创建,开始扫描字段...
🚀 [ORM 永久注册] 成功为类 'User' 绑定了表名 'user_table' 并固化了字段仓库!

--- 🏁 现场像素级验证 ORM 黑魔法 ---
1. User 类绑定的真实数据库表名是 -> user_table
2. User 类的原始属性字典里还有 username 吗? -> False
3. User 永久固化的数据库字段仓库内容 -> ['username', 'age']
  • 场景二:API 接口自动化框架(动态拦截并强制校验命名规范)

应用背景:在大厂的大型微服务架构中,为了防止几百个架构师乱写 API 名字,通常会用元类在代码加载期强行检阅所有类的方法名。如果发现谁没有遵守驼峰命名法(CamelCase)或下划线命名法(snake_case),直接拒绝让这个类诞生,将 Bug 彻底杀死在编译期!

示例代码:

class APIComplianceMeta(type):
    def __new__(mcs, name, bases, attrs):
        # 排除基类
        if name == "BaseAPI":
            return super().__new__(mcs, name, bases, attrs)

        print(f"🔍 [大厂合规性审查] 正在全线扫描 API 类 '{name}' 的命名规范...")

        # 遍历所有人写的方法原材料
        for key, value in attrs.items():
            # 排除系统内置的双下划线方法(魔法方法)
            if not key.startswith("__"):
                # 强行规定:所有的 API 方法必须以 'api_' 开头
                if not key.startswith("api_"):
                    # 💥 降维打击:不抛出运行期异常,直接在代码加载加载期抛出硬核安全规范异常!
                    # 让你这个类连在内存里诞生的机会都没有!
                    raise NameError(
                        f"\n❌ [拒绝诞生安全警告]:类 '{name}' 中的方法 '{key}' 严重违反了大厂 API 命名规范!\n"
                        f"👉 必须以 'api_' 作为前缀!"
                    )

        print(f"✅ [合规性通过] 类 '{name}' 100% 符合安全规范,准予开辟内存创建!")
        return super().__new__(mcs, name, bases, attrs)

class BaseAPI(metaclass=APIComplianceMeta):
    pass

# ---- 业务代码 ----
try:
    print("--- 🧪 场景 A:好员工提交了合规的代码 ---")
    class OrderAPI(BaseAPI):
        def api_get_order(self): pass
        def api_create_order(self): pass

    print("\n--- 🧪 场景 B:粗心的员工提交了违规的代码 ---")
    class PaymentAPI(BaseAPI):
        def api_pay_verify(self): pass
        def checkout_payment(self): pass # 💥 故意不加 api_ 前缀,违反大厂军规

except NameError as e:
    print(e)

运行结果

--- 🧪 场景 A:好员工提交了合规的代码 ---
🔍 [大厂合规性审查] 正在全线扫描 API 类 'OrderAPI' 的命名规范...
✅ [合规性通过] 类 'OrderAPI' 100% 符合安全规范,准予开辟内存创建!

--- 🧪 场景 B:粗心的员工提交了违规的代码 ---
🔍 [大厂合规性审查] 正在全线扫描 API 类 'PaymentAPI' 的命名规范...

❌ [拒绝诞生安全警告]:类 'PaymentAPI' 中的方法 'checkout_payment' 严重违反了大厂 API 命名规范!
👉 必须以 'api_' 作为前缀!
  • 场景三:基础架构组件注册器(全自动单例模式 / 插件工厂)

应用背景:在编写中间件、游戏引擎的动态插件系统、或者数据库连接池时,我们希望凡是继承了某个基类的组件,在诞生的一瞬间,自动把自己注册进中央控制大本营里,不需要人工去写各种 register() 函数。

示例代码:

# 中央控制大本营(全局组件注册中心)
GLOBAL_PLUGIN_REGISTRY = {}

class PluginRegisterMeta(type):
    def __new__(mcs, name, bases, attrs):
        # 通过 type.__new__ 把类对象在神明层给真正捏出来
        cls_object = super().__new__(mcs, name, bases, attrs)

        # 排除基类本身
        if name != "BasePlugin":
            # 💥 高能一瞬间:类对象刚在内存里落地,元类立刻把它揪住,
            # 自动把它的类名作为 Key,它的内存实体作为 Value,永久挂载进全局大本营!
            GLOBAL_PLUGIN_REGISTRY[name] = cls_object
            print(f"📦 [中央大本营联动] 插件类 '{name}' 诞生即完成全自动注册!")

        return cls_object

class BasePlugin(metaclass=PluginRegisterMeta):
    pass

# ---- 业务代码(插件开发者只需要老老实实写自己的业务,根本不需要懂中间件的注册逻辑) ----
class MySQLDriverPlugin(BasePlugin):
    pass

class RedisCachePlugin(BasePlugin):
    pass

# ---- 验证闭环 ----
print("\n--- 🏁 现场像素级验证中央大本营自动注册结果 ---")
print(f"目前中央大本营里已经静静躺着的插件实体有:\n{GLOBAL_PLUGIN_REGISTRY}")

运行结果

📦 [中央大本营联动] 插件类 'MySQLDriverPlugin' 诞生即完成全自动注册!
📦 [中央大本营联动] 插件类 'RedisCachePlugin' 诞生即完成全自动注册!

--- 🏁 现场像素级验证中央大本营自动注册结果 ---
目前中央大本营里已经静静躺着的插件实体有:
{'MySQLDriverPlugin': <class '__main__.MySQLDriverPlugin'>, 'RedisCachePlugin': <class '__main__.RedisCachePlugin'>}

9.内存模型与垃圾回收

Python 的内存模型主要由 ‌CPython 解释器自动管理,其核心设计目标是高效利用内存并减轻开发者的负担。与 C/C++ 需要手动管理内存不同,Python 通过 引用计数垃圾回收内存池机制三大支柱来协同工作。

核心机制:如何追踪和管理对象?

引用计数(Reference Counting)—— 第一道防线

这是 Python 内存管理最基础且最高效的机制。

  • 原理
  • 每个 Python 对象内部都有一个计数器,记录有多少个变量或引用指向它。
  • 当创建一个新引用时(如 a = obj),计数器 +1。
  • 当引用被删除或重新赋值时(如 del a 或 a = 1),计数器 -1。
  • 回收时机:当计数器归零时,说明该对象不再被任何地方使用,解释器会立即释放其占用的内存。
  • 优点:实时性高,内存一旦无用即刻释放。
  • 缺点:无法处理 ‌循环引用‌(Circular Reference)。例如,对象 A 引用对象 B,对象 B 又引用对象 A,即使外部没有引用它们,它们的计数器也永远不为 0,导致内存泄漏。

示例代码:

import gc
import sys

class Node:
    def __init__(self, name):
        self.name = name
        self.partner = None

    def __del__(self):
        print(f"🗑️ 对象 {self.name} 被销毁")

# 1. 创建循环引用
a = Node("A")
b = Node("B")
a.partner = b
b.partner = a

print(f"初始引用计数 a: {sys.getrefcount(a)}") # 通常为 3 (a, b.partner, getrefcount参数)

# 2. 删除外部引用
del a
del b

# 此时 a 和 b 的引用计数不为 0(因为互相引用),但已不可达
print("⏳ 触发垃圾回收...")
gc.collect() # 手动触发 GC,内部执行标记-清除

运行结果

初始引用计数 a: 3
⏳ 触发垃圾回收...
🗑️ 对象 A 被销毁
🗑️ 对象 B 被销毁
垃圾回收(Garbage Collection, GC)—— 解决循环引用

为了解决引用计数的短板,Python 引入了标记-清除和分代回收机制。

  • 标记-清除(Mark-and-Sweep):
    • 专门用于检测和处理循环引用。
    • 过程:从“根对象”(如全局变量、栈中的局部变量)出发,遍历所有可达对象并进行“标记”。遍历结束后,未被标记的对象即为不可达的“垃圾”,会被清除。
  • 分代回收(Generational Collection) 为了提高效率,Python 将对象分为三代:0代、1代、2代。
    • 规则:
      • 新创建的对象默认放在 0代。
      • 如果一次垃圾回收后对象依然存活,它会被晋升到下一代(0 -> 1 -> 2)。
      • 存活越久的对象,被认为是垃圾的概率越低‌。
    • 扫描频率:0代扫描最频繁,1代次之,2代最少。这种策略大幅减少了全量扫描的性能开销。

示例代码:

import gc

# 1. 设置极小的阈值,方便观察
gc.set_threshold(5, 2, 2) 

# 2. 创建对象并触发 0 代回收
objs_gen0 = []
for i in range(6): # 超过阈值 5
    objs_gen0.append(object())

print(f"0 代回收次数: {gc.get_count()}") 
gc.collect() # 强制回收,存活对象晋升到 1 代

# 3. 再次创建对象,观察 1 代情况
objs_gen1 = []
for i in range(6):
    objs_gen1.append(object())

# 此时 0 代再次满员,触发回收,之前的 objs_gen0 若仍存活则继续在 1 代
gc.collect()
print(f"当前各代对象数量: {gc.get_count()}") # (0代剩余, 1代剩余, 2代剩余)

运行结果

0 代回收次数: (150, 11, 0)
当前各代对象数量: (0, 0, 0)

内存分配策略:如何高效申请内存?

Python 并非每次创建对象都直接向操作系统申请内存,而是采用了分层分配策略:

(1)内存池机制(Memory Pool / pymalloc)

  • 适用场景:小于 256 KB的小对象。
  • 原理:Python 维护了一个私有的内存池。当需要小内存时直接从池中分配,而不是频繁调用系统的 malloc/free。
  • 优势:
  • 避免内存碎片。
  • 减少系统调用开销,提升分配速度。
  • 当池中没有空闲块时,才会向操作系统申请更大的内存块(Arena)。

(2)系统级分配

  • 适用场景:大于 256 KB的大对象。
  • 原理:直接调用操作系统的 malloc 或 mmap 进行分配。
  • 回收:大对象释放后直接归还给操作系统。

(3)小整数缓存(Small Integer Cache)

  • 范围:-5 到 256 之间的整数。
  • 机制:Python 在启动时会预先创建这些整数对象并缓存起来。
  • 效果:当你执行 a = 100 和 b = 100 时,a 和 b 指向的是内存中同一个对象。这既节省了内存,又加快了常见整数的访问速度。
a = 100
b = 100
print(a is b)  # True (指向同一内存地址)

c = 1000
d = 1000
print(c is d)  # False (通常指向不同对象,除非在同一代码块优化下)

Python 内存模型架构图

# Python 内存模型架构图
# ==============================================

[用户代码层]
    ↓ 执行创建对象操作
[Python 对象层]
    ├─ 整数/字符串/列表/自定义类实例等所有运行时对象
    └─ 每个对象内置 ob_refcnt(引用计数器)
    ↓ 内存管理调度
[内存管理层]
    ├─ 核心机制1:引用计数
    │   ├─ 引用+1:新建别名、传入函数
    │   ├─ 引用-1:del删除、变量重赋值
    │   └─ 触发回收:引用计数归0 → 立即释放对象内存
    │
    └─ 核心机制2:自动垃圾回收(GC)
        ├─ 解决痛点:循环引用场景下引用计数无法归零
        ├─ 算法1:标记-清除
        │   ├─ 从根对象(栈/全局变量)遍历所有可达对象
        │   └─ 未标记的不可达对象 → 判定为垃圾直接回收
        └─ 算法2:分代回收
            ├─ 0代(新生代):新创建对象,扫描频率最高
            ├─ 1代(中生代):0代存活对象晋升,扫描频率中等
            └─ 2代(老年代):1代存活对象晋升,扫描频率最低
    ↓ 内存分配调度
[内存分配层]
    ├─ 分支1:小对象分配(<256KB)
    │   └─ 调用pymalloc → 私有内存池
    │       ├─ Arena(大块内存区)
    │       ├─ Pool(内存池单元)
    │       └─ Block(最小分配单元)
    │
    ├─ 分支2:大对象分配(>256KB)
    │   └─ 直接调用系统接口 → malloc/mmap
    │
    └─ 内置缓存机制
        ├─ 小整数池:预创建-5~256所有整数对象,全局复用
        └─ 字符串驻留池:部分短字符串全局共享,避免重复创建
    ↓ 底层交互
[操作系统内存层]
    └─ 物理内存/虚拟内存页
未经允许不得转载:云端笔记 » 【Python】类与对象

相关文章

评论 (0)

5 + 1 =

contact