变量和赋值机制

组成部分

  • 变量是由三部分组成
# 变量名 赋值符号 变量值
name = "Tom"

Python变量与C++的不同

  • 在C++中,我们对变量的认识应该相当于一个标识符,创建一个变量,即开辟一块内存然后给这块内存起个名字,那么这个名字就是“变量”,当然,这个变量的含义和内存地址以及指针都是不相同的。
  • 在Python中,变量就是一个指针
  • 首先,python中的变量是没有类型的,有类型的是“对象”,而不是变量。变量没有类型,那么就意味着它可以随意指向任何对象。在强类型语言中,变量其实都是有具体的类型来限制的,规定一个类型的变量只能被赋值与该类型相同或兼容的值。但是在python中,显然变量的自由度更大。
  • 其次,之前学过体系结构的同学都应该了解,指针的内存空间大小是与类型无关的,其内存空间只是保存了所指向数据的内存地址。之所以说指针也有类型,是因为在计算偏移量的时候,确实需要类型相关的信息。所以,从深层次的含以上来理解,python中的变量与强类型语言中的指针非常相似。
  • C++通过操作内存地址而间接操作数据,数据处于被动地位;Python则是直接操控数据,数据处于主动地位,变量只是作为一种引用的关系存在,而不再拥有存储的功能

什么是对象

  • 对象=确定内存空间+存储在这块内存空间中的值。
  • 这一点其实和java有些相似。Java中,对象是分配在堆上的,存储真正的数据,而引用是在堆栈中开辟的内存空间用于引用某一个对象。
  • Python中,对象才有类型,不同的对象可以拥有不同类型的数据。
  • 在Python中,任何东西都是对象,Python使用对象模型来储存数据,任何类型的值都是一个对象
  • 所有的python对象都有3个特征:身份、类型和值
  • 身份:每一个对象都有自己的唯一的标识,可以使用内建函数id()来得到它。这个值可以被认为是该对象的内存地址
  • 类型:对象的类型决定了该对象可以保存的什么类型的值,可以进行什么操作,以及遵循什么样的规则。type()函数来查看python 对象的类型。
  • :对象表示的数据项。

什么是引用

  • 在 Python 程序中,每个对象都会在内存中申请开辟一块空间来保存该对象,该对象在内存中所在位置的地址被称为引用。
  • 引用在Python中的语义应该是一种关系,即变量和对象之间的关系,就是指针指向某一块内存空间的关系。
  • 对于一个对象来说,和不同的变量可能存在着多个“引用”关系。因为变量是无类型的,他想关联谁就可以指向谁。
  • 在开发程序时,所定义的变量名实际就对象的地址引用。

变量的比较

  • “==” 比较的是对象所存储的数据的值是否相等
  • “is” 比较的是两个变量是否都引用了同一个对象

关于变量的总结

重要原则

  1. python中,万物皆对象,不存在所谓的传值调用,一切传递的都是对象的引用,也可以认为是传址
  2. python中的一切数据都是存储在对象
  3. python中的一切变量都是对对象的引用
  4. 对象不能被覆盖,也不能被直接销毁(python中有垃圾回收机制来回收不用地对象,比如引用计数机制)
  5. 变量采用地址引用的方式,变量存储的内容只是一个变量值所在的内存地址,而不是变量值(数据)本身

赋值机制

  • Python的赋值机制("=")是将一个变量存储的其变量值对象的地址赋给一个变量
  • python如何定义变量 python定义变量不赋值_不可变对象

a = 3
b = a # 将变量a对应的数据对象3的地址赋给变量b

可变对象和不可变对象

概念

  • 可变对象与不可变对象的区别在于对象本身是否可变。
  • 可变对象是指,一个对象在不改变其所指向的地址的前提下,可以修改其所指向的地址中的值;
  • 不可变对象是指,一个对象所指向的地址上值是不能修改的,如果你修改了这个对象的值,那么它指向的地址就改变了,相当于你把这个对象指向的值复制出来一份,然后做了修改后存到另一个地址上了,本质上并没有对对象的值进行修改,而是重新创建了一个新的对象。但是可变对象就不会做这样的动作,而是直接在对象所指的地址上把值给改变了,而这个对象依然指向这个地址。

Python内置的对象类型的分类

代码示例

# 可变对象
>>> a = [1, 2, 3]
>>> a[1] = 4
>>> a
[1, 4, 3]

# 不可变对象
>>> b = (1, 2, 3)
>>> b[1] = 4
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

可变对象和不可变对象的地址问题

可变对象由于其本身可变,所以当修改可变对象时,该对象的地址是没有改变的

>>> a = [1, 2, 3]
>>> id(a)
2139167175368
>>> a[1] = 4
>>> id(a)
2139167175368

