目录
前言
本文要求对C语言有一定了解,从一些底层机制深入探讨了python的变量、对象、函数参数、拷贝概念,并综合了大量其他人的讲解,力图透彻讲解全面的知识,一劳永逸解决读者在这方面的困扰
一、预备知识
object:python中把所有东西都视为一个对象,对象是内存中的数据
变量:python中变量则类似指向对象的指针,只有通过变量才能访问到object
id:python中所有对象在内存中的地址,可以赋值给一个C语言指针。两个id相同的变量指向同一片内存。相同id的变量使用is方法后得到的结果是True。
不可变对象:数字,字符串,元组,也可以定义一个class,并禁止用户修改它。
可变对象:列表,字典,集合,大部分用户定义的class实例化之后的对象,例如torch中的tensor
可变与不可变的界限并不清晰,根本区别在于:是否能改变该object的内容。Python中把修改字符串的路都封死了,str[0]="s"是非法的,所以字符串是不可变的。
a = 1
b = 1
print(a is b) # 输出: True,因为 a 和 b 都引用同一个整数对象 1
print(id(a)==id(b)) # 输出: True 因为 a 和 b 都引用同一个整数对象 1
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a is b) # 输出: False,因为 a 和 b 是不同的列表对象
print(id(a)==id(b)) # 输出: False,因为 a 和 b 是不同的列表对象
print(a is c) # 输出: True,因为 c 引用了 a
左值:C语言中能出现在赋值表达式左边的称为左值。左值必须有一块内存空间。
C++中的引用&:相当于隐式的指针,永久绑定到一个变量(也就是内存)上。使用的时候自带解引用*号,返回该指针指向的数据。
二、python中的object(对象)概念
一定要时时刻刻记住:python中的object类似于C++中的class(类)概念!
object被分配了一片内存,在栈上完成构造。注意区分object和变量的概念
如下代码,首先是在等式右边构造了一个类型为int,值为1 的object,然后再让左边的变量“x”指向该object "1"。注意python里的“=”不是赋值,而是一个类似于指针“指向”的动作。
x = 1
三、python中的变量
C++的变量与内存是一一对应的。python中的变量实际上是C++中的引用"&",但不同点在于python的变量不是一直绑定在一片内存空间上的;在这一点上python的变量有点像指针,可以指向不同的内存。示例如下:
x = 1 #构建了一个值为1的object,使用变量x指向该object “1”
x = 2 #构建了一个值为2的object,使用变量x重新指向该object “2”
因为python中的变量类似于指针,所以当没有任何指针指向一片内存时,这片内存就丢了,再也找不到了(python会启动垃圾回收机制,下面再谈),因此python的变量起到了提供内存位置的作用。
基于引用和指针的说法,python中的变量实际上有两种行为:
1. 作为指针,指向新的对象,语法为“variable = object ” 。例如:x = torch.tensor([1,2,3])
2. 作为引用,修改引用对象,语法为“variable.method(object)”。例如:list[0] = torch.tensor([1,2,3]),注意这里调用的是引用对象的setitem方法
以下就混用指针和引用来描述python的变量,不作区别
闭包
python在编译成字节码的时候确定了三种变量:
- 全局变量
- 局部变量
- 闭包变量。指局部作用域中用到了在该作用域外的定义的外部变量
python会让内部的闭包函数持有一个对外部变量的引用计数,这样外部的变量不会随着外部函数返回而消亡。内部的函数仍然能访问到该变量。
def dead():
a = 1
def alive():
a = 2
print(a)
return alive
alive = dead()
alive() # 输出: 2
垃圾回收
python采用引用计数。这里引入“容器”的概念。容器指能引用其他对象的对象。例如:列表、字典。下面的列表a对内部的tensor对象有一个引用计数,所以删除x之后,该对象并没有被完全删除
x = torch.tensor([1,2,3])
a = [x]
del x
print(a[0])#tensor([1, 2, 3])
当没有变量指向和对象引用某一对象的时候,该对象会被回收。python实际上的垃圾回收机制更加复杂,这里就不详细谈了。
四、python中的函数
函数参数传递
python函数参数都是变量,因此实际上传递的都是指针类型。在函数中构造一个新的局部变量,和传进来的参数变量指向同一地址。注意:函数中使用的变量和传进来的参数不一样,不是同一个指针!
注意Python是原封不动的复制一个局部变量,和参数变量指向同一地址(也就是id相同)。无论是可变对象还是不可变都是如此,下面分别是不可变对象(元组)和可变对象(tensor)的示例,可以看到id都是相同的。
a = (1, 2, 3)
print(id(a))#2288691677824
def f(b):
print(id(b))#2288691677824
b = b + (4,)
f(a)
print(a)#(1, 2, 3)
上面的元组变量a并没有被函数f改变,因为函数f仅仅是构建了一个局部变量b,并且让b指向了一个新的object。它没有让变量a指向新的内容,所以打印出a的值还是(1,2,3)。
a = torch.tensor([1,2,3],dtype=torch.float32)
print(id(a))#2288795504784
def f(b):
print(id(b))#2288795504784
b = b + 1
f(a)
python中的列表对象保存了一些指针,指向列表中保存的对象。由于下面的my_list只是一个指针,lst.append(4)相当于(1)复制一个新的指针 int * lst = &my_list(2)修改该指针lst指向内存中的内容 *(lst+3) = 4。所以指针my_list指向的内存中的内容也改变了。
def modify_mutable(lst):
lst.append(4) # 直接修改列表
my_list = [1, 2, 3]
modify_mutable(my_list)
print(my_list) # 输出: [1, 2, 3, 4]
函数默认参数
这里有一个坑,函数使用默认参数初始化的时候只会初始一次这个对象,导致之后都使用这个对象。例如下面的代码,使用bug类实例化的所有对象都共享一个列表,因为这些列表本质上是同一个对象。
class bug:
def __init__(self,args=[]):
pass
torch.tensor对象也是同理。参见参考资料
class bug:
def __init__(self,args=torch.Tensor([1])):
self.args = args
a = bug()
b = bug()
a.args+=1
print(b.args)#输出tensor([2.])
python列表
据我猜测,Python的列表首先是一个指向内存地址的指针,然后这个列表中存储的也是指针,指针指向其他对象。因此,据“垃圾回收”一节,列表对其他对象有引用计数。
b = []
a =[b,1]
a[0]=1
print(b) # [] 不变
深拷贝与浅拷贝
两种拷贝都是创建了新的对象(新旧对象id不同),分别对应copy模块的copy和deepcopy方法,下面是不同之处:
浅拷贝:(不递归的)创建新的对象,只进行最浅层的创建,其他部分都是复制的。
深拷贝:递归复制,创建新的对象,新对象和旧对象的id不一样,内存完全独立。
a = [1] #原始对象
b = copy.copy(a) #浅拷贝对象
print(id(a[0]) == id(b[0]))#True
print(id(a) == id(b))#False
b[0] = 2
print('a:',a)# a: [1]
print('b:',b)# b: [2]
上面浅拷贝的a和b两个列表并不是同一个列表对象的引用,但列表中存储的内容是同一个对象(的指针)。
五、C++和python的对比
属性 | C++ | python |
变量 | 变量与内存是一对一的关系,一个变量对应一块固定的内存空间 | python变量是C++中的引用,但这个引用不是永久绑定的 |
变量声明 | 声明变量的类型,根据类型大小分配内存空间 | 无 |
数据/内存 | 声明变量的时候在栈上分配了空间。(使用new的时候在堆上分配了空间) | 在python虚拟机的栈(frame)上构造一个对象(我猜的) |
初始化 | 声明变量时,使用"="在分配的空间上初始化,例如int a=1就是初始化,而int a;则没有 | 无 |
“=”符号 | 变量初始化,或者赋值。x=2,意思是变量x对应的内存空间上的值改为2 | 让变量代表的指针指向新的对象。 |
函数参数 | 分为值传递、引用传递。 | 都是引用传递。但是引用不是一直绑定的,可以绑定到新的对象。 |
垃圾回收 | 用户自己管理。在栈上开的随着栈消失自动回收,在堆上开的需要手动delete,或者析构 | 因为变量是指针,所以要使用引用计数。 |
六、杂谈:原地操作、切片与缓存
切片
python中的切片操作如何表现主要还是看如何定义的。下面先谈全部的切片[ : ]
python列表的切片是返回一个深拷贝,修改新对象不会影响旧对象。
list1 = ['Google', 'Runoob', 'Taobao', 'Baidu']
list2 = list1[:]
print(id(list1)==id(list2))# False
list2[1] = 'b'
print(list1) # ['Google', 'Runoob', 'Taobao', 'Baidu']
而字符串的切片是返回一个原来的对象。
list1 = 'Google'
list2 = list1[:]
print(id(list1)==id(list2))# True
pytorch在这里的表现很奇怪,它们共享地址,但id却不同。把切片换成original_tensor[:-2]也是共享内存。解释是:new_tensor是一个新的对象,所以id不同。但在pytorch中它们可以共享内存。
original_tensor = torch.tensor([1, 2, 3, 4, 5])
new_tensor = original_tensor[:]
print(id(original_tensor)==id(new_tensor))#False
new_tensor[0] = 100
print(original_tensor)#tensor([100, 2, 3, 4, 5])
因此pytorch提供了clone()方法,来产生一个新的一模一样的张量。也因此,在pytorch中进行切片要小心。
原地操作
原地操作的定义是改变对象本身,不产生一个新的副本。在python中常表现为结尾为下划线的方法,以及+=方法。python中的+=操作实际上是__iadd__魔法方法,取决于用户如何定义
数字常量是无法进行原地操作的,因为它是常量。+=操作前后的id是不一样的。
a=1
b=a
b+=1
print(a)# 1 没有改变
pytorch中的原地操作
pytorch中有视图和拷贝的说法。
首先,pytorch中的加法(常量)显然不是原地操作,但是+=是原地操作
a = torch.tensor([1,2,3,4,5])
print(id(a))#1549428237200
a = a + 1
print(id(a))#1549420619888 改变了
------------------------
a = torch.tensor([1,2,3],dtype=torch.float32)
b = a
print(a)
b += 100
print(a)
print(id(a)==id(b))#True,原地操作,没有改变
小心pytorch中的加法操作,这会构建计算图,哪怕参与加法的是常量1,下面第三行代码中可见a保存了grad_fn的属性,也就是“加法”和“输入值”等东西,构建了计算图。grad_fn对原来的张量构成了引用,所以就算变量a指向了新的张量,原来的张量还是被保存在计算图中。
而最下面的+=操作报错,说明这是原地操作,会导致梯度无法计算。
a = torch.tensor([[-0.0000, 0.0000]],requires_grad=True)
a = a+1
print(a.grad_fn)#<AddBackward0 object at 0x00000168C11F7A30>
#----------------------------------
a = torch.tensor([-0.0000, 0.0000],requires_grad=True)
a+=1
print(a.grad_fn)#RuntimeError: a leaf Variable that requires grad is being used in an in-place operation.
缓存机制
关于“缓存池”机制,参见最后两个参考资料
python会缓存一些简单的对象,在第二次使用时会先到缓存区查一下,如果已经有了就不会创建新的对象。下面的两个“a+b”创建了两个对象赋给变量c,变量c和a引用的值都是相同的,但由于第一次a的值是1000,所以没有缓存起来。读者也可以用字符串的加法自己试一试。
a = 1000
b = 0
c = a+b
print(id(a)==id(c))#False
a = 1
c = a+b
print(id(a)==id(c))#True
下面同理,可见python中不可变对象的id并不是固定的。而是缓存造成了这种错觉
a = 1
b = 1
print(a is b)# True
a = 10000000
b = 10000000
print(a is b)# False
总结
本人是编程小白,所以对python的内部机制存在一些臆想,也没有精力去证明或考究,难免有错误,欢迎指正。
参考:
部分内容摘自下面的参考,如侵权请联系我删除
一篇就懂:python浅拷贝copy与深拷贝deepcopy_python deepcopy-优快云博客
面试题:深拷贝和浅拷贝(超级详细,有内存图)_深copy和浅copy面试-优快云博客
Python:可变对象、不可变对象、迭代器可迭代对象_python可变对象和不可变对象-优快云博客
【python笔记】可变对象和不可变对象_不可变对象python-优快云博客
https://zhuanlan.zhihu.com/p/582524231
500 Lines or LessA Python Interpreter Written in Python
【python】详解变量作用域,局部闭包还是全局?_哔哩哔哩_bilibili
【Python】我精心设计的默认参数,怎么就出问题了呢?_哔哩哔哩_bilibili
【python】mutable和immutable其实根本没区别?带你了解这个概念背后你没思考过的东西_哔哩哔哩_bilibili