6.1 一切皆对象
1. 运算符
运算符是可以定义的,运算符有如:+、-、<、>、and、or等。
我们用特殊方法__add__()来定义加法,__sub__()来定义减法,__mul__()来定义乘法,__or__()来定义或的逻辑运算......
定义运算符,对复杂对象的处理极其有帮助,例如某种物体的属性多样,若只取其中一种属性进行比较而决定物体的好坏,那么物体之间的比较就可以 定义成 其中的一种属性的比较。
2.元素引用
如同运算符一般,元素引用的许多形式或函数都是通过调用特殊方法来实现的。有如:
- li[3]→li.__getitem__(3)
- li.insert(3,0)→li.__setitem__(3,0)
- dict.pop("a")→dict.__delitem__("a")
......
3. 内置函数的实现
直接列举一些:
1.len([1,2,3])→[1,2,3].__len__()
2.abs(-1)→(-1).__abs__()
3.int(2.3)→(2.3).__int__()
......
6.2 属性管理
1.属性覆盖
以前我们学到了继承,其中提及到属性覆盖,但未进行深究,此刻便稍微对继承中的属性覆盖进行更为深刻的了解。
在理解属性覆盖之前,我们需要知道的是Python的__dict__属性。当一个类或者对象拥有属性时,便会记录在字典__dict__中。其中键为属性名,值为对应的属性值。Python在寻找对象的属性时,会依照继承关系来依次查找__dict__。
首先,类Object是类层次结构的根类,Object类是所有类的父类,所以放在最底层,然后到定义的一个大类即父类(相对其属下),父类又拥有子类,最后到属于子类的对象。显然这是一个分层管理机制。当我们要调用某个属性时,会采取就近原则,即从对象往底层搜寻字典__dict__。正因如此,某个属性可能在不同的层被重复定义过,于是在向下历遍的过程中,取最近的便会发生属性覆盖。
这是属性覆盖的原理所在。
查看属性:
对象名/类名.__dict__
调用属性时,Python会分层查找;但是如果是赋值时,Python只会在该层进行操作,在__dict__中添加或修改属性。
也可以不考虑继承,直接修改父类的属性。
2. 特性
即时生成属性的方法之一——特性(property),即特殊的属性。目的是希望修改属性时,依赖其的其他属性也能同时变化。
内置函数property()创建特性:
class num(object):
def __init__(self,value):
self.value = value
def get_neg(self):
return -self.value
def set_neg(self,value):
self.value = -value
def del_neg(self):
print("value also deleted")
del self.value
neg = property(get_neg,set_neg,del_neg,"I'm negative")
#property函数最多可以加载4个参数,分别用于设置获取、修改和删除特性,最后一个为说明文档
x = num(1.1)
print(x.neg)
x.neg = -22
print(x.value)
print(num.neg.__doc__)
del x.neg
3. __getattr__方法
除了内置函数property来查询即时生成的属性,还可以用__getattr__(self,name)方法。当__dict无法找到要调用的属性时,就会调用__getattr__()来即时生成该属性。(__getattr__()只能用来查询__dict__不存在的属性,__getattribute__()则可以查询任意属性)
示例:
class Bird(object):
feather = True
class chicken(Bird):
fly = False
def __init__(self,age):
self.age = age
def __getattr__(self,name):
if name == "adult":
if self.age > 1.0:
return True
else:
return False
else:
raise AttributeError(name)
summer = chicken(2)
print(summer.adult) #True
summer.age = 0.5
print(summer.adult) #False
print(summer.male) #抛出错误AttributeError
__getattr__()可以将所有即时生成的属性集中在一个函数内进行处理,其根据函数名区别处理不同的属性。
另外:__setattr__(self,name,value)和__delattr__(self,name)分别用于修改和删除属性,可用于任意属性。
Python描述符 (descriptor) 详解
对象属性的访问顺序:
① __getattribute__(), 无条件调用
② 数据描述符:由 ① 触发调用 (若人为的重载了该 __getattribute__() 方法,可能会调职无法调用描述符)
③ 实例对象的字典(若与描述符对象同名,会被覆盖哦)
④ 类的字典
⑤ 非数据描述符
⑥ 父类的字典
⑦ __getattr__() 方法
6.3 我是风儿,我是沙
1. 动态类型
典型的动态类型的体现就是变量,变量不需要事先声明,其可以任意多次赋值以改变它的值。来一个简单的语句:
a = 1
a是对象名,1是对象,对象名是指向对象的引用,就是对象名相当于中介,因为对象是存储在内存中的实体,我们碰不到它,所以要一个中间人去接触它。
通过内置函数id()我们可以查看引用指向的是哪个对象。
如此,重新赋值就是换个对象而已,中介不变。即变量名是个随时变更指向的引用,自然,类型也可以"随变"。
Python会缓存小的整数和段字符串,可以通过is运算来判断引用之间是否指向同一对象。
2. 可变与不可变对象
一个对象可有多个引用。
引用之间呈独立性,即改变一个引用,不会影响其他引用的指向。
对于以下情况:
list2 = [1,2,3]
list1 = list2
list1[0] = 10
print(list2)
并非失去了引用的独立性,而是list1和list2依旧指向同一列表,但是list1[0] = 10改变的正是指向的列表中的第一位元素的指向,即只改变了其中一个元素的指向,因此指向该列表的对象名(引用)都会受到影响。
可变对象:如列表、词典这种自身能发生改变的对象。
不可变对象:如整数、浮点数、字符串、元组这种不能改变对象本身,赋值最多只能改变引用的指向的不可变数据对象。
3. 从动态类型看函数的参数传递
函数的参数传递,本质上传递的是引用。
def nishama(x):
print(id(x))
x = 100
print(id(x))
a = 1
print(id(a))
nishama(a)
print(a)
这里是不可变对象的操作,其实就是赋值操作。
def nishama(x):
x[0] = 10
print(x)
a = [1,2,3]
nishama(a)
print(a) #打印[10,2,3]
这是可变对象的操作,通过引用操作可变对象会影响到其他的引用。
6.4 内存管理
1. 引用管理
一个对象可以有多个引用,引用次数就是来记录这些引用总数的。可以用标准库中sys包中的getrefcount()来查看某个对象的引用次数。
当使用某个引用为参数,传递给getrefcount()时,参数实际上是创建了一个临时的引用。故getrefcount()所得到的结果会比期望多1:
from sys import getrefcount
a = [1,2,3]
print(getrefcount(a)) #打印2,即比期望多1,1+1=2
b = a
print(getrefcount(b)) #打印3,2+1=3
2. 对象引用对象
对于可变对象,如列表和词典,对象是包含对象的。准确地说,容器对象包含的是指向各个元素对象的引用。
若在主程序中使用a = 1,就会把这个关系存入到一个词典中,这个词典是记录所有全局引用的,可以用globals()来查看这个词典。
当一个对象a被另外一个对象b引用时,a的引用计数将增加1:
from sys import getrefcount
a = [1,2,3]
print(getrefcount(a)) #打印2
b = [a,a]
print(getrefcount(a)) #打印4,1+1+2=4,引用2次a所以增加2
容器对象的引用可能会构成很复杂的拓扑结构(其实可能就是花样套娃),于是可以用objgraph包来描绘其引用关系:
x = [1,2,3]
y = [x,dict(key1=x)]
z = [y,(x,y)]
import objgraph
objgraph.show_refs([z],filename='ref_topo.png') #第二个参数说明绘图文件名
两个对象相互引用,形成引用环。
del关键字:1、删除某个引用:del a
2、删除容器中的元素:del a[0]
引用的转移会使对象的引用次数减少,就是有一个中介人不做你的生意了,你的收入就少一份。
3. 垃圾回收
Python的垃圾回收机制即对于没有引用次数的对象,会进行删除回收,将其所占内存清除。
垃圾回收的启动机制:会设置一个关于分配对象和取消分配对象的次数差值的阈值。一旦高于这个阈值,垃圾回收才会启动。可以通过gc模块的get_threshold()方法来查看阈值或改变阈值。(会返回三个值,第一个就是回收启动的阈值,后面两个值是与分代回收相关的阈值)
手动启动回收:使用gc.collect()
上面讲的是基础回收,下面讲分代回收。
分代回收将对象分为0、1、2三代,新建的是0代,经历过一次垃圾回收仍留下的对象就归为1代,如果再经历过一次回收依旧留下的对象就归为2代。垃圾回收启动时,肯定对0代全部对象进行扫描。若0代经过一定次数的垃圾回收,就对0代和1代进行扫描清理。1代也经历了一定次数的回收,就对0、1、2代进行扫描,全部对象的扫描。
前面提到的后两个值就是(每n次0代垃圾回收会配合1次1代的垃圾回收),(每n次1代就会有1次2代清查)。
4. 孤立的引用环
还记得上上一小节讲的引用环吗?它对垃圾回收机制带来很大的麻烦。简而言之,就是引用环锁住了一些对象,使一些对象的引用次数不降为0,但是这些对象又偏偏被删除了引用。
为了这样的引用环,Python出了一个针对性解决的方法:就是复制每个对象的引用次数,记作gc_ref,假设每个对象i,计数就为gc_ref_i。Python历遍所有的对象i,对于每个对象所引用的对象j,将相应的gc_ref_j减去1,当历遍结束以后,保留gc_ref不为0的对象和这些对象引用的对象及更下游引用的对象(一个套娃有效,它的里面全部套娃就有效),其他的进行回收。
总结:Python是一种动态类型的语言,其对象与引用分离,内置垃圾回收以释放内存,因是以引用次数为依据的简单垃圾回收机制,所以需要对孤立引用环的问题进行解决。