Python与Golang的赋值与拷贝

今天用Go写代码时,遇到map的拷贝问题,想到Python中拷贝是引用,但对这里的概念有些模糊。这里做个归纳整理。


数据类型

Python

在python中,系统默认提供六个标准数据类型:

可变数据类型为如下三种

  • List
  • Dict
  • Set

不可变类型为

  • Number (int、float、bool、complex(复数))
  • String
  • Tuple

Golang

在Golang中,Map与Slice也是可变类型

不可变类型包括:

  • Byte
  • Rune
  • String
  • 数字类型
  • Bool

变量赋值

对变量赋值分为简单赋值、浅拷贝、深拷贝这几种“拷贝”方式。

对于简单类型的数据来说,赋值就是深拷贝。对于复杂类型的数据(对象)来说,有浅拷贝和深拷贝之分。

浅拷贝对于值类型的话是完全拷贝一份,而对于引用类型是拷贝其地址。也就是拷贝的对象修改引用类型的变量同样会影响到源对象。

Python中的变量都是指针,因为变量是指针,所以变量没有类型限制,可以指向任意对象。指针的内存空间大小是与类型无关的,其内存空间只是保存了所指向数据的内存地址。所以在Python与Go中有些细微的区别。

Python与Go赋值区别

python中的切片赋值可以生成新的地址空间,是深拷贝,而Go语言中则是引用。

这里对Go与Python中的值类型与切片赋值举例

Go中的情形如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func Change1(){
a := []string{"1","2","3","4"}
b := a
b[2] = "1"
fmt.Println("a= ",a, &a)
fmt.Println("b= ",b, &b)

c := []string{"1","2","3","4"}
d := c[:]
d[2] = "1"
fmt.Println("c= ",&c)
fmt.Println("d= ",&d)

e := 1
f := e
fmt.Println("e= ",e, &e)
fmt.Println("f= ",f, &f)
}

输出:
a= [1 2 1 4] &[1 2 1 4]
b= [1 2 1 4] &[1 2 1 4]
c= &[1 2 1 4]
d= &[1 2 1 4]
e= 1 0xc00009c470
f= 1 0xc00009c478

**分析:**可以看出,在Go中,可变类型赋值及切片形式赋值并不会生成新的空间。对于不可变类型,Go会直接生成新的空间。

Python中的情形如下所示:

1
2
3
4
5
a = [1,2,3,4]
b = a
b[2] = 1
print("a = ",a,id(a))
print("b = ",b,id(b))
1
2
a =  [1, 2, 1, 4] 140183767803616
b = [1, 2, 1, 4] 140183767803616
1
2
3
4
5
a = [1,2,3,4]
b = a[:]
b[2] = 1
print("a = ",a,id(a))
print("b = ",b,id(b))
1
2
a =  [1, 2, 3, 4] 140183767804176
b = [1, 2, 1, 4] 140183761986576
1
2
3
4
5
a = 1
b = a
b = 2
print("a = ",a,id(a))
print("b = ",b,id(b))
1
2
a =  1 94734743720704
b = 2 94734743720736

可以看出,如果要生成一份新的空间时,在Python中可以对列表使用切片来进行,Go中不可行。

这里需要注意的是,切片拷贝无法拷贝嵌套结构,示例如下:

1
2
3
4
5
6
a = [1,2,3,[4,5,6]]
b = a[:]
b[2] = 0
b[3][0] = 1
print("a = ",a,id(a),id(a[3]),id(a[2]))
print("b = ",b,id(b),id(b[3]),id(b[2]))
1
2
a =  [1, 2, 3, [1, 5, 6]] 140183759486800 140183767686672 94734743720768
b = [1, 2, 0, [1, 5, 6]] 140183759484240 140183767686672 94734743720672

可以看出,嵌套结构a[3]并没有被重新赋予新的空间。若想赋予新的空间,可以使用 copy 标准库内的 deepcopy 方法,来达到嵌套深拷贝的目的

1
2
3
4
5
6
7
import copy
a = [1,2,3,[4,5,6]]
b = copy.deepcopy(a)
b[2] = 0
b[3][0] = 1
print("a = ",a,id(a),id(a[3]),id(a[2]))
print("b = ",b,id(b),id(b[3]),id(b[2]))
1
2
a =  [1, 2, 3, [4, 5, 6]] 140183767648208 140183769006480 94734743720768
b = [1, 2, 0, [1, 5, 6]] 140183767646768 140183762524512 94734743720672

