Python官网教程

https://docs.python.org/zh-cn/3/tutorial/classes.html

变量和对象

a = 3

b = 'hello'

其中分别创建了,int型对象3和string对象‘hello’;a和b是变量,分别指向对象3和对象‘hello’

对象

对象分为可变对象和不可变对象。(不可变指的是对象的值不可变)

不可变对象:int、float、long、str、tuple

可变对象:dict、list、set

差别在于:对于不可变类型的对象,如果要更改变量的值,则会创建一个新的值,把变量绑定到新的对象上,而旧的值如果没有被引用就等待辣鸡回收

可变类型的数据对对象操作的时候,不需要在重新分配地址,只需要在原对象的后面连续申请地址(+/-)即可,对于对象整体list/dict而言,内存地址保持不变,但是整体大小会变化

>>> a = 'hello'
>>> id(a)
56835584
>>> a = 'nihao'
>>> id(a)
56835840

对象类型是str,不可变,当改变变量a的时候,因为原‘hello’是不能被改变的,重新分配地址,创建新的变量‘nihao’,然后变量a指向新的对象

>>> a = [1,2,3,4]
>>> id(a)
55434536
>>> a.append(5)
>>> id(a)
55434536

对象类型是列表, 属于可变类型,当对对象操作的时候,无非就是增加或者删除,是在列表或者字典之后申请地址,改变的不是开始的地址,而是整体的大小。因此,内存地址并没有变。变量的指向自然也不变。

变量就像是C中的指针,但是变量没有类型,有类型之别的是对象

 

传对象引用

Python中变量之间不是单纯的传值或者传引用,而是采用的“传对象引用”的方式:

  1. 如果对象是可变类型,那么相当于相当于传引用{传地址}
  2. 如果对象是不可变类型,相当于传对象(传个新的对象)

为什么会这样?就是因为可变类型和不可变类型啊,一个可变,一个不可变,不可变的无法在原地址上改变,只能创建新的,可变的地址不变、归根结底还是传地址,只不过不可变类型的要重建对象,地址改变了,是个新的对象,可变类型的不需要重建对象,地址没有改变而已

记忆:传对象地址,就是传的对象的地址,可变类型对象地址不变,不可变类型对象地址改变

区别在哪里呢?

a = [1,2,3,4]
b = a
a[1] = 0
print(b[1])

结果是 0

因为传的是对象引用,也就是对象的地址,a中存的是列表的地址,a把地址给b,也就是说a和b指向了同一片存储空间,对a的操作,因为是列表类型(属于可变类型),地址不会改变,所以结果同样反映在b上

a = [1,2,3,4]
b = a[:]
a[1] = 0
print(b[1])

结果是2

a[:] 代表的是列表中的全部元素,这个时候相当于赋值一份给了b

对a的操作不影响b

a = 1
b = a
a = 2
print(b)

结果是1

传对象引用,a把对象1所在的地址传给b,a 指向 对象2 所在的空间,int型数据是不可变类型,对象1和对象2所在的内空间是完全不同的

 

深拷贝和浅拷贝

浅拷贝:实质上是复制引用,拷贝几个元素就复制对应元素的引用(复制的是每个元素的引用)。

区别于赋值,赋值是将整个对象的引用给复制了一份给变量。

复制引用? a = 110;b = a ; 这个就是把对象110 的地址赋给 b,在Python中是变量之间是传对象引用的方式

深拷贝:和浅拷贝本质上的区别是,他不会复制任何引用,对象内的所有元素,子元素,孙子元素...的数据都会复制过来,实现原理是递归,只要里面还有子元素,就会复制到元素数据到新的内存地址

区别:对原对象的操作会影响浅拷贝,但深拷贝的不会有任何变化。因为两者一个是复制引用,一个是复制数据。

import copy

list = [1,2,3,4,['a','b']]
a = list.copy()
# a = copy.copy(list) # 浅拷贝
b = copy.deepcopy(list) # 深拷贝
c = list # 复制,传对象引用

list.append(5) # 修改对象
list[4].append('c') # 修改对象a中的列表
list[0] = 0 # 修改第一个不可变元素

print(list)
print(a)
print(b)
print(c)

结果是:

[0, 2, 3, 4, ['a', 'b', 'c'], 5]print(a)[1, 2, 3, 4, ['a', 'b', 'c']]

//浅拷贝,拷贝的引用,如果原对象中的可变类型元素改变,这个也会改变,比如元素列表['a','b'] -> ['a', 'b', 'c'];

