引言

前面关于面向对象的几篇文章,其实主要围绕着面向对象的第一个核心理念——封装,进行面向对象的介绍。从类、对象的静态构成的角度,对类与对象的定义及使用进行介绍。

在进入面向对象另外两个理念的介绍之前,我觉得有必要对Python中对象的生命周期管理进行一些介绍,从而知道我们通过代码定义了类对象、实例对象,这些对象是怎么创建出来的,又是怎么被销毁的。从动态的视角对对象的定义及使用进行一个补充说明。

再看类对象

在探讨实例对象的生命周期之前,有必要先回顾一下关于类也是对象的概念。

Python中一切皆对象,类也是对象。

我们以一个简单的类定义示例,查看类对象的特性与实例对象的关联关系:

import sys


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


if __name__ == '__main__':
    # 类对象的输出
    print(DaGongRen)
    print(id(DaGongRen))
    print(type(DaGongRen))
    if isinstance(DaGongRen, type):
        print('True')
    # 输出当前的类对象的引用计数,先不要管这个引用计数的初始值
    print(sys.getrefcount(DaGongRen))
    # 实例化该类的第一个实例对象
    zs = DaGongRen('张三')
    # 可以看到类对象的引用计数+1
    print(sys.getrefcount(DaGongRen))
    # 与id(DaGongRen)输出相同
    print(id(zs.__class__))
    # 与id(DaGongRen)输出相同,type类返回对象所属的类对象
    print(id(type(zs)))
    # 实例化该类的第二个实例对象
    ls = DaGongRen('李四')
    # 与id(DaGongRen)输出相同
    print(id(ls.__class__))
    # 可以看到类对象的引用计数+1
    print(sys.getrefcount(DaGongRen))
    del zs
    # 可以看到类对象的引用计数-1
    print(sys.getrefcount(DaGongRen))


执行结果:

27、Python之面向对象:方生方死?对象生命周期是如何管理的-LMLPHP

从上面的执行结果中,我们可以得出这样的一些结论:

1、类也是对象,有对应的id,type()返回type,表示类对象是由type类进行实例化得来的对象。

2、实例化对象中有一个属性__class__存储该对象所属类对象的引用;当然我们也可以通过DaGongRen.__class__获取类对象所属类对象的引用,会得到<class 'type'>。

3、每实例化一个实例化对象,都会增加一个对类对象的引用,引用计数+1,每销毁一个实例化对象,则会减少一个类对象的引用。

4、类对象是一种单例模式的存在,通过type(obj)返回的类对象、通过obj.__class__获取的类对象,或者通过类名本身引用的类对象,id都是相同的,所以都是同一个类对象。

通过上面这些结论,我们也能大概知道为什么对象能够访问到类属性、类方法,因为实例化对象持有类对象的引用,也就是__class__属性。

实例对象的生命周期

类对象相对特殊一些,但是,从上面的示例也能看出来,类对象也都是type类的实例化对象。接下来,我们对DaGongRen类的定义进行扩充,来看实例对象的生命周期管理。

首先,简单补充一下Python垃圾回收的相关内容:

1、最朴素的垃圾回收原理,是基于引用计数的方式来进行的,如果一个对象没有被引用了,也就是引用计数为0,一定是不可用的,所以该对象会被标记为可回收,在恰当的时机会被进行垃圾回收,从而释放对象所占用的内存。

2、在CPython中,垃圾回收使用的主要算法是引用计数。实际上,每个对象都会统计有多少引用指向自己。当引用计数归零时,对象会被销毁:CPython会首先在对象上调用__del__方法(如果定义了),然后释放分配给对象的内存。

3、引用计数是存在缺陷的,如果存在两个对象的循环引用,则会始终导致无法进行垃圾回收。

4、CPython2.0增加了分代垃圾回收算法,用于检测引用循环中涉及的对象组——如果一组对象之间全是相互引用,那么即使再出色的引用方式也会导致组中的对象不可达。

直接通过代码实例来看:

class DaGongRen:
    # 对象实例化的魔法函数,或者钩子函数,会被自动调用
    # 一般不要重写
    def __new__(cls, *args, **kwargs):
        self = super().__new__(cls)
        print(f"实例化方法:__new__被调用,分配内存,实例化对象")
        return self

    # 对实例化对象进行初始化操作,主要是定义属性并赋予默认值
    def __init__(self, name):
        self.name = name
        print(f"初始化方法:__init__被调用,进行对象属性初始化")

    # 实例化对象被垃圾回收之前,会调用该方法,有时也称为析构方法
    # 可以进行一些资源释放、关闭的相关操作,一般不要重写
    def __del__(self):
        print(f"析构方法:__del__被调用,对象{id(self)}将被销毁,并释放内存")


if __name__ == '__main__':
    zs = DaGongRen('张三')

执行结果:

27、Python之面向对象:方生方死?对象生命周期是如何管理的-LMLPHP

通过代码示例,可以有如下结论:

1、Python中的对象的生命周期主要有三个阶段:对象实例化、对象初始化、对象被销毁。

2、生命周期的三个阶段,分别会对应3个钩子方法:__new__、__init__、__del__。

3、钩子方法通常不需要我们自己手动调用,Python解释器会根据对象实例化操作或者垃圾回收自动调用对应的方法。

27、Python之面向对象:方生方死?对象生命周期是如何管理的-LMLPHP

上面的代码只是用于进行对象生命周期的演示,在实际应用中,需要注意的是:

1、类的__new__() 方法很少通过用户代码定义。如果定义了它,它通常是用原型__new__(cls, *args, **kwargs) 编写的,其中args 和kwargs 与传递给__init__() 的参数相同。__new__() 始终是一个类方法,接受类对象作为第一个参数。尽管__new__() 会创建一个实例,但它不会自动调用__init__() 。

2、如果在类中定义了__new__() ,通常表明这个类会做两件事之一。首先,该类可能继承自一个基类,该基类的实例是不可变的。如果定义的对象继承自不可变的内置类型(如整数、字符串或元组),常常会遇到这种情况,因为__new__() 是唯一在创建实例之前执行的方法,也是唯一可以修改值的地方(也可以在__init__() 中修改,但这时修改可能为时已晚)。

3、创建实例之后,实例将由引用计数来管理。如果引用计数到达0,实例将立即被销毁。当实例即将被销毁时,解释器首先会查找与对象相关联的__del__() 方法并调用它。而实际上,很少有必要为类定义__del__() 方法。唯一的例外是在销毁对象之后需要执行清除操作(如关闭文件、关闭网络连接或释放其他系统资源)。即使在这种情况下,依靠__del__() 来完全关闭实例也存在一定的危险,因为无法保证在解释器退出时会调用该方法。更好的方案是定义一个方法,如close() ,程序可以使用该方法显式执行关闭操作。

4、对象绝不会自行销毁,然而,当对象不可达时,可能会被当做垃圾回收。而所谓的垃圾回收最根本的一个保证,不是一定回收垃圾,而是不要把非垃圾对象进行错误回收。

总结

今天的文章中,首先回顾了Python中类也是对象的概念,然后粗略介绍了Python中对象的生命周期管理。通过这些内容的介绍,可以对Python中一个对象从创建到销毁的全过程,有个整体上的认知,从而为进行Python内容更深入的学习打下基础。

感谢您拨冗阅读,如果能对您稍微有一点点帮助,那就是本文的最大价值了。

27、Python之面向对象:方生方死?对象生命周期是如何管理的-LMLPHP

08-07 13:15