运行的Python3版本为3.6.4。
IDE为PyCharm2018.
首先
x = 20
这里的x是在Python3中是一个引用,指向对象20。
其次,通过id()方法可以来查看对象的地址,该方法返回值为十进制数值。
那么
c = 2.0
d = 2.0
print(id(c), id(d), id(2.0)) # 2591934537544 2591934537544 2591934537544
print(c == d)  # True
print(c is d)  # True
c = 23456789.012345679
d = 23456789.012345679
print(id(c), id(d), id(23456789.012345679)) # 2591934537112 2591934537112 2591934537112
print(c == d)  # True
print(c is d)  # True
a = 123456789.0123456798
b = 123456789 + 0.0123456798
print(id(123456789.0123456798), id(a), id(b))  # 2207184187632 2207184187632 2207181987752
print(a == b)  # True 两者的值相同
print(a is b)  # False 
# 从输出来看地址不同,这说明通过计算得到相同结果b,这个结果b的地址不同于a
# 即动态计算后重新给结果分配地址
print("%x"%id(a), hex(id(a)))  # 201e66df0f0 0x201e66df0f0
str1 = "哈哈哈哈哈哈哈哈哈呵呵呵呵呵呵呵嘿嘿嘿嘿嘿嘿嘿"
str2 = "哈哈哈哈哈哈哈哈哈呵" + "呵呵呵呵呵呵嘿嘿嘿嘿嘿嘿嘿"
print(id(str1), id(str2), id("哈哈哈哈哈哈哈哈哈呵呵呵呵呵呵呵嘿嘿嘿嘿嘿嘿嘿"))
# 1978135986344 1978539980960 1978135986344
print(str1 == str2)  # True
print(str1 is str2)  # False
其中
==是比较值,is是比较地址。其实可以通过
%x或者hex()来将十进制数转成十六进制,一般内存地址都用十六进制表示。
另外
print(id(-1024))  # 2112961157104
print(id(-512))  # 2112961157136
print(id(-256))  # 2112961157168
print(id(-128))  # 2112961157200
print(id(-64))  # 2112961157232
print(id(-32))  # 2112961157264
print(id(-16))  # 2112961157296
print(id(-8))  # 2112961157328
print(id(-6))  # 2112961157360
print(id(-5))  # 1775398176
print(id(-4))  # 1775398208
print(id(-3))  # 1775398240
print(id(-2))  # 1775398272 
print(id(-1))  # 1775398304 
print(id(0))  # 1775398336  
print(id(1))  # 1775398368  0x69d26de0
print(id(2))  # 1775398400  
print(id(3))  # 1775398432
print(id(4))  # 1775398464
print(id(8))  # 1775398592
print(id(16))  # 1775398848
print(id(32))  # 1775399360
print(id(64))  # 1775400384
print(id(128))  # 1775402432
print(id(256))  # 1775406528
print(id(257))  # 2106773662832  这是第二遍执行代码是的地址,所以与上面负值的地址有不同
print(id(258))  # 2106773662800
print(id(512))  # 2106773662672
print(id(1024))  # 2108514667280
print(id(‘a‘))  # 1971266667328
print(id(‘b‘))  # 1971266649200
print(id(‘c‘))  # 1971265848688
print(id(‘A‘))  # 1971267734136
print(id(‘B‘))  # 1971266981648
print(id(‘C‘))  # 1971266981536
print(id(‘+‘))  # 1971269703528
print(id(‘-‘))  # 1971265849472
print(id(‘*‘))  # 1971265502488
print(id(‘/‘))  # 1971265693096
Python3对于[-5, 256]内到小整数会提前缓存到内存中,为了重复使用,以提高效率。
但是在网上有看到说一些小的字符也会提前缓存,不过我不知道怎么能实验得到。
a = 1.0
b = 1.0
print(a == b)  # True
print(a is b)  # True
str1 = "这是字符串"
str2 = "这是字符串"
print(str1 == str2)  # True
print(str1 is str2)  # True
t1 = ()
t2 = ()
print(t1 == t2)  # True
print(t1 is t2)  # True
l1 = []
l2 = []
print(l1 == l2)  # True
print(l1 is l2)  # False
d1 = {}
d2 = {}
print(d1 == d2)  # True
print(d1 is d2)  # False
s1 = set()
s2 = set()
print(s1 == s2)  # True
print(s1 is s2)  # False
class A:
    pass
