python 高级编程④元类、垃圾回收

一元类

1类也是对象

在大多数编程语言中,类就是一组用来描述如何生成一个对象的代码段。在Python中这一点仍然成立。

但是,Python中的类还远不止如此。类同样也是一种对象。

类对象拥有创建对象(实例对象)的能力。但是,它的本质仍然是一个对象,于是乎你可以对它做如下的操作:

1.你可以将它赋值给一个变量

2.你可以拷贝它

3.你可以为它增加属性

4.你可以将它作为函数参数进行传递


2动态的创建类


3使用type创建类

type还有一种完全不同的功能,动态的创建类。

type可以接受一个类的描述作为参数,然后返回一个类。(要知道,根据传入参数的不同,同一个函数拥有两种完全不同的用法是一件很傻的事情,但这在Python中是为了保持向后兼容性)

type可以像这样工作:

type(类名,由父类名称组成的元组(针对继承的情况,可以为空),包含属性的字典(名称和值))


4使用type创建带有属性的类


Foochild继承了Test2的bar属性

·type的第2个参数,元组中是父类的名字,而不是字符串

·添加的属性是类属性,并不是实例属性

5使用type创建带有方法的类

为type创建的类添加实例方法

为type创建的类添加类方法


为type创建的类添加静态方法


6什么是元类?

元类就是创建类的‘东西’,在python中万物皆对象,类也是对象。

type就是一个元类,可以用来创建类。

Python中所有的东西,注意,我是指所有的东西——都是对象。这包括整数、字符串、函数以及类。它们全部都是对象,而且它们都是从一个类创建而来,这个类就是type。

元类就是创建类这种对象的东西。type就是Python的内建元类,当然了,你也可以创建自己的元类。

7__metaclass__属性

你可以在定义一个类的时候为其添加__metaclass__属性。

classFoo(object):

__metaclass__ = something…

...省略...

如果你这么做了,Python就会用元类来创建类Foo。小心点,这里面有些技巧。你首先写下class Foo(object),但是类Foo还没有在内存中创建。Python会在类的定义中寻找__metaclass__属性,如果找到了,Python就会用它来创建类Foo,如果没有找到,就会用内建的type来创建这个类。把下面这段话反复读几次。当你写如下代码时:

classFoo(Bar):

pass

Python做了如下的操作:

1.Foo中有__metaclass__这个属性吗?如果是,Python会通过__metaclass__创建一个名字为Foo的类(对象)

2.如果Python没有找到__metaclass__,它会继续在Bar(父类)中寻找__metaclass__属性,并尝试做和前面同样的操作。

3.如果Python在任何父类中都找不到__metaclass__,它就会在模块层次中去寻找__metaclass__,并尝试做同样的操作。

4.如果还是找不到__metaclass__,Python就会用内置的type来创建这个类对象。

现在的问题就是,你可以在__metaclass__中放置些什么代码呢?答案就是:可以创建一个类的东西。那么什么可以用来创建一个类呢?type,或者任何使用到type或者子类化type的东东都可以。

8自定义元类

newAttr = {}

forname,valueinfuture_class_attr.items():

ifnotname.startswith("__"):

newAttr[name.upper()] = value

#调用type来创建一个类

returntype(future_class_name, future_class_parents, newAttr)

classFoo(object, metaclass=upper_attr):

bar ='bip'

print(hasattr(Foo,'bar'))

print(hasattr(Foo,'BAR'))

f = Foo()

print(f.BAR)

现在让我们再做一次,这一次用一个真正的class来当做元类。

#coding=utf-8

classUpperAttrMetaClass(type):

# __new__是在__init__之前被调用的特殊方法

# __new__是用来创建对象并返回之的方法

#而__init__只是用来将传入的参数初始化给对象

#你很少用到__new__,除非你希望能够控制对象的创建

#这里,创建的对象是类,我们希望能够自定义它,所以我们这里改写__new__

#如果你希望的话,你也可以在__init__中做些事情

#还有一些高级的用法会涉及到改写__call__特殊方法,但是我们这里不用

def__new__(cls, future_class_name, future_class_parents, future_class_attr):

#遍历属性字典,把不是__开头的属性名字变为大写

