变量名是内存地址的引用(Reference)
这个概念其实大多数人都晓得了, 就简单提一下
当我们给一个变量赋值时, Python 会在内存地址中新建一个对象, 然后变量名只是指向了这个对象的内存地址, 变量名只是一个引用而已
引用计数(Reference Counting)
定义
我们先看看维基百科对引用计数的定义
引用计数是计算机编程语言中的一种内存管理技术, 是指将资源 (可以是对象, 内存或磁盘空间等等) 的被引用次数保存起来, 当被引用次数变为零时就将其释放的过程. 使用引用计数技术可以实现自动资源管理的目的. 同时引用计数还可以指使用引用计数技术回收未使用资源的垃圾回收算法.
字面意思还是很好理解这个概念的
- my_var = 10
- # 这时候指向存储 "10" 这个对象的引用为 1
- my_var_2 = my_var
- # 此时引用数目变成了 2
- my_var = "hello"
- # 引用数目变成 1
- my_var_2 = "goodbye"
- # 引用数目变成 0, 内存得以释放
获取引用数
Python 中有两种方法获取对象的应用数目
一个是 sys.getrefcount(spam)
还有一个是 ctypes.c_long.from_address(address).value
- import sys, ctypes
- spam = [4,5,6]
- r1 = sys.getrefcount(spam)
- r2 = ctypes.c_long.from_address(id(spam)).value
- print(r1,r2)
输出
2 1
这里要注意下, 当我们使用函数时, 有一个实参传给形参的过程, 在这个过程中, 形参也是指向了同样的地址, 所以在函数终结之前, 会有两个引用数
而第二个虽然也是用了 id 函数, 但是 id 函数调用结束后, 行参就被 "销毁" 了, 所以这时候再检测内存地址时, 就只有一个引用数了
垃圾回收(Garbage Collector)
循环引用(Circular References)
我们先来看一个简单的例子
定义一个 my_var 变量, 指向 ObjectA 的内存地址, 而 ObjectA 里面又有一个变量 var_1 指向 ObjectB
如果我们将 my_var 指向其他内存地址, 比如说 my_var = None, 这时候 ObjectA 的内存地址没有被引用, 因此被销毁, 这导致 var_1 也被跟着一起销毁, ObjectB 的内存地址也在此时没被引用, 被销毁
上面一切都没有问题, 但是我们看看下面这个情况
与之前不同的是, ObjectB 内的变量 var_2 如果指向 ObjectA 的话, 如果取消 my_var 对 ObjectA 的引用, 按照内存管理的规则, 最终会导致一个内部循环
这些对象无法被外界获取, 但是又实实在在地存在内存中, 大量的积累就会导致内存泄漏的情况, 这并不是我们希望的, 所以就有了垃圾回收的机制
垃圾回收(Garbage Collector)
可以通过导入 gc 模块来控制
默认情况下是开启的
如果能确保自己不会写出循环引用的代码可以选择将其关闭, 以此来释放资源(其实这释放的量很小啦)
下面我们通过代码来看看这个机制
- import ctypes, gc
- gc.disable()
- # 关闭垃圾回收
- def ref_count(address):
- return ctypes.c_long.from_address(address).value
- def object_by_id(object_id):
- for obj in gc.get_objects():
- if id(obj) == object_id:
- return "Object exists"
- return "Not Found"
- class A():
- def __init__(self):
- self.b = B(self)
- print('A: self: {0} , b: {1}'.format(hex(id(self)),hex(id(self.b))))
- class B():
- def __init__(self, a):
- self.a = a
- print('B: self: {0} , a: {1}'.format(hex(id(self)),hex(id(self.a))))
- my_var = A()
我们建立来两个类, A 中的 b 变量指向 B 的实例, 而 B 中的 a 变量又指向 A 本身, 最后建立 A 的实例, 正好符合上面的循环
我们现在看下 A,B 的实例个有多少个引用
- a_id = id(my_var)
- b_id = id(my_var.b)
- print("ref_count_of_a:{0}, ref_count_of_b:{1}".format(ref_count(a_id),ref_count(b_id)))
输出
- B: self: 0x1068480f0 , a: 0x1068480b8
- A: self: 0x1068480b8 , b: 0x1068480f0
- ref_count_of_a:2, ref_count_of_b:1
没问题
现在我们令 my_var = None
再来打印输出结果 输出
- B: self: 0x10b0ec128 , a: 0x10b0ec0f0
- A: self: 0x10b0ec0f0 , b: 0x10b0ec128
- ref_count_of_a:1, ref_count_of_b:1
由于之前关闭了垃圾回收, 所以这里的内部循环就没有被销毁
这时我们即时执行下垃圾回收 gc.collect(), 就会输出我们想要的结果了
ref_count_of_a:0, ref_count_of_b:0
动态类型与静态类型(Dynamic Typing and Static Typing)
Python 的定义变量时是动态的, 也就是说一个变量名可以直接从一个字符串对象重新指向数字对象, 像其它的静态类型的语言, 比如 Java, 如果重新给一个字符串变量赋值数字, 就会出错
- String my_var = "hello"
- mv_var = 123 //(会报错, 因为 Java 的变量是静态类型)
有个点提一下, 当我们给变量重新赋值时, Python 做的事情是在内存中新建一个对象, 而不是在原有内存地址上改变对象内容
可变性(Mutability)
对象内部的数据 (state) 如果可以改变, 就叫可变 (Mutable) 的; 否则就是不可变的(Immutable)
Python 中不可变的数据类型
- Numbers(int,float,Booleans,etx)
- Strings
- Tuples
- Frozen Sets
- User-Defined Classes
可变的数据类型
- Lists
- Sets
- Dictionaries
- User-Defined Classes
元组是不可变的, 但是却可以包含可变的元素, 例如如下例子
- a = [1,2]
- b = [3,4]
- t = (a,b) # 此时 t = ([1,2],[3,4])
- a.append(3)
- b.append(5) # 此时 t = ([1,2,3],[3,4,5])
共享引用 (Shared References) 和可变性(Mutablility)
如果你在 Python 中这样定义
- a = 10
- b = 10
- # id(a) 等于 id(b)
Python 的内存管理会让两个变量名自动共享同一个内存地址(相当于执行来 b), 这是对于部分简单的不可变的对象而言
而如果定义任何两个可变的对象, Python 则不会这么做
- a = [1,2,3]
- b = [1,2,3]
- # id(a) 不等于 id(b)
一切皆为对象
"一切都是对象" 在别的编程语言中可能还有点牵强; 但是在 Python 中, 这是真真切切的道理
除了简单的数据类型, 像一些运算符 (+,-,*,/,is,...) 这些也是某个类的实例
我们平常接触的 function,class(类本身, 不是实例)也都是 Function Type, Class Type 的实例
所以可以得到一个这样的结论
所有的对象 (包括函数) 都给可以赋值给一个变量
而所有的对象 (包括函数) 有可以作为参数传递给函数
函数又可以返回一个对象(包括函数)
可以参考如下例子
- def square(a):
- return a ** 2
- def cube(a):
- return a ** 3
- def select_function(fn_id):
- return square if fn_id == 1 else cube
- f = select_function(1)
- print(f(3))
- f = select_function(3)
- print(f(3))
- def exec_function(fn, n):
- return fn(n)
- print(exec_function(cube,5))
输出
9
27
125
驻留(Interning)
概念
按需复用对象(reusing objects on-demand)
整数驻留
在启动时, Python(CPython)会预载一定范围的整数类型([-5,256])
任何这个范围内的整数在创建时, 都会产生一个共享引用, 从而减少内存的占用
字符串驻留
当 Python 代码被编译的时候, 有写标识符 (identifier) 会被驻留, 其中包括变量名称, 函数名称, 类名称等
如果一个字符串长得像标识符(identifier), 哪怕是一个无效的的标识符, 例如 1spam 这样的字符串, 也有可能会被驻留
通过 sys.intern()方法可以强制驻留特定的字符串
为什么需要驻留
速度优化!
如果让 Python 比较两个字符串是否相等, python 是要从字符串的第一个字符开始进行逐个比较的, 如果字符串相当长, 那么基于逐个比较的方法速度就会特别慢, 而驻留之后只需要比较内存地址, 可以极大地优化速度
我们来做一个测试
- import time, sys
- def compare_using_equals(n):
- a = 'a long string that is not interned' * 500
- b = 'a long string that is not interned' * 500
- for i in range(n):
- if a == b:
- pass
- def compare_using_interning(n):
- a = sys.intern('a long string that is not interned' * 500)
- b = sys.intern('a long string that is not interned' * 500)
- for i in range(n):
- if a is b:
- pass
- e_start = time.perf_counter()
- compare_using_equals(10000000)
- e_end = time.perf_counter()
- i_start = time.perf_counter()
- compare_using_interning(10000000)
- i_end = time.perf_counter()
- print('Compare using equals finished test in {0} seconds \n Compare using interning finished test in {1} seconds'.format(e_end-e_start,i_end-i_start))
我们看下输出
- Compare using equals finished test in 8.94854034400123 seconds
- Compare using interning finished test in 0.4564070480009832 seconds
我们可以看出, 驻留的效率比逐个比较的效率快了近 20 倍
一些其它的优化
常量表达式
当我们输入 a = 24*60 时, python 会提前计算数值, 并在编译的时候直接替换该数值
当常量序列表达式的结果的长度小于 20 时, 也会被提前计算
- def my_func():
- a = 24*60
- b = (1,2) * 5
- c = 'abc' * 3
- d = 'ab' * 11
- e = 'the quick brown fox' * 5
- f = ['a', 'b'] * 3
- print(my_func.__code__.co_consts)
输出
(None, 24, 60, 1, 2, 5, 'abc', 3, 'ab', 11, 'the quick brown fox', 'a', 'b', 1440, (1, 2), (1, 2, 1, 2, 1, 2, 1, 2, 1, 2), 'abcabcabc')
从上面的结果看到, a,b,c 的值被计算后放在了 co_consts 里面, 而 d,e 的值大于 20 了, f 的值是可变的, 所以这三个并没有放到 co_consts 里面
成员测验
当我们写
- def my_func(e):
- if e in [1,2,3]:
- pass
这样的代码时, python 会将阵列转为元组.
同样的, 如果是数组, python 也会自动将其转换为冷冻数组(frozenset)
- def my_func(e):
- if e in [1,2,3]:
- pass
- print(my_func.__code__.co_consts)
输出(None, 1, 2, 3, (1, 2, 3))
- def my_func(e):
- if e in {1,2,3}:
- pass
- print(my_func.__code__.co_consts)
输出(None, 1, 2, 3, frozenset({1, 2, 3}))
在成员测试中, set 的效率要远远高于阵列或者元组, 来做一个测验
- import string, time
- def membership_test(n,container):
- start = time.perf_counter()
- for i in range(n):
- if 'z' in container:
- pass
- end = time.perf_counter()
- return(end-start)
- print('list: %s' % membership_test(10000000,list(string.ascii_letters)),
- 'tuple: %s' % membership_test(10000000,tuple(string.ascii_letters)),
- 'set: %s' % membership_test(10000000,set(string.ascii_letters)),
- sep='\n')
输出
- list: 6.4466956019969075
- tuple: 6.477438930000062
- set: 0.6009954499968444
- [Finished in 13.6s]
从上面看出, set 的速度明显要快很多
来源: https://juejin.im/post/5be945e3518825713f68d1e1