x = A()
y = A()
print(x == y)  # False
print(x is y)  # False
可以看到在Python3中对于不可变类型数字类型:int、bool、float、complex、long(Py2.x);字符串 str;元组 tuple定义时的值相同,那么都是对相同对象引用,即变量指向同一个对象
而对于可变类型列表 list;字典 dict;集合 set,哪怕值相同,也是新创建的对象,变量对不同对象(但值可能相同)的引用。
自定义类也类似。
在Python中每个对象都存有指向该对象的引用总数,称作引用计数(reference count)。
对于引用计数,可以通过sys包中的getrefcount()函数来查看。但是使用某个对象的引用作为该函数的参数时,这个对象就又被引用了一次,所以减去1才能得到,代码中其他地方对该对象的引用次数。
from sys import getrefcount
# 引用计数
a = 1
b = a
c = b
print(getrefcount(a))  # 4646
d = [4, 5, 6]
print(getrefcount(d))  # 2
e = d
print(getrefcount(e))  # 3
其实这里对象[4, 5, 6]被 变量d 引用1次,被 变量e 引用1次,被getrefcount()内的参数引用了1次,所有最后是3次。
但是直接getrefcount([4, 5, 6])引用了1次,把[4, 5, 6]当作新对象了,id()查看地址可以发现二者地址不同
print(getrefcount([4, 5, 6]))  # 1
print(id(d), id([4, 5, 6]))  # 2345607282440 2345612824648 id()并不涉及对应引用
最后将 变量d 放进 变量f 中,变量d对应的 [4, 5, 6]又被引用了2次,但是对于 变量f 是一个新对象,也有自己的地址,只不过其内存空间内存了两个变量d的[4, 5, 6]的引用罢了。
f = [d, d]
print(getrefcount(d))  # 5
print(getrefcount(f))  # 2
Python中del关键字和del()函数可以删除某个引用
del d
print(getrefcount(e))  # 4 删除了d
del(e)
print(getrefcount(f[0]))  # 3 删除了e
换句话来说,就是在最刚开始创建的对象[4, 5, 6]
被 变量d 引用1次
被 变量e 引用1次
被 变量f的列表 引用了2次
通过del关键字或del()函数
[4, 5, 6]的引用 减1[4, 5, 6]的引用 减1[4, 5, 6]的引用 减2[4, 5, 6]的引用 减1[4, 5, 6]的引用都会相应的 减1使用getrefcount()传入变量(比如变量d)查看 变量d 引用的对象(即[4, 5, 6])的引用次数,函数内部的形参会再引用1次对象[4, 5, 6]。这1次,会算在返回的引用次数中。
另外如果直接getrefcount([4, 5, 6]),这里面的对象[4, 5, 6]与变量d/e/f引用的[4, 5, 6]不是同一个对象,而是一个新创建的对象,会被函数内部形参引用1次。
变量引用了别的对象,原对象的引用计数减1。
class A:
    def __init__(self, obj):
        self.obj = obj;
x = []
y = [x]
z = {1: x, 2: y}
a = A(z)
print(id(x), id(y[0]), id(z[1]))  # 1419887596040 1419887596040 1419887596040
print(id(y), id(z[2]))  # 1419887596296 1419887596296
print(id(z), id(a.obj))  # 1421627810224 1421627810224
print(getrefcount(x))  # 4
print(getrefcount(y))  # 3
print(getrefcount(z))  # 3
这里变量x引用的对象[]
被 变量x 引用了1次
被 变量y引用的对象[x] 引用了1次
被变量z引用的对象{1: x, 2: y} 引用了1次
这里 变量a引用的的对象A() 只是引用了 变量z引用的对象{1: x, 2: y},因此并不会直接对 变量x引用的对象[] 进行引用
另外这也是深拷贝和浅拷贝的原理所在。
Python3垃圾回收机制(garbage collection)会将引用计数为0 的对象从内存上销毁。
x = {}  # 对象{}被创建,其引用计数为1
del x  # 删除对象{}的引用x,对象{}的引用计数减1,降为0
但是并不是引用计数降为0了,该对象就立即被销毁。
只有当Python3运行垃圾回收程序时,程序才会去扫描内存,并将引用计数为0 的对象从内存上销毁。
值得注意的是,Python在3运行垃圾回收程序时,无法进行其他任务,而且运行垃圾回收程序也会消耗资源,对象特别少也没必要清理。
因此Python3会在特定条件下,自动启动垃圾回收。
Python3在运行时,会记录其分配对象(object allocation)和取消分配对象(object deallocation)的次数,当两者的差值高于某个阈值,垃圾回收才会启动。
  import gc
  print(gc.get_threshold())  # (700, 10, 10)
  
  print(gc.get_count())  # (1, 0, 2)  0代 1代 2代的数量
  gc.collect()  # 手动启动垃圾回收
  print(gc.get_count())  # (0, 0, 0)
