文章目录
一、对象
1、对象的本质
Python 中,一切皆对象。每个对象由:标识(identity)、类型(type)、value(值)组成。
1)标识:用于唯一标识对象,通常对应于对象在计算机内存中的地址。使用内置函数 id(obj)
可返回对象 obj 的标识。
2)类型:用于表示对象存储的“数据”的类型。类型可以限制对象的取值范围以及可执行的操作。可以使用 type(obj)
获得对象的所属类型。
3) 值:表示对象所存储的数据的信息。使用 print(obj)
可以直接打印出值。
对象的本质就是:一个内存块(堆),拥有特定的值,支持特定类型的相关操作。
2、可变对象和不可变对象
常见,如 | |
---|---|
可变对象 | 列表、字典、集合、自定义对象 |
不可变对象 | 数值、字符串、元组、函数 |
不可变对象,变量所指向的对象所在的内存中的值不能被改变。当要改变它的值的时候,会把原来的值复制一份,再修改副本,再让变量指向这个副本所在的内存块。
>>> s = 'Ez'
>>> id(s)
2535889475096
>>> s += 'real'
>>> s
'Ezreal'
>>> id(s)
2535888199952
可变对象,即变量所指向的对象所在的内存中的值可以被改变,并且是在原内存块上直接修改。
>>> a = [1, 2, 4]
>>> id(a)
2535888755848
>>> a.append(8)
>>> a
[1, 2, 4, 8]
>>> id(a)
2535888755848
二、变量
1、变量的本质
在Python 中,变量也成为:对象的引用。因为,变量存储的就是对象的地址。
变量通过地址引用了“对象”。
变量位于:栈内存。
对象位于:堆内存。
一个简单的例子
>>> a = "Hello World"
>>> id(a)
1628889058416
在Python中,变量不需要显式声明类型。因为PVM会根据变量引用的对象,自动确定数据类型。
所以Python是动态语言。
2、变量的作用域
变量起作用的范围称为变量的作用域(Scope),不同作用域内同名变量之间互不影响。
变量分为:全局变量、局部变量。
全局变量:
1)在函数和类定义之外声明的变量。作用域为定义的模块,从定义位置开始直到模块结束。
2)全局变量降低了函数的通用性和可读性。应尽量避免全局变量的使用。
3)全局变量一般做常量使用。
4)函数内要改变全局变量的值,使用global 声明一下
局部变量:
1)在函数体中(包含形式参数)声明的变量。
2)局部变量的引用比全局变量快,优先考虑使用。
3)局部变量优先于全局变量的使用(如果同名)。
局部变量的查询和访问速度比全局变量快,优先考虑使用,尤其是在循环的时候。
在特别强调效率的地方或者循环次数较多的地方,可以通过将全局变量转为局部变量提高运行速度。
三、序列的内存分析
序列是一种数据存储方式,用来存储一系列的数据。在内存中,序列就是一块用来存放
多个值的连续的内存空间。
序列中存储的是对象的地址,而不是对象的值。
Python中常见的序列结构有:字符串、列表、元组、字典、集合。
1、字符串
字符串缓存池
Python 支持字符串缓存池机制,对于符合标识符规则的字符串(仅包含下划线(_)、字母和数字)会启用字符串驻留机制驻留机制。
字符串的比较
我们可以直接使用 ==
或 !=
对字符串进行比较,是否含有相同的字符。
我们使用 is
或 not is
,判断两个对象是否同一个对象。比较的是对象的地址,即 id(obj1) 是否和 id(obj2) 相等。
一个例子
>>> a = "abd_33"
>>> b = "abd_33"
>>> a is b
True
>>> c = "dd#"
>>> d = "dd#"
>>> c is d
False
>>> str1 = "aa"
>>> str2 = "bb"
>>> str1+str2 is "aabb"
False
>>> str1+str2 == "aabb"
True
2、列表
列表是内置可变序列,是包含多个元素的有序连续的内存空间。
>>> l = ['a', 'b', 'c', 'd']
>>> id(l)
1628897973960
>>> for x in l:
print(id(x))
1628888690168
1628888688488
1628887945320
1628887946776
3、字典:底层原理和内存分析
字典对象的核心是散列表。
散列表是一个稀疏数组(总是有空白元素的数组),数组的每个单元叫做bucket。每个bucket 有两部分:一个是键对象的引用,一个是值对象的引用。
由于,所有bucket 结构和大小一致,我们可以通过偏移量来读取指定bucket。
一个键值对创建的过程
我们知道 dict 的底层是数组(列表)。
那么如何将一个键值对放进字典的问题,可以转化为如何将一个 key 映射为 数组的下标。
>>> d = {} # 创建一个空字典,我们假设它底层数组的长度为 8
>>> name = 'Ez' # 想将 name = 'Ez' 添加到字典 t 中
>>> bin(hash(name))
'-0b111110111010011110001000100110110010001001110000110011110010010'
考虑到数组的长度为8,
下标(十进制) | 二进制 |
---|---|
0 | 000 |
1 | 001 |
2 | 010 |
3 | 011 |
4 | 100 |
5 | 101 |
6 | 110 |
7 | 111 |
则我们从 bin(hash(name))
中取出最右的3位,即010,对应的下标为2。
检测数组的偏移量2(即下标为2的位置),对应的bucket 是否为空。如果为空,则将键值对放进去。如果不为空,则依次取右边3 位作为偏移量,直到成功添加。
当散列表使用程度接近 2/3 时,数组就会扩容:创造更大的数组,将原有内容拷贝到新数组中。
检索一个键值对的过程
和存储的底层流程算法一致,也是依次取散列值的不同位置的数字。
先计算键对象的散列值,根据数组长度确定每次取多少位散列值。
不为空,则将这个bucket 的键对象计算对应散列值,和我们的散列值进行比较,如果相等。则将对应“值对象”返回。如果如果不相等,则再依次取其他几位数字,重新计算偏移量。依次取完后,仍然没有找到。则返回None。
4、集合
集合是无序可变,元素不能重复。
实际上,集合底层是字典实现,集合的所有元素都是字典中的“键对象”,因此是不能重复的且唯一的。
四、函数的内存分析
函数也是对象(废话,在Python中,一切皆对象)。
1、函数的参数传递
函数的参数传递本质上就是:从实参到形参的赋值操作。
Python 中“一切皆对象”,所有的赋值操作都是“引用的赋值”。所以,Python 中参数的传递都是“引用传递”,不是“值传递”。
具体操作时分为两类:
1)对“可变对象”进行“写操作”,直接作用于原对象本身。
2) 对“不可变对象”进行“写操作”,会产生一个新的“对象空间”,并用新的值填
充这块空间。(起到其他语言的“值传递”效果,但不是“值传递”)
可变对象有:字典、列表、集合、自定义的对象等。
不可变对象有:数字、字符串、元组、function 等。
可变对象
传递参数是可变对象,实际传递的还是对象的引用。在函数体中不创建新的对象拷贝,而是可以直接修改所传递的对象。
b = [10,20]
def f1(m):
print("m的id:",id(m)) # b 和 m 是同一个对象
m.append(30) # 由于 m 是可变对象,不创建对象拷贝,直接修改这个对象
f1(b)
print("b的id:",id(b))
print(b)
输出:
m的id: 2092043399944
b的id: 2092043399944
[10, 20, 30]
不可变对象
传递参数是不可变对象,实际传递的是对象的引用。在”赋值操作”时,由于不可变对象无法修改,系统会新创建一个对象。
a = 100
def f2(n):
print("n的id:",id(n)) #传递进来的是a 对象的地址
n = n+200 #由于a 是不可变对象,因此创建新的对象n
print("n的id:",id(n)) #n 已经变成了新的对象
print(n)
f2(a)
print("a的id:",id(a))
输出:
n的id: 1663816464
n的id: 46608592
300
a的id: 1663816464
2、浅拷贝和深拷贝
浅拷贝:不拷贝子对象的内容,只是拷贝子对象的引用。
深拷贝:会连子对象的内存也全部拷贝一份,对子对象的修改不会影响源对象
我们可以通过内置函数来实现:copy(浅拷贝)、deepcopy(深拷贝)。
import copy
def testCopy():
'''测试浅拷贝'''
a = [10, 20, [5, 6]]
b = copy.copy(a)
print("a", a)
print("b = copy.copy(a)")
print("b", b)
b.append(30)
b[2].append(7)
print("修改b")
print("a", a)
print("b", b)
def testDeepCopy():
'''测试深拷贝'''
a = [10, 20, [5, 6]]
b = copy.deepcopy(a)
print("a", a)
print("b = copy.deepcopy(a)")
print("b", b)
b.append(30)
b[2].append(7)
print("修改b")
print("a", a)
print("b", b)
print("-----------测试浅拷贝------------")
testCopy()
print("-----------测试深拷贝------------")
testDeepCopy()
输出:
-----------测试浅拷贝------------
a [10, 20, [5, 6]]
b = copy.copy(a)
b [10, 20, [5, 6]]
修改b
a [10, 20, [5, 6, 7]]
b [10, 20, [5, 6, 7], 30]
-----------测试深拷贝------------
a [10, 20, [5, 6]]
b = copy.deepcopy(a)
b [10, 20, [5, 6]]
修改b
a [10, 20, [5, 6]]
b [10, 20, [5, 6, 7], 30]
3、函数的调用过程(栈帧)
每个函数的每次调用,都会在栈内存为这个函数创建一个栈帧(Stack Frame),用于保存函数调用框架、函数参数、函数的局部变量、函数执行完后返回到哪里等等。
栈帧也叫过程活动记录,是编译器用来实现函数调用过程的一种数据结构。从逻辑上讲,栈帧就是一个函数执行的环境。
函数调用完,它的栈帧即销毁。如果函数在执行过程中又调用了其他函数,则在当前栈帧中再创建一个栈帧。层层嵌套。
当然,嵌套的层数是有限的。当超出范围,就会报栈溢出异常。有时候,递归函数报错就是这个原因。