Python中的引用、拷贝对象(深拷贝deepcopy与浅拷贝copy)、 深入理解python变量作用域及其陷阱

Python中的对象之间赋值时是按引用传递的,如果需要拷贝对象,需要使用标准库中的copy模块。
1. copy.copy 浅拷贝 只拷贝父对象,不会拷贝对象的内部的子对象。
2. copy.deepcopy 深拷贝 拷贝对象及其子对象。

一 引用vs拷贝

values = [0, 1, 2]
values[1] = values
values        #[0, [...], 2]

我预想应当是 

[0, [0, 1, 2], 2]

但结果却为何要赋值无限次?

可以说 Python 没有赋值,只有引用。你这样相当于创建了一个引用自身的结构,所以导致了无限循环。为了理解这个问题,有个基本概念需要搞清楚。
Python 没有「变量」,我们平时所说的变量其实只是「标签」,是引用。

(1)没有限制条件的分片表达式(L[:])能够复制序列,但此法只能浅层复制。
(2)字典 copy 方法,D.copy() 能够复制字典,但此法只能浅层复制
(3)有些内置函数,例如 list,能够生成拷贝 list(L)
(4)copy 标准库模块能够生成完整拷贝:deepcopy 本质上是递归 copy
(5)对于不可变对象和可变对象来说,浅复制都是复制的引用。【1】只是因为复制不变对象和复制不变对象的引用是等效的(因为对象不可变,当改变时会新建对象重新赋值)。所以看起来浅复制只复制不可变对象(整数,实数,字符串等)。【2】而对于可变对象,浅复制其实是创建了一个对于该对象的引用,也就是说只是给同一个对象贴上了另一个标签而已。

 增强赋值以及共享引用 

x = x + y,x 出现两次,必须执行两次,性能不好,合并必须新建对象 x,然后复制两个列表合并
属于复制/拷贝
x += y,x 只出现一次,也只会计算一次,性能好,不生成新对象,只在内存块末尾增加元素。
当 x、y 为list时, += 会自动调用 extend 方法进行合并运算,in-place change。
属于共享引用

L = [1, 2]
M = L
L = L + [3, 4]
print L, M
print "-------------------"
L = [1, 2]
M = L
L += [3, 4]
print L, M

#输出结果
#[1, 2, 3, 4] [1, 2]
#-------------------
#[1, 2, 3, 4] [1, 2, 3, 4]

关于切片复制 [:]

成对象的拷贝或者是复制序列,不再是引用和共享变量,但此法只能顶层复制。
往更深处说,values[:] 复制操作是所谓的「浅复制」(shallow copy),当列表对象有嵌套的时候(如其中有列表、字典等)也会产生出乎意料的错误。 

二 Python 拷贝对象(深拷贝deepcopy与浅拷贝copy)

1. copy.copy 浅拷贝 只拷贝父对象,不会拷贝对象的内部的子对象。

2. copy.deepcopy 深拷贝 拷贝对象及其子对象

一个很好的例子:

import copy
a=[1,2,[3,4],{'a':1}]   # 原始对象
b=a     # 赋值,传对象的引用
c=copy.copy(a)  # 对象拷贝,浅拷贝
d=copy.deepcopy(a)  # 对象拷贝,深拷贝
e=a[:]  # 能复制序列,浅拷贝

a.append('add1')    # 修改对象a
a[2].append('add2') # 修改对象a中的[3,4]数组对象
a[3]='666'
print('a:',a)
print('b:',b)
print('c:',c)
print('d:',d)
print('e:',e)

#输出结果:
# ('a:', [1, 2, [3, 4, 'add2'], '666', 'add1'])
# ('b:', [1, 2, [3, 4, 'add2'], '666', 'add1'])
# ('c:', [1, 2, [3, 4, 'add2'], {'a': 1}])
# ('d:', [1, 2, [3, 4], {'a': 1}])
# ('e:', [1, 2, [3, 4, 'add2'], {'a': 1}])

三 深入理解python变量作用域及其陷阱

1、可变对象&不可变对象

在Python中,对象分为两种:可变对象和不可变对象,不可变对象包括int,float,long,str,tuple等,可变对象包括list,set,dict等。需要注意的是:这里说的不可变指的是值的不可变。对于不可变类型的变量,如果要更改变量,则会创建一个新值,把变量绑定到新值上,而旧值如果没有被引用就等待垃圾回收。另外,不可变的类型可以计算hash值,作为字典的key。可变类型数据对对象操作的时候,不需要再在其他地方申请内存,只需要在此对象后面连续申请(+/-)即可,也就是它的内存地址会保持不变,但区域会变长或者变短。

a='hello'
print(id(a))    # 1991608735200
a='python'
print(id(a))    # 1991608735368