newAttr = {}

forname,valueinfuture_class_attr.items():

ifnotname.startswith("__"):

newAttr[name.upper()] = value

#方法1:通过'type'来做类对象的创建

# return type(future_class_name, future_class_parents, newAttr)

#方法2:复用type.__new__方法

#这就是基本的OOP编程,没什么魔法

# return type.__new__(cls, future_class_name, future_class_parents, newAttr)

#方法3:使用super方法

returnsuper(UpperAttrMetaClass, cls).__new__(cls, future_class_name, future_class_parents, newAttr)

#python2的用法

classFoo(object):

__metaclass__ = UpperAttrMetaClass

bar ='bip'

# python3的用法

# class Foo(object, metaclass = UpperAttrMetaClass):

#     bar = 'bip'

print(hasattr(Foo,'bar'))

#输出: False

print(hasattr(Foo,'BAR'))

#输出:True

f = Foo()

print(f.BAR)

1.拦截类的创建

2.修改类

3.返回修改之后的类

二垃圾回收

1小整数池对象

为了优化速度,python使用了小整数对象池,避免为整数频频申请和销毁内存空间。

Python对小整数的定义是[-5, 257)这些整数对象是提前建立好的,不会被垃圾回收。在一个Python的程序中,所有位于这个范围内的整数使用的都是同一个对象.

同理,单个字母也是这样的。

但是当定义2个相同的字符串时,引用计数为0,触发垃圾回收


2大整数对象池

每一个对象均创建一个新的对象


3intern机制

在python中,对于相同的字符串只开辟一个内存空间,靠引用计数来维护何时释放。

·小整数[-5,257)共用对象,常驻内存

·单个字符共用对象,常驻内存

·单个单词,不可修改,默认开启intern机制,共用对象,引用计数为0,则销毁

�在字符串中,若含有空格,不开启intern机制,不共用对象,引用计数为0销毁

大整数不共用内存,引用计数为0,销毁

数值类型和字符串类型在Python中都是不可变的,这意味着你无法修改这个对象的值,每次对变量的修改,实际上是创建一个新的对象

4Garbage collection(GC垃圾回收)

①python采用的是引用计数机制为主,标记-清除和分代收集两种机制为辅的策略

python里每一个东西都是对象,它们的核心就是一个结构体:PyObject

typedefstruct_object {

intob_refcnt;

struct_typeobject *ob_type;

} PyObject;

PyObject是每个对象必有的内容,其中ob_refcnt就是做为引用计数。当一个对象有新的引用时,它的ob_refcnt就会增加,当引用它的对象被删除,它的ob_refcnt就会减少

当引用计数为0时,该对象生命就结束了。

②引用计数机制的优点:

·简单

·实时性:一旦没有引用,内存就直接释放了。不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时。

③引用计数机制的缺点:

·维护引用计数消耗资源

·循环引用

list1 = []

list2 = []

list1.append(list2)

list2.append(list1)

list1与list2相互引用,如果不存在其他对象对它们的引用,list1与list2的引用计数也仍然为1,所占用的内存永远无法被回收,这将是致命的。 对于如今的强大硬件,缺点1尚可接受,但是循环引用导致内存泄露,注定python还将引入新的回收机制。(标记清除和分代收集)

5 Ruby与Python垃圾回收

①应用程序那颗跃动的心

GC系统所承担的工作远比"垃圾回收"多得多。实际上,它们负责三个重要任务。它们

·    为新生成的对象分配内存

·     识别那些垃圾对象

      ·从垃圾对象那回收内存

        我认为垃圾回收就是应用程序那颗跃动的心。像心脏为身体其他器官提供血液和营养物那样,垃圾回收器为你的应该程序提供内存和对象。如果心脏停跳,过不了几秒钟人就完了。如果垃圾回收器停止工作或运行迟缓,像动脉阻塞,你的应用程序效率也会下降,直至最终死掉。

②一个简单的例子

classNode:

def__init__(self,val):

self.value=val

print(Node(1))

print(Node(2))

③Python的对象分配


与Ruby不同,当创建对象时Python立即向操作系统请求内存。(Python实际上实现了一套自己的内存分配系统,在操作系统堆之上提供了一个抽象层。但是我今天不展开说了。)

