Python——对象操作的内存分析

一、对象
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 支持字符串缓存池机制,对于符合标识符规则的字符串(仅包含下划线(_)、字母和数字)会启用字符串驻留机制驻留机制。

更多关于Python中的缓存池机制点这里~

字符串的比较

我们可以直接使用 ==!= 对字符串进行比较,是否含有相同的字符。

我们使用 isnot 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,

下标(十进制)二进制
0000
1001
2010
3011
4100
5101
6110
7111

则我们从 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),用于保存函数调用框架、函数参数、函数的局部变量、函数执行完后返回到哪里等等。

栈帧也叫过程活动记录,是编译器用来实现函数调用过程的一种数据结构。从逻辑上讲,栈帧就是一个函数执行的环境。

函数调用完,它的栈帧即销毁。如果函数在执行过程中又调用了其他函数,则在当前栈帧中再创建一个栈帧。层层嵌套。

当然,嵌套的层数是有限的。当超出范围,就会报栈溢出异常。有时候,递归函数报错就是这个原因。

更多具体细节点这里~

  • 5
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值