如果变量对应的是可变对象时

# 修改变量a对应的列表则b也跟着变,因为他们始终指向同一个地址
>>> a = [1, 2, 3]
>>> id(a)
139683460043584
>>> b = a
>>> id(b)
139683460043584
>>> a[1] = 4
>>> a
[1, 4, 3]
>>> b
[1, 4, 3]
>>> id(a)
139683460043584
>>> id(b)
139683460043584


# 如果是重新将新的列表的地址赋给变量b,则变量a和变量b则对应不同的地址,相互不影响
>>> b = [4, 5, 6]
>>> id(b)
139683460117696
>>> id(a)
139683460043584
>>> b
[4, 5, 6]
>>> a
[1, 4, 3]

如果变量对应的是不可变对象

# 因为不可变对象不可以修改,所以当值不同时即是在内存空间中新建了一块区域用以存放新值
# a改变后,它的地址也发生了变化,而b则维持原来的地址,原来地址中的内容也没有发生变化
>>> a = (1, 2, 3)
>>> id(a)
2139167074776
>>> b = a
>>> a = (4, 5, 6)
>>> a
(4, 5, 6)
>>> b
(1, 2, 3)
>>> id(a)
2139167075928
>>> id(b)
2139167074776

作为函数参数时

可变对象

>>> def myfunc(l):
...     l.append(1)
...     print(l)
...
>>> l = [1, 2, 3]
>>> myfunc(l)
[1, 2, 3, 1]
>>> l
[1, 2, 3, 1]
  • 可变对象作为参数传入时,在函数中对其本身进行修改,是会影响到全局中的这个变量值的,因为函数直接对该地址的值进行了修改

不可变对象

>>> def myfunc(a):
...     a += 1
...     print(a)
...
>>> a = 2
>>> myfunc(a)
3
>>> a
2
  • 对于不可变对象来说,虽然函数中的a值变了,但是全局中的a值没变,因为函数中的a值已经对应了另外一个地址,而全局中的a值指向的原来地址的值是没有变的
  • 关于函数的参数传递
    python的函数传递其实就是将实参对应的对象地址赋值给形参,即形参和实参都具有相同的对象地址,指向相同的内存区域,且形参的作用域只是在函数内部

总结

  • python中向函数传递参数只能是传对象引用,表示把它的地址都传进去了,这才会带来上面的现象
  • 有的编程语言允许值传递,即只是把值传进去,在里面另外找一个地址来放,这样就不会影响全局中的变量

可变对象在类中使用

class Myclass:
    def __init__(self, a):
        self.a = a
    def printa(self):
        print(self.a)

aa = [1, 2]
my = Myclass(aa)
my.printa()
aa.append(3)
print(aa)
my.printa()
(bbn) jyzhang@admin2-X10DAi:~/test$ python net.py
[1, 2]
[1, 2, 3]
[1, 2, 3]
  • 类中的变量和全局变量地址依然是共用的,无论在哪里修改都会影响对方

创建可变对象和不可变对象时的内存机制

不可变对象

代码块
  • 一个模块,一个函数,一个文件,一个类都是一个代码块
  • 在交互模式下,每一行是一个代码块
小数据池
  • 小数据池,是Python对内存的一种优化
  • 它将-5到256的整数,以及一定规则的字符串,提前在内存中建了池,容器里固定放了这些数
  • 小数据池的应用数据类型:整型,字符串,bool
不可变对象的内存机制
  1. 同一代码块中具有相同值的不可变对象具有相同的内存地址。初始化不可变对象时,会优先看是否之前在该代码块中创建过相同值的对象,若有,会复用该对象。
  2. 不同代码块中满足小数据池条件的相同值的不可变对象具有相同的内存地址。
  3. 不同值的不可变对象具有不同的内存地址。

可变对象

  • 具有相同值的可变对象具有不同的内存地址,即每次初始化可变对象即要重新创建一个对象

深拷贝和浅拷贝

直接赋值

Bill = ["Gates", 50, ["Python", "C#", "JavaScript"]]
Jack = Bill
print(id(Bill))
print(Bill)
print([id(ele) for ele in Bill])
print(id(Jack))
print(Jack)
print([id(ele) for ele in Jack])
print(Jack is Bill)
#output:
97597256
['Gates', 50, ['Python', 'C#', 'JavaScript']]
[97625072L, 34047944L, 97620808L]
97597256
['Gates', 50, ['Python', 'C#', 'JavaScript']]
[97625072L, 34047944L, 97620808L]
True
Bill[0] = "Mark"
Bill[2].append("Scala")
print(id(Bill))
print(Bill)
print([id(ele) for ele in Bill])
print(id(Jack))
print(Jack)
print([id(ele) for ele in Jack])
#output:
97262024
['Mark', 50, ['Python', 'C#', 'JavaScript', 'Scala']]
[97615224L, 34047944L, 97596616L]
97262024
['Mark', 50, ['Python', 'C#', 'JavaScript', 'Scala']]
[97615224L, 34047944L, 97596616L]
  • 这说明采用直接赋值的方式,变量BillJack的地址完全相同,同时他们的内部元素的地址也完全相同,Bill的元素修改Jack上也成立。需要注意的是strint等原子类型是不可变类型,在不改变地址的前提下,无法改变值,所以Bill[0]的值发生了变换,且分配了一个新的地址。而list ,dict等容器类型式可变类型,在地址不变的前提下,可以更改值,所以Bill[2]的值发生了变化,但是地址不变