# 重新赋值之后,变量a的内存地址已经变了
# 'hello'是str类型,不可变,所以赋值操作知识重新创建了str 'python'对象,然后将变量a指向了它

l1=[1,2,3]
print(id(l1))   # 2262493958280
l1.append(4)
print(id(l1))   # 2262493958280

# list重新赋值之后,变量l1的内存地址并未改变
# [1, 2, 3]是可变的,append操作只是改变了其value,变量l1指向没有变

2、函数值传递

def func_int(a):
    a += 4
def func_list(a_list):
    a_list[0] = 4

t = 0
func_int(t)
print t                # output: 0

t_list = [1, 2, 3]
func_list(t_list)
print t_list            # output: [4, 2, 3]

对于上面的输出,不少Python初学者都比较疑惑:第一个例子看起来像是传值,而第二个例子确实传引用。其实,解释这个问题也非常容易,主要是因为可变对象和不可变对象的原因:对于可变对象,对象的操作不会重建对象,而对于不可变对象,每一次操作就重建新的对象。

    在函数参数传递的时候,Python其实就是把参数里传入的变量对应的对象的引用依次赋值给对应的函数内部变量。参照上面的例子来说明更容易理解,func_int中的局部变量"a"其实是全部变量"t"所指向对象的另一个引用,由于整数对象是不可变的,所以当func_int对变量"a"进行修改的时候,实际上是将局部变量"a"指向到了整数对象"1"。所以很明显,func_list修改的是一个可变的对象,局部变量"a"和全局变量"t_list"指向的还是同一个对象。

3、为什么修改全局的dict变量不用global关键字

为什么修改字典d的值不用global关键字先声明呢?

s = 'foo'
d = {'a':1}
def f():
    s = 'bar'
    d['b'] = 2
f()
print s  # foo
print d  # {'a': 1, 'b': 2}

这是因为,【1】在s = 'bar'这句中,它是“有歧义的“,因为它既可以是表示引用全局变量s,也可以是创建一个新的局部变量,所以在python中,默认它的行为是创建局部变量,除非显式声明global,global定义的本地变量会变成其对应全局变量的一个别名,即是同一个变量。【2】在d['b']=2这句中,它是“明确的”,因为如果把d当作是局部变量的话,它会报KeyError,所以它只能是引用全局的d,故不需要多此一举显式声明global。

上面这两句赋值语句其实是不同的行为,一个是rebinding(不可变对象), 一个是mutation(可变对象).

但是如果是下面这样:

d = {'a':1}
def f():
    d = {}
    d['b'] = 2
f()
print d  # {'a': 1}

在d = {}这句,它是”有歧义的“了,所以它是创建了局部变量d,而不是引用全局变量d,所以d['b']=2也是操作的局部变量。

推而远之,这一切现象的本质就是”它是否是明确的“。

仔细想想,就会发现不止dict不需要global,所有”明确的“东西都不需要global。因为int类型str类型之类的不可变对象,每一次操作就重建新的对象,他们只有一种修改方法,即x = y, 恰好这种修改方法同时也是创建变量的方法,所以产生了歧义,不知道是要修改还是创建。而dict/list/对象等可变对象,操作不会重建对象,可以通过dict['x']=y或list.append()之类的来修改,跟创建变量不冲突,不产生歧义,所以都不用显式global。

4 可变对象 list 的 = 和 append/extend 差别在哪?

接上面 5.3 的理论,下面咱们再看一例常见的错误:

# coding=utf-8
# 测试utf-8编码
import sys
reload(sys)
sys.setdefaultencoding('utf-8')

list_a = []
def a():
    list_a = [1]      ## 语句1
a()
print list_a    # []

print "======================"

list_b = []
def b():
    list_b.append(1)    ## 语句2
b()
print list_b    # [1]

大家可以看到为什么 语句1 不能改变 list_a 的值,而 语句2 却可以?他们的差别在哪呢?

因为 = 创建了局部变量,而 .append() 或者 .extend() 重用了全局变量。

5、陷阱:使用可变的默认参数

我多次见到过如下的代码:

def foo(a, b, c=[]):
# append to c
# do some more stuff

永远不要使用可变的默认参数,可以使用如下的代码代替:

def foo(a, b, c=None):
    if c is None:
        c = []
    # append to c
    # do some more stuff

‍‍与其解释这个问题是什么,不如展示下使用可变默认参数的影响:‍‍

In[2]: def foo(a, b, c=[]):
...        c.append(a)
...        c.append(b)
...        print(c)
...
In[3]: foo(1, 1)
[1, 1]
In[4]: foo(1, 1)
[1, 1, 1, 1]
In[5]: foo(1, 1)
[1, 1, 1, 1, 1, 1]

同一个变量c在函数调用的每一次都被反复引用。这可能有一些意想不到的后果。

参考链接

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值