当我们创建第二个对象的时候,再次像OS请求内存:


④Python住在卫生之家


在内部,创建一个对象时,Python总是在对象的C结构体里保存一个整数,称为引用数。期初,Python将这个值设置为1:


值为1说明分别有个一个指针指向或是引用这三个对象。假如我们现在创建一个新的Node实例,JKL:


与之前一样,Python设置JKL的引用数为1。然而,请注意由于我们改变了n1指向了JKL,不再指向ABC,Python就把ABC的引用数置为0了。 此刻,Python垃圾回收器立刻挺身而出!每当对象的引用数减为0,Python立即将其释放,把内存还给操作系统。

Python的这种垃圾回收算法被称为引用计数。

假如我们让n2引用n1:


'DEF'上的引用数被python减少了,垃圾回收器立刻回收DEF实例,同时JKL的引用数已经变为了2,因为n1和n2都指向它

⑤标记-删除 vs 引用计数

引用计数并不像第一眼看上去那样简单。有许多原因使得不许多语言不像Python这样使用引用计数GC算法:

首先,它不好实现。Python不得不在每个对象内部留一些空间来处理引用数。这样付出了一小点儿空间上的代价。但更糟糕的是,每个简单的操作(像修改变量或引用)都会变成一个更复杂的操作,因为Python需要增加一个计数,减少另一个,还可能释放对象。

第二点,它相对较慢。虽然Python随着程序执行GC很稳健(一把脏碟子放在洗碗盆里就开始洗啦),但这并不一定更快。Python不停地更新着众多引用数值。特别是当你不再使用一个大数据结构的时候,比如一个包含很多元素的列表,Python可能必须一次性释放大量对象。减少引用数就成了一项复杂的递归过程了

最后,它不是总奏效的。引用计数不能处理环形数据结构--也就是含有循环引用的数据结构。

⑥在Python中的零代(Generation Zero)

我们希望Python的垃圾回收机制能够足够智能去释放这些对象并回收它们占用的内存空间。但是这不可能,因为所有的引用计数都是1而不是0。Python的引用计数算法不能够处理互相指向自己的对象。

Python使用一种不同的链表来持续追踪活跃的对象。而不将其称之为“活跃列表”,Python的内部C代码将其称为零代(Generation Zero)。每次当你创建一个对象或其他什么值的时候,Python会将其加入零代链表:


现在零代包含了两个节点对象。(他还将包含Python创建的每个其他值,与一些Python自己使用的内部值。)

⑦检测循环引用


从上面可以看到ABC和DEF节点包含的引用数为1.有三个其他的对象同时存在于零代链表中,蓝色的箭头指示了有一些对象正在被零代链表之外的其他对象所引用。(接下来我们会看到,Python中同时存在另外两个分别被称为一代和二代的链表)


通过识别内部引用,Python能够减少许多零代链表对象的引用计数。在上图的第一行中你能够看见ABC和DEF的引用计数已经变为零了,这意味着收集器可以释放它们并回收内存空间了。剩下的活跃的对象则被移动到一个新的链表:一代链表。

⑧Python中的GC阈值

Python什么时候会进行这个标记过程?随着你的程序运行,Python解释器保持对新创建的对象,以及因为引用计数为零而被释放掉的对象的追踪。从理论上说,这两个值应该保持一致,因为程序新建的每个对象都应该最终被释放掉。

当然,事实并非如此。因为循环引用的原因,并且因为你的程序使用了一些比其他对象存在时间更长的对象,从而被分配对象的计数值与被释放对象的计数值之间的差异在逐渐增长。一旦这个差异累计超过某个阈值,则Python的收集机制就启动了,并且触发上边所说到的零代算法,释放“浮动的垃圾”,并且将剩下的对象移动到一代列表。

随着时间的推移,程序所使用的对象逐渐从零代列表移动到一代列表。而Python对于一代列表中对象的处理遵循同样的方法,一旦被分配计数值与被释放计数值累计到达一定阈值,Python会将剩下的活跃对象移动到二代列表。


三 gc模块

1垃圾回收机制

Python中的垃圾回收是以引用计数为主,分代收集为辅。

