Python 程序设计讲义(62):从内存的角度分析 Python 的变量与对象
目录
一、Python
的变量、对象和引用
Python
中需要明确三个概念:变量,引用,对象。
1、变量
python
中的变量简而言之就是指针,变量存放的是对象的引用,或者说存放的是对象的地址,可以使用函数id()
查看对象的地址。
(1)任何变量所需要的存储空间大小相同,与所指向的对象类型无关,因为任何变量都只是保存指向对象的内存地址。
(2)变量没有类型(变量都是对象的引用,变量的类型其实是变量指向的数据的类型),这意味着变量可以指向任何对象。
(3)变量分配在栈内存中,用来指向某一个对象。
(4)变量分为可变变量和不可变变量。
2、对象
对象就是分配的一块内存+存储的值。对象有id
(地址),type
(类型)和value
(值)。
(1)对象分配在堆内存中,用来存放真正的数据。
(2)对象有类型,不同对象有不同的类型,如str
、int
等。
(3)不同类型的对象根据存储的数据大小不同,所占用的内存空间也不同。
(4)python
中一切皆对象,一个列表、值、函数、类等都可以成为对象。
3、引用
引用是建立变量和对象之间的关系,即某个变量指向了某个对象,二者之间的关系便是引用。
因为变量是无类型的,它可以指向任何对象。同一个对象可能被多个变量引用(指向),因此会涉及一个对象引用计数,回收机制,以及引用同一个对象的不同变量之间的操作影响关系。
4、变量,对象和引用之间的关系
变量,对象和引用之间的关系如下图所示:
当执行代码str1='hello world'
时,python
解释器做了以下三件事情:
(1)创建一个变量,名称为str1
。
(2)创建一个对象(字符串类型)并开辟一块存储空间,该对象的id
是0x020
,type
是str
,存储的value
是hello world
。
(3)将变量和对象通过指针链接起来。从变量到对象的连接,称为引用。
例如:
print(str1)
print('变量str1的地址:',id(str1))
print('变量str2的地址:',id(str2))
print('字符串Hello Python的地址:',id("Hello Python"))
print('变量str1的类型:',type(str1))
print('字符串Hello Python的类型:',type("Hello Python"))
str2="Python"
print('变量str1的地址:',id(str1))
print('变量str2的地址:',id(str2))
print('字符串Python的地址:',id("Python"))
程序的执行结果为:
Hello Python
变量str1的地址: 1975302064560
变量str2的地址: 1975302064560
字符串Hello Python的地址: 1975302064560
变量str1的类型: <class 'str'>
字符串Hello Python的类型: <class 'str'>
变量str1的地址: 1975302064560
变量str2的地址: 1975301640128
字符串Python的地址: 1975301640128
如果变量引用的是一个组合对象(如:列表,元组,集合,字典等),则该变量指向组合对象,组合对象中的每个元素又指向其他对象。如下图所示:
可以使用如下代码查看变量的引用情况:
l1=[80,90]
l2=['张静',l1,'xinxiang']
print(l2)
print('列表l2的地址:',id(l2))
print('列表l2中第1个元素的地址:',id(l2[0]))
print('字符串"张静"的地址:',id("张静"))
print('列表l2中第2个元素的地址:',id(l2[1]))
print('列表l1的地址:',id(l1))
print('列表l2中第3个元素的地址:',id(l2[2]))
print('字符串"xinxiang"的地址:',id("xinxiang"))
程序的执行结果为:
['张静', [80, 90], 'xinxiang']
列表l2的地址: 2261667505856
列表l2中第1个元素的地址: 2261673852032
字符串"张静"的地址: 2261673852032
列表l2中第2个元素的地址: 2261667653376
列表l1的地址: 2261667653376
列表l2中第3个元素的地址: 2261671059696
字符串"xinxiang"的地址: 2261671059696
python
的变量本质是一个指针,变量不需要指定类型(变量本来就没有类型),它可以指向任意类型的对象。称为动态类型机制,任何时候根据需要,变量都可以重新绑定在另一个不同类型的对象上。
当一个对象不存在任何引用时,就通过垃圾回收机制被回收。
而在强类型语言(如C
语言)中,创建一个变量就是开辟一块存储空间,并且给该空间命名,这个名字就是变量。因此在强类型语言中,定义变量时都需要指定变量类型。
二、可变对象与不可变对象
1、不可变对象
一旦创建后不允许被修改的对象称为不可变对象。包括:整数(int
)、浮点数(float
)、字符串(str
)、元组(tuple
)等。这些对象的共同特点是一旦创建,它们的内存中的数据不可改变。
当修改变量时,Python
会创建一个新的对象,并让变量引用新的对象,而不会修改原对象。如下图所示:
验证:
(1)字符串
str1 = "Hello"
str2 = str1.replace('H', 'J') # 更改字符串的内容,其实是生成一个新字符串,赋值给str2
print(str1)
print('str1的地址:', id(str1))
print(str2)
print('str2的地址:', id(str2))
程序的运行结果为:
Hello
str1的地址: 2644748157744
Jello
str2的地址: 2644750983376
(2)数值
x = 10
y = x
print('x=',x)
print('变量x的地址:', id(x))
print('y=',y)
print('变量y的地址:', id(y))
x = x + 1 #修改x的内容,其实是让x执行一个新对象
print('x=',x)
print('变量x的地址:', id(x))
print('y=',y)
print('变量y的地址:', id(y))
程序的运行结果为:
x= 10
变量x的地址: 140708086826184
y= 10
变量y的地址: 140708086826184
x= 11
变量x的地址: 140708086826216
y= 10
变量y的地址: 140708086826184
(3)元组
不能直接更改元组中的元素。如果尝试修改元组中的某个元素,Python
将抛出一个错误。
例如:
tup = (1, 2, 3)
tup[0] = 100 # 尝试修改元组的第一个元素,将抛出一个错误
程序的运行结果为:
Traceback (most recent call last):
File "C:\Users\wgx58\PycharmProjects\PythonProject1\hello.py", line 2, in <module>
tup[0] = 100 # 尝试修改元组的第一个元素,将抛出一个错误
~~~^^^
TypeError: 'tuple' object does not support item assignment
说明:尽管元组本身是不可变的,但如果元组中包含可变对象,如列表,这些可变对象内部的内容是可以被修改的。这种情况下,元组的不可变性只适用于顶层结构,不影响内部元素的可变性。
例如:
t1 = (1, [2, 3], 4)
t1[1][1]=20
print(t1) # 输出 (1, [2, 20], 4)
程序的运行结果为:
(1, [2, 20], 4)
上述的修改过程可使用下图表示:
2、可变对象
创建后可以改变的对象称为可变对象。包括:列表(list
)、字典(dict
)、集合(set
)等。
当通过一个变量修改对象时,所有引用该对象的变量都会受到影响。如下图所示:
验证:
(1)列表
l1 = [1, 2, 3]
l2=l1
print('l1的地址:',id(l1))
print('l2的地址:',id(l2))
l1[1]=20
print(l1) # 输出 [1, 20, 3],列表内容被修改
print(l2) # 输出 [1, 20, 3],因为l2仍然指向原来的列表
print('l1的地址:',id(l1))
print('l2的地址:',id(l2))
程序的运行结果为:
l1的地址: 2309674149632
l2的地址: 2309674149632
[1, 20, 3]
[1, 20, 3]
l1的地址: 2309674149632
l2的地址: 2309674149632
(2)字典
d1 = {1:"tom", 2:"jack", 3:"mark"}
d2=d1
print('d1的地址:',id(d1))
print('d2的地址:',id(d2))
d1[1]="black"
print(d1) # 输出 {1:"black", 2:"jack", 3:"mark"},列表内容被修改
print(d2) # 输出 {1:"black", 2:"jack", 3:"mark"},因为d2仍然指向原来的字典
print('d1的地址:',id(d1))
print('d2的地址:',id(d2))
程序的运行结果为:
d1的地址: 2389055546368
d2的地址: 2389055546368
{1: 'black', 2: 'jack', 3: 'mark'}
{1: 'black', 2: 'jack', 3: 'mark'}
d1的地址: 2389055546368
d2的地址: 2389055546368
(3)集合
尽管集合是可变的,但也有一些操作上的限制。集合只能包含不可变(可哈希)的对象。因此不能将列表或另一个集合作为元素添加到集合中。
s1={1,2,3}
s2=s1
print('s1的地址:',id(s1))
print('s2的地址:',id(s2))
s1.add(4)
print(s1) # 输出{1, 2, 3, 4},列表内容被修改
print(s2) # {1, 2, 3, 4},因为d2仍然指向原来的集合
print('s1的地址:',id(s1))
print('s2的地址:',id(s2))
程序的运行结果为:
s1的地址: 2326100593120
s2的地址: 2326100593120
{1, 2, 3, 4}
{1, 2, 3, 4}
s1的地址: 2326100593120
s2的地址: 2326100593120
三、变量的删除
Python
提供了 del
语句来删除变量,使其不再指向任何对象。
例如:
x = 10
del x # 删除变量 x
print(x) #抛出变量未定义错误
程序的运行结果为:
Traceback (most recent call last):
File "C:\Users\wgx58\PycharmProjects\PythonProject1\hello.py", line 3, in <module>
print(x) #抛出变量不存在错误
^
NameError: name 'x' is not defined
四、函数的参数传递问题
Python
中参数传递方式:按引用传递,实际是对象的引用地址传递。
1、不可变对象的传递
向函数传递不可变对象时,对该对象的任何修改都不会影响原始对象,因为实际上是创建了一个新的对象传递给函数的形参。
例如:
def update_number(n):
n=n*2
return n
x=5
print(update_number(x)) # 把对象5传递给实参,x不受影响。输出10
print(x) # 输出5
程序的运行结果为:
10
5
2、可变对象的传递
传递可变对象时,函数内对对象的修改将影响原始对象。因为形参和实参指向同一个对象。
例如:
def update_list(x):
x[1]=60
l1=["张平",85]
update_list(l1) # l1和x指向同一个列表对象
#列表l1的元素发生变化时,应为l1仍然指向该列表。因此:l1的内容发生变化
print(l1)
程序的运行结果为:
['张平', 60]