通过gc模块的get_threshold()方法可以查看阈值,其中两个10是与分代回收有关的阈值。700是垃圾回收启动的阈值。
可以通过set_threshold()方法来重新设置垃圾回收启动的阈值。
通过gc.disable()关闭自动的垃圾回收,改为手动。
通过gc.collect()手动启动垃圾回收。
Python3中还采用了分代(generation)回收策略。
对于存活越久的对象,越不可能在后续的程序中变成垃圾。
因此,处于信任和效率,用代来描述这些对象在Python3中的存活时间。
新创建的对象为0代对象
当0代对象经历一定次数的垃圾回收后存活,就变为1代对象
当1代对象经历一定次数的垃圾回收后存活,就变为2代对象
上面打印的阈值(700, 10, 10)中
可以通过gc.set_threshold()来设置这三个阈值
存活的越久被回收的概率就越低
另外手动调用垃圾回收时gc.collect(generation)可以传递参数(0、1、2)
(700, 10, 10)中第一个阈值(700, 10, 10)中第一、二阈值(700, 10, 10)中三个阈值Python中两个对象互相引用
x = []
y = [x]
x.append(y)
print(x)  # [[[...]]]
print(getrefcount(x))  # 3
# 或者 我引用我
z = []
z.append(z)
print(z)  # [[...]]
print(getrefcount(z))  # 3
这样就构成了一个引用环(reference cycle),或叫循环引用。
这同时就会存在一个问题
x = []
y = [x]
x.append(y)
print(x)  # [[[...]]]
print(getrefcount(x))  # 3
# 当
del x
del y
[]的引用 减1[]的引用 减1[]和外层对象[]互相引用,因此二者最终的引用次数都为1。这就无法被垃圾回收掉。
__del__()方法Python3中__del__()方法指出了在用del关键字消除对象时除了释放内存空间以外的操作。
对象的__del__()是对象在被垃圾回收时起作用的方法。
from sys import getrefcount
class A:
    
    def __del__(self):
        print("销毁对象A")
a = A()
del a  # 打印 销毁对象A
class B:
    
    def __del__(self):
        print("销毁对象B")
        del self
        print("销毁了吗")
b = B()
# 或 B.__del__(b)
b.__del__()  # 打印 销毁对象B 销毁了吗
print(id(b))  # 打印 1903121153384
print(getrefcount(b))  # 2
# ... 其他代码
#  如果在 其他代码前执行了del b 然后再 gc.collect() 就会再次打印 销毁对象B 销毁了吗
#  不然会在 其他代码全执行完后(程序结束前)打印了 销毁对象B 销毁了吗
#  都是在打印过后才真正在内存上销毁对象
可以看到命名执行了__del__()方法却输出了两次销毁对象B 销毁了吗 。
del关键字并不会主动调用__del__()方法,只有在引用计数为0时,__del__()才会被执行。
所以在直接b.__del__()时主动调用该方法,但是这时变量b还在引用的对象B(),
del关键字或者del()函数才会删除对象的引用。在代码中del self执行后,并没有删除了变量b,这时对象B()的引用计数还为1
对象B()的地址,还能输出引用次数print(getrefcount(b))  # 2del self,并不会抛异常。(笔者并不知道这步会发生什么作用)而程序执行完成之后(或者对象B()的引用计数将为0了后)垃圾回收程序启动,就还会执行对象B()的__del__()方法,才会又打印了一次销毁对象B 销毁了吗。
对象B()才真正的在内存上被销毁del self,也不会抛异常。所以,并不是只要调用了
__del__()方法就会销毁对象。而是,销毁对象前一定会调用
__del__()方法。
但是,自定义了__del__()方法,会带来一些问题
class C:
    cname = "C啊"
    def __del__(self):
        print("父类")
class D(C):
    def __del__(self):
        print(super().cname)
        print("子类")
        # super().__del__()  调用该行才能销毁C类对象
        
