最近天天跑深度学习的代码,突然在代码中一眼看到类的定义,然后借助豆包了解了下相关内容,发现自己对python类的知识还有较多欠缺,谨以此文章记录下我对类空间和实例空间这两者对象的理解。(感觉理解了这块后,就能清晰的知道函数类内每一块内容的基础变动了)
为了后续的一些定义让大家都清楚地明白,我先将代码放这并且标注好每行代码分别代表的含义。
class MyClass:
class_attr = "I'm a class attribute" #这是类属性
def __init__(self): #__init__是python的一个魔法方法,他是实例的初始化方法
self.instance_attr = "I'm an instance attribute" #实例对象的属性,其中self就代表着实例对象
obj = MyClass() #其中MyClass就是类的定义,这个 ‘=’就表示实例化的实现,其中obj就是MyClass类的一个实例化对象
# 首先在实例空间查找 class_attr,找不到则到类空间查找
print(obj.class_attr)
# 直接在实例空间找到 instance_attr
print(obj.instance_attr)
1. 类空间与实例空间的定义
-
类空间(类命名空间):类空间是类对象所拥有的命名空间,它用于存储类的属性和方法,类属性是所有实例共享的属性,其中类属性可以是任意数据类型(如列表、整型、浮点型、元组、字典等),类方法可以操作这些类属性。类空间在类定义时被创建,所有属于该类的实例都可以访问类空间中的内容。
-
实例空间(实例命名空间):实例空间是每个实例对象独有的命名空间,它用于存储实例的属性(实例属性)。每个实例都有自己独立的实例空间,不同实例的实例属性是相互独立的。实例空间在实例化对象时被创建。
定义部分已经说明完毕,但要完全理解各自的内容,我们得按照类空间与实例空间内部的一些属性通过代码来按条列出才能更容易地理解。
1.1 类空间与实例空间之间的继承
继承关系,实例空间会继承类空间中的属性和方法,当访问一个实例的属性或方法时,python解释器会首先在实例空间中查找,如果找不到就会到类空间中查找。
class MyClass:
class_attr = "I'm a class attribute"
a = 'b'
def __init__(self):
self.instance_attr = "I'm an instance attribute"
self.a = 'c'
obj = MyClass()
# 首先在实例空间查找 class_attr,找不到则到类空间查找
print(obj.class_attr) #输出I'm a class attribute
# 直接在实例空间找到 instance_attr
print(obj.instance_attr) #输出I'm an instance attribute
#首先在实例空间查找 a,发现实例空间内有a后就不在类空间内查找了
print(obj.a) #输出 c
首先我们分析一下这个类MyClass
的定义,其中的class_attr
是类属性,__init__
是实例对象的初始化方法,其中的instance_attr
是实例属性,python在处理实例化对象时,他的运行流程是当实例化对象运行类中的一些属性或方法时,实例对象的查找顺序一般都是先从实例空间中进行查找,若在实例空间内查找到相应的属性就不会到类空间内进行查找了(如上述代码中的obj.a
),如果在实例空间内未找到,就会再去类空间去查找(如上述代码的obj.class_attr
)。
1.2 类空间与实例空间相互独立
独立性:实例空间和类空间是相互独立的, 实例可以有自己特有的属性和方法,这些属性和方法不会影响类空间。同样,类空间的修改也不会直接影响实例已经拥有的实例属性。
class MyClass:
class_attr = "original class attr"
def __init__(self):
self.instance_attr = "original instance attr"
obj = MyClass()
# 修改实例属性,不影响类属性
obj.instance_attr = "modified instance attr"
# 修改类属性,不影响实例已经有的实例属性
MyClass.class_attr = "modified class attr"
print(obj.instance_attr) # 输出: modified instance attr
print(MyClass.class_attr) # 输出: modified class attr
1.3 类空间与实例空间中方法与属性之间的访问方式
class MyClass:
class_attr = 10
obj = MyClass()
# 访问类属性
print(obj.class_attr) # 输出: 10
为了轻松理解类空间与实例空间之间的访问机制,我们首先来分析这个代码,可以直接得知MyClass
这个类中,他的实例空间是没有的,只有一个类属性class_attr
,那么我们最后打印出来的obj.class_attr
输出的结果还是10,和类属性一致。
这背后的python知识:在实例化对象obj后,obj.class_attr
是obj这个实例的一个实例属性,与类属性无关!!!但如果碰到我们所需要实例化的属性不存在时,python会自动创建我们需要实例化属性(这里就是obj这个实例的class_attr
这个属性),我们发现MyClass并没有__init__这个实例化对象的方法,但为什么打印出来的结果为10呢?这就说明python首先在实例空间内生成了一个新的实例属性class_attr
(他是新的实例属性表示实例空间定义的class_attr
与类空间的class_attr
不是一个东西,这个解释见下面代码),并且由于实例空间内没有class_attr
(注意,一般自己在实例空间内要新添一个未新建的实例属性时,需要自己去赋好值),那么python就会从类空间内寻找是否有相同的属性名称,所以就找到类空间内的类属性class_attr=10
了,所以最终打印出10这个结果。
下面进阶下:
class MyClass:
class_attr = 10
def __init__(self):
self.class_attr += 1
obj = MyClass()
# 访问类属性
print(obj.class_attr) # 输出: 11
# 在实例上赋值同名属性,创建新的实例属性
obj.class_attr = 20
print(obj.class_attr) # 输出: 20,这里访问的是实例属性,而是由于上一行代码的直接对obj的class_attr进行实例化,python直接找到对应的的内容,所以并未执行+1操作
print(MyClass.class_attr) # 输出: 10,类属性未被修改
这段代码首先实例化完obj这个实例,之后print(obj.class_attr) # 输出: 11
——就这个流程而言,是可以通过上面那段代码进行解释。
关键在于obj.class_attr = 20;print(obj.class_attr) # 输出: 20
——这个点,有了上面案例的性质,一开始会认为这块应该输出11而不是10,但程序的输出结果就是20,在最后print(MyClass.class_attr) # 输出: 10
——这个点,MyClass.class_attr
是类直接访问本身的属性(提醒下除了此处,上面我们访问的都是实例空间的实例属性!!!),结果表明类属性的内容并未受到实例属性修改的影响,即类属性和实例属性是相互不影响的,就是两个完全不一样的东西,只是在极端情况下实例属性的值会依靠类属性的值赋予。
**总结:**若新添的实例空间中没有对应的属性,实例空间会自动新建一个该属性(该属性一般会自己进行赋值操作,如obj.class_attr=0
),如果该属性未自己进行赋值并且该属性与类空间中的属性名相同,那么该新添的属性的值是与类空间相应属性的值是一样的,如果上述条件均为满足,则程序会出现类似AttributeError: 'xx' object has no attribute 'a'
的错误。类属性与实例属性是两个完全不同的内容。
1.4 多个实例对象间的关系与类属性的关系
class Student:
def __init__(self, name):
self.name = name
s1 = Student("Alice")
s2 = Student("Bob")
s1.name = "Charlie"
print(s1.name) # 输出 Charlie
print(s2.name) # 输出 Bob
这段代码表示同一个类的不同实例s1、s2
之间的实例属性name中,即使s1的name被修改后,也不会干扰到实例s2的name的结果,即不同实例对象属性之间也是互不干扰。
class Student:
student_num = 0
s1 = Student()
s2 = Student()
Student.student_num = 1
print(s1.student_num) # 输出 1
print(s2.student_num) # 输出 1
print(Student.student_num) #输出1
如果定义了多个实例的话,其中实例属性是不可能修改类属性的,如果类属性是通过类的直接访问这种情况,那么所有实例对象的对应类属性都会被相应修改,如上述代码的s1、s2
实例对象,当Student这个类直接对其类属性student_num
进行访问并修改值,即Student.student_num=1
,s1与s2对应的类属性都会被修改(不知道大家还记不记得1.3节的案例,我们这儿前两个实例s1、s2
输出的内容分别是实例s1、s2
的实例属性而不是类属性,只是这块就是新建实例属性与类属性的命名相同导致输出的值相同)。
1.5 让新建的实例对象的消失
既然想到了创建那么多的实例对象,那么自然会有办法让新建的实例对象消失哈哈哈。
此处借助 del 命令删除实例对象的属性。
class MyClass:
class_attr = 10
obj = MyClass()
obj.class_attr = 20
# 手动删除实例属性
del obj.class_attr
# 此时再访问 obj.class_attr 会报错
try:
print(obj.class_attr)
except AttributeError:
print("实例属性 class_attr 已被删除")
此处是借助del 命令删除实例对象以及销毁该实例对象的所有实例属性
class MyClass:
class_attr = 10
obj = MyClass()
obj.class_attr = 20
# 删除实例对象
del obj
# 此时再尝试访问 obj 会报错
try:
print(obj.class_attr)
except NameError:
print("obj 已被删除,无法访问")
还有一个在python内比较重要的垃圾回收机制,这里涉及对象的引用计数的重要概念,当一个对象的引用计数降为0时,python的垃圾回收机制会自动回收该对象及其所有属性。比如当一个实例对象没有任何变量引用他时,其引用计数会降为0,实例属性也会随之被回收而消失。
class MyClass:
class_attr = 10
def create_obj():
obj = MyClass()
obj.class_attr = 20
return obj
# 调用函数创建实例对象
new_obj = create_obj()
print(new_obj.class_attr) # 可以正常访问实例属性
# 解除对实例对象的引用
new_obj = None
# 此时 new_obj 引用的实例对象引用计数降为 0,会被垃圾回收,实例属性也消失
此时若在该程序的尾部添加print(new_obj.class_attr)
会报错:AttributeError: 'NoneType' object has no attribute 'class_attr'
,其中NoneType
表示没有任何类型,即表示该实例对象已经被垃圾回收机制处理并销毁。
1.6 类方法、实例方法与普通方法
类方法
类方法是使用 @classmethod
装饰器修饰的方法,且他的第一个参数为 cls
,cls表示类本身。
class MyClass:
class_variable = 10
def instance_method(self):
print("This is an instance method")
@classmethod
def class_method(cls):
print(f"Class variable value: {cls.class_variable}")
obj = MyClass()
obj.instance_method() # 实例调用实例方法
MyClass.class_method() # 类名调用类方法
obj.class_method() # 实例调用类方法
其中class_method(cls)
就是类方法,他是通过@classmethod
这个装饰器进行标记的,
- 类的生命周期:类方法的生命周期与类本身的生命周期一致,当类被定义时,类方法就被创建并存在于类的命名空间中;只要类存在于内存中,类方法就可以被调用;只有当类对象被垃圾回收(通常是在程序结束或类对象不再被引用且满足垃圾回收条件时),类方法才会从内存中移除。
- 类方法的作用:类方法可以直接访问和修改类属性,因为他的第一个参数
cls
代表类本身,例如上面的class_method
方法就可以访问class_variable
类属性。 创建类的实例:类方法可以用于创建类的实例,尤其是在需要根据不同条件创建实例的情况下 - 类方法与普通方法、实例方法的调用不同:类方法可以通过类名以及实例直接调用,而普通方法、实例方法需要以实例对象去调用;普通方法可以访问和修改实例属性和类属性;类方法主要用于访问和修改类属性
实例方法
实例方法始终是第一个形参为self
的方法就属于实例可以调用的方法
class Student:
def method1(self, a):
print("This is method 1")
def __init__(self):
print("Initializing instance")
def method2(self, b):
print("This is method 2")
s = Student()
s.method1(a = 1)
s.method2(b = 2)
普通方法
普通方法如下,即没有类方法与实例方法规则的要求
class Student:
def class_method(a, b):
print("This is a ordinary method")
静态方法
python中静态方法是使用@staticmethod
装饰器修饰的方法,他可以定义在类内部也可以定义在类的外部(即也可以作为静态函数),但他既不依赖于类本身(即不需要类作为参数),也不依赖于类的实例(即不需要实例作为参数)。
class MathUtils:
@staticmethod
def add(a, b):
return a + b
obj = MathUtils()
# 调用静态方法
result_1 = MathUtils.add(3, 5) #通过类名去调用该静态方法
result_2 = obj.add(3, 5) #通过实例去调用该静态方法
print(result_1) #输出 8
print(result_2) #输出 8
根据上面的代码,可知add(a, b)
的形参的第一个位置并没有cls、self
,表明他既不依赖于类本身(cls)也不依赖于类的实例(self)。静态方法的性质其实和普通独立的函数类似,他是用于专门完成特定的任务,不会涉及类或实例的状态。静态方法的主要功能是更好地组织代码以及避免整个程序空间中的命名冲突问题,并且静态方法和类方法一样均可以直接通过类名或实例对象通过.
去直接调用。
1.7 为实例对象额外添加新的内容
- 通过__init__方法来初始化实例属性,并且在类进行实例化对象时直接手动将形参赋予意义。
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
# 创建不同的实例并添加不同属性
student1 = Student("Alice", 20)
student2 = Student("Bob", 22)
print(student1.name, student1.age) #输出 Alice 20
print(student2.name, student2.age) #输出 Bob 22
- 在实例创建后,也能进行动态地位实例添加属性
class Student:
pass
student1 = Student()
student2 = Student()
# 为 student1 添加额外属性
student1.hobby = "Reading"
# 为 student2 添加不同的额外属性
student2.sport = "Basketball"
print(hasattr(student1, 'hobby')) #输出 True
print(hasattr(student2, 'sport')) #输出 True
这儿是直接通过 .
以实例对新建的属性访问,这儿实例对象分别为student1、student2
,他们各自新建的实例属性分别为hobby sport
,分别赋予的实例属性取值为Reading Basketball
。
- 同时我们也可以借助为不同实例添加不同的函数方法
这里借助types.MethodType
把不同的方法绑定到不同的实例上。
import types
class Student:
pass
def study(self):
print(f"{self.name} is studying.")
def play(self):
print(f"{self.name} is playing.")
student1 = Student()
student1.name = "Alice"
# 为 student1 绑定 study 方法
student1.study = types.MethodType(study, student1)
student2 = Student()
student2.name = "Bob"
# 为 student2 绑定 play 方法
student2.play = types.MethodType(play, student2)
student1.study() #输出Alice is studying.
student2.play() #输出Bob is playing.
分析下代码流程:
首先定义好了Student类以及两个普通函数def study(self);def play(self)
,随后实例化student1、student2
,并且student1.name = "Alice",student2.name = "Bob"
即均额外添加新的实例属性,随后借助types.MethodType
命令把不是类内空间与实例空间的函数方法捆绑到实例空间内,并且self即实例对象本身,即在student1:self->student1,student2:self->student2
**总结:**至此,类空间与实例空间之间的相互关系以及实例对象进行动态添加属性以及属性值的过程都通过代码可视化,我在学习的过程中又突然意识到一个问题,学习代码一开始尽可能理解被封装好的代码表明的流程,随后要懂得查看每一个封装好的命令的背后的源码操作,这个点就是能直接知晓每个函数背后的输入输出吧,因为python的生态本身较大,命令特别多也容易记混,可能自己潜意识的对某个命令认为他是这么输出的,其实不然,而且如果自己做的东西因为这种潜意识的误判而导致实验的错误进行,之后的纠错成本简直吓人。