2导致引用计数+1的情况

①对象被创建,例如a=123

②对象被引用,例如b=a

③对象作为参数,传入一个函数中,例如func(a)

④对象作为一个元素,存储在容器中,例如list1=[a,a]

3导致引用计数-1的情况

①对象的别名被显式销毁,例如del a

②对象的别名被赋予新的对象,例如a=124

③一个对象离开它的作用域,例如f函数执行完毕时,func函数中的局部变量(全局变量不会)

④对象所在的容器被销毁,或从容器中删除对象

4查看一个对象的引用计数

import sys

a=‘hello world’

sys.getrefcount(a)

可以查看a对象的引用计数,但比正常计数大1,因为调用查看引用计数的函数时将a传入,使a的引用计数+1

5循环引用导致内存泄漏问题

引用计数的缺陷是循环引用的问题

importgc

classClassA():

def__init__(self):

print('object born,id:%s'%str(hex(id(self))))

deff2():

whileTrue:

c1 = ClassA()

c2 = ClassA()

c1.t = c2

c2.t = c1

delc1

delc2

#把python的gc关闭

gc.disable()

f2()

执行f2(),进程占用的内存会不断增大。

·创建了c1,c2后这两块内存的引用计数都是1,执行c1.t=c2和c2.t=c1后,这两块内存的引用计数变成2.

·在del c1后,内存1的对象的引用计数变为1,由于不是为0,所以内存1的对象不会被销毁,所以内存2的对象的引用数依然是2,在del c2后,同理,内存1的对象,内存2的对象的引用数都是1。

·虽然它们两个的对象都是可以被销毁的,但是由于循环引用,导致垃圾回收器都不会回收它们,所以就会导致内存泄露。

说明:

垃圾回收后的对象会放在gc.garbage列表里面

·gc.collect()会返回不可达的对象数目,4等于两个对象以及它们对应的dict

有三种情况会触发垃圾回收:

1.调用gc.collect(),

2.当gc模块的计数器达到阀值的时候。

3.程序退出的时候

6gc模块常用功能解析

gc模块提供一个接口给开发者设置垃圾回收的选项,上面说到,采用引用计数的方法管理内存的一个缺陷是循环引用,而gc模块的一个主要功能是解决循环引用的问题。

7常用函数

1、gc.set_debug(flags)设置gc的debug日志,一般设置为gc.DEBUG_LEAK

2、gc.collect([generation])显式进行垃圾回收,可以输入参数,0代表只检查第一代的对象,1代表检查一,二代的对象,2代表检查一,二,三代的对象,如果不传参数,执行一个full collection,也就是等于传2。 返回不可达(unreachable objects)对象的数目

3、gc.get_threshold()获取的gc模块中自动执行垃圾回收的频率。

4、gc.set_threshold(threshold0[, threshold1[, threshold2])设置自动执行垃圾回收的频率。

5、gc.get_count()获取当前自动执行垃圾回收的计数器,返回一个长度为3的列表

gc模块的自动垃圾回收机制

必须要import gc模块,并且is_enable()=True才会启动自动垃圾回收。

这个机制的主要作用就是发现并处理不可达的垃圾对象。

垃圾回收=垃圾检查+垃圾回收

例如(488,3,0),其中488是指距离上一次一代垃圾检查,Python分配内存的数目减去释放内存的数目,注意是内存分配,而不是引用计数的增加。例如:

printgc.get_count()# (590, 8, 0)

a = ClassA()

printgc.get_count()# (591, 8, 0)

del a

printgc.get_count()# (590, 8, 0)

3是指距离上一次二代垃圾检查,一代垃圾检查的次数,同理,0是指距离上一次三代垃圾检查,二代垃圾检查的次数。

gc模快有一个自动垃圾回收的阀值,即通过gc.get_threshold函数获取到的长度为3的元组,例如(700,10,10)每一次计数器的增加,gc模块就会检查增加后的计数是否达到阀值的数目,如果是,就会执行对应的代数的垃圾检查,然后重置计数器

例如,假设阀值是(700,10,10):


注意点

gc模块唯一处理不了的是循环引用的类都有__del__方法,所以项目中要避免定义__del__方法

推荐阅读更多精彩内容