6.1 一切皆对象
实际上,许多语法如运算符、元素引用、内置函数中,其实都来自一些特殊的对象。
1.运算符
运算符+、-、* 、/、>、<、or、and都是通过特殊方法实现的。
比如,list是列表类,用dir(list)查看列表的属性,会发现有一个特殊方法__add__()
,它就特殊在定义了两个列表的相加,即 +。对于其他对象,只要定义了特殊方法,也就可以进行相应的运算了。
print([1,5,6] + [2,3,4])
结果:
[1, 5, 6, 2, 3, 4]
字符串也有+
print("abc"+"xyz")
结果:
abcxyz
上面两个例子实际上调用了__add__()
方法
print([1,5,6].__add__([2,3,4]))
print("abc".__add__("xyz"))
结果:
[1, 5, 6, 2, 3, 4]
abcxyz
其他特殊方法
print(1.8.__mul__(2)) # 1.8 * 2
print(True.__or__(False)) # True or False
print(5.0.__floordiv__(2)) # 5.0 // 2
结果:
3.6
True
2.0
这些运算相关的特殊方法还能改变执行运算的方式。
比如,列表中是不允许相减运算的,我们可以创建一个子类,通过增加__sub__()
方法来实现减法操作。
#创建列表的子类,定义__sub__()方法
class SuperList(list):
def __sub__(self,b):
a = self[:] #由于继承list,self可以利用[:]的引用来表示整个列表
b = b[:] # 用b来接收列表b
while(len(b) > 0): # 当b的长度不为0,
element_b = b.pop() #弹出b最后一个元素,赋值给element_b
if element_b in a: # 如果element_b在a列表中
a.remove(element_b) # 就删元素element_b
return a # 最后返回a列表
print(SuperList([1,2,3]) - SuperList([2,3,4])) #创建两个SuperList对象做减法
结果:
[1]
上面例子用内置函数__sub__()
方法定义了-
操作。即使__sub__()
方法在父类中已经定义过,我们可以进行重写覆盖。
2.元素引用
我们想要获取列表某一个元素时,通常如下
li = [1,2,3,4,5]
print(li[3]) # 找到列表索引为3的元素
结果:
4
实际上,这个引用调用了__getitem__()特殊方法
print(li.__getitem__(3))
结果:
4
有__getitem__()
方法,那么就有__setitem__()
方法
li = [1,2,3,4,5]
li.__setitem__(3,0) # 将索引为3的元素设置为0
print(li)
结果:
[1,2,3,0,5]
字典也有相应的特殊方法,如__delitem__()
方法
dict1 = {"a":1,"b":2}
dict1.__delitem__("a") # 删除键为"a"的键值对
print(dict1)
结果:
{"b":2}
3.内置函数的实现
许多内置函数也是调用对象的特殊方法。比如len()
方法、abs()
方法、int()
方法。
print([1,2,3].__len__()) # 显示列表的长度
print((-1).__abs__()) # 取绝对值
print(2.3.__int__()) # 转型
结果:
3
1
2
6.2 属性管理
1.属性覆盖的背后
为了了解属性覆盖,首先得了解__dict__
属性。
当我们调用对象的属性时,这个属性可能不仅来自于自身对象属性和类属性,还可以来自于父类甚至租父类。一个类或对象拥有的属性,会记录在__dict__
中。这是一个词典,键为属性名,值为某个属性。
Python在寻找对象的属性时,会按照继承关系依次寻找__dict__
举例:People类继承object类,Asian类继承People类,而People1是Asian类的对象。
class People(object):
Name = True
def eat(self):
print("吃东西")
class Asian(People):
Sex = True
def __init__(self,Age):
self.Age = Age
def eat(self):
print("吃粥")
People1 = Asian(100) # 创建Asian类的对象 People1
print("People1的__dict__")
print(People1.__dict__)
print()
print("Asian的__dict__")
print(Asian.__dict__)
print()
print("People的__dict__")
print(People.__dict__)
print()
print("object的__dict__")
print(object.__dict__)
结果如下:
People1的__dict__
{'Age': 100}
Asian的__dict__
{'__module__': '__main__',
'Sex': True,
'__init__': <function Asian.__init__ at 0x0000018B015039D8>,
'eat': <function Asian.eat at 0x0000018B01503378>,
'__doc__': None}
People的__dict__
{'__module__': '__main__',
'Name': True,
'eat': <function People.eat at 0x0000018B01503BF8>,
'__dict__': <attribute '__dict__' of 'People' objects>,
'__weakref__': <attribute '__weakref__' of 'People' objects>,
'__doc__': None}
object的__dict__
{'__repr__': <slot wrapper '__repr__' of 'object' objects>,
'__hash__': <slot wrapper '__hash__' of 'object' objects>,
'__str__': <slot wrapper '__str__' of 'object' objects>,
'__getattribute__': <slot wrapper '__getattribute__' of 'object' objects>,
'__setattr__': <slot wrapper '__setattr__' of 'object' objects>,
'__delattr__': <slot wrapper '__delattr__' of 'object' objects>,
'__lt__': <slot wrapper '__lt__' of 'object' objects>, '__le__':
<slot wrapper '__le__' of 'object' objects>,
'__eq__': <slot wrapper '__eq__' of 'object' objects>,
'__ne__': <slot wrapper '__ne__' of 'object' objects>,
'__gt__': <slot wrapper '__gt__' of 'object' objects>,
'__ge__': <slot wrapper '__ge__' of 'object' objects>,
'__init__': <slot wrapper '__init__' of 'object' objects>,
'__new__': <built-in method __new__ of type object at 0x00007FFADC3B6D30>,
'__reduce_ex__': <method '__reduce_ex__' of 'object' objects>,
'__reduce__': <method '__reduce__' of 'object' objects>,
'__subclasshook__': <method '__subclasshook__' of 'object' objects>,
'__init_subclass__': <method '__init_subclass__' of 'object' objects>,
'__format__': <method '__format__' of 'object' objects>,
'__sizeof__': <method '__sizeof__' of 'object' objects>,
'__dir__': <method '__dir__' of 'object' objects>,
'__class__': <attribute '__class__' of 'object' objects>,
'__doc__': 'The most base type'}
第一部分是People1对象自身的属性,即Age。
第二部分是Asian类的属性,如Sex和__init__()
方法。
第三部分是People类的属性,如Name。
最后一个是object类的属性,如__doc__
属性。
用dir()来查看People1对象时,可以看到People1对象包含了这四部分的属性。这说明了,对象的属性是分层管理的。当我们调用对象的属性时,Python会一层一层的寻找,直到找到最先遇见的那个。这就是属性覆盖的原理所在。如,People类和Asian类都有eat()方法,如果对象调用eat()方法,首先调用的是Asian类中的eat()方法。
People1.eat()
结果:
吃粥
子类的属性比父类的同名属性具有优先权,这就是覆盖的关键。
上面是对属性的调用,下面来看看属性的赋值。创建People2对象,给Name属性赋值,看看会不会影响People类的Name属性。
class People(object):
Name = True
def eat(self):
print("吃东西")
class Asian(People):
Sex = True
def __init__(self,Age):
self.Age = Age
def eat(self):
print("吃粥")
People1 = Asian(100)
People2 = Asian(200)
print("未修改Name属性之前,People2的__dict__")
print(People2.__dict__)
print()
People2.Name = "盘古"
print("给People2的Name赋值后,看看People1的Name会不会被影响:",People1.Name)
print("再看看People类中的Name会不会改变:",People.Name)
print()
print("修改Name属性之后,People2的__dict__")
print(People2.__dict__)
结果:
未修改Name属性之前,People2的__dict__
{'Age': 100}
给People2的Name赋值后,看看People1的Name会不会被影响: True
再看看People类中的Name会不会改变: True
修改Name属性之后,People2的__dict__
{'Age': 100, 'Name': '盘古'}
从上面的结果可以看出,即使修改了People2的Name属性,也不会影响父类的Name属性,反而是在People2中,新创建了一个名为Name的属性。
因此,Python在对对象进行赋值时,会搜索对象本身的__dict__
,如果找不到对应的属性,就会在__dict__
中新建一个属性。
对于方法,如果用self引用对象,也会遵守相同的规则。
如果想要修改父类中的某个属性,可以直接按照下面的操作。如
People.Name = "亚当"
这相当于修改了People类的__dict__
People.__dict__["Name"] = "亚当" # 词典的赋值操作
2.特性
同一个对象的不同属性之间存在依赖关系。当某个属性被修改时,我们希望依赖于该属性的其他属性也同时变化。比如,是否成年(isAdult)
这个属性依赖于年龄(Age)
,当Age≥18岁,isAdult要变为True,Age<18岁,isAdult要变为False。这时,我们不能通过__dict__
的静态方式来存储属性了。
Python提供了特性(property)来实现。特性就是特殊的属性。
如我们为人类添加一个是否成年(isAdult)的特性。
class People(object):
Name = True
Sex = True
def __init__(self,Age):
self.Age = Age
def get_adult(self):
if self.Age >= 18:
return True
else:
return False
isAdult = property(get_adult) # isAdult特性
People1 = People(10)
print(People1.isAdult)
People2 = People(100)
print(People2.isAdult)
结果:
False
True
特性用内置函数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")
del self.value
neg = property(get_neg,set_neg,del_neg,"我是负数")
x = Num(9)
print("我是",x.value,"的负数:",x.neg)
x.neg = 10 # 修改neg的值
print("我已经不是原来的x了:",x.value)
print("我是新x的负数",x.neg)
print("我是neg特性的描述",Num.neg.__doc__)
del x.neg
结果:
我是 9 的负数: -9
我已经不是原来的x了: 10
我是新x的负数 -10
我是neg特性的描述 我是负数
删除属性value
3.getattr()方法
class People(object):
Name = True
Sex = True
def __init__(self,Age):
self.Age = Age
def __getattr__(self,name):
if name == "isAdult":
if self.Age >= 18:
return True
else:
return False
else:
raise AttributeError(name)
People1 = People(19)
print(People1.isAdult)
People1.Age = 10
print(People1.isAdult)
print(People1.male)
结果:
True
False
AttributeError: male
6.3 我是风儿,我是沙
1.动态类型
Python中的变量不需要说明。在赋值时,变量可以重新复制为任意其他值。这种一会是沙一会是风的能力,就是动态类型的体现。如
a = 1
在这里,整数"1"是一个整数对象,对象的名字是a。也就是说1这个对象存储在引用a里面,我们不能直接接触1,而是通过引用a来引用对象。
对象名是指向对象的引用。
这就像住酒店。对象1是入住客人,引用a就是房间号。
我们可以通过id()内置函数来查看对象,该函数能返回对象的编号。这就像入住酒店,需要客人的身份证,通过身份证查看客人的身份证号。
a = 1
print(id(1))
print(id(a))
结果:
140715413902144
140715413902144
在Python中,赋值其实就是不同的客人(对象)入住到房间(引用)中。引用能随时指向一个对象。
a = 3
print(id(a))
a = "another"
print(id(a))
结果:
140715413902208
1696533891760
上面的引用a从指向对象3到指向对象"another",而且id()返回的值不一样,因此引用的指向发生了变化。也就是说,变量名是随时可以改变的引用,那么它的类型就可以动态变化。因此,Python是一门动态类型的语言。
除了通过id比较两个引用是否指向同一个对象,我们还可以有is来进行判断。
a = 3
b = 3
print(a is b)
结果:
True
2.可变与不可变对象
一个对象可以有多个引用。
a = 5 # 让a指向对象5
b = a # 让b指向引用a所指向的对象5
c = b # 让c指向引用b所指向的引用所指向的对象5
print(id(a))
print(id(b))
print(id(c))
print()
a = a +2 # a指向(a原来指向的对象5 加上 对象2 得到的对象7)
print(id(a))
print(id(7))
print(id(b)) # 看b是指向原来的a所指向的对象5,还是新指向的对象7
结果:
140715413902272
140715413902272
140715413902272
140715413902336
140715413902336
140715413902272
第一个例子可以看出,a指向5,b指向a,c指向b,都是指向了对象5。
第二个例子,让a增加2,再赋值给a,可以看到a指向了对象7,而b仍然指向对象5。这就好比,对象5用自己的id开了两个房间a和b,但是a房间不是给对象5住,而是给7住。改变一个引用的指向,并不会影响其他引用的指向。也即是各个房间互不影响。
像这种就被称为不可变对象。常见的有整数、浮点数以及字符串和元组。
对于不可变对象,有如下情况
li1 = [1,3,5,7]
li2 = li1
print(li1)
li2[0] = 9
print(li1)
结果:
[1,3,5,7]
[9,3,5,7]
从上述的例子可以看出改变了li2的内容,li1也改变了。
实际上是因为引用li1和引用li2指向的是整个同一个列表对象,但是一个li1和li2包含了多个引用,每一个元素就是一个指向具体对象的引用,比如li1[0]、li2[0]指向的对象是1。而li2[0]=9这一赋值操作,使得li2[0]指向的对象变为9,但不改变li2的指向,即li2仍然指向整个列表对象。也就是说,列表对象的一部分,即列表中一个引用的指向改变,所有指向该列表对象的引用也随之改变。如下图
3.从动态类型看函数的参数传递
函数的参数传递,本质上传递的是引用
- 可变对象传参
def f(x):
x = 100
print(id(x))
a = 1
print(id(a))
f(a)
print(id(a))
结果:
140715413902144
140715413905312
140715413902144
参数x是一个新引用。当我们调用f时,a传递给参数x,因此x会指向a所指的对象即1,也就是将1赋给了x,然后在f内,x又指向100,此时打印id(x),发现id(x)改变了,然而函数外面的id(a)不会被影响。这也体现了,不可变对象的引用不会相互影响。
- 不可变对象传参
下面介绍不可变对象的参数传递。
def f(x):
print(x)
x[0] = 100
print(x)
a = [1,3,5,7]
print(a)
f(a)
print(a)
结果:
[1, 3, 5, 7]
[1, 3, 5, 7]
[100, 3, 5, 7]
[100, 3, 5, 7]
从上面我们可以看出,a指向了一个可变的列表,之后调用f,a给x,a和x就指向了同一个列表[1,3,5,7]。然后,x中的一个引用x[0]的指向变为了对象100,接着打印x,可以肯定引用x的内容肯定改变。接着再打印引用a,发现a的引用也改变了。也就是说,函数内部的对引用x的改变,也影响了函数外部的引用a。这就是说明了,通过一个引用操作可变对象,其他指向同一个可变对象的引用也会被改变。
6.4 内存管理
1.引用管理
对象内存的管理,是基于对引用的管理。在Python中,引用与对象分离。一个对象可以有多个引用,而对于每个对象都存在一个指向该对象的引用总数,即对引用计数。
我们可以通过sys包中的getrefcount(),来查看对象的引用计数。
注意,由于当引用作为参数传递给getrefcount()方法时,该参数本身也是一个引用。因此,getrefcount()所得到的结果比预想的多1个。
from sys import getrefcount
a = [1,2,3,4]
print(getrefcount(a))
b = a
print(getrefcount(b))
结果:
2
3
2.对象引用对象
列表和词典。都是容器对象,可以包含多个对象,但实际上,他们包含的不是对象本身,而是对象的引用。如上面提过的例子我们也可以自己定义一个对象,来引用其他对象。如下
# 创建一个自定义类
class from_obj(object):
def __init__(self,to_obj):
self.to_obj = to_obj
a = [1,23,4] # a是一个列表对象
b = from_obj(a) # b是自定义对象,且b引用了a
print(id(b.to_obj)) # b的to_obj是a指向的列表对象
print(id(a)) # a指向的列表对象
结果:
1696533754568
1696533754568
可以看到自定义对象b引用了列表对象a,即对象引用对象。
再如,a=1时,Python会把引用关系存入到词典中。该词典对象用于记录所有的全局引用。可以通过globals()方法查看该词典。
print(globals())
当一个对象a被另一个对象b引用时,a的引用次数将加1
from sys import getrefcount
a = [1,2,3,4]
print(getrefcount(a))
b = [a,a]
print(getrefcount(a))
结果:
2
4
由于对象b引用了两次a,因此再打印a的引用计数时,变成了4。
容器对象的引用可能会形成复杂的拓扑结构。我们可以用objgraph包来进行绘制引用关系。objgraph是一个第三方包,可以通过pip安装。除了安装objgraph,还需要按照graphviz软件。
安装教程如下https://blog.csdn.net/HNUCSEE_LJK/article/details/86772806
import objgraph
x = [1,2,3]
y = [x,dict(key1 = x)] # 第二个元素意思为:以key1为键,列表x为值构建字典
z = [y,(x,y)]
objgraph.show_refs([z],filename="sample.jpg")
生成的图片如下两个对象相互引用,从而构成引用环,如
a = [1,2,3]
b = [a]
a.append(a)
print(a)
print(b)
结果:
[1, 2, 3, [...]]
[[1, 2, 3, [...]]]
单个对象,自己调用自己也能够形成引用环
from sys import getrefcount
a = [1,2,3]
a.append(a)
print(a)
print(getrefcount(a))
结果:
[1, 2, 3, [...]]
3
引用环的形成会浪费空间,使垃圾回收很麻烦。
某个对象的引用计数可能会减少。比如,使用del关键字删除某个引用:
from sys import getrefcount
a = [1,2,3] # 对象[1,2,3]使用了引用a
b = a # b引用了a
print(getrefcount(b)) #该参数又引用了一次
del a #删除引用a
print(getrefcount(b))
结果:
3
2
前面我们说到,列表容器对象里面包含的其实也是引用,
所有也可以用del删除列表里面的引用
a = [1,2,3]
print(a)
del a[0]
print(a)
结果:
[1,2,3]
[2,3]
除了上述情况会是引用计数减少,另一种减少情况如下:
如果某个引用指向对象a,当这个引用被重新指向其他对象b时,对象a的引用计数也会减少。
from sys import getrefcount
a = [1,2,3]
b = a
print(getrefcount(b))
a = 1
print(getrefcount(b))
结果:
3
2
3.垃圾回收
当对象越来越多,占据的内存越来越大,Python自动地启动垃圾回收,将没用的对象清除。
原理上,当某个对象的引用次数为0时,即没有任何引用指向该对象,该对象就会成为回收的垃圾。如
a = [1,2,3] # 列表对象[1,2,3]被引用a引用,增加了1个引用
del a # 删除a引用,列表对象[1,2,3]被回收。
然而,频繁地回收垃圾,会使得Python的工作效率降低。如果内存中的对象不多,那么Python不会频繁的回收垃圾。所有,Python只会在特定条件下进行垃圾回收。
当Python运行时,会记录其中分配对象和取消分配对象的次数。当两者差值高于某个阈值时,垃圾回收才启动。
我们可以通过gc模块的get_threshold()
方法,查看该阈值:
import gc
print(gc.get_threshold())
结果:
(700, 10, 10)
700就是启动垃圾回收的阈值,后面两个10是与分代回收相关的阈值,后文说明。启动垃圾回收的阈值可以通过set_threshold()
方法进行重新设定。我们也可以手动回收垃圾,用collect()
方法实现。
除了上述基础的垃圾回收方法,Python同时还采用了分代回收的策略。给策略的假设是,存活越久的对象,越不可能在后面的程序变为垃圾。程序中通常产生大量的对象,有的对象很快产生且消失,而有的对象就长期保留在程序中被使用。出于信任和效率,对于这些长期对象,我们认为其有用,所有减少他们在垃圾回收中被扫描的频率。如何划分对象是长期的呢?
Python将所有对象分为0、1、2代三代对象。当某一代对象经历垃圾回收而没有被清除,依旧存在于程序中,它将被归入下一代对象。所有新建对象都是0代对象。垃圾回收启动时,一定会扫描所有的0代对象,如果0代对象经历一定次数的垃圾回收,那么就启动对0代和1代对象的扫描清理,当1代对象经历到一定次数垃圾回收,就会启动0、1、2代的扫描,对所有对象进行扫描。
上面两个10的意思就是,每10次0代回收,会配合1次1代回收;每10次1代回收,会进行1次2代回收。我们也可以通过set_threshold()
方法对回收次数更改。
4.孤立的引用环
引用环的存在让上面的垃圾回收机制造成很大的困难。
这些引用环可能构成无法使用,但是引用次数又不为0的一些对象。
a = []
b = []
a.append(b)
del a
del b
我们创建了两个列表,并相互引用,构成一个引用环。删除了引用a和b之后,这两个对象无法再从程序中调用,没什么用处,但是由于环的存在,这两个对象的引用次数不为0,不会被垃圾回收。为了回收这样的孤立环,Python会复制每个对象的引用计数,可以记为gc_ref。假设,每个对象i,该计数为gc_ref_i。Python会遍历所有的对象i,然后对于对象i引用的对象j,将相应的gc_ref_j减少1,遍历后如下
遍历结束之后,gc_ref不为0的对象和这些对象的引用对象以及继续更下游的引用对象被保留,其他的对象都被回收。