浅拷贝

  1. 浅拷贝
import copy
 
Bill = ["Gates", 50, ["Python", "C#", "JavaScript"]]
Jack = copy.copy(Bill)
print(id(Bill))
print(Bill)
print([id(ele) for ele in Bill])
print(id(Jack))
print(Jack)
print([id(ele) for ele in Jack])
print(Jack is Bill)

#output:
97263496
['Gates', 50, ['Python', 'C#', 'JavaScript']]
[97625072L, 34047944L, 97262024L]
98156168
['Gates', 50, ['Python', 'C#', 'JavaScript']]
[97625072L, 34047944L, 97262024L]
False
  1. 修改值
Bill[0] = "Mark"
Bill[2].append("Scala")
print(id(Bill))
print(Bill)
print([id(ele) for ele in Bill])
print(id(Jack))
print(Jack)
print([id(ele) for ele in Jack])

#output:
97263496
['Mark', 50, ['Python', 'C#', 'JavaScript', 'Scala']]
[97613904L, 34047944L, 97262024L]
98156168
['Gates', 50, ['Python', 'C#', 'JavaScript', 'Scala']]
[97625072L, 34047944L, 97262024L]
  1. 这说明,浅拷贝的变量JackBill的地址不同,即 Jack is not Bill, 但是它俩的元素地址相同,即Jack[i] is Bill[i]。浅拷贝之后,对原变量元素进行赋值。Bill[0]Bill[-1]都发生了变化,且由于Bill[0]是不可变类型,分配了新的地址,而浅拷贝对象Jack[0]还指向原地址,所以Jack[0]的值还是原来的值。而Bill[-1]是原地变化,所以Bill[-1]也进行了相应的变化。

深拷贝

  1. 深拷贝
import copy
 
Bill = ["Gates", 50, ["Python", "C#", "JavaScript"]]
Jack = copy.deepcopy(Bill)
print(id(Bill))
print(Bill)
print([id(ele) for ele in Bill])
print(id(Jack))
print(Jack)
print([id(ele) for ele in Jack])
print(Jack is Bill)

#output:
97262024
['Gates', 50, ['Python', 'C#', 'JavaScript']]
[97625072L, 34047944L, 97598216L]
97263496
['Gates', 50, ['Python', 'C#', 'JavaScript']]
[97625072L, 34047944L, 97596872L]
False
  1. 修改值
Bill[1] = 38
Bill[2].append("Scala")
print(id(Bill))
print(Bill)
print([id(ele) for ele in Bill])
print(id(Jack))
print(Jack)
print([id(ele) for ele in Jack])

#output:
97262024
['Gates', 38, ['Python', 'C#', 'JavaScript', 'Scala']]
[97625072L, 34048232L, 97598216L]
97263496
['Gates', 50, ['Python', 'C#', 'JavaScript']]
[97625072L, 34047944L, 97596872L]
  1. 结论
  • 可以看到,JackBill的深拷贝对象,Jack is not Bill,and Jack[i] is not Bill[i],从而实现效果上,两个变量的彻底剥离。
  • 在初始化的时候,由于前两个元素都是不可变类型,在同一个代码块中JackBill的这两个元素的地址相同。重点是对于可变类型元素,两个变量的地址是不同的,因此,不论元素类型是否可变,原对象的变化都不影响拷贝对象。

总结

=copydeepcopy的区别如下:
考虑对象A的变化对拷贝对象B的影响,应该这么考虑:

  1. AB是否指向同一个地址(=),若是则A在当前地址的任何变化都会导致B的变化,(若A被重新赋值至一个新的对象除外)。
  2. AB指向的地址不同,若B的元素与A的元素指向地址相同(浅拷贝),且A的元素有可变类型,那么A的可变类型元素的变化会导致B的相应元素的变化。
  3. AB指向的地址不同,且AB的元素指向的地址也不同(深拷贝,不可变类型元素地址相同,不影响),那么A的任何类型元素的变化都不会导致B的元素变化。