另外,在python中如果只是变量赋值的话,不可变类型也不会生成新的空间,这与Go中不同,如下所示:

1
2
3
4
a = (1,2,3,4)
b = a
print("输出结果a = ",a,id(a))
print("输出结果b = ",b,id(b))
1
2
输出结果a =  (1, 2, 3, 4) 140183762321488
输出结果b = (1, 2, 3, 4) 140183762321488
1
2
3
4
5
a = (1,2,3,4)
b = a
b = (1,2)
print("输出结果a = ",a,id(a))
print("输出结果b = ",b,id(b))
1
2
输出结果a =  (1, 2, 3, 4) 140183766821008
输出结果b = (1, 2) 140183769335600

对可变类型Map的拷贝,在Python和Go中是相同的,都是引用

Go中的拷贝举例

下面在Go中使用一个复杂的例子来弄清楚上述情形,及深拷贝。Python中的深拷贝可以使用copy包,在Go中可以使用反射。代码如下:

我们使用句子信息来构建一个大的结构体,其中包含一些可变与不可变类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"bytes"
"encoding/gob"
"fmt"
)

//词信息
type WordInfo struct {
Word string //词
Tag string //词性
Strokes []int //笔画
}

//分词
type Cut struct {
Count int //分词数
WordCut []WordInfo
}

// 句子信息
type Sentence struct {
Text string //句子
IsNegative bool //是否疑问句
Cut Cut //分词
Idf map[string]float64 //词的Idf
}

使用两种方式进行直接赋值

方式1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func SentenceTest1() {
St1 := Sentence{
Text: "去北京玩",
IsNegative: false,
Cut: Cut{
Count: 3,
WordCut: []WordInfo{
{"去", "v", []int{5}},
{"北京", "n", []int{5, 8}},
{"玩", "v", []int{8}}}},
Idf: map[string]float64{"去": 1.0, "玩": 2.0},
}

//浅拷贝
St2 := St1
fmt.Printf("Before St1:%v\n", St1)
fmt.Printf("Before St2:%v\n", St2)

St2.Text = "不去上海玩"
St2.IsNegative = true
St2.Cut.Count = 4
St2.Cut.WordCut[0] = WordInfo{"不", "v", []int{4}}
St2.Cut.WordCut[1] = WordInfo{"去", "v", []int{5}}
St2.Cut.WordCut[2] = WordInfo{"上海", "n", []int{3,10}}
St2.Cut.WordCut = append(St2.Cut.WordCut,WordInfo{"玩", "v", []int{8}})
St2.Idf["去"] = 1.1
St2.Idf["玩"] = 2.2
fmt.Printf("After St1:%v\n", St1)
fmt.Printf("After St2:%v\n", St2)
}
1
2
3
4
Before St1:{去北京玩 false {3 [{去 v [5]} {北京 n [5 8]} {玩 v [8]}]} map[去:1 玩:2]}
Before St2:{去北京玩 false {3 [{去 v [5]} {北京 n [5 8]} {玩 v [8]}]} map[去:1 玩:2]}
After St1:{去北京玩 false {3 [{不 v [4]} {去 v [5]} {上海 n [3 10]}]} map[去:1.1 玩:2.2]}
After St2:{不去上海玩 true {4 [{不 v [4]} {去 v [5]} {上海 n [3 10]} {玩 v [8]}]} map[去:1.1 玩:2.2]}

