Python中类的实例化对象的过程到底做了什么事情?
·
实例化对象的过程大体上可以分成4件事,用比较形象的话来讲就是:产生空白实例、注入灵魂、绑定类、引用赋值。接下来用一个简单的例子我们具体来剖析一下
-
以下是一个简单的小案例
# 创建Dog类 class Dog: def __init__(self,name): self.name = name def bark(self): print(f"{self.name} say woof!") # 实例化Dog类 dog1 = Dog("Fido") -
第一步:分配内存空间(产生空白实例)
Python 首先会调用类的内置魔术方法 __new__()(通常是隐式调用的) 动作:在计算机的堆内存(Heap)中开辟一块全新的干净空间 结果:产生了一个“空壳”对象。这个对象此时已经有了一个唯一的内存地址(例如 0xA001) -
第二步:初始化对象属性(注入灵魂)
接着,Python 会自动调用你在类里定义的 __init__() 方法(构造函数),并将刚刚创建的“空壳”对象作为第一个参数 self 传进去。 # 代码对应: def __init__(self, name): self.name = name 动作:此时 self 就指向了 0xA001 这块内存。 代码 self.name = "Fido" 实际上就是在 0xA001 的独立字典(__dict__)中,贴上了一个键值对:"name": "Fido"。 结果:这个实例从此有了自己独一无二的特征(实例属性) -
第三步:建立“认祖归宗”的引用(绑定类)
这是一个非常关键的底层细节(实例对象内部的 __class__ 指针): 动作:新创建的实例对象(0xA001)内部会生成一个隐藏的属性 __class__,这个指针会死死指向创建它的类对象(即 Dog 类所在的内存地址 0xC001)。 为什么需要它? 因为实例对象为了省内存,自己内部只保存属性(数据),不保存方法。当你后面调用 dog1.bark() 时,dog1 就是通过这个 __class__ 指针找到 Dog 类,进而找到并执行 bark 函数对象的 -
第四步: 引用赋值(贴标签)
最后,Python 把这个制作完成的实例对象的内存地址(0xA001),返回并赋值给栈内存(Stack)中的变量名 dog1。 从此,dog1 就成了这块内存的代号
恭喜你!现在这四个步骤你已经学会了!
但此时你会不会好奇当我们通过实例对象去找类方法时具体是怎么查找的呢?这个过程同样分为四个步骤:属性查找、核心魔法、入栈传参、代码执行与寻值,下面让我们具体来看下吧
- 第一步:属性查找(通过 class 认祖归宗)
当写下 dog1.bark(先不看括号)时,Python 开始在内存中寻找 bark 这个名字: 去实例找:Python 首先查看 dog1 对象的堆内存空间(即它的 __dict__)。Python 发现里面只有 {"name": "Fido"},没有 bark。 去类里找:既然实例没有,Python 就会顺着实例内部的 __class__ 指针,瞬间移动到 Dog 类对象的内存空间(0xC001)。 找到了:在 Dog 类的 __dict__ 中,Python 成功找到了 "bark": <function Dog.bark>(这是一个普通的函数对象)。 - 第二步:核心魔法——将“函数”包装为“绑定方法”
这是最关键的一步。Python 发现你在实例上调用了一个类里定义的方法,它不会直接把类里的函数吐给你,而是会在内存中临时创建一个绑定方法对象(Bound Method Object)。 这个“绑定方法”对象就像是一个双面胶,它内部默默地打包了两个核心信息: __func__:指向刚才在 Dog 类里找到的那个真正的方法体(bark 函数对象)。 __self__:指向当前调用它的实例对象的内存地址(即 dog1 的地址 0xA001)。 所以,当你输入 dog1.bark 时,在 Python 底层,它其实已经悄悄变成了:“一个死死捆绑了 dog1 实例的 bark 函数”。 # 代码演示 class Person: def __init__(self, name): self.name = name def run(self): print("跑步") xm = Person("小明") # 可以讲此时的 xm.run 理解为这个 绑定方法对象 print(hex(id(xm))) # 和 xm.run.__self__ 是同一个地址 print(xm.run.__self__) print(hex(id(Person.__dict__['run']))) # 和 xm.run.__func__ 是同一个地址 print(xm.run.__func__) - 第三步: 压入栈区,传递参数(括号 () 触发执行)
现在,你加上了括号:dog1.bark()。 这个括号触发了函数的执行,Python 开始在栈区(Stack)为这次调用开辟临时栈帧。 关于参数的具体传递逻辑如下: 开辟栈帧:栈区为 bark 方法生成一个专属的栈帧。 自动填充 self:Python 解释器会从刚才那个“绑定方法”对象的 __self__ 属性中,取出 dog1 的内存地址(0xA001),自动把它作为第一个参数强行塞给栈帧中的局部变量 self。 填充其他参数:如果你在括号里传了其他参数,比如 dog1.bark("loudly"),Python 会把 "loudly" 依次排在后面,赋值给方法定义的第二个、第三个形参。因为你这里是 dog1.bark() 没传参,所以其他位置留空。 - 第四步:代码执行与寻值
现在,控制权交给了 Dog 类里的 bark 函数代码体。 代码开始执行:print(f"{self.name} says woof!")程序在当前栈帧的局部变量里找到了 self(它代表堆区的 0xA001)。 遇到 self.name,程序再次去 0xA001 堆区对象的 __dict__ 字典里查找 "name" 这个键。成功找到字符串 "Fido"。控制台打印出 "Fido says woof!"。 方法执行完毕,该栈帧从栈区弹出并销毁,调用结束。恭喜你,到这里你完全懂了!!!
通过打印地址演示
class Student:
def __init__(self,name):
self.name = name
def study(self):
pass
xm = Student("小明")
print('实例对象地址 -> xm:',xm) # 0x10bb3cee0
print('实例对象xm存储的类__class__ -> Student:', hex(id(xm.__class__))) # 0x7fbc7bb05280
print('-'*50)
print('类地址 Student:', hex(id(Student))) # 0x7fbc7bb05280
print('类地址方法 study 地址:', hex(id(Student.study))) # 0x10bb2faf0
print('-'*50)
study = xm.study
print('根据xm创建的绑定方法对象:', study) # <bound method Student.study of <__main__.Student object at 0x10bb3cee0>>
print('根据xm创建的绑定方法对象地址:', hex(id(study))) # 0x10bb38f00
print('-'*50)
print('绑定方法中存储的属性', study.__func__) # 类方法地址 0x10bb2faf0
print('绑定方法中存储的属性', study.__self__) # 实例对象地址 0x10bb3cee0
更多推荐



所有评论(0)