一文深度解析python中的变量(非常详细!)

目录

前言

一、预备知识

二、python中的object(对象)概念

三、python中的变量

闭包

垃圾回收

四、python中的函数

函数参数传递

函数默认参数

python列表

深拷贝与浅拷贝

五、C++和python的对比

六、杂谈:原地操作、切片与缓存

切片

原地操作

pytorch中的原地操作

缓存机制

总结

参考:



前言

本文要求对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在编译成字节码的时候确定了三种变量:

  1. 全局变量
  2. 局部变量
  3. 闭包变量。指局部作用域中用到了在该作用域外的定义的外部变量

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的内部机制存在一些臆想,也没有精力去证明或考究,难免有错误,欢迎指正。

参考:

部分内容摘自下面的参考,如侵权请联系我删除

Python3 函数 | 菜鸟教程

一篇就懂: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

【python】你绝对不知道的字符串缓存机制!这个知识点有点太偏了_哔哩哔哩_bilibili

Python之引用_python 引用-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值