方式2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func SentenceTest2() {
St1 := Sentence{
Text: "去北京玩",
IsNegative: false,
Cut: Cut{
Count: 3,
WordCut: []WordInfo{
{"去", "v", []int{5}},
{"北京", "n", []int{5, 8}},
{"玩", "v", []int{8}}}},
Idf: map[string]float64{"去": 1.0, "玩": 2.0},
}

//浅拷贝
St2 := St1
fmt.Printf("Before St1:%v\n", St1)
fmt.Printf("Before St2:%v\n", St2)

St2.Text = "不去上海玩"
St2.IsNegative = true
St2.Cut.Count = 4
St2.Cut.WordCut = []WordInfo{
{"不", "v", []int{4}},
{"去", "v", []int{5}},
{"上海", "n", []int{3,10}},
{"玩", "v", []int{8}}}
St2.Idf = map[string]float64{"去": 1.1, "玩": 2.2}
fmt.Printf("After St1:%v\n", St1)
fmt.Printf("After St2:%v\n", St2)
}
1
2
3
4
Before St1:{去北京玩 false {3 [{去 v [5]} {北京 n [5 8]} {玩 v [8]}]} map[去:1 玩:2]}
Before St2:{去北京玩 false {3 [{去 v [5]} {北京 n [5 8]} {玩 v [8]}]} map[去:1 玩:2]}
After St1:{去北京玩 false {3 [{去 v [5]} {北京 n [5 8]} {玩 v [8]}]} map[去:1 玩:2]}
After St2:{不去上海玩 true {4 [{不 v [4]} {去 v [5]} {上海 n [3 10]} {玩 v [8]}]} map[去:1.1 玩:2.2]}

分析:

可以看到只有Slice 和 Map会受到影响,我们这里重新生成一个Slice和Map就可以不受影响。

通过函数赋值(测试Go中的参数传递)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func SentenceTest1() {
St1 := Sentence{
Text: "去北京玩",
IsNegative: false,
Cut: Cut{
Count: 3,
WordCut: []WordInfo{
{"去", "v", []int{5}},
{"北京", "n", []int{5, 8}},
{"玩", "v", []int{8}}}},
Idf: map[string]float64{"去": 1.0, "玩": 2.0},
}

//浅拷贝
St2 := St1
fmt.Printf("Before St:%v\n", St1)

Modify(St2)
fmt.Printf("After St1:%v\n", St1)
fmt.Printf("After St2:%v\n", St2)
}

// 通过函数改变St2的值
func Modify(query Sentence){
fmt.Printf("Modify Before St1:%v\n", query)
query.Text = "不去上海玩"
query.IsNegative = true
query.Cut.Count = 4
query.Cut.WordCut[0] = WordInfo{"不", "v", []int{4}}
query.Cut.WordCut[1] = WordInfo{"去", "v", []int{5}}
query.Cut.WordCut[2] = WordInfo{"上海", "n", []int{3,10}}
query.Cut.WordCut = append(query.Cut.WordCut,WordInfo{"玩", "v", []int{8}})
query.Idf["去"] = 1.1
query.Idf["玩"] = 2.2
fmt.Printf("Modify After St1:%v\n", query)
}

输出结果如下所示:

1
2
3
4
5
6
7
Before St:{去北京玩 false {3 [{去 v [5]} {北京 n [5 8]} {玩 v [8]}]} map[去:1 玩:2]}

Modify Before St1:{去北京玩 false {3 [{去 v [5]} {北京 n [5 8]} {玩 v [8]}]} map[去:1 玩:2]}
Modify After St1:{不去上海玩 true {4 [{不 v [4]} {去 v [5]} {上海 n [3 10]} {玩 v [8]}]} map[去:1.1 玩:2.2]}

After St1:{去北京玩 false {3 [{不 v [4]} {去 v [5]} {上海 n [3 10]}]} map[去:1.1 玩:2.2]}
After St2:{不去上海玩 true {4 [{不 v [4]} {去 v [5]} {上海 n [3 10]} {玩 v [8]}]} map[去:1.1 玩:2.2]}

分析结果:

可以看到,在函数中St1的值已经随着St2的值改变了,但函数外确没变。因为在Golang方法中传递的参数被拷贝了一份,修改的Slice 和 Map都是同一份地址,但是值类型是重新赋值的(这里IsNegative,count,Text在St1中都未改变)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func deepCopy(dst, src interface{}) error {
var buf bytes.Buffer
if err := gob.NewEncoder(&buf).Encode(src); err != nil {
return err
}
return gob.NewDecoder(bytes.NewBuffer(buf.Bytes())).Decode(dst)
}