d = D()
del d
会打印
C啊
子类
D类继承了C类,创建D类对象时,也创建C类对象,并由D类对象引用C类对象。
但这时自定义了__del__()方法,垃圾回收D类对象时正常,但是C类对象无法被正常回收,也无法被使用。这就导致了内存泄漏。这是垃圾回收机制无法解决的。
因此需要在D类的__del__()方法中调用父类的__del__()方法。
Python3的垃圾回收也无法解决自定义__del__()方法后循环引用引起的内存泄漏。
因为Python3无法判断调用它们的__del__()方法时不会调用到对方那个对象
比如假设已经调用过B.__del__()把b销毁掉了(这里假设A中没有调用A的资源)
再调用A.__del__()时可能会在方法内用到_b(其中_b是B中的一个属性)
这时如果B已经被销毁了,无法找到_b就会抛异常
对于上面所提到的引用环(循环引用)。
场景1:对象A引用了对象B,对象B引用了对象A。外部没有对对象A/B的引用了。
那么
这样就把循环引用的关系给去掉了。
注意:这种方法应该只是减引用计数,并不是去掉对象A和对象B之间的引用关系。(笔者不太确定)
场景2:对象A引用了对象B,对象B引用了对象A。变量b还引用对象B。
这时如果还按场景1的解决方法来做,对象B的引用计数为1,对象A的引用计数为0
如果把对象A销毁了,因为对象B还存在,对象B内引用了对象A,这时对象A就成了不可达到的对象!
这就冲突了!
因此为了应对这种情况,Python3有一个标记-清除的回收机制,会把存引用计数的内存块一分为二
即为两个链表,root链表和unreachable链表
对于场景2按场景1的方法
因此,标记-清除回收机制是在考虑
对于没有被垃圾回收掉的对象和导致内存泄漏的对象,Python程序关闭后会将其无差别的销毁。
Python3中分为大内存和小内存
256K为界限来区分大小内存
大内存使用malloc进行分配
小内存使用内存池进行分配
这样做的初衷是:创建大量内存花费少的对象时,频繁的调用malloc会导致大量的内存碎片,使得效率较低。
为此就用到了内存池机制
Python3内存的金字塔结构:
第3层:最上层,由用户对Python3对象的直接操作
第2层和第1层:内存池,由Python3的接口函数PyMem_Malloc实现
第0层:大内存
第-1层和第-2层:操作系统进行操作
栈空间里存放变量名和引用对象的地址。
堆空间里存放的是具体的对象。
另外Python3会对匿名对象以及短字符串创建缓冲区。
print(id([1, 2, 3]), id([1, 2, 3]))  # 2362108854024 2362108854024
print(id("哈哈哈哈"), id("哈哈哈哈"))  # 2361673366520 2361673366520
print(id("这时一串字符串啊这时一串字符串啊这时一串字符串啊这时一串字符串啊这时一串字符串啊这时一串字符串啊这时一串字符串啊这时一串字符串啊"), id("这时一串字符串啊这时一串字符串啊这时一串字符串啊这时一串字符串啊这时一串字符串啊这时一串字符串啊这时一串字符串啊这时一串字符串啊这时一串字符串啊这时一串字符串啊"))
# 2362108671872 2362084502016 长字符串还是单独开辟新空间存储
这时这些对象没有被引用,用完后续会被垃圾回收掉
但是小整数池(常量池)[-5, 256]有独立分配的内存空间,不会新创建内存来存储,也不会被销毁而释放掉内存。
del数字、字符串、字典,但可以主动del空列表和空元组(似乎没什么用)。在Python3命令行中执行上述代码不一定和PyCharm中执行的结果一致
因为PyCharm自带对变量和对象进行优化管理,这种优化管理不是真正Python3内存管理的效果
x = 1000
y = 1000
print(x == y)  # True
print(x is y)  # 命令行False PyCharm True
https://www.cnblogs.com/cccy0/p/9061799.html
https://www.cnblogs.com/franknihao/p/7326849.html
https://www.cnblogs.com/geaozhang/p/7111961.html
https://www.cnblogs.com/tingguoguoyo/p/10725891.html
笔者水平有限,对于Python3更底层的源码并没有太深入的了解,并且网上一些文章也有很多这方面的细节描述的不一致。
因此建议读者若想要了解更深入,可以去看官方的文档和阅读源码。
原文:https://www.cnblogs.com/jiyou/p/13986544.html