python 对象赋值原则


本文所用的可嵌套对象和不可嵌套对象,官方称之为可变对象和不可变对象。我只是觉得前者更便于理解。

首先有两个基本知识:

  • Python 中的所有变量皆为对象
  • Python 函数参数传递的是对象的引用(跟C 语言的指针差不多)

不可嵌套对象赋值常量

不可嵌套对象赋于之前不同的值时,会重新分配内存,并将引用指向新的内存。下例将2 这个整型对象的引用赋值给a

1
2
3
4
5
6
7
a = 1
print(a, id(a))
a = 2
print(a, id(a))

1 140709099219384
2 140709099219416

不可嵌套对象赋值不可嵌套对象

1
2
3
4
5
6
7
8
9
10
11
12
a = 1
b = a
print(a, b)
print(id(a), id(b))
b = 2
print(a, b)
print(id(a), id(b))

1 1
140709083031992 140709083031992
1 2
140709083031992 140709083032024

不可嵌套对象赋值可嵌套对象

1
2
3
4
5
6
7
a = 1
print(a, id(a))
a = [1, 2, 3]
print(a, id(a))

1 140709083031992
[1, 2, 3] 1588587600064

这里a变成了整个列表对象的引用

不可嵌套对象作为函数参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def test(param1):
print(param1, id(param1))

param1 = 2
print(param1, id(param1))
return param1
a = 1
print(a, id(a))
a = test(a)
print(a, id(a))

1 140711413885368
1 140711413885368
2 140711413885400
2 140711413885400

可以看到a 作为函数参数或是返回值时,传递的始终时a 的引用。由于a 为不可嵌套对象,所以在函数内部赋值的时候引用发生了改变。

不可嵌套对象作为全局变量时被函数内部访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
a = 1

def test():
print(a, id(a))
a = 2
print(a, id(a))

if __name__ == '__main__':
test()
print(a, id(a))

Traceback (most recent call last):
File "c:\Users\Xe-131\Desktop\test.py", line 45, in <module>
test()
File "c:\Users\Xe-131\Desktop\test.py", line 40, in test
print(a, id(a))
^
UnboundLocalError: cannot access local variable 'a' where it is not associated with a value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
a = 1

def test():
print(a, id(a))
print(a.__class__)
print(a.__add__(2))
print(a, id(a))

if __name__ == '__main__':
test()
print(a, id(a))

1 140710631975352
<class 'int'>
3
1 140710631975352
1 140710631975352

可以看到在函数内部对a进行赋值会报错,但使用a内部的属性或者方法时却没问题。如果函数内部有赋值操作,python会将a理解为局部变量,从而第一句print(a, id(a))就会报错,因为在这句话之前a并没有初始化。

如果需要对a 进行赋值,有两种对待方式:

  • 当成全局变量:在函数内部用关键词声明
    • 这里全程都是用的时全局变量a,函数执行结束后,全局变量a的值确实改变了
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      a = 1

      def test():
      global a
      print(a, id(a))
      a = 2
      print(a, id(a))

      if __name__ == '__main__':
      print(a, id(a))

      test()

      print(a, id(a))

      1 140711413885368
      1 140711413885368
      2 140711413885400
      2 140711413885400
  • 当成局部变量:在用到之前先初始化声明一下
    • 函数内部的a跟外部的a是两个东西
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      a = 1

      def test():
      a = 2
      print(a, id(a))

      if __name__ == '__main__':
      print(a, id(a))

      test()

      print(a, id(a))

      1 140710631975352
      2 140710631975384
      1 140710631975352


可嵌套对象赋值常量(不可嵌套对象)

1
2
3
4
5
6
7
8
a = [1, 2, 3]
print(a, id(a))

a = 1
print(a, id(a))

[1, 2, 3] 2023166382400
1 140710631975352

可嵌套对象元素赋值常量(不可嵌套对象)

1
2
3
4
5
6
7
8
a = [1, 2, 3]
print(a, id(a), a[0], id(a[0]))

a[0] = 4
print(a, id(a), a[0], id(a[0]))

[1, 2, 3] 1281264521536 1 140710631975352
[4, 2, 3] 1281264521536 4 140710631975448


a的元素赋值并不会改变a本身的指向,而是改变其元素的指向

可嵌套对象赋值可嵌套对象

1
2
3
4
5
6
7
8
9
a = [1, 2, 3]
b = a
print(a, id(a), b, id(b))

b[0] = 4
print(a, id(a), b, id(b))

[1, 2, 3] 2346644148544 [1, 2, 3] 2346644148544
[4, 2, 3] 2346644148544 [4, 2, 3] 2346644148544

这里我们改变b的内部元素,就连a内部元素也一起改变了。是因为a b都指向一个列表,b[0] = 4 操作是将列表中的引用改变,无论改变与否它都属于这个列表。

想要获得两个互不相干的列表,可以去学习深拷贝和浅拷贝的内容。

可嵌套对象作为函数参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def test(param):
print(param, id(param))

param[0] = 4
print(param, id(param))

a = [1, 2, 3]
print(a, id(a))

test(a)
print(a, id(a))

[1, 2, 3] 2395825574208
[1, 2, 3] 2395825574208
[4, 2, 3] 2395825574208
[4, 2, 3] 2395825574208

函数传递的是列表的引用,所以列表内部元素的改变会被传递到函数外部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def test(param):
print(param, id(param))

param = [4, 5, 6]
print(param, id(param))

a = [1, 2, 3]
print(a, id(a))

test(a)
print(a, id(a))

[1, 2, 3] 2408679280960
[1, 2, 3] 2408679280960
[4, 5, 6] 2408681482880
[1, 2, 3] 2408679280960

这里对传入的引用改变方向,指向了另一个列表

通过实验,我们可以知道,将某个对象传入函数时,实际上是创建了一个新的指向原对象的引用

可嵌套对象作为全局变量被函数访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def test():
print(a, id(a))

a = [4, 5, 6]
print(a, id(a))
a = [1, 2 ,3]
if __name__ == '__main__':
print(a, id(a))
test()
print(a, id(a))

[1, 2, 3] 1967735312704
Traceback (most recent call last):
File "c:\Users\Xe-131\Desktop\test.py", line 122, in <module>
test()
File "c:\Users\Xe-131\Desktop\test.py", line 111, in test
print(a, id(a))
^
UnboundLocalError: cannot access local variable 'a' where it is not associated with a value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def test():
print(a, id(a))

a[0] = 4
print(a, id(a))

a = [1, 2 ,3]

if __name__ == '__main__':
print(a, id(a))

test()
print(a, id(a))

[1, 2, 3] 1741292507456
[1, 2, 3] 1741292507456
[4, 2, 3] 1741292507456
[4, 2, 3] 1741292507456

全局变量的作用就是,这个变量的引用时全局可见的。因为并不是通过参数传递给函数的,所以函数内部使用的引用就是原来的引用。

原理与不可嵌套函数作为全局变量被函数访问一样,当进行a = [1, 2 ,3]时,python 就会混淆,它不知道你是想新建一个与a 同名的局部变量还是将全局变量a 的指向改变。解决方法与不可嵌套变量一样。

但是如果只改变其内部属性或者元素的话,并不会引起混淆,所以不会报错。