如果是不可变类型,如list[0] = 1 ,就不会改变;

浅拷贝没有复制的元素的引用,就不会出现。比如新增的元素 5

也就是说浅拷贝复制的是每个元素的引用,且同样符合可变元素和不可变元素的性质。

print(b)[1, 2, 3, 4, ['a', 'b']]

//深拷贝,拷贝的什么,还是什么,不会受原对象的变化的影响print(c)

[0, 2, 3, 4, ['a', 'b', 'c'], 5]

//赋值,传对象引用,结果一摸一样,因为指向同一片内存空间

 

>>> dict_a = {}
>>> dict_a[1] = [1,2,3]
>>> dict_a[2] = [2,3,4]
>>> dict_a
{1: [1, 2, 3], 2: [2, 3, 4]}
>>> dict_b = dict_a.copy()
>>> dict_a[1].append(4)
>>> dict_a,dict_b
({1: [1, 2, 3, 4], 2: [2, 3, 4]}, {1: [1, 2, 3, 4], 2: [2, 3, 4]})
>>> dict_a[1][0] = 0
>>> dict_a
{1: [0, 2, 3, 4], 2: [2, 3, 4]}
>>> dict_b
{1: [0, 2, 3, 4], 2: [2, 3, 4]}

对于字典来说,这个浅拷贝现象很正常,因为复制的是引用,所以对原字典的改变会反映在浅拷贝上。

python作用域和命名空间

  • 作用域
  1. 重要的是意识到作用域,是按字面文本来确定的。函数和类,内部是局部作用域,整个模块是全局作用域
  2. 在函数内新建函数,会引入一层新的作用域,所以特别强调:按字面文本就能确定作用域。比如在局部作用域中使用全局变量都要使用global语句
  3. 赋值不会复制数据,只是像指针一样,让名称指向对象,数字也好,字符串也好也是如此;或者说名称绑定到对象(在Python中什么都是对象)
  4. nonlocal语句表明特定变量生存于外部作用域,并且在其中应当被重新绑定;global语句表明特定变量生存于全局作用域,而且在其中应当被重新绑定
  5. 如果没有使用global或者nonlocal会怎么样?

会在该作用域内 新建一个局部变量 进行绑定

  • 命名空间
  1. 命名空间:是一组从名字到对象的映射。
  2. 这个映射是怎么实现的呢?

python字典实现

  1. 命名空间是作用域实现的基石

命名空间举例:

  1. 内置名称的命名空间(内置名称也存在一个模块中,称作builtins
  2. import的模块的命名空间
  3. __ main__ 模块的命名空间
  4. 函数的命名空间,其被调用时创建本地命名空间,用完或错误是删除,每次递归调用都会有自己的本地命名空间
  1. 为什么说命名空间是作用域实现的基石呢?

在作用域中 调用名称,python就会尝试从适合这个作用域的命名空间中找到对应的对象,然后实现调用比如调用__ main__ 模块的函数,函数的命名空间在模块命名空间中,从里面找到名称对应的对象变量如此,函数如此,类呢?

  1. 简而言之,一个作用域对应着一个命名空间,名称和对象的对应关系定义在命名空间中。而且命名空间存在着生存期,比如调用函数之后,函数中的变量会被释放,函数对应的命名空间也就被释放了,当再次调用的时候会恢复。

注意:xx.yy,我们习惯把 . 之后的称为 xx 的属性


def scope_test():
    def do_local():
        spam = "local spam"# 方法里定义的spam

    def do_nonlocal():
        nonlocal spam # 告诉Python spam在外一层有定义(生存在外一层),去找到它
        spam = "nonlocal spam" # 重新绑定

    def do_global():
        global spam# 告诉Python spam在全局作用域有定义(生存在全局作用域)
        spam = "global spam"

    spam = "test spam"# 定义在方法外一层作用域
    do_local()
    print("After local assignment:", spam)# 打印方法外一层的spam
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    do_global()# 这个执行的时候,在全局作用域中并没有找到spam,那么意味着新建全局变量spam
    print("After global assignment:", spam)# 这里打印的是同一层作用域中的spam而非全                                         
                          # 局中的

scope_test()
print("In global scope:", spam)# 打印全局作用域中的sapm

代码的输出是

After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

上述例子,定义了三层作用域

  • 类定义时,将创建一个新的命名空间,并将其作为局部作用域-对应着一个新的命名空间
  • 在C中,全局变量在哪里也能引用,但是在Python中,需要使用global和nonlocal来声明