func SentenceTest2() {
St1 := &Sentence{
Text: "去北京玩",
IsNegative: false,
Cut: Cut{
Count: 3,
WordCut: []WordInfo{
{"去", "v", []int{5}},
{"北京", "n", []int{5, 8}},
{"玩", "v", []int{8}}}},
Idf: map[string]float64{"去": 1.0, "玩": 2.0},
}

//深拷贝
St2 := new(Sentence)
if err := deepCopy(St2, St1); err != nil {
panic(err.Error())
}
fmt.Printf("SentenceInfo St1:%v, St2:%v\n", St1, St2)

Modify(*St2)
fmt.Printf("SentenceInfo St1:%v, St2:%v\n", St1, St2)
}

结果如下:

1
2
3
4
5
6
Before St1:&{去北京玩 false {3 [{去 v [5]} {北京 n [5 8]} {玩 v [8]}]} map[去:1 玩:2]}
Before St2:&{去北京玩 false {3 [{去 v [5]} {北京 n [5 8]} {玩 v [8]}]} map[去:1 玩:2]}
Modify Before St1:{去北京玩 false {3 [{去 v [5]} {北京 n [5 8]} {玩 v [8]}]} map[去:1 玩:2]}
Modify After St1:{不去上海玩 true {4 [{不 v [4]} {去 v [5]} {上海 n [3 10]} {玩 v [8]}]} map[去:1.1 玩:2.2]}
After St1:&{去北京玩 false {3 [{去 v [5]} {北京 n [5 8]} {玩 v [8]}]} map[去:1 玩:2]}
After St2:&{去北京玩 false {3 [{不 v [4]} {去 v [5]} {上海 n [3 10]}]} map[去:1.1 玩:2.2]}

分析:

可以看到St2经过参数传递(浅拷贝)在修改Slice和Map受到影响的也只有St2和St2的浅拷贝对象。对于St1没有任何影响。

若想改变St2的值可以通过指针实现。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
func ModifyPoint(query *Sentence){
fmt.Printf("Modify Before St2:%v\n", query)
query.Text = "不去上海玩"
query.IsNegative = true
query.Cut.Count = 4
query.Cut.WordCut = []WordInfo{
{"不", "v", []int{4}},
{"去", "v", []int{5}},
{"上海", "n", []int{3,10}},
{"玩", "v", []int{8}}}
query.Idf = map[string]float64{"去": 1.1, "玩": 2.2}
fmt.Printf("Modify After St2:%v\n", query)
}

func SentenceTest3() {
St1 := &Sentence{
Text: "去北京玩",
IsNegative: false,
Cut: Cut{
Count: 3,
WordCut: []WordInfo{
{"去", "v", []int{5}},
{"北京", "n", []int{5, 8}},
{"玩", "v", []int{8}}}},
Idf: map[string]float64{"去": 1.0, "玩": 2.0},
}

//深拷贝
St2 := new(Sentence)
if err := deepCopy(St2, St1); err != nil {
panic(err.Error())
}
fmt.Printf("Before St1:%v\n", St1)
fmt.Printf("Before St2:%v\n", St2)

ModifyPoint(&*St2)
fmt.Printf("After St1:%v\n", St1)
fmt.Printf("After St2:%v\n", St2)
}
1
2
3
4
5
6
Before St1:&{去北京玩 false {3 [{去 v [5]} {北京 n [5 8]} {玩 v [8]}]} map[去:1 玩:2]}
Before St2:&{去北京玩 false {3 [{去 v [5]} {北京 n [5 8]} {玩 v [8]}]} map[去:1 玩:2]}
Modify Before St2:&{去北京玩 false {3 [{去 v [5]} {北京 n [5 8]} {玩 v [8]}]} map[去:1 玩:2]}
Modify After St2:&{不去上海玩 true {4 [{不 v [4]} {去 v [5]} {上海 n [3 10]} {玩 v [8]}]} map[去:1.1 玩:2.2]}
After St1:&{去北京玩 false {3 [{去 v [5]} {北京 n [5 8]} {玩 v [8]}]} map[去:1 玩:2]}
After St2:&{不去上海玩 true {4 [{不 v [4]} {去 v [5]} {上海 n [3 10]} {玩 v [8]}]} map[去:1.1 玩:2.2]}

St2的改变对St1不造成影响

总结

关于Go与Python中的变量赋值应该视情况使用不同的方式,通过函数进行赋值时需要特别注意

参考:

https://blog.csdn.net/weixin_40